aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-04 19:30:33 +0200
committerhachem <im@hachem.wtf>2026-09-04 19:30:33 +0200
commit3f33c536c06535052d029c7270023a3dae351696 (patch)
treeb072f2135bfc3f7d996aff9c21299dfcd8affaf3
parentdcb23f8b4b889061e96d43949d570581469cab84 (diff)
refactor: overhaul the ui
the old ui interface was very disconnected, each tool was its own panel, without much interaction. this commit is an attempt to fix this.
-rw-r--r--src/ui/source_view.cpp124
-rw-r--r--src/ui/source_view.h5
-rw-r--r--src/ui/ui.cpp796
-rw-r--r--src/ui/ui.h66
4 files changed, 830 insertions, 161 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index 0a6a230..3c15c08 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -274,12 +274,12 @@ namespace Hsdbg
macros.find(word) != nullptr)
kind = SyntaxKind::Macro;
- if (kind != SyntaxKind::Default)
- {
- flush_default(start);
- emit(start, at - start, kind);
- run = at;
- }
+ // every word gets its own span, including plain identifiers:
+ // isolating them lets the source view hit-test a name under
+ // the cursor and show its live value
+ flush_default(start);
+ emit(start, at - start, kind);
+ run = at;
continue;
}
@@ -324,11 +324,64 @@ namespace Hsdbg
const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255);
const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90);
+ // muted blue-grey for the live values shown after a line, distinct from the
+ // green of comments so they do not read as part of the code
+ 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;
}
+ // the frame local named exactly `word`, or null; only names with a value to
+ // show qualify, so pure aggregates without a summary are skipped
+ 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;
+ }
+
+ // whether `name` appears in `line` as a whole identifier rather than as a
+ // fragment of a longer word
+ 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;
+ }
+
+ // a single-line, length-capped rendering of a value for the inline annotation
+ 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;
+ }
+
// grab a macro invocation out of a line starting at the name: the name
// alone for an object-like use, or the name plus a balanced argument list
// for a function-like one. a call that runs off the end of the line falls
@@ -430,6 +483,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;
@@ -495,6 +555,13 @@ namespace Hsdbg
m_scroll_to_highlight = false;
}
+ // the selected frame's locals, but only when we are stopped in this very
+ // file (a non-zero highlight means show_frame put us here); used both to
+ // annotate lines and to answer a hover over a name
+ 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);
@@ -596,6 +663,23 @@ 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());
+
+ // hovering a name that is a live local shows its value
+ 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());
+ }
+
+ // right-click any name to add it to the watch list
+ if (is_name && ImGui::IsItemClicked(ImGuiMouseButton_Right))
+ m_watch_request = std::string(word);
}
else if (span.kind == SyntaxKind::Macro)
{
@@ -610,6 +694,34 @@ namespace Hsdbg
}
}
}
+
+ // trailing live values: the locals that appear on this line, shown
+ // only up to the line the pc sits on, since anything past it has
+ // not run yet and would read as a stale or unset value
+ 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 0c10efa..4c2cbe4 100644
--- a/src/ui/source_view.h
+++ b/src/ui/source_view.h
@@ -66,6 +66,10 @@ namespace Hsdbg
// hand to the macros panel; cleared once taken
auto take_macro_request() -> std::optional<std::string>;
+ // right-clicking a name in the source stashes it here for the ui to add to
+ // the watch list; cleared once taken
+ auto take_watch_request() -> std::optional<std::string>;
+
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
@@ -80,6 +84,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 14fd1d8..a82bfb6 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";
@@ -239,6 +239,19 @@ namespace Hsdbg
});
}
+ // whether the file lives anywhere under this node, so a folder can tell if
+ // it is on the path to the open source and should reveal itself
+ 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)
@@ -512,6 +525,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();
// the innermost frames are often runtime internals with no source, and
@@ -628,32 +646,57 @@ namespace Hsdbg
m_focus_macros = true;
}
+ // right-clicking a name in the source adds it to the watch list
+ if (std::optional<std::string> request = m_source_view.take_watch_request())
+ add_watch(debugger, std::move(*request));
+
+ // let the profiling views appear on their own the moment work starts:
+ // the profiler (which now holds the flame chart, timings and graphs) when
+ // something is being sampled, traced or instrumented. the latch means it
+ // only springs up on the rising edge, so closing it makes it stay closed
+ 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);
// a window claims its tab when it is first submitted, so this can only be
- // asked for once every panel in the node exists
+ // asked for once every panel in the node exists. within each tabbed node
+ // the later focus wins, so raise the tab we want in front last; the very
+ // last call also lands the keyboard focus, which belongs on the source
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;
}
@@ -679,6 +722,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
@@ -687,28 +736,43 @@ namespace Hsdbg
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->WorkSize);
+ // four regions frame a central code view, each holding one stage of the
+ // debugging loop so related panels sit together instead of scattered:
+ // left navigator - where to go (files, symbols) over where execution
+ // currently is (call stack, threads)
+ // center code - source and disassembly, both tracking the pc
+ // right inspector - the selected frame's state: locals over registers
+ // bottom output - the console and the auxiliary read-outs
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);
+
+ // navigator: where to go, then where execution is
+ 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);
+ // code
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);
+
+ // inspector: locals and the watch list share the top, the frame's state
+ // over the frame's registers
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);
+
+ // output drawer
ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id);
+ ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_MACROS, bottom_id);
@@ -735,6 +799,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();
@@ -788,12 +855,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();
@@ -882,6 +949,30 @@ namespace Hsdbg
ImGui::TextDisabled("|");
ImGui::SameLine();
+ // the way in to profiling, and the way back out: the panel only exists
+ // while this is on, and lights up to show that it is. the button toggles
+ // that state, so the tint is keyed off a copy taken before the click or
+ // the push and pop would not balance
+ 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());
@@ -1193,8 +1284,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))
{
@@ -1232,9 +1322,25 @@ namespace Hsdbg
}
else if (breakpoint.line != 0)
{
- ImGui::Text("%s:%u",
- breakpoint.file.filename().string().c_str(),
- breakpoint.line);
+ // clicking the location takes the source view there, so
+ // the breakpoints list doubles as a jump list. plain
+ // text with a click test, not a Selectable: a full-width
+ // Selectable in this stretch-sized column feeds its own
+ // width back into the column solver and lands on NaN
+ 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
{
@@ -1364,7 +1470,11 @@ namespace Hsdbg
{
debugger.select_thread(thread.id);
+ // land the source, disassembly and inspector on this
+ // thread's own frame instead of leaving them on the last
m_scroll_to_program_counter = true;
+ m_scroll_to_symbol = true;
+ follow_selected_frame(debugger);
}
ImGui::SameLine();
@@ -1397,6 +1507,11 @@ namespace Hsdbg
const SourceNode tree = build_source_tree(files);
const std::filesystem::path& open = m_source_view.path();
+ // folders stay collapsed until they are needed. when the open file
+ // changes, the one frame where it differs from what we last showed
+ // expands the chain down to it; after that the user is in control
+ 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
@@ -1428,12 +1543,17 @@ 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;
+ // a filter opens every folder so its matches are visible; with
+ // no filter, only the chain down to the open file unfurls, and
+ // only on the frame it changed so manual collapses survive
+ 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);
@@ -1479,6 +1599,8 @@ namespace Hsdbg
ImGui::TextDisabled("no source files");
else
draw_node(tree);
+
+ m_revealed_source = open;
}
}
@@ -1520,6 +1642,148 @@ 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))
+ {
+ // re-run the expressions whenever the target stops again or the frame
+ // the ui is looking at moves, but never every frame: each call jits and
+ // runs code in the target
+ 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)
@@ -1742,61 +2006,59 @@ 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())
+ // compact header: how calls are being gathered. instrumentation, when
+ // the target carries it, is exact and needs nothing; otherwise sampling
+ // approximates on any binary. either way calls flow into the flame chart
+ 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");
+ // the hero view: the call flame chart, given the lion's share of the
+ // panel, with the detail sections collapsed beneath it
+ 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())
+ // exact per-function timings, and the box to add another
+ 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);
@@ -1855,8 +2117,32 @@ namespace Hsdbg
}
}
- if (ImGui::CollapsingHeader("debugger self-timing"))
+ // the live footprint of the process under debug
+ 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));
+ }
+
+ // hsdbg's own frame cost, tucked away: useful when the ui itself feels
+ // slow, not part of profiling the target
+ 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();
@@ -1880,119 +2166,102 @@ 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
- {
- // resolve a trace id to its function name for labels and tooltips
- const auto name_of = [&](uint32_t trace_id) -> const char*
- {
- return debugger.span_label(trace_id);
- };
+ return;
+ }
- // the time range to fit and how tall the call stack gets
- double t_min = spans.front().start;
- double t_max = t_min;
- uint32_t max_depth = 0;
+ // resolve a trace id to its function name for labels and tooltips
+ 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);
- }
+ // the time range to fit and how tall the call stack gets
+ 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();
- // claim the region so hovering resolves against it
- 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);
+ // claim the region so hovering resolves against it
+ ImGui::InvisibleButton("##flame_canvas", ImVec2(canvas_w, canvas_h));
+ const bool canvas_hovered = ImGui::IsItemHovered();
- const float scale = canvas_w / static_cast<float>(range); // pixels per second
- const float baseline = origin.y + canvas_h; // row 0 rests on the bottom
+ 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); // pixels per second
+ const float baseline = origin.y + canvas_h; // row 0 rests on the bottom
- 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& /*debugger*/) -> void
@@ -2110,6 +2379,231 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::draw_command_palette(Debugger& debugger) -> void
+ {
+ // cmd/ctrl+k anywhere, or the menu item, opens it; opening resets the query
+ // so it always starts clean and asks for the text field's focus next frame
+ 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))
+ {
+ // esc or a click outside closed it
+ m_palette_open = false;
+ return;
+ }
+
+ // every command the palette can run; only the ones that make sense for the
+ // current state are offered, so the list never dangles a dead action
+ 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");
+
+ // files and symbols only clutter the list once there is a query to match
+ 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);
+
+ // arrow keys walk the results; the input field ignores up/down itself, so
+ // reading them here does not fight the cursor
+ 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);
+
+ // keep the keyboard-picked row in view as it moves
+ 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 3365e51..2bc758d 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -46,19 +46,28 @@ 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;
+
+ // these two are not standing panels: they earn their place only once
+ // there is something to show. the profiler opens when you go to profile
+ // (its flame chart, timings and graphs all live inside it), the macros
+ // view when you unroll a macro from source
+ bool profiler = false;
+ bool macros = false;
bool demo = false;
};
auto apply_style() -> void;
static auto build_default_layout(uint32_t dockspace_id) -> void;
+ // a fuzzy omnibar (cmd/ctrl+k) over files, symbols and execution commands,
+ // so navigating and driving the target never means hunting for a panel
+ 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;
@@ -72,12 +81,20 @@ 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;
+
+ // adds an expression to the watch list and evaluates it right away if the
+ // target is stopped, so it never shows a blank until the next stop
+ 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;
+
+ // the call flame chart, the hero of the profiler panel: draws into whatever
+ // region the caller has opened, no window of its own
+ static auto draw_flamegraph(const Debugger& debugger) -> void;
auto draw_macros_panel(Debugger& debugger) -> void;
auto push_console(std::string line) -> void;
@@ -85,6 +102,11 @@ namespace Hsdbg
auto follow_stop(Debugger& debugger) -> void;
auto follow_target(Debugger& debugger) -> void;
+
+ // selects the innermost frame that has source and shows it, so a new
+ // thread or stop lands the whole ui on real code rather than a runtime
+ // internal frame with nothing to display
+ auto follow_selected_frame(Debugger& debugger) -> void;
auto show_frame(const StackFrame& frame) -> void;
Window* m_window = nullptr;
@@ -119,6 +141,33 @@ namespace Hsdbg
std::string m_source_filter;
std::string m_trace_input;
+ // one user-entered watch expression and its last evaluation
+ struct Watch
+ {
+ std::string expression;
+ std::string value;
+ bool ok = false;
+ };
+
+ std::vector<Watch> m_watches;
+ std::string m_watch_input;
+
+ // the (stop, thread, frame) the watches were last evaluated against, so
+ // they re-run when execution stops again or the selection moves, but not
+ // every frame (each evaluation jits code into the target)
+ uint64_t m_watch_stop = 0;
+ uint64_t m_watch_thread = 0;
+ uint32_t m_watch_frame = 0;
+ bool m_watch_evaluated = false;
+
+ // command palette: whether it is up, a one-shot request to grab the text
+ // field's focus, the query, and which result the keyboard has landed on
+ 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;
+
// macros panel: the invocation being unrolled, the current unroll depth,
// and the read-only buffer that shows the tokens at that depth
std::string m_macro_input;
@@ -128,12 +177,21 @@ namespace Hsdbg
uint64_t m_followed_stop = 0;
std::filesystem::path m_followed_target;
+ // the source file whose folder chain the tree last expanded; when the
+ // open file moves away from it the tree reveals the new path once
+ std::filesystem::path m_revealed_source;
+
+ // rising-edge latch so the profiler surfaces itself the first time there is
+ // profiling activity, without fighting the user if they then close it
+ 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;