hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-03-14 19:30:33 +0100
committerhachem <im@hachem.wtf>2026-03-14 19:30:33 +0100
commitfadc56fa2952ad4031247e9e642a0fa2eab92ee0 (patch)
treec8d535fad584825cc0746fa20987eade349de743 /src/ui
parent75c96946c9ad945fadede7d16130966f601ccaa7 (diff)
refactor: overhaul the ui
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/source_view.cpp106
-rw-r--r--src/ui/source_view.h3
-rw-r--r--src/ui/ui.cpp729
-rw-r--r--src/ui/ui.h43
4 files changed, 724 insertions, 157 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index dfd0bfb..a98594c 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -267,12 +267,9 @@ namespace Hsdbg
macros.find(word) != nullptr)
kind = SyntaxKind::Macro;
- if (kind != SyntaxKind::Default)
- {
- flush_default(start);
- emit(start, at - start, kind);
- run = at;
- }
+ flush_default(start);
+ emit(start, at - start, kind);
+ run = at;
continue;
}
@@ -315,11 +312,57 @@ namespace Hsdbg
const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255);
const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90);
+ const ImU32 INLINE_VALUE_COLOR = IM_COL32(122, 140, 170, 235);
+
auto line_number_width(size_t line_count) -> float
{
return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x;
}
+ auto find_local(std::span<const Variable> locals, std::string_view word) -> const Variable*
+ {
+ const auto match = std::ranges::find_if(locals, [&](const Variable& local)
+ {
+ return !local.value.empty() && local.name == word;
+ });
+
+ return match != locals.end() ? &*match : nullptr;
+ }
+
+ auto contains_word(const std::string& line, const std::string& name) -> bool
+ {
+ if (name.empty())
+ return false;
+
+ for (size_t at = line.find(name); at != std::string::npos; at = line.find(name, at + 1))
+ {
+ const bool left = at > 0 && is_word(line[at - 1]);
+ const bool right = at + name.size() < line.size() && is_word(line[at + name.size()]);
+
+ if (!left && !right)
+ return true;
+ }
+
+ return false;
+ }
+
+ auto inline_value(const Variable& local) -> std::string
+ {
+ std::string text = local.name + " = " + local.value;
+
+ if (const auto newline = text.find('\n'); newline != std::string::npos)
+ text.resize(newline);
+
+ constexpr size_t cap = 48;
+ if (text.size() > cap)
+ {
+ text.resize(cap - 3);
+ text += "...";
+ }
+
+ return text;
+ }
+
auto capture_invocation(const std::string& line, uint32_t start) -> std::string
{
const size_t size = line.size();
@@ -415,6 +458,13 @@ namespace Hsdbg
return request;
}
+ auto SourceView::take_watch_request() -> std::optional<std::string>
+ {
+ std::optional<std::string> request = std::move(m_watch_request);
+ m_watch_request.reset();
+ return request;
+ }
+
auto SourceView::set_highlighted_line(uint32_t line) -> void
{
m_highlighted_line = line;
@@ -480,6 +530,10 @@ namespace Hsdbg
m_scroll_to_highlight = false;
}
+ const std::span<const Variable> locals =
+ (debugger.is_stopped() && m_highlighted_line != 0) ? debugger.locals()
+ : std::span<const Variable>{};
+
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(m_lines.size()), text_height);
@@ -580,6 +634,21 @@ namespace Hsdbg
if (span.kind == SyntaxKind::Default)
{
ImGui::TextUnformatted(begin, end);
+
+ const std::string_view word(begin, static_cast<size_t>(end - begin));
+ const bool is_name = !word.empty() && is_word_start(word.front());
+
+ if (!locals.empty() && ImGui::IsItemHovered())
+ {
+ if (const Variable* local = find_local(locals, word))
+ ImGui::SetTooltip("%s = %s%s%s", local->name.c_str(),
+ local->value.c_str(),
+ local->type.empty() ? "" : "\n",
+ local->type.c_str());
+ }
+
+ if (is_name && ImGui::IsItemClicked(ImGuiMouseButton_Right))
+ m_watch_request = std::string(word);
}
else if (span.kind == SyntaxKind::Macro)
{
@@ -594,6 +663,31 @@ namespace Hsdbg
}
}
}
+
+ if (!locals.empty() && line_number <= m_highlighted_line)
+ {
+ int shown = 0;
+
+ for (const Variable& local : locals)
+ {
+ if (shown >= 4)
+ break;
+
+ if (local.value.empty() || !contains_word(text, local.name))
+ continue;
+
+ ImGui::SameLine(0.0f, shown == 0 ? GUTTER_MARGIN * 3.0f : GUTTER_MARGIN);
+ ImGui::PushStyleColor(ImGuiCol_Text, INLINE_VALUE_COLOR);
+ ImGui::TextUnformatted(inline_value(local).c_str());
+ ImGui::PopStyleColor();
+
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("%s = %s%s%s", local.name.c_str(), local.value.c_str(),
+ local.type.empty() ? "" : "\n", local.type.c_str());
+
+ ++shown;
+ }
+ }
}
}
diff --git a/src/ui/source_view.h b/src/ui/source_view.h
index bd0ae82..8b4c0a3 100644
--- a/src/ui/source_view.h
+++ b/src/ui/source_view.h
@@ -58,6 +58,8 @@ namespace Hsdbg
auto take_macro_request() -> std::optional<std::string>;
+ auto take_watch_request() -> std::optional<std::string>;
+
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
@@ -70,6 +72,7 @@ namespace Hsdbg
std::vector<std::vector<SourceSpan>> m_spans;
MacroTable m_macros;
std::optional<std::string> m_macro_request;
+ std::optional<std::string> m_watch_request;
bool m_highlight = false;
bool m_highlighting_enabled = true;
bool m_line_numbers = true;
diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp
index 773399e..2c95d9c 100644
--- a/src/ui/ui.cpp
+++ b/src/ui/ui.cpp
@@ -34,12 +34,12 @@ namespace Hsdbg
constexpr const char* PANEL_THREADS = "threads";
constexpr const char* PANEL_SOURCE_TREE = "source tree";
constexpr const char* PANEL_LOCALS = "locals";
+ constexpr const char* PANEL_WATCH = "watch";
constexpr const char* PANEL_REGISTERS = "registers";
constexpr const char* PANEL_SYMBOLS = "symbols";
constexpr const char* PANEL_DISASSEMBLY = "disassembly";
constexpr const char* PANEL_CONSOLE = "console";
constexpr const char* PANEL_PROFILER = "profiler";
- constexpr const char* PANEL_TIMELINE = "timeline";
constexpr const char* PANEL_MACROS = "macros";
constexpr const char* LOAD_TARGET_POPUP = "load target";
@@ -231,6 +231,17 @@ namespace Hsdbg
});
}
+ auto node_contains(const SourceNode& node, const std::filesystem::path& target) -> bool
+ {
+ if (!node.path.empty())
+ return node.path == target;
+
+ return std::ranges::any_of(node.children, [&](const SourceNode& child)
+ {
+ return node_contains(child, target);
+ });
+ }
+
auto preferred_source(std::span<const std::filesystem::path> files) -> std::filesystem::path
{
const auto named_main = std::ranges::find_if(files, [](const std::filesystem::path& file)
@@ -499,6 +510,11 @@ namespace Hsdbg
m_scroll_to_program_counter = true;
m_scroll_to_symbol = true;
+ follow_selected_frame(debugger);
+ }
+
+ auto Ui::follow_selected_frame(Debugger& debugger) -> void
+ {
const std::span<const StackFrame> stack = debugger.call_stack();
const auto frame = std::ranges::find_if(stack, [](const StackFrame& candidate)
@@ -611,30 +627,48 @@ namespace Hsdbg
m_focus_macros = true;
}
+ if (std::optional<std::string> request = m_source_view.take_watch_request())
+ add_watch(debugger, std::move(*request));
+
+ const bool profiling_active = debugger.sampling_enabled() ||
+ debugger.instrumentation_active() ||
+ !debugger.traces().empty();
+
+ if (profiling_active && !m_profiler_revealed)
+ m_visible.profiler = true;
+ m_profiler_revealed = profiling_active;
+
draw_breakpoints_panel(debugger);
draw_call_stack_panel(debugger);
draw_source_tree_panel(debugger);
draw_threads_panel(debugger);
draw_locals_panel(debugger);
+ draw_watch_panel(debugger);
draw_registers_panel(debugger);
draw_symbols_panel(debugger);
draw_disassembly_panel(debugger);
draw_console_panel(debugger);
draw_profiler_panel(debugger);
- draw_timeline_panel(debugger);
draw_macros_panel(debugger);
if (m_visible.demo)
ImGui::ShowDemoWindow(&m_visible.demo);
draw_preferences_window();
+ draw_command_palette(debugger);
if (m_select_default_tabs)
{
+ ImGui::SetWindowFocus(PANEL_SYMBOLS);
+ ImGui::SetWindowFocus(PANEL_SOURCE_TREE);
+ ImGui::SetWindowFocus(PANEL_THREADS);
+ ImGui::SetWindowFocus(PANEL_CALL_STACK);
+ ImGui::SetWindowFocus(PANEL_WATCH);
ImGui::SetWindowFocus(PANEL_LOCALS);
+ ImGui::SetWindowFocus(PANEL_DISASSEMBLY);
ImGui::SetWindowFocus(PANEL_BREAKPOINTS);
+ ImGui::SetWindowFocus(PANEL_CONSOLE);
ImGui::SetWindowFocus(PANEL_SOURCE);
- ImGui::SetWindowFocus(PANEL_SOURCE_TREE);
m_select_default_tabs = false;
}
@@ -660,6 +694,12 @@ namespace Hsdbg
ImGui::SetWindowFocus(PANEL_MACROS);
m_focus_macros = false;
}
+
+ if (m_focus_profiler)
+ {
+ ImGui::SetWindowFocus(PANEL_PROFILER);
+ m_focus_profiler = false;
+ }
}
auto Ui::build_default_layout(uint32_t dockspace_id) -> void
@@ -670,26 +710,29 @@ namespace Hsdbg
ImGuiID center_id = dockspace_id;
const ImGuiID left_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.20f, nullptr, &center_id);
- const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.22f, nullptr, &center_id);
- const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.24f, nullptr, &center_id);
+ const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24f, nullptr, &center_id);
+ const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26f, nullptr, &center_id);
ImGuiID left_top_id = left_id;
const ImGuiID left_bottom_id = ImGui::DockBuilderSplitNode(left_top_id, ImGuiDir_Down, 0.5f, nullptr, &left_top_id);
ImGuiID right_top_id = right_id;
- const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.5f, nullptr, &right_top_id);
+ const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.45f, nullptr, &right_top_id);
+
+ ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id);
+ ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_top_id);
+ ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, left_bottom_id);
+ ImGui::DockBuilderDockWindow(PANEL_THREADS, left_bottom_id);
ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id);
ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id);
- ImGui::DockBuilderDockWindow(PANEL_TIMELINE, center_id);
- ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id);
- ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id);
- ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id);
+
ImGui::DockBuilderDockWindow(PANEL_LOCALS, right_top_id);
- ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, right_top_id);
+ ImGui::DockBuilderDockWindow(PANEL_WATCH, right_top_id);
ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id);
- ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
+
ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id);
+ ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_MACROS, bottom_id);
@@ -714,6 +757,9 @@ namespace Hsdbg
ImGui::Separator();
+ if (ImGui::MenuItem("command palette...", "cmd+k"))
+ m_palette_request = true;
+
ImGui::MenuItem("preferences...", nullptr, &m_show_preferences);
ImGui::Separator();
@@ -767,12 +813,12 @@ namespace Hsdbg
ImGui::MenuItem(PANEL_CALL_STACK, nullptr, &m_visible.call_stack);
ImGui::MenuItem(PANEL_THREADS, nullptr, &m_visible.threads);
ImGui::MenuItem(PANEL_LOCALS, nullptr, &m_visible.locals);
+ ImGui::MenuItem(PANEL_WATCH, nullptr, &m_visible.watch);
ImGui::MenuItem(PANEL_REGISTERS, nullptr, &m_visible.registers);
ImGui::MenuItem(PANEL_SYMBOLS, nullptr, &m_visible.symbols);
ImGui::MenuItem(PANEL_DISASSEMBLY, nullptr, &m_visible.disassembly);
ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console);
ImGui::MenuItem(PANEL_PROFILER, nullptr, &m_visible.profiler);
- ImGui::MenuItem(PANEL_TIMELINE, nullptr, &m_visible.timeline);
ImGui::MenuItem(PANEL_MACROS, nullptr, &m_visible.macros);
ImGui::Separator();
@@ -861,6 +907,26 @@ namespace Hsdbg
ImGui::TextDisabled("|");
ImGui::SameLine();
+ const bool profiler_open = m_visible.profiler;
+
+ if (profiler_open)
+ ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
+
+ if (ImGui::Button("profiler"))
+ {
+ m_visible.profiler = !m_visible.profiler;
+
+ if (m_visible.profiler)
+ m_focus_profiler = true;
+ }
+
+ if (profiler_open)
+ ImGui::PopStyleColor();
+
+ ImGui::SameLine();
+ ImGui::TextDisabled("|");
+ ImGui::SameLine();
+
ImGui::AlignTextToFramePadding();
ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data());
@@ -1165,8 +1231,7 @@ namespace Hsdbg
else
{
constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
- ImGuiTableFlags_SizingStretchProp |
- ImGuiTableFlags_ScrollY;
+ ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##breakpoints", 8, flags))
{
@@ -1204,9 +1269,20 @@ namespace Hsdbg
}
else if (breakpoint.line != 0)
{
- ImGui::Text("%s:%u",
- breakpoint.file.filename().string().c_str(),
- breakpoint.line);
+ const std::string location = std::format(
+ "{}:{}", breakpoint.file.filename().string(), breakpoint.line);
+
+ ImGui::TextUnformatted(location.c_str());
+
+ if (ImGui::IsItemHovered())
+ ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
+
+ if (ImGui::IsItemClicked() && !breakpoint.file.empty())
+ {
+ open_source(breakpoint.file);
+ m_source_view.set_highlighted_line(breakpoint.line);
+ ImGui::SetWindowFocus(PANEL_SOURCE);
+ }
}
else
{
@@ -1335,6 +1411,8 @@ namespace Hsdbg
debugger.select_thread(thread.id);
m_scroll_to_program_counter = true;
+ m_scroll_to_symbol = true;
+ follow_selected_frame(debugger);
}
ImGui::SameLine();
@@ -1367,6 +1445,8 @@ namespace Hsdbg
const SourceNode tree = build_source_tree(files);
const std::filesystem::path& open = m_source_view.path();
+ const bool reveal = open != m_revealed_source;
+
ImDrawList* const draw_list = ImGui::GetWindowDrawList();
const auto draw_node = [&](this const auto& self, const SourceNode& node) -> void
@@ -1398,12 +1478,14 @@ namespace Hsdbg
return;
}
- ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
- ImGuiTreeNodeFlags_OpenOnArrow |
- ImGuiTreeNodeFlags_OpenOnDoubleClick;
+ constexpr ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
+ ImGuiTreeNodeFlags_OpenOnArrow |
+ ImGuiTreeNodeFlags_OpenOnDoubleClick;
- if (m_source_filter.empty())
- flags |= ImGuiTreeNodeFlags_DefaultOpen;
+ if (!m_source_filter.empty())
+ ImGui::SetNextItemOpen(true, ImGuiCond_Always);
+ else if (reveal && node_contains(node, open))
+ ImGui::SetNextItemOpen(true, ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_Text, SOURCE_FOLDER_COLOR);
const bool open_node = ImGui::TreeNodeEx(node.name.c_str(), flags);
@@ -1444,6 +1526,8 @@ namespace Hsdbg
ImGui::TextDisabled("no source files");
else
draw_node(tree);
+
+ m_revealed_source = open;
}
}
@@ -1485,6 +1569,145 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::add_watch(Debugger& debugger, std::string expression) -> void
+ {
+ if (expression.empty())
+ return;
+
+ if (std::ranges::any_of(m_watches, [&](const Watch& watch)
+ { return watch.expression == expression; }))
+ {
+ m_visible.watch = true;
+ return;
+ }
+
+ Watch watch;
+ watch.expression = std::move(expression);
+
+ if (debugger.is_stopped())
+ {
+ if (const auto result = debugger.evaluate(watch.expression))
+ {
+ watch.value = *result;
+ watch.ok = true;
+ }
+ else
+ {
+ watch.value = result.error();
+ }
+ }
+
+ m_watches.push_back(std::move(watch));
+ m_visible.watch = true;
+ }
+
+ auto Ui::draw_watch_panel(Debugger& debugger) -> void
+ {
+ if (!m_visible.watch)
+ return;
+
+ if (ImGui::Begin(PANEL_WATCH, &m_visible.watch))
+ {
+ const bool stopped = debugger.is_stopped();
+ const bool moved = debugger.stop_count() != m_watch_stop ||
+ debugger.selected_thread() != m_watch_thread ||
+ debugger.selected_frame() != m_watch_frame;
+
+ if (stopped && (moved || !m_watch_evaluated))
+ {
+ m_watch_stop = debugger.stop_count();
+ m_watch_thread = debugger.selected_thread();
+ m_watch_frame = debugger.selected_frame();
+ m_watch_evaluated = true;
+
+ for (Watch& watch : m_watches)
+ {
+ if (const auto result = debugger.evaluate(watch.expression))
+ {
+ watch.value = *result;
+ watch.ok = true;
+ }
+ else
+ {
+ watch.value = result.error();
+ watch.ok = false;
+ }
+ }
+ }
+
+ if (!stopped)
+ m_watch_evaluated = false;
+
+ ImGui::SetNextItemWidth(-60.0f);
+ const bool submitted = ImGui::InputTextWithHint("##watch_input", "expression to watch",
+ &m_watch_input,
+ ImGuiInputTextFlags_EnterReturnsTrue);
+
+ ImGui::SameLine();
+ const bool add_clicked = ImGui::Button("watch", ImVec2(-1.0f, 0.0f));
+
+ if ((submitted || add_clicked) && !m_watch_input.empty())
+ {
+ add_watch(debugger, m_watch_input);
+ m_watch_input.clear();
+ }
+
+ if (m_watches.empty())
+ {
+ ImGui::TextDisabled("watch an expression; it re-evaluates on every stop");
+ }
+ else
+ {
+ constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
+ ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
+
+ if (ImGui::BeginTable("##watches", 3, flags))
+ {
+ ImGui::TableSetupColumn("expression", ImGuiTableColumnFlags_WidthFixed, 140.0f);
+ ImGui::TableSetupColumn("value");
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f);
+ ImGui::TableSetupScrollFreeze(0, 1);
+ ImGui::TableHeadersRow();
+
+ size_t remove_index = m_watches.size();
+
+ for (size_t index = 0; index < m_watches.size(); ++index)
+ {
+ const Watch& watch = m_watches[index];
+
+ ImGui::TableNextRow();
+ ImGui::PushID(static_cast<int>(index));
+
+ ImGui::TableNextColumn();
+ ImGui::TextUnformatted(watch.expression.c_str());
+
+ ImGui::TableNextColumn();
+ if (!m_watch_evaluated && !stopped)
+ ImGui::TextDisabled("-");
+ else if (watch.ok)
+ ImGui::TextUnformatted(watch.value.c_str());
+ else
+ ImGui::TextColored(ImVec4(0.85f, 0.45f, 0.45f, 1.0f), "%s",
+ watch.value.c_str());
+
+ ImGui::TableNextColumn();
+ if (ImGui::SmallButton("x"))
+ remove_index = index;
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndTable();
+
+ if (remove_index < m_watches.size())
+ m_watches.erase(m_watches.begin() + static_cast<std::ptrdiff_t>(remove_index));
+ }
+ }
+ }
+
+ ImGui::End();
+ }
+
auto Ui::draw_registers_panel(const Debugger& debugger) -> void
{
if (!m_visible.registers)
@@ -1707,61 +1930,53 @@ namespace Hsdbg
if (ImGui::Begin(PANEL_PROFILER, &m_visible.profiler))
{
- bool paused = m_profiler.paused();
- if (ImGui::Checkbox("pause", &paused))
- m_profiler.set_paused(paused);
-
- ImGui::SameLine();
- if (ImGui::Button("reset"))
- m_profiler.reset();
-
- ImGui::Separator();
+ const bool instrumented = debugger.instrumentation_active();
+ const std::span<const FunctionTrace> traces = debugger.traces();
- if (!debugger.has_target())
+ if (instrumented)
{
- ImGui::TextDisabled("no target: load and run a process to profile it");
+ ImGui::TextDisabled("instrumented target — every function timed automatically");
}
else
{
- const TimeSeries& memory = m_profiler.target_memory_mb();
-
- const std::string memory_overlay =
- std::format("{:.1f} MB (peak {:.1f})", memory.latest(), memory.maximum());
+ bool sampling = debugger.sampling_enabled();
+ if (ImGui::Checkbox("sample while running", &sampling))
+ debugger.set_sampling_enabled(sampling);
- ImGui::TextUnformatted("resident memory");
- ImGui::PlotLines("##target_memory", memory.values(), memory.count(), memory.offset(),
- memory_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 90.0f));
+ ImGui::SameLine();
+ ImGui::TextDisabled("(any binary, approximate)");
}
- ImGui::Separator();
- ImGui::TextUnformatted("function tracing");
+ const float flame_height = std::max(ImGui::GetContentRegionAvail().y * 0.6f, 150.0f);
- ImGui::SetNextItemWidth(-70.0f);
- const bool submitted = ImGui::InputTextWithHint("##trace_input", "function to time",
- &m_trace_input,
- ImGuiInputTextFlags_EnterReturnsTrue);
+ if (ImGui::BeginChild("##flamegraph", ImVec2(0.0f, flame_height), ImGuiChildFlags_Borders))
+ draw_flamegraph(debugger);
- ImGui::SameLine();
- const bool add_clicked = ImGui::Button("trace", ImVec2(-1.0f, 0.0f));
+ ImGui::EndChild();
- if ((submitted || add_clicked) && !m_trace_input.empty())
+ if (ImGui::CollapsingHeader("function timings", ImGuiTreeNodeFlags_DefaultOpen))
{
- debugger.add_trace(m_trace_input);
- m_trace_input.clear();
- }
+ ImGui::SetNextItemWidth(-70.0f);
+ const bool submitted = ImGui::InputTextWithHint("##trace_input", "function to time",
+ &m_trace_input,
+ ImGuiInputTextFlags_EnterReturnsTrue);
- const std::span<const FunctionTrace> traces = debugger.traces();
+ ImGui::SameLine();
+ const bool add_clicked = ImGui::Button("trace", ImVec2(-1.0f, 0.0f));
- if (traces.empty())
- {
- ImGui::TextDisabled("no traced functions yet");
- }
- else
- {
- constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
- ImGuiTableFlags_SizingStretchProp;
+ if ((submitted || add_clicked) && !m_trace_input.empty())
+ {
+ debugger.add_trace(m_trace_input);
+ m_trace_input.clear();
+ }
- if (ImGui::BeginTable("##traces", 6, flags))
+ if (traces.empty())
+ {
+ ImGui::TextDisabled("name a function above to count and time its calls");
+ }
+ else if (ImGui::BeginTable("##traces", 6,
+ ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
+ ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("function");
ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 52.0f);
@@ -1819,8 +2034,29 @@ namespace Hsdbg
}
}
- if (ImGui::CollapsingHeader("debugger self-timing"))
+ if (debugger.has_target() &&
+ ImGui::CollapsingHeader("resident memory", ImGuiTreeNodeFlags_DefaultOpen))
{
+ const TimeSeries& memory = m_profiler.target_memory_mb();
+
+ ImGui::Text("%.1f MB", memory.latest());
+ ImGui::SameLine();
+ ImGui::TextDisabled("peak %.1f MB", memory.maximum());
+
+ ImGui::PlotLines("##target_memory", memory.values(), memory.count(), memory.offset(),
+ nullptr, 0.0f, FLT_MAX, ImVec2(-1.0f, 60.0f));
+ }
+
+ if (ImGui::CollapsingHeader("hsdbg self-timing"))
+ {
+ bool paused = m_profiler.paused();
+ if (ImGui::Checkbox("pause graphs", &paused))
+ m_profiler.set_paused(paused);
+
+ ImGui::SameLine();
+ if (ImGui::SmallButton("reset"))
+ m_profiler.reset();
+
const TimeSeries& frame_ms = m_profiler.frame_times();
const TimeSeries& fps = m_profiler.frame_rates();
@@ -1844,116 +2080,99 @@ namespace Hsdbg
ImGui::End();
}
- auto Ui::draw_timeline_panel(Debugger& debugger) -> void
+ auto Ui::draw_flamegraph(const Debugger& debugger) -> void
{
- if (!m_visible.timeline)
- return;
+ const std::span<const TimelineSpan> spans = debugger.timeline();
- if (ImGui::Begin(PANEL_TIMELINE, &m_visible.timeline))
+ if (spans.empty())
{
- const std::span<const TimelineSpan> spans = debugger.timeline();
-
if (debugger.instrumentation_active())
- {
- ImGui::TextDisabled("instrumented: every function traced automatically");
- }
+ ImGui::TextDisabled("instrumented target — run it to lay out its calls here");
+ else if (debugger.sampling_enabled())
+ ImGui::TextDisabled("sampling — run the target and the calls will appear here");
else
- {
- bool sampling = debugger.sampling_enabled();
- if (ImGui::Checkbox("sample while running", &sampling))
- debugger.set_sampling_enabled(sampling);
+ ImGui::TextDisabled("turn on sampling, or trace a function below, then run");
- ImGui::SameLine();
- ImGui::TextDisabled("(any binary, approximate)");
- }
-
- if (spans.empty())
- {
- ImGui::TextDisabled("run an instrumented target, or trace functions, then stop to see calls");
- }
- else
- {
- const auto name_of = [&](uint32_t trace_id) -> const char*
- {
- return debugger.span_label(trace_id);
- };
+ return;
+ }
- double t_min = spans.front().start;
- double t_max = t_min;
- uint32_t max_depth = 0;
+ const auto name_of = [&](uint32_t trace_id) -> const char*
+ {
+ return debugger.span_label(trace_id);
+ };
- for (const TimelineSpan& span : spans)
- {
- t_min = std::min(t_min, span.start);
- t_max = std::max(t_max, span.start + span.duration);
- max_depth = std::max(max_depth, span.depth);
- }
+ double t_min = spans.front().start;
+ double t_max = t_min;
+ uint32_t max_depth = 0;
- const double range = std::max(t_max - t_min, 1.0e-6);
+ for (const TimelineSpan& span : spans)
+ {
+ t_min = std::min(t_min, span.start);
+ t_max = std::max(t_max, span.start + span.duration);
+ max_depth = std::max(max_depth, span.depth);
+ }
- ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size());
+ const double range = std::max(t_max - t_min, 1.0e-6);
- constexpr float row_height = 20.0f;
- const float rows = static_cast<float>(max_depth + 1);
+ ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size());
- const ImVec2 origin = ImGui::GetCursorScreenPos();
- const ImVec2 avail = ImGui::GetContentRegionAvail();
+ constexpr float row_height = 20.0f;
+ const float rows = static_cast<float>(max_depth + 1);
- const float canvas_w = std::max(avail.x, 1.0f);
- const float canvas_h = std::max(avail.y, rows * row_height + 4.0f);
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const ImVec2 avail = ImGui::GetContentRegionAvail();
- ImGui::InvisibleButton("##timeline_canvas", ImVec2(canvas_w, canvas_h));
- const bool canvas_hovered = ImGui::IsItemHovered();
+ const float canvas_w = std::max(avail.x, 1.0f);
+ const float canvas_h = std::max(avail.y, rows * row_height + 4.0f);
- ImDrawList* draw_list = ImGui::GetWindowDrawList();
- draw_list->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true);
+ ImGui::InvisibleButton("##flame_canvas", ImVec2(canvas_w, canvas_h));
+ const bool canvas_hovered = ImGui::IsItemHovered();
- const float scale = canvas_w / static_cast<float>(range);
- const float baseline = origin.y + canvas_h;
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ draw_list->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true);
- const ImVec2 mouse = ImGui::GetMousePos();
+ const float scale = canvas_w / static_cast<float>(range);
+ const float baseline = origin.y + canvas_h;
- for (const TimelineSpan& span : spans)
- {
- const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale);
- const float width = std::max(1.0f, static_cast<float>(span.duration * scale));
- const float y1 = baseline - static_cast<float>(span.depth) * row_height;
- const float y0 = y1 - (row_height - 2.0f);
+ const ImVec2 mouse = ImGui::GetMousePos();
- const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f);
+ for (const TimelineSpan& span : spans)
+ {
+ const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale);
+ const float width = std::max(1.0f, static_cast<float>(span.duration * scale));
+ const float y1 = baseline - static_cast<float>(span.depth) * row_height;
+ const float y0 = y1 - (row_height - 2.0f);
- draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f);
- draw_list->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f);
+ const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f);
- if (width > 24.0f)
- {
- draw_list->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true);
- draw_list->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255),
- name_of(span.trace_id));
- draw_list->PopClipRect();
- }
+ draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f);
+ draw_list->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f);
- const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width &&
- mouse.y >= y0 && mouse.y <= y1;
+ if (width > 24.0f)
+ {
+ draw_list->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true);
+ draw_list->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255),
+ name_of(span.trace_id));
+ draw_list->PopClipRect();
+ }
- if (over)
- {
- ImGui::BeginTooltip();
- ImGui::TextUnformatted(name_of(span.trace_id));
- ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0);
- if (span.duration > 0.0)
- ImGui::Text("duration %.3f ms", span.duration * 1000.0);
- else
- ImGui::TextDisabled("running...");
- ImGui::EndTooltip();
- }
- }
+ const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width &&
+ mouse.y >= y0 && mouse.y <= y1;
- draw_list->PopClipRect();
+ if (over)
+ {
+ ImGui::BeginTooltip();
+ ImGui::TextUnformatted(name_of(span.trace_id));
+ ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0);
+ if (span.duration > 0.0)
+ ImGui::Text("duration %.3f ms", span.duration * 1000.0);
+ else
+ ImGui::TextDisabled("running...");
+ ImGui::EndTooltip();
}
}
- ImGui::End();
+ draw_list->PopClipRect();
}
auto Ui::draw_macros_panel(Debugger&) -> void
@@ -2069,6 +2288,222 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::draw_command_palette(Debugger& debugger) -> void
+ {
+ const bool open_requested =
+ ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_K) || m_palette_request;
+
+ m_palette_request = false;
+
+ if (open_requested)
+ {
+ m_palette_open = true;
+ m_palette_focus = true;
+ m_palette_selection = 0;
+ m_palette_query.clear();
+ ImGui::OpenPopup("##command_palette");
+ }
+
+ if (!m_palette_open)
+ return;
+
+ const ImGuiViewport* viewport = ImGui::GetMainViewport();
+ const ImVec2 size(std::min(viewport->WorkSize.x - 80.0f, 640.0f), 0.0f);
+
+ ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f,
+ viewport->WorkPos.y + 90.0f),
+ ImGuiCond_Always, ImVec2(0.5f, 0.0f));
+ ImGui::SetNextWindowSize(size);
+
+ constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
+ ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
+
+ if (!ImGui::BeginPopup("##command_palette", flags))
+ {
+ m_palette_open = false;
+ return;
+ }
+
+ enum class Kind : uint8_t { Command, File, Symbol };
+ enum Command : uint8_t
+ {
+ Run, Continue, Pause, Stop, StepOver, StepInto, StepOut, ToggleProfiler
+ };
+
+ struct Entry
+ {
+ std::string label;
+ const char* hint = "";
+ Kind kind = Kind::Command;
+ Command command = Run;
+ std::filesystem::path file;
+ uint64_t symbol = 0;
+ };
+
+ const bool has_target = debugger.has_target();
+ const bool running = debugger.is_running();
+ const bool stopped = debugger.is_stopped();
+
+ std::vector<Entry> entries;
+
+ const auto add_command = [&](bool enabled, Command command, const char* label)
+ {
+ if (enabled && matches_filter(label, m_palette_query))
+ entries.push_back({ label, "command", Kind::Command, command, {}, 0 });
+ };
+
+ add_command(has_target && !running, Run, "run");
+ add_command(stopped, Continue, "continue");
+ add_command(running, Pause, "pause");
+ add_command(has_target, Stop, "stop");
+ add_command(stopped, StepOver, "step over");
+ add_command(stopped, StepInto, "step into");
+ add_command(stopped, StepOut, "step out");
+ add_command(true, ToggleProfiler, m_visible.profiler ? "hide profiler" : "show profiler");
+
+ if (!m_palette_query.empty())
+ {
+ for (const std::filesystem::path& file : debugger.source_files())
+ {
+ if (entries.size() >= 60)
+ break;
+
+ if (matches_filter(file.filename().string(), m_palette_query))
+ entries.push_back({ file.filename().string(), "file", Kind::File, Run, file, 0 });
+ }
+
+ for (const Symbol& symbol : debugger.symbols())
+ {
+ if (entries.size() >= 120)
+ break;
+
+ if (matches_filter(symbol.name, m_palette_query))
+ entries.push_back({ symbol.name, "symbol", Kind::Symbol, Run, {}, symbol.file_address });
+ }
+ }
+
+ if (entries.empty())
+ m_palette_selection = 0;
+ else
+ m_palette_selection = std::clamp(m_palette_selection, 0, static_cast<int>(entries.size()) - 1);
+
+ if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && !entries.empty())
+ m_palette_selection = (m_palette_selection + 1) % static_cast<int>(entries.size());
+
+ if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && !entries.empty())
+ {
+ m_palette_selection = (m_palette_selection - 1 + static_cast<int>(entries.size())) %
+ static_cast<int>(entries.size());
+ }
+
+ const auto activate = [&](const Entry& entry)
+ {
+ switch (entry.kind)
+ {
+ case Kind::File:
+ open_source(entry.file);
+ ImGui::SetWindowFocus(PANEL_SOURCE);
+ break;
+
+ case Kind::Symbol:
+ if (debugger.select_symbol(entry.symbol))
+ {
+ m_focus_disassembly = true;
+ m_scroll_to_program_counter = true;
+ }
+ break;
+
+ case Kind::Command:
+ switch (entry.command)
+ {
+ case Run:
+ {
+ LaunchSpec spec;
+ spec.executable = debugger.target_path();
+ spec.stop_at_entry = m_preferences.stop_at_entry;
+ report(debugger.launch(spec), "run");
+ break;
+ }
+ case Continue: report(debugger.resume(), "continue"); break;
+ case Pause: report(debugger.pause(), "pause"); break;
+ case Stop: report(debugger.terminate(), "stop"); break;
+ case StepOver: report(debugger.step_over(), "step over"); break;
+ case StepInto: report(debugger.step_into(), "step into"); break;
+ case StepOut: report(debugger.step_out(), "step out"); break;
+ case ToggleProfiler:
+ m_visible.profiler = !m_visible.profiler;
+ if (m_visible.profiler)
+ m_focus_profiler = true;
+ break;
+ }
+ break;
+ }
+
+ m_palette_open = false;
+ ImGui::CloseCurrentPopup();
+ };
+
+ if (m_palette_focus)
+ {
+ ImGui::SetKeyboardFocusHere();
+ m_palette_focus = false;
+ }
+
+ ImGui::SetNextItemWidth(-FLT_MIN);
+ const bool submitted = ImGui::InputTextWithHint("##palette_query",
+ "jump to a file or symbol, or run a command",
+ &m_palette_query,
+ ImGuiInputTextFlags_EnterReturnsTrue);
+
+ if (submitted && !entries.empty())
+ {
+ activate(entries[static_cast<size_t>(m_palette_selection)]);
+ ImGui::EndPopup();
+ return;
+ }
+
+ ImGui::Separator();
+
+ if (entries.empty())
+ {
+ ImGui::TextDisabled("no matches");
+ }
+ else
+ {
+ const float row = ImGui::GetTextLineHeightWithSpacing();
+ ImGui::BeginChild("##palette_results", ImVec2(0.0f, std::min(row * 10.0f + 4.0f,
+ row * static_cast<float>(entries.size()) + 4.0f)));
+
+ for (int index = 0; index < static_cast<int>(entries.size()); ++index)
+ {
+ const Entry& entry = entries[static_cast<size_t>(index)];
+ const bool selected = index == m_palette_selection;
+
+ ImGui::PushID(index);
+
+ if (ImGui::Selectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns))
+ activate(entry);
+
+ if (selected && (ImGui::IsKeyPressed(ImGuiKey_UpArrow) ||
+ ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsWindowAppearing()))
+ ImGui::SetScrollHereY(0.5f);
+
+ ImGui::SameLine(0.0f, 0.0f);
+ ImGui::TextUnformatted(entry.label.c_str());
+
+ const float hint_width = ImGui::CalcTextSize(entry.hint).x;
+ ImGui::SameLine(ImGui::GetContentRegionAvail().x - hint_width);
+ ImGui::TextDisabled("%s", entry.hint);
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndChild();
+ }
+
+ ImGui::EndPopup();
+ }
+
auto Ui::push_console(std::string line) -> void
{
m_console_lines.push_back(std::move(line));
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 2898fd2..0dcb7e6 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -45,19 +45,22 @@ namespace Hsdbg
bool threads = true;
bool source_tree = true;
bool locals = true;
+ bool watch = true;
bool registers = true;
bool symbols = true;
bool disassembly = true;
bool console = true;
- bool profiler = true;
- bool timeline = true;
- bool macros = true;
+
+ bool profiler = false;
+ bool macros = false;
bool demo = false;
};
auto apply_style() -> void;
static auto build_default_layout(uint32_t dockspace_id) -> void;
+ auto draw_command_palette(Debugger& debugger) -> void;
+
auto draw_menu_bar(Debugger& debugger) -> void;
auto draw_toolbar(Debugger& debugger) -> void;
auto draw_status_bar(const Debugger& debugger) -> void;
@@ -71,12 +74,16 @@ namespace Hsdbg
auto draw_threads_panel(Debugger& debugger) -> void;
auto draw_source_tree_panel(const Debugger& debugger) -> void;
auto draw_locals_panel(const Debugger& debugger) -> void;
+ auto draw_watch_panel(Debugger& debugger) -> void;
auto draw_registers_panel(const Debugger& debugger) -> void;
+
+ auto add_watch(Debugger& debugger, std::string expression) -> void;
auto draw_symbols_panel(Debugger& debugger) -> void;
auto draw_disassembly_panel(Debugger& debugger) -> void;
auto draw_console_panel(Debugger& debugger) -> void;
auto draw_profiler_panel(Debugger& debugger) -> void;
- auto draw_timeline_panel(Debugger& debugger) -> void;
+
+ static auto draw_flamegraph(const Debugger& debugger) -> void;
auto draw_macros_panel(Debugger& debugger) -> void;
auto push_console(std::string line) -> void;
@@ -84,6 +91,8 @@ namespace Hsdbg
auto follow_stop(Debugger& debugger) -> void;
auto follow_target(Debugger& debugger) -> void;
+
+ auto follow_selected_frame(Debugger& debugger) -> void;
auto show_frame(const StackFrame& frame) -> void;
Window* m_window = nullptr;
@@ -115,6 +124,27 @@ namespace Hsdbg
std::string m_source_filter;
std::string m_trace_input;
+ struct Watch
+ {
+ std::string expression;
+ std::string value;
+ bool ok = false;
+ };
+
+ std::vector<Watch> m_watches;
+ std::string m_watch_input;
+
+ uint64_t m_watch_stop = 0;
+ uint64_t m_watch_thread = 0;
+ uint32_t m_watch_frame = 0;
+ bool m_watch_evaluated = false;
+
+ bool m_palette_open = false;
+ bool m_palette_request = false;
+ bool m_palette_focus = false;
+ std::string m_palette_query;
+ int m_palette_selection = 0;
+
std::string m_macro_input;
std::string m_macro_output;
int m_macro_level = 0;
@@ -122,12 +152,17 @@ namespace Hsdbg
uint64_t m_followed_stop = 0;
std::filesystem::path m_followed_target;
+ std::filesystem::path m_revealed_source;
+
+ bool m_profiler_revealed = false;
+
bool m_layout_built = false;
bool m_select_default_tabs = false;
bool m_console_scroll_pending = false;
bool m_load_target_pending = false;
bool m_focus_breakpoints = false;
bool m_focus_macros = false;
+ bool m_focus_profiler = false;
bool m_focus_symbols = false;
bool m_focus_disassembly = false;
bool m_scroll_to_program_counter = false;