#include "ui/ui.h" #include "core/log.h" #include "core/window.h" #include "debugger/debugger.h" #include "ui/icons.h" #include "ui/widgets.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef HSDBG_ASSET_DIR #define HSDBG_ASSET_DIR "" #endif namespace Hsdbg { namespace { // Title Case label with a leading icon; the "###id" keeps the persistent // dock id stable (hashes to the old lowercase name) so saved layouts match constexpr const char* PANEL_SOURCE = ICON_PH_FILE_CODE " Source###source"; constexpr const char* PANEL_BREAKPOINTS = ICON_PH_CIRCLE " Breakpoints###breakpoints"; constexpr const char* PANEL_CALL_STACK = ICON_PH_STACK " Call Stack###call stack"; constexpr const char* PANEL_THREADS = ICON_PH_BRANCH " Threads###threads"; constexpr const char* PANEL_SOURCE_TREE = ICON_PH_FOLDER " Source Tree###source tree"; constexpr const char* PANEL_LOCALS = ICON_PH_CUBE " Locals###locals"; constexpr const char* PANEL_WATCH = ICON_PH_EYE " Watch###watch"; constexpr const char* PANEL_REGISTERS = ICON_PH_CPU " Registers###registers"; constexpr const char* PANEL_SYMBOLS = ICON_PH_FUNCTION " Symbols###symbols"; constexpr const char* PANEL_DISASSEMBLY = ICON_PH_LIST " Disassembly###disassembly"; constexpr const char* PANEL_CONSOLE = ICON_PH_TERMINAL " Console###console"; constexpr const char* PANEL_PROFILER = ICON_PH_GAUGE " Profiler###profiler"; constexpr const char* PANEL_MACROS = ICON_PH_BRACKETS " Macros###macros"; constexpr const char* LOAD_TARGET_POPUP = "load target"; // the lowest glsl that every 3.2+ core profile accepts, and the window // asks for a 3.3 core context on all three platforms constexpr const char* GLSL_VERSION = "#version 150"; constexpr size_t MAX_CONSOLE_LINES = 2048; // breathing room between the window edge and everything docked inside it const ImVec2 ROOT_PADDING(8.0f, 6.0f); const ImVec2 TOOLBAR_PADDING(4.0f, 4.0f); // the same green the source view puts behind the current line const ImU32 CURRENT_INSTRUCTION_COLOR = IM_COL32(58, 72, 46, 255); constexpr float BREAKPOINT_RADIUS = 5.0f; constexpr float DISASSEMBLY_GUTTER_WIDTH = 22.0f; const ImU32 BREAKPOINT_COLOR = IM_COL32(226, 84, 84, 255); const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255); const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90); // folders take a cool tint; the file open in the source view keeps its // accent even when unselected, so it stays easy to find in a long tree const ImU32 SOURCE_FOLDER_COLOR = IM_COL32(150, 178, 214, 255); const ImU32 SOURCE_OPEN_FILE_COLOR = IM_COL32(126, 194, 126, 255); // the dim connectors drawn between a folder and its children const ImU32 SOURCE_TREE_LINE_COLOR = IM_COL32(110, 110, 122, 160); auto breakpoint_at(const Debugger& debugger, const Instruction& instruction) -> const Breakpoint* { const std::span breakpoints = debugger.breakpoints(); const auto found = std::ranges::find_if(breakpoints, [&](const Breakpoint& candidate) { if (instruction.file_address != 0 && candidate.file_address == instruction.file_address) { return true; } return candidate.address != 0 && candidate.address == instruction.address; }); return found != breakpoints.end() ? &*found : nullptr; } auto toggle_instruction_breakpoint(Debugger& debugger, const Instruction& instruction) -> void { if (const Breakpoint* existing = breakpoint_at(debugger, instruction); existing != nullptr) { debugger.remove_breakpoint(existing->id); return; } if (instruction.file_address != 0) debugger.add_address_breakpoint(instruction.file_address); } auto matches_filter(std::string_view name, std::string_view filter) -> bool { if (filter.empty()) return true; if (filter.size() > name.size()) return false; const auto equal = [](char left, char right) { return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); }; return std::ranges::search(name, filter, equal).begin() != name.end(); } // the sixteen standard ansi colours, tuned to read on a dark console (the // vs code terminal set); indices 0-7 are the normal colours, 8-15 bright constexpr ImU32 ANSI_COLORS[16] = { IM_COL32( 90, 90, 90, 255), IM_COL32(205, 49, 49, 255), IM_COL32( 13, 188, 121, 255), IM_COL32(229, 229, 16, 255), IM_COL32( 59, 142, 234, 255), IM_COL32(188, 63, 188, 255), IM_COL32( 17, 168, 205, 255), IM_COL32(204, 204, 204, 255), IM_COL32(127, 127, 127, 255), IM_COL32(241, 76, 76, 255), IM_COL32( 35, 209, 139, 255), IM_COL32(245, 245, 67, 255), IM_COL32( 96, 165, 250, 255), IM_COL32(214, 112, 214, 255), IM_COL32( 41, 184, 219, 255), IM_COL32(255, 255, 255, 255), }; // the colour an xterm 256-colour index maps to: the 16 base colours, then // the 6x6x6 rgb cube, then the 24-step grey ramp auto xterm_256_color(int index) -> ImU32 { if (index < 16) return ANSI_COLORS[index]; if (index < 232) { const int value = index - 16; const auto step = [](int component) { return component == 0 ? 0 : 55 + component * 40; }; return IM_COL32(step(value / 36), step((value / 6) % 6), step(value % 6), 255); } const int grey = 8 + (index - 232) * 10; return IM_COL32(grey, grey, grey, 255); } // the foreground state an SGR (colour) escape run leaves behind struct AnsiStyle { int color = -1; // 0-15 palette index, or -1 for the theme default bool bold = false; bool truecolor = false; ImU32 rgb = 0; }; // applies the numeric parameters of one "\x1b[ ... m" sequence to the style auto apply_sgr(const std::string& text, size_t begin, size_t end, AnsiStyle& style) -> void { std::vector params; int value = 0; for (size_t at = begin; at < end; ++at) { if (text[at] >= '0' && text[at] <= '9') value = value * 10 + (text[at] - '0'); else if (text[at] == ';') { params.push_back(value); value = 0; } } params.push_back(value); // a bare "\x1b[m" collapses to {0}, i.e. reset for (size_t index = 0; index < params.size(); ++index) { const int code = params[index]; if (code == 0) style = AnsiStyle{}; else if (code == 1) style.bold = true; else if (code == 22) style.bold = false; else if (code == 39) style = AnsiStyle{ -1, style.bold, false, 0 }; else if (code >= 30 && code <= 37) style = AnsiStyle{ code - 30, style.bold, false, 0 }; else if (code >= 90 && code <= 97) style = AnsiStyle{ code - 90 + 8, style.bold, false, 0 }; else if ((code == 38 || code == 48) && index + 1 < params.size()) { // extended colour; 48 (background) is parsed but not applied const int mode = params[index + 1]; if (mode == 5 && index + 2 < params.size()) { if (code == 38) { style.truecolor = true; style.rgb = xterm_256_color(params[index + 2]); } index += 2; } else if (mode == 2 && index + 4 < params.size()) { if (code == 38) { style.truecolor = true; style.rgb = IM_COL32(params[index + 2], params[index + 3], params[index + 4], 255); } index += 4; } } } } auto ansi_color(const AnsiStyle& style) -> std::optional { if (style.truecolor) return style.rgb; if (style.color < 0) return std::nullopt; // leave the theme's default text colour in place const int index = (style.bold && style.color < 8) ? style.color + 8 : style.color; return ANSI_COLORS[index]; } // renders one console line, honouring ansi SGR colour escapes and dropping // any other control sequence so it never shows as garbage auto draw_ansi_line(const std::string& line) -> void { AnsiStyle style; bool first = true; const size_t size = line.size(); size_t at = 0; while (at < size) { if (line[at] == '\x1b' && at + 1 < size && line[at + 1] == '[') { size_t final = at + 2; while (final < size && !(line[final] >= '@' && line[final] <= '~')) ++final; if (final < size && line[final] == 'm') apply_sgr(line, at + 2, final, style); at = final < size ? final + 1 : size; continue; } size_t run_end = line.find('\x1b', at); if (run_end == std::string::npos) run_end = size; if (run_end > at) { if (!first) ImGui::SameLine(0.0f, 0.0f); first = false; const std::optional color = ansi_color(style); if (color) ImGui::PushStyleColor(ImGuiCol_Text, *color); ImGui::TextUnformatted(line.c_str() + at, line.c_str() + run_end); if (color) ImGui::PopStyleColor(); } at = run_end; } // a line that was empty or only escapes still takes a row if (first) ImGui::TextUnformatted(""); } struct SourceNode { std::string name; std::filesystem::path path; std::vector children; }; auto find_or_add_child(std::vector& nodes, std::string_view name) -> SourceNode& { const auto existing = std::ranges::find(nodes, name, &SourceNode::name); if (existing != nodes.end()) return *existing; nodes.push_back({ std::string(name), {}, {} }); return nodes.back(); } auto common_directory(std::span files) -> std::filesystem::path { if (files.empty()) return {}; std::filesystem::path prefix = files.front().parent_path(); for (const std::filesystem::path& file : files) { while (true) { if (file.parent_path() == prefix) break; std::error_code error; const std::filesystem::path relative = std::filesystem::relative(file.parent_path(), prefix, error); const std::string text = relative.generic_string(); if (!error && (text.empty() || text == "." || !text.starts_with(".."))) break; const std::filesystem::path parent = prefix.parent_path(); if (parent == prefix) break; prefix = parent; } } return prefix; } auto build_source_tree(std::span files) -> SourceNode { SourceNode root; const std::filesystem::path prefix = common_directory(files); root.name = prefix.empty() ? "/" : prefix.filename().string(); if (root.name.empty()) root.name = prefix.string(); for (const std::filesystem::path& file : files) { std::error_code error; std::filesystem::path relative = std::filesystem::relative(file, prefix, error); if (error || relative.empty()) relative = file.filename(); SourceNode* current = &root; const std::vector parts(relative.begin(), relative.end()); for (size_t index = 0; index < parts.size(); ++index) { SourceNode& child = find_or_add_child(current->children, parts[index].string()); if (index + 1 == parts.size()) child.path = file; else current = &child; } } const auto sort_nodes = [](this const auto& self, std::vector& nodes) -> void { std::ranges::sort(nodes, [](const SourceNode& left, const SourceNode& right) { const bool left_directory = left.path.empty(); const bool right_directory = right.path.empty(); if (left_directory != right_directory) return left_directory; return left.name < right.name; }); for (SourceNode& node : nodes) self(node.children); }; sort_nodes(root.children); return root; } auto source_node_matches(const SourceNode& node, std::string_view filter) -> bool { if (filter.empty()) return true; if (!node.path.empty()) return matches_filter(node.name, filter); return std::ranges::any_of(node.children, [&](const SourceNode& child) { return source_node_matches(child, filter); }); } // 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 files) -> std::filesystem::path { const auto named_main = std::ranges::find_if(files, [](const std::filesystem::path& file) { return file.stem() == "main" && std::filesystem::exists(file); }); if (named_main != files.end()) return *named_main; const auto existing = std::ranges::find_if(files, [](const std::filesystem::path& file) { return std::filesystem::exists(file); }); return existing != files.end() ? *existing : std::filesystem::path{}; } auto draw_instruction_table(Debugger& debugger, std::span instructions, bool scroll_to_current) -> void { constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_NoPadOuterX; if (!ImGui::BeginTable("##instructions", 5, flags, ImVec2(0.0f, 0.0f))) return; ImGui::TableSetupColumn("##gutter", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize, DISASSEMBLY_GUTTER_WIDTH); ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableSetupColumn("Mnemonic", ImGuiTableColumnFlags_WidthFixed, 70.0f); ImGui::TableSetupColumn("Operands"); ImGui::TableSetupColumn("Comment"); ImGui::TableHeadersRow(); const float row_height = ImGui::GetTextLineHeightWithSpacing(); if (scroll_to_current) { int scroll_index = 0; for (int index = 0; index < static_cast(instructions.size()); ++index) { if (instructions[static_cast(index)].current) { scroll_index = index; break; } } ImGui::SetScrollY(static_cast(scroll_index) * row_height - ImGui::GetWindowHeight() * 0.35f); } ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImGuiListClipper clipper; clipper.Begin(static_cast(instructions.size()), row_height); while (clipper.Step()) { for (int index = clipper.DisplayStart; index < clipper.DisplayEnd; ++index) { const Instruction& instruction = instructions[static_cast(index)]; const Breakpoint* breakpoint = breakpoint_at(debugger, instruction); ImGui::TableNextRow(); ImGui::PushID(index); if (instruction.current) { ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, CURRENT_INSTRUCTION_COLOR); } ImGui::TableNextColumn(); const ImVec2 gutter_min = ImGui::GetCursorScreenPos(); if (ImGui::InvisibleButton("##gutter", ImVec2(DISASSEMBLY_GUTTER_WIDTH, row_height))) toggle_instruction_breakpoint(debugger, instruction); const bool gutter_hovered = ImGui::IsItemHovered(); const ImVec2 marker_center(gutter_min.x + DISASSEMBLY_GUTTER_WIDTH * 0.5f, gutter_min.y + row_height * 0.5f); if (breakpoint != nullptr) { const ImU32 color = breakpoint->enabled ? BREAKPOINT_COLOR : BREAKPOINT_DISABLED_COLOR; if (breakpoint->resolved) draw_list->AddCircleFilled(marker_center, BREAKPOINT_RADIUS, color); else draw_list->AddCircle(marker_center, BREAKPOINT_RADIUS, color, 0, 1.5f); } else if (gutter_hovered) { draw_list->AddCircleFilled(marker_center, BREAKPOINT_RADIUS, BREAKPOINT_HOVER_COLOR); } ImGui::TableNextColumn(); ImGui::Text("0x%012llx", static_cast(instruction.address)); if (scroll_to_current && instruction.current) ImGui::SetScrollHereY(0.35f); ImGui::TableNextColumn(); ImGui::TextUnformatted(instruction.mnemonic.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(instruction.operands.c_str()); ImGui::TableNextColumn(); if (!instruction.comment.empty()) ImGui::TextDisabled("; %s", instruction.comment.c_str()); ImGui::PopID(); } } ImGui::EndTable(); } auto state_color(TargetState state) -> ImVec4 { switch (state) { case TargetState::Running: return ImVec4(0.45f, 0.78f, 0.45f, 1.0f); case TargetState::Stopped: return ImVec4(0.95f, 0.76f, 0.35f, 1.0f); case TargetState::Crashed: return ImVec4(0.89f, 0.33f, 0.33f, 1.0f); case TargetState::Exited: return ImVec4(0.60f, 0.60f, 0.65f, 1.0f); default: return ImVec4(0.55f, 0.55f, 0.60f, 1.0f); } } auto draw_variable(const Variable& variable) -> void { ImGui::TableNextRow(); ImGui::TableNextColumn(); if (variable.children.empty()) { ImGui::TreeNodeEx(variable.name.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth); ImGui::TableNextColumn(); ImGui::TextUnformatted(variable.type.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(variable.value.c_str()); return; } const bool open = ImGui::TreeNodeEx(variable.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth); ImGui::TableNextColumn(); ImGui::TextUnformatted(variable.type.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(variable.value.c_str()); if (!open) return; for (const Variable& child : variable.children) draw_variable(child); ImGui::TreePop(); } } Ui::Ui(Window& window) : m_window(&window) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigWindowsMoveFromTitleBarOnly = true; // the bundled ui / mono / icon faces; these re-rasterize at any size, so // scaling the ui keeps the text crisp load_fonts(); m_source_view.set_mono_font(m_font_mono); // a saved layout beats the built in one, so only build when there is none m_layout_built = io.IniFilename != nullptr && std::filesystem::exists(io.IniFilename); // sit the settings file next to imgui's own layout file m_preferences_path = io.IniFilename != nullptr ? std::filesystem::path(io.IniFilename).parent_path() / "hsdbg.ini" : std::filesystem::path("hsdbg.ini"); // on a first run nothing is saved, so let the theme seed accent / rounding / // syntax; after that the saved values win, keeping the user's tweaks const bool had_preferences = std::filesystem::exists(m_preferences_path); m_preferences = load_preferences(m_preferences_path); load_selected_theme(!had_preferences); apply_style(); ImGui_ImplGlfw_InitForOpenGL(m_window->handle(), true); ImGui_ImplOpenGL3_Init(GLSL_VERSION); Log::info("imgui {}", IMGUI_VERSION); } Ui::~Ui() { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } auto Ui::begin_frame() -> void { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); } auto Ui::end_frame() -> void { ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); if (m_mascot_pending && m_mascot_texture != 0) { if (!m_image_renderer_ready) m_image_renderer_ready = m_image_renderer.init(); if (m_image_renderer_ready) { const ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale; const int width = static_cast(m_window->framebuffer_width()); const int height = static_cast(m_window->framebuffer_height()); m_image_renderer.draw(m_mascot_texture, m_mascot_x0 * scale.x, m_mascot_y0 * scale.y, m_mascot_x1 * scale.x, m_mascot_y1 * scale.y, width, height); } } m_mascot_pending = false; } auto Ui::open_source(const std::filesystem::path& path) -> void { if (const auto result = m_source_view.open(path); !result) { Log::error("{}", result.error()); push_console(std::format("error: {}", result.error())); } } auto Ui::follow_stop(Debugger& debugger) -> void { if (!debugger.is_stopped()) { if (debugger.is_running()) m_source_view.set_highlighted_line(0); return; } if (debugger.stop_count() == m_followed_stop) return; m_followed_stop = debugger.stop_count(); // wanted even when no frame has source to show 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 stack = debugger.call_stack(); // the innermost frames are often runtime internals with no source, and // pointing at them tells the user nothing const auto frame = std::ranges::find_if(stack, [](const StackFrame& candidate) { return candidate.line != 0 && !candidate.file.empty(); }); if (frame == stack.end()) return; debugger.select_frame(frame->index); show_frame(*frame); } auto Ui::follow_target(Debugger& debugger) -> void { if (!debugger.has_target()) { m_followed_target.clear(); return; } if (debugger.target_path() == m_followed_target) return; m_followed_target = debugger.target_path(); m_scroll_to_symbol = true; m_scroll_to_program_counter = true; if (m_source_view.path().empty()) { if (const std::filesystem::path source = preferred_source(debugger.source_files()); !source.empty()) { open_source(source); } } } auto Ui::show_frame(const StackFrame& frame) -> void { if (frame.file.empty() || frame.line == 0) return; if (m_source_view.path() != frame.file) open_source(frame.file); m_source_view.set_highlighted_line(frame.line); } auto Ui::draw(Debugger& debugger) -> void { apply_preferences(); m_profiler.sample_frame(ImGui::GetIO().DeltaTime); m_profiler.sample_target(debugger.resident_memory(), debugger.has_target()); const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); ImGui::SetNextWindowViewport(viewport->ID); constexpr ImGuiWindowFlags root_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ROOT_PADDING); // the ground between panels is the theme's deepest level, so the panels // read as separated tiles once the docking separators widen into gaps ImGui::PushStyleColor(ImGuiCol_WindowBg, m_theme.bg_low); ImGui::Begin("##hsdbg_root", nullptr, root_flags); ImGui::PopStyleColor(); ImGui::PopStyleVar(3); draw_menu_bar(debugger); draw_toolbar(debugger); follow_stop(debugger); follow_target(debugger); const ImGuiID dockspace_id = ImGui::GetID("##hsdbg_dockspace"); const float status_bar_height = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // NoWindowMenuButton drops the little ▼ docking/collapse menu that imgui // otherwise stamps into the corner of every panel's tab bar ImGui::DockSpace(dockspace_id, ImVec2(0.0f, ImGui::GetContentRegionAvail().y - status_bar_height), ImGuiDockNodeFlags_PassthruCentralNode | static_cast(ImGuiDockNodeFlags_NoWindowMenuButton)); if (!m_layout_built) { build_default_layout(dockspace_id); m_layout_built = true; m_select_default_tabs = true; } draw_status_bar(debugger); draw_load_target_popup(debugger); ImGui::End(); draw_source_panel(debugger); // a click on a highlighted macro in the source view loads it into the // macros panel, brings the panel up and jumps its focus there if (std::optional request = m_source_view.take_macro_request()) { m_macro_input = std::move(*request); m_macro_level = 0; m_visible.macros = true; m_focus_macros = true; } // right-clicking a name in the source adds it to the watch list if (std::optional request = m_source_view.take_watch_request()) add_watch(debugger, std::move(*request)); // reveal the profiler on the rising edge of any profiling activity, but let // the user close it again (the latch only fires once) 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_macros_panel(debugger); draw_preferences_window(); draw_command_palette(debugger); // a window claims its tab only once submitted, so raise tabs after every // panel exists; the later focus wins, and the last call takes 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); m_select_default_tabs = false; } if (m_focus_symbols) { ImGui::SetWindowFocus(PANEL_SYMBOLS); m_focus_symbols = false; } else if (m_focus_disassembly) { ImGui::SetWindowFocus(PANEL_DISASSEMBLY); m_focus_disassembly = false; } if (m_focus_breakpoints) { ImGui::SetWindowFocus(PANEL_BREAKPOINTS); m_focus_breakpoints = false; } if (m_focus_macros) { 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 { ImGui::DockBuilderRemoveNode(dockspace_id); ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->WorkSize); // four regions frame the central code view, each a stage of the loop: left // navigator (files/symbols, call stack/threads), center code (source + // disassembly), right inspector (locals/registers), bottom output ImGuiID center_id = dockspace_id; const ImGuiID left_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.20f, nullptr, ¢er_id); const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24f, nullptr, ¢er_id); const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26f, nullptr, ¢er_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.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); // 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_WATCH, right_top_id); ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_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); ImGui::DockBuilderFinish(dockspace_id); } auto Ui::draw_menu_bar(Debugger& debugger) -> void { if (!ImGui::BeginMenuBar()) return; // brand wordmark in the bold face ImGui::AlignTextToFramePadding(); ImGui::PushFont(m_font_strong, 0.0f); ImGui::TextUnformatted("hsdbg"); ImGui::PopFont(); ImGui::SameLine(0.0f, 18.0f); if (ImGui::BeginMenu("File")) { // a menu is its own popup, so the request has to be handed back to the // root window or BeginPopupModal never sees a matching id if (ImGui::MenuItem("Load Target…")) { m_target_input = debugger.target_path().string(); m_load_target_pending = true; } if (ImGui::MenuItem("Unload Target", nullptr, false, debugger.has_target())) debugger.unload_target(); ImGui::Separator(); if (ImGui::MenuItem("Command Palette…", "Cmd+K")) m_palette_request = true; ImGui::MenuItem("Preferences…", nullptr, &m_show_preferences); ImGui::Separator(); if (ImGui::MenuItem("Quit", "Cmd+Q")) m_window->set_should_close(true); ImGui::EndMenu(); } if (ImGui::BeginMenu("Run")) { const bool has_target = debugger.has_target(); if (ImGui::MenuItem("Continue", "F5", false, has_target)) report(debugger.resume(), "continue"); if (ImGui::MenuItem("Pause", nullptr, false, debugger.is_running())) report(debugger.pause(), "pause"); if (ImGui::MenuItem("Stop", nullptr, false, has_target)) report(debugger.terminate(), "stop"); ImGui::Separator(); const StepMode step_mode = m_preferences.step_by_instruction ? StepMode::Instruction : StepMode::Line; if (ImGui::MenuItem("Step Over", "F10", false, has_target)) report(debugger.step_over(step_mode), "Step Over"); if (ImGui::MenuItem("Step Into", "F11", false, has_target)) report(debugger.step_into(step_mode), "Step Into"); if (ImGui::MenuItem("Step Out", "Shift+F11", false, has_target)) report(debugger.step_out(), "Step Out"); ImGui::Separator(); if (ImGui::MenuItem("Clear Breakpoints", nullptr, false, !debugger.breakpoints().empty())) debugger.clear_breakpoints(); ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { ImGui::MenuItem(PANEL_SOURCE, nullptr, &m_visible.source); ImGui::MenuItem(PANEL_SOURCE_TREE, nullptr, &m_visible.source_tree); ImGui::MenuItem(PANEL_BREAKPOINTS, nullptr, &m_visible.breakpoints); 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_MACROS, nullptr, &m_visible.macros); ImGui::Separator(); if (ImGui::MenuItem("Reset Layout")) m_layout_built = false; ImGui::EndMenu(); } // a right-aligned command-palette affordance, echoing the ⌘K shortcut const char* palette_hint = ICON_PH_SEARCH " Run a command Cmd+K"; const float hint_w = ImGui::CalcTextSize(palette_hint).x; ImGui::SameLine(ImGui::GetWindowWidth() - hint_w - 14.0f); ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); ImGui::TextUnformatted(palette_hint); ImGui::PopStyleColor(); if (ImGui::IsItemClicked()) m_palette_request = true; if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::EndMenuBar(); } auto Ui::draw_toolbar(Debugger& debugger) -> void { const bool has_target = debugger.has_target(); const bool running = debugger.is_running(); if (!m_mascot_loaded) { m_mascot_loaded = true; m_mascot.load(std::filesystem::path(HSDBG_ASSET_DIR) / "peepocry.gif"); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 9.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, TOOLBAR_PADDING); // the square size for the rounded icon buttons that make up the transport const float control = ImGui::GetFrameHeight(); const bool has_mascot = m_preferences.show_mascot && m_mascot.valid() && m_mascot.height() > 0.0f; const float mascot_height = has_mascot ? control * m_preferences.mascot_scale : 0.0f; const float row_height = std::max(control, mascot_height); ImGui::BeginChild("##toolbar", ImVec2(0.0f, row_height + TOOLBAR_PADDING.y * 2.0f)); const float row_top = ImGui::GetCursorPosY(); ImGui::SetCursorPosY(row_top + (row_height - control) * 0.5f); const StepMode step_mode = m_preferences.step_by_instruction ? StepMode::Instruction : StepMode::Line; // a spacer between logical groups of transport controls const auto gap = [] { ImGui::SameLine(0.0f, 14.0f); }; if (Widgets::icon_button("run", ICON_PH_PLAY, has_target && !running, "Run", !has_target || running, control)) { LaunchSpec spec; spec.executable = debugger.target_path(); spec.stop_at_entry = m_preferences.stop_at_entry; report(debugger.launch(spec), "run"); } ImGui::SameLine(); if (Widgets::icon_button("pause", ICON_PH_PAUSE, false, "Pause", !running, control)) report(debugger.pause(), "pause"); ImGui::SameLine(); if (Widgets::icon_button("stop", ICON_PH_STOP, false, "Stop", !has_target, control)) report(debugger.terminate(), "stop"); gap(); if (Widgets::icon_button("step_over", ICON_PH_STEP_OVER, false, "Step Over", !has_target, control)) report(debugger.step_over(step_mode), "Step Over"); ImGui::SameLine(); if (Widgets::icon_button("step_into", ICON_PH_STEP_INTO, false, "Step Into", !has_target, control)) report(debugger.step_into(step_mode), "Step Into"); ImGui::SameLine(); if (Widgets::icon_button("step_out", ICON_PH_STEP_OUT, false, "Step Out", !has_target, control)) report(debugger.step_out(), "Step Out"); // the way in to profiling and back out: the panel only exists while this // is on, and the accent fill shows that it is gap(); if (Widgets::icon_button("profiler", ICON_PH_GAUGE, m_visible.profiler, "Profiler", false, control)) { m_visible.profiler = !m_visible.profiler; if (m_visible.profiler) m_focus_profiler = true; } gap(); Widgets::chip(std::string(to_string(debugger.state())).c_str(), ImGui::ColorConvertFloat4ToU32(state_color(debugger.state()))); if (has_mascot) { const ImVec2 size(mascot_height * (m_mascot.width() / m_mascot.height()), mascot_height); ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - size.x); ImGui::SetCursorPosY(row_top); // reserve the spot but let imgui draw nothing here; the mascot is // rendered by our own shader after imgui, straight to the framebuffer const ImVec2 origin = ImGui::GetCursorScreenPos(); ImGui::Dummy(size); if (ImGui::IsItemHovered()) ImGui::SetTooltip("It's going to be okay"); const ImVec2 viewport = ImGui::GetMainViewport()->Pos; m_mascot_pending = true; m_mascot_texture = m_mascot.texture(); m_mascot_x0 = origin.x - viewport.x; m_mascot_y0 = origin.y - viewport.y; m_mascot_x1 = m_mascot_x0 + size.x; m_mascot_y1 = m_mascot_y0 + size.y; } ImGui::EndChild(); ImGui::PopStyleVar(2); } auto Ui::draw_status_bar(const Debugger& debugger) -> void { ImGui::Separator(); ImGui::AlignTextToFramePadding(); ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (debugger.has_target()) ImGui::Text("%s", debugger.target_path().filename().string().c_str()); else ImGui::TextDisabled("No target"); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("%zu breakpoints", debugger.breakpoints().size()); if (m_preferences.show_fps) { const std::string frame_stats = std::format("{:.1f} fps", ImGui::GetIO().Framerate); const float text_width = ImGui::CalcTextSize(frame_stats.c_str()).x; ImGui::SameLine(ImGui::GetContentRegionMax().x - text_width - ImGui::GetStyle().ItemSpacing.x); ImGui::TextDisabled("%s", frame_stats.c_str()); } } auto Ui::draw_load_target_popup(Debugger& debugger) -> void { if (m_load_target_pending) { ImGui::OpenPopup(LOAD_TARGET_POPUP); m_load_target_pending = false; } const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImVec2 center(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f, viewport->WorkPos.y + viewport->WorkSize.y * 0.5f); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (!ImGui::BeginPopupModal(LOAD_TARGET_POPUP, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; ImGui::TextDisabled("Path to an executable to debug"); ImGui::SetNextItemWidth(420.0f); const bool submitted = ImGui::InputText("##target_path", &m_target_input, ImGuiInputTextFlags_EnterReturnsTrue); if (ImGui::Button("Load") || submitted) { if (const auto result = debugger.load_target(m_target_input); result) { push_console(std::format("loaded target {}", debugger.target_path().string())); ImGui::CloseCurrentPopup(); } else { push_console(std::format("error: {}", result.error())); } } ImGui::SameLine(); if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } auto Ui::apply_preferences() -> void { // FontScaleMain re-rasterizes the vector font crisply, unlike the legacy // FontGlobalScale which just stretched the baked atlas ImGui::GetStyle().FontScaleMain = m_preferences.ui_scale; // one-shot: report the framebuffer scale (on macos this, not content scale, // carries retina density) so font crispness can be reasoned about static bool logged_scale = false; if (!logged_scale) { logged_scale = true; const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale; Log::info("display: framebuffer scale {}x{}", fb.x, fb.y); } m_source_view.set_highlighting(m_preferences.syntax_highlighting); m_source_view.set_line_numbers(m_preferences.show_line_numbers); m_source_view.set_highlight_current_line(m_preferences.highlight_current_line); const auto pack = [](const float c[3]) { return IM_COL32(static_cast(c[0] * 255.0f), static_cast(c[1] * 255.0f), static_cast(c[2] * 255.0f), 255); }; m_source_view.set_syntax_color(1, pack(m_preferences.color_keyword)); m_source_view.set_syntax_color(2, pack(m_preferences.color_type)); m_source_view.set_syntax_color(3, pack(m_preferences.color_string)); m_source_view.set_syntax_color(4, pack(m_preferences.color_number)); m_source_view.set_syntax_color(5, pack(m_preferences.color_comment)); m_source_view.set_syntax_color(6, pack(m_preferences.color_preprocessor)); m_source_view.set_current_line_color(pack(m_preferences.color_current_line)); // the theme colours and rounding only need rebuilding when they change if (m_restyle_pending) { apply_style(); m_restyle_pending = false; } } auto Ui::draw_preferences_window() -> void { if (!m_show_preferences) { m_prefs_open_prev = false; return; } // rescan the themes folder when the window opens, so a .toml dropped in // while the app is running shows up without a restart if (!m_prefs_open_prev) m_themes = list_themes(themes_directory()); m_prefs_open_prev = true; const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImVec2 center(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f, viewport->WorkPos.y + viewport->WorkSize.y * 0.5f); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(560.0f, 380.0f), ImGuiCond_Appearing); ImGui::SetNextWindowSizeConstraints(ImVec2(460.0f, 300.0f), ImVec2(FLT_MAX, FLT_MAX)); if (!ImGui::Begin("Preferences###preferences", &m_show_preferences, ImGuiWindowFlags_NoDocking)) { ImGui::End(); return; } static constexpr const char* CATEGORIES[] = { "Appearance", "Editor", "Debugger" }; static constexpr const char* CATEGORY_ICONS[] = { ICON_PH_SLIDERS, ICON_PH_FILE_CODE, ICON_PH_BUG }; bool changed = false; bool restyle = false; const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y; // left: the category list; right: that category's settings ImGui::BeginChild("##pref_categories", ImVec2(158.0f, -footer), ImGuiChildFlags_Borders); for (int index = 0; index < IM_ARRAYSIZE(CATEGORIES); ++index) { const std::string label = std::string(CATEGORY_ICONS[index]) + " " + CATEGORIES[index]; if (Widgets::selectable_row(label.c_str(), m_preferences_tab == index)) m_preferences_tab = index; } ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##pref_content", ImVec2(0.0f, -footer)); ImGui::PushItemWidth(-150.0f); const auto help = [](const char* text) { ImGui::SameLine(); ImGui::TextDisabled("(?)"); if (ImGui::BeginItemTooltip()) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * 20.0f); ImGui::TextUnformatted(text); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } }; const auto accent_swatch = [&](const char* label, float (&value)[3]) { if (ImGui::ColorEdit3(label, value, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha)) changed = true; }; // a settings row: the label on the left, an ios-style switch pinned to the // right edge and vertically centred against the text const auto toggle_row = [&](const char* label, bool* value, const char* help_text = nullptr) { const float toggle_h = ImGui::GetFrameHeight() * 0.78f; const float toggle_w = toggle_h * 1.8f; ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(label); if (help_text != nullptr) help(help_text); ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - toggle_w); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (ImGui::GetFrameHeight() - toggle_h) * 0.5f); if (Widgets::toggle(label, value)) changed = true; }; if (m_preferences_tab == 0) { Widgets::section_header("theme"); // preview swatches for the loaded theme, so the accent/rounding // pickers below read as tweaks on top of a named starting point const auto swatch = [](ImVec4 colour) { ImGui::ColorButton("##sw", colour, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, ImVec2(14.0f, 14.0f)); ImGui::SameLine(0.0f, 4.0f); }; swatch(m_theme.bg); swatch(m_theme.bg_high); swatch(m_theme.surface); swatch(m_theme.text); swatch(ImVec4(m_preferences.accent[0], m_preferences.accent[1], m_preferences.accent[2], 1.0f)); ImGui::NewLine(); const char* current = m_preferences.theme.c_str(); for (const auto& entry : m_themes) { if (entry.id == m_preferences.theme) { current = entry.name.c_str(); break; } } if (ImGui::BeginCombo("Theme", current)) { for (const auto& entry : m_themes) { const bool selected = entry.id == m_preferences.theme; if (ImGui::Selectable(entry.name.c_str(), selected)) select_theme(entry); // seeds the tweaks below and saves if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } help("Themes live in assets/themes as .toml files. drop a new one in, reopen this " "window, and it appears here. picking one resets the tweaks below to its values."); Widgets::section_header("interface"); changed |= Widgets::slider_float("UI scale", &m_preferences.ui_scale, 0.75f, 2.0f, "%.2fx"); help("Scales every font. the text stays crisp because it is re-rasterized, not stretched."); if (ImGui::ColorEdit3("Accent colour", m_preferences.accent, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha)) { changed = true; restyle = true; } if (Widgets::slider_float("Corner rounding", &m_preferences.rounding, 0.0f, 12.0f, "%.0f px")) { changed = true; restyle = true; } toggle_row("Show FPS in the status bar", &m_preferences.show_fps); Widgets::section_header("layout"); if (ImGui::Button("Reset window layout")) m_layout_built = false; help("Restores the default arrangement of all the docked panels."); Widgets::section_header("mascot"); toggle_row("Show the crying pepe", &m_preferences.show_mascot); ImGui::BeginDisabled(!m_preferences.show_mascot); changed |= Widgets::slider_float("Pepe size", &m_preferences.mascot_scale, 1.0f, 3.0f, "%.1fx"); ImGui::EndDisabled(); } else if (m_preferences_tab == 1) { Widgets::section_header("source view"); toggle_row("Syntax highlighting", &m_preferences.syntax_highlighting); toggle_row("Show line numbers", &m_preferences.show_line_numbers); toggle_row("Highlight the current line", &m_preferences.highlight_current_line); Widgets::section_header("colours"); ImGui::BeginDisabled(!m_preferences.syntax_highlighting); accent_swatch("Keyword", m_preferences.color_keyword); accent_swatch("Type", m_preferences.color_type); accent_swatch("String", m_preferences.color_string); accent_swatch("Number", m_preferences.color_number); accent_swatch("Comment", m_preferences.color_comment); accent_swatch("Preprocessor", m_preferences.color_preprocessor); ImGui::EndDisabled(); accent_swatch("Current line", m_preferences.color_current_line); } else if (m_preferences_tab == 2) { Widgets::section_header("launching"); toggle_row("Break at entry point on launch", &m_preferences.stop_at_entry, "stop on the very first instruction instead of running to your breakpoints."); Widgets::section_header("stepping"); { static const char* const step_modes[] = { "Line", "Instruction" }; const int current_mode = m_preferences.step_by_instruction ? 1 : 0; const int next_mode = Widgets::segmented("##step_mode", step_modes, 2, current_mode); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Step granularity"); help("The step over/into buttons advance a whole source line, or a single " "machine instruction."); if (next_mode != current_mode) { m_preferences.step_by_instruction = (next_mode == 1); changed = true; } } } ImGui::PopItemWidth(); ImGui::EndChild(); ImGui::Separator(); if (ImGui::Button("Reset to defaults")) { m_preferences = Preferences{}; // realign the neutral palette with the default theme so the reset // accent does not sit on top of the previously loaded theme's levels load_selected_theme(false); changed = true; restyle = true; } ImGui::SameLine(); ImGui::TextDisabled("Saved to %s", m_preferences_path.filename().string().c_str()); // persist the moment anything changes, so nothing is lost to a crash if (changed) save_preferences(m_preferences_path, m_preferences); if (restyle) m_restyle_pending = true; ImGui::End(); } auto Ui::draw_source_panel(Debugger& debugger) -> void { if (!m_visible.source) return; if (ImGui::Begin(PANEL_SOURCE, &m_visible.source)) m_source_view.draw(debugger); ImGui::End(); } auto Ui::draw_breakpoints_panel(Debugger& debugger) -> void { if (!m_visible.breakpoints) return; if (ImGui::Begin(PANEL_BREAKPOINTS, &m_visible.breakpoints)) { if (debugger.breakpoints().empty()) { Widgets::empty_state(ICON_PH_CIRCLE, "No breakpoints yet"); } else { constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##breakpoints", 8, flags)) { ImGui::TableSetupColumn("On", ImGuiTableColumnFlags_WidthFixed, 26.0f); ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 30.0f); ImGui::TableSetupColumn("Location"); ImGui::TableSetupColumn("Address"); ImGui::TableSetupColumn("Condition"); ImGui::TableSetupColumn("Skip", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableSetupColumn("Hits", ImGuiTableColumnFlags_WidthFixed, 40.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 26.0f); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGui::PushFont(m_font_mono, 0.0f); uint32_t pending_removal = 0; for (const Breakpoint& breakpoint : debugger.breakpoints()) { ImGui::PushID(static_cast(breakpoint.id)); ImGui::TableNextRow(); ImGui::TableNextColumn(); bool enabled = breakpoint.enabled; if (ImGui::Checkbox("##enabled", &enabled)) debugger.set_breakpoint_enabled(breakpoint.id, enabled); ImGui::TableNextColumn(); ImGui::Text("%u", breakpoint.id); ImGui::TableNextColumn(); if (breakpoint.by_address) { ImGui::Text("0x%llx", static_cast(breakpoint.file_address)); } else if (breakpoint.line != 0) { // clicking jumps the source view here. plain text + a click // test, not a Selectable — a full-width one in this stretch // column feeds its width to the 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 { ImGui::Text("%s()", breakpoint.function.c_str()); } if (ImGui::IsItemHovered() && !breakpoint.file.empty()) ImGui::SetTooltip("%s", breakpoint.file.string().c_str()); ImGui::TableNextColumn(); if (breakpoint.resolved) { ImGui::Text("0x%llx", static_cast(breakpoint.address)); if (ImGui::IsItemHovered() && !breakpoint.function.empty()) ImGui::SetTooltip("%s", breakpoint.function.c_str()); } else { ImGui::TextDisabled("Pending"); } // imgui keeps its own buffer while an input is focused, so // these copies only seed it and receive the final text ImGui::TableNextColumn(); std::string condition = breakpoint.condition; ImGui::SetNextItemWidth(-1.0f); if (ImGui::InputTextWithHint("##condition", "Stop when…", &condition, ImGuiInputTextFlags_EnterReturnsTrue)) { debugger.set_breakpoint_condition(breakpoint.id, condition); } ImGui::TableNextColumn(); int ignore_count = static_cast(breakpoint.ignore_count); ImGui::SetNextItemWidth(-1.0f); if (ImGui::InputInt("##skip", &ignore_count, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue)) { debugger.set_breakpoint_ignore_count(breakpoint.id, static_cast(std::max(ignore_count, 0))); } ImGui::TableNextColumn(); ImGui::Text("%u", breakpoint.hit_count); ImGui::TableNextColumn(); if (Widgets::remove_button("##remove")) pending_removal = breakpoint.id; ImGui::PopID(); } ImGui::PopFont(); ImGui::EndTable(); if (pending_removal != 0) debugger.remove_breakpoint(pending_removal); } } } ImGui::End(); } auto Ui::draw_call_stack_panel(Debugger& debugger) -> void { if (!m_visible.call_stack) return; if (ImGui::Begin(PANEL_CALL_STACK, &m_visible.call_stack)) { if (debugger.call_stack().empty()) { Widgets::empty_state(ICON_PH_STACK, "No call stack"); } else { for (const StackFrame& frame : debugger.call_stack()) { const bool selected = frame.index == debugger.selected_frame(); const std::string label = std::format("{:>2} {}", frame.index, frame.function); if (Widgets::selectable_row(label.c_str(), selected)) { debugger.select_frame(frame.index); show_frame(frame); m_scroll_to_program_counter = true; m_scroll_to_symbol = true; } if (frame.line == 0) continue; ImGui::SameLine(); ImGui::PushFont(m_font_mono, 0.0f); ImGui::TextDisabled("%s:%u", frame.file.filename().string().c_str(), frame.line); ImGui::PopFont(); } } } ImGui::End(); } auto Ui::draw_threads_panel(Debugger& debugger) -> void { if (!m_visible.threads) return; if (ImGui::Begin(PANEL_THREADS, &m_visible.threads)) { if (debugger.threads().empty()) { Widgets::empty_state(ICON_PH_BRANCH, "No threads"); } else { for (const Thread& thread : debugger.threads()) { const bool selected = thread.id == debugger.selected_thread(); const std::string label = std::format("{} {}", thread.id, thread.name); if (Widgets::selectable_row(label.c_str(), selected)) { 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(); ImGui::TextDisabled("%s", to_string(thread.stop_reason).data()); } } } ImGui::End(); } auto Ui::draw_source_tree_panel(const Debugger& debugger) -> void { if (!m_visible.source_tree) return; if (ImGui::Begin(PANEL_SOURCE_TREE, &m_visible.source_tree)) { const std::span files = debugger.source_files(); if (files.empty()) { Widgets::empty_state(ICON_PH_FOLDER, "No source files"); } else { ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##source_filter", "Filter files", &m_source_filter); const SourceNode tree = build_source_tree(files); const std::filesystem::path& open = m_source_view.path(); // folders stay collapsed until needed; when the open file changes, // the chain to it expands once, then 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 { if (!source_node_matches(node, m_source_filter)) return; if (!node.path.empty()) { const bool selected = node.path == open; if (selected) ImGui::PushStyleColor(ImGuiCol_Text, SOURCE_OPEN_FILE_COLOR); const bool clicked = Widgets::selectable_row( (std::string(ICON_PH_FILE " ") + node.name).c_str(), selected); if (selected) ImGui::PopStyleColor(); if (clicked) { open_source(node.path); ImGui::SetWindowFocus(PANEL_SOURCE); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", node.path.string().c_str()); return; } constexpr ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; // a filter opens every folder; with none, only the chain to the // open file unfurls, and only on the frame it changed 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( (std::string(ICON_PH_FOLDER " ") + node.name).c_str(), flags); ImGui::PopStyleColor(); if (!open_node) return; // draw the folder→child spine by hand (a vertical line down the // gutter with a tick to each child); the glyphs would be tofu const float indent = ImGui::GetStyle().IndentSpacing; const float half_row = ImGui::GetTextLineHeight() * 0.5f; const float spine_x = ImGui::GetCursorScreenPos().x - indent * 0.5f; const float top_y = ImGui::GetCursorScreenPos().y; float last_y = top_y; for (const SourceNode& child : node.children) { if (!source_node_matches(child, m_source_filter)) continue; const float row_y = ImGui::GetCursorScreenPos().y + half_row; draw_list->AddLine(ImVec2(spine_x, row_y), ImVec2(spine_x + indent * 0.5f - 3.0f, row_y), SOURCE_TREE_LINE_COLOR); last_y = row_y; self(child); } draw_list->AddLine(ImVec2(spine_x, top_y), ImVec2(spine_x, last_y), SOURCE_TREE_LINE_COLOR); ImGui::TreePop(); }; if (tree.children.empty()) Widgets::empty_state(ICON_PH_FOLDER, "No source files"); else draw_node(tree); m_revealed_source = open; } } ImGui::End(); } auto Ui::draw_locals_panel(const Debugger& debugger) -> void { if (!m_visible.locals) return; if (ImGui::Begin(PANEL_LOCALS, &m_visible.locals)) { if (debugger.locals().empty()) { Widgets::empty_state(ICON_PH_CUBE, "No locals"); } else { constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##locals", 3, flags)) { ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("Type"); ImGui::TableSetupColumn("Value"); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGui::PushFont(m_font_mono, 0.0f); for (const Variable& variable : debugger.locals()) draw_variable(variable); ImGui::PopFont(); ImGui::EndTable(); } } } 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 watches on each stop or frame change, never every frame: // each evaluation 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(); ImGui::PushFont(m_font_mono, 0.0f); 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(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 (Widgets::remove_button("##remove")) remove_index = index; ImGui::PopID(); } ImGui::PopFont(); ImGui::EndTable(); if (remove_index < m_watches.size()) m_watches.erase(m_watches.begin() + static_cast(remove_index)); } } } ImGui::End(); } auto Ui::draw_registers_panel(const Debugger& debugger) -> void { if (!m_visible.registers) return; if (ImGui::Begin(PANEL_REGISTERS, &m_visible.registers)) { if (debugger.registers().empty()) { Widgets::empty_state(ICON_PH_CPU, "No registers"); } else { constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##registers", 2, flags)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 70.0f); ImGui::TableSetupColumn("Value"); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGui::PushFont(m_font_mono, 0.0f); for (const Register& entry : debugger.registers()) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(entry.name.c_str()); ImGui::TableNextColumn(); ImGui::Text("0x%016llx", static_cast(entry.value)); } ImGui::PopFont(); ImGui::EndTable(); } } } ImGui::End(); } auto Ui::draw_symbols_panel(Debugger& debugger) -> void { if (!m_visible.symbols) return; if (ImGui::Begin(PANEL_SYMBOLS, &m_visible.symbols)) { const std::span symbols = debugger.symbols(); if (symbols.empty()) { Widgets::empty_state(ICON_PH_FUNCTION, "No symbols"); } else { ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##symbol_filter", "Filter symbols", &m_symbol_filter); std::vector visible; for (size_t index = 0; index < symbols.size(); ++index) { if (matches_filter(symbols[index].name, m_symbol_filter)) visible.push_back(index); } ImGui::TextDisabled("%zu / %zu", visible.size(), symbols.size()); constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_NoPadOuterX; if (ImGui::BeginTable("##symbols", 2, flags)) { ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 110.0f); ImGui::TableSetupColumn("Name"); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); ImGui::PushFont(m_font_mono, 0.0f); const float row_height = ImGui::GetTextLineHeightWithSpacing(); if (m_scroll_to_symbol) { for (int index = 0; index < static_cast(visible.size()); ++index) { if (symbols[visible[static_cast(index)]].file_address == debugger.selected_symbol()) { ImGui::SetScrollY(static_cast(index) * row_height - ImGui::GetWindowHeight() * 0.35f); break; } } m_scroll_to_symbol = false; } ImGuiListClipper clipper; clipper.Begin(static_cast(visible.size()), row_height); while (clipper.Step()) { for (int index = clipper.DisplayStart; index < clipper.DisplayEnd; ++index) { const Symbol& symbol = symbols[visible[static_cast(index)]]; const bool selected = symbol.file_address == debugger.selected_symbol(); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::PushID(static_cast(symbol.file_address)); if (ImGui::Selectable(std::format("0x{:012x}", symbol.address).c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { debugger.select_symbol(symbol.file_address); m_focus_disassembly = true; m_scroll_to_program_counter = true; } if (ImGui::IsItemHovered() && symbol.name.size() > 40) ImGui::SetTooltip("%s", symbol.name.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(symbol.name.c_str()); ImGui::PopID(); } } ImGui::PopFont(); ImGui::EndTable(); } } } ImGui::End(); } auto Ui::draw_disassembly_panel(Debugger& debugger) -> void { if (!m_visible.disassembly) return; if (ImGui::Begin(PANEL_DISASSEMBLY, &m_visible.disassembly)) { const std::span instructions = debugger.disassembly(); if (instructions.empty()) { Widgets::empty_state(ICON_PH_LIST, "No disassembly"); } else { if (!debugger.disassembly_name().empty()) ImGui::TextUnformatted(debugger.disassembly_name().data()); ImGui::PushFont(m_font_mono, 0.0f); draw_instruction_table(debugger, instructions, m_scroll_to_program_counter); ImGui::PopFont(); m_scroll_to_program_counter = false; } } ImGui::End(); } auto Ui::draw_console_panel(Debugger& debugger) -> void { if (!m_visible.console) return; if (ImGui::Begin(PANEL_CONSOLE, &m_visible.console)) { const float input_height = ImGui::GetFrameHeightWithSpacing(); if (ImGui::BeginChild("##console_output", ImVec2(0.0f, -input_height))) { ImGui::PushFont(m_font_mono, 0.0f); for (const std::string& line : debugger.console_output()) draw_ansi_line(line); for (const std::string& line : m_console_lines) draw_ansi_line(line); if (m_console_scroll_pending) { ImGui::SetScrollHereY(1.0f); m_console_scroll_pending = false; } ImGui::PopFont(); } ImGui::EndChild(); ImGui::SetNextItemWidth(-1.0f); const bool submitted = ImGui::InputTextWithHint("##console_input", "Expression to evaluate", &m_console_input, ImGuiInputTextFlags_EnterReturnsTrue); if (submitted && !m_console_input.empty()) { push_console(std::format("> {}", m_console_input)); if (const auto result = debugger.evaluate(m_console_input); result) push_console(*result); else push_console(std::format("error: {}", result.error())); m_console_input.clear(); ImGui::SetKeyboardFocusHere(-1); } } ImGui::End(); } auto Ui::draw_profiler_panel(Debugger& debugger) -> void { if (!m_visible.profiler) return; if (ImGui::Begin(PANEL_PROFILER, &m_visible.profiler)) { const bool instrumented = debugger.instrumentation_active(); const std::span traces = debugger.traces(); // header: how calls are gathered — instrumentation (exact, if the target // carries it) or sampling (approximate, any binary) if (instrumented) { ImGui::TextDisabled("Instrumented target — every function timed automatically"); } else { bool sampling = debugger.sampling_enabled(); if (ImGui::Checkbox("sample while running", &sampling)) debugger.set_sampling_enabled(sampling); ImGui::SameLine(); ImGui::TextDisabled("(any binary, approximate)"); } // 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); if (ImGui::BeginChild("##flamegraph", ImVec2(0.0f, flame_height), ImGuiChildFlags_Borders)) draw_flamegraph(debugger); ImGui::EndChild(); // exact per-function timings, and the box to add another if (ImGui::CollapsingHeader("function timings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SetNextItemWidth(-70.0f); const bool submitted = ImGui::InputTextWithHint("##trace_input", "Function to time", &m_trace_input, ImGuiInputTextFlags_EnterReturnsTrue); ImGui::SameLine(); const bool add_clicked = ImGui::Button("Trace", ImVec2(-1.0f, 0.0f)); if ((submitted || add_clicked) && !m_trace_input.empty()) { debugger.add_trace(m_trace_input); m_trace_input.clear(); } 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); ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 72.0f); ImGui::TableSetupColumn("Min", ImGuiTableColumnFlags_WidthFixed, 72.0f); ImGui::TableSetupColumn("Max", ImGuiTableColumnFlags_WidthFixed, 72.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f); ImGui::TableHeadersRow(); uint32_t remove_id = 0; for (const FunctionTrace& trace : traces) { // milliseconds read easier than the seconds we store const double avg_ms = trace.completed_count > 0 ? trace.total_time / static_cast(trace.completed_count) * 1000.0 : 0.0; ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(trace.function.c_str()); ImGui::TableNextColumn(); ImGui::Text("%llu", static_cast(trace.call_count)); ImGui::TableNextColumn(); if (trace.completed_count > 0) ImGui::Text("%.3f ms", avg_ms); else ImGui::TextDisabled("-"); ImGui::TableNextColumn(); if (trace.completed_count > 0) ImGui::Text("%.3f ms", trace.min_time * 1000.0); else ImGui::TextDisabled("-"); ImGui::TableNextColumn(); if (trace.completed_count > 0) ImGui::Text("%.3f ms", trace.max_time * 1000.0); else ImGui::TextDisabled("-"); ImGui::TableNextColumn(); ImGui::PushID(static_cast(trace.id)); if (Widgets::remove_button("##remove")) remove_id = trace.id; ImGui::PopID(); } ImGui::EndTable(); if (remove_id != 0) debugger.remove_trace(remove_id); } } // 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(); const std::string frame_overlay = std::format("{:.2f} ms (avg {:.2f} peak {:.2f})", frame_ms.latest(), frame_ms.average(), frame_ms.maximum()); ImGui::TextUnformatted("Frame time"); ImGui::PlotLines("##frame_ms", frame_ms.values(), frame_ms.count(), frame_ms.offset(), frame_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 70.0f)); const std::string fps_overlay = std::format("{:.0f} fps (avg {:.0f})", fps.latest(), fps.average()); ImGui::TextUnformatted("Frame rate"); ImGui::PlotLines("##fps", fps.values(), fps.count(), fps.offset(), fps_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 70.0f)); } } ImGui::End(); } auto Ui::draw_flamegraph(const Debugger& debugger) -> void { const std::span spans = debugger.timeline(); if (spans.empty()) { if (debugger.instrumentation_active()) 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 ImGui::TextDisabled("Turn on sampling, or trace a function below, then run"); return; } // 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); }; // 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; 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); } const double range = std::max(t_max - t_min, 1.0e-6); ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size()); constexpr float row_height = 20.0f; const float rows = static_cast(max_depth + 1); const ImVec2 origin = ImGui::GetCursorScreenPos(); const ImVec2 avail = ImGui::GetContentRegionAvail(); const float canvas_w = std::max(avail.x, 1.0f); const float canvas_h = std::max(avail.y, rows * row_height + 4.0f); // claim the region so hovering resolves against it ImGui::InvisibleButton("##flame_canvas", ImVec2(canvas_w, canvas_h)); const bool canvas_hovered = ImGui::IsItemHovered(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); draw_list->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true); const float scale = canvas_w / static_cast(range); // pixels per second const float baseline = origin.y + canvas_h; // row 0 rests on the bottom const ImVec2 mouse = ImGui::GetMousePos(); for (const TimelineSpan& span : spans) { const float x0 = origin.x + static_cast((span.start - t_min) * scale); const float width = std::max(1.0f, static_cast(span.duration * scale)); const float y1 = baseline - static_cast(span.depth) * row_height; const float y0 = y1 - (row_height - 2.0f); const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f); 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); 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(); } const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width && mouse.y >= y0 && mouse.y <= y1; 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(); } } draw_list->PopClipRect(); } auto Ui::draw_macros_panel(Debugger& /*debugger*/) -> void { if (!m_visible.macros) return; if (ImGui::Begin(PANEL_MACROS, &m_visible.macros)) { const MacroTable& table = m_source_view.macros(); ImGui::TextDisabled("Unroll a #define one layer per level"); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::InputTextWithHint("##macro_input", "Click a macro in Source, or type one like MAX(a, b)", &m_macro_input)) m_macro_level = 0; if (table.empty()) ImGui::TextDisabled("No #define macros found in the open source file"); if (m_macro_input.empty()) { ImGui::End(); return; } const MacroExpansion expansion = expand_stages(table, m_macro_input); const int max_level = static_cast(expansion.levels.size()) - 1; m_macro_level = std::clamp(m_macro_level, 0, max_level); ImGui::BeginDisabled(m_macro_level <= 0); if (ImGui::ArrowButton("##macro_prev", ImGuiDir_Left)) --m_macro_level; ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(m_macro_level >= max_level); if (ImGui::ArrowButton("##macro_next", ImGuiDir_Right)) ++m_macro_level; ImGui::EndDisabled(); ImGui::SameLine(); if (max_level > 0) { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("full reset").x - ImGui::GetStyle().FramePadding.x * 6.0f - ImGui::GetStyle().ItemSpacing.x * 2.0f); ImGui::SliderInt("##macro_level", &m_macro_level, 0, max_level, "level %d"); } else { ImGui::TextDisabled("Nothing to expand here"); ImGui::SameLine(); } ImGui::SameLine(); if (ImGui::SmallButton("Full")) m_macro_level = max_level; ImGui::SameLine(); if (ImGui::SmallButton("Reset")) m_macro_level = 0; // status: where we are, and whether the tail is truly the fixpoint if (max_level == 0) ImGui::TextDisabled("Already fully expanded"); else if (m_macro_level == max_level && expansion.fully_expanded()) ImGui::TextDisabled("Level %d of %d — fully expanded", m_macro_level, max_level); else if (m_macro_level == max_level) ImGui::TextDisabled("Level %d — stopped at the expansion cap", m_macro_level); else ImGui::TextDisabled("Level %d of %d", m_macro_level, max_level); // which macros the next layer will unroll, so the step reads ahead if (m_macro_level < max_level) { const std::vector& next = expansion.expanded[static_cast(m_macro_level) + 1]; if (!next.empty()) { std::string names; for (const std::string& name : next) { if (!names.empty()) names += ", "; names += name; } ImGui::SameLine(); ImGui::TextDisabled("| next: %s", names.c_str()); } } ImGui::Separator(); m_macro_output = expansion.levels[static_cast(m_macro_level)]; ImGui::InputTextMultiline("##macro_output", &m_macro_output, ImGui::GetContentRegionAvail(), ImGuiInputTextFlags_ReadOnly); } 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 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(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(entries.size()); if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && !entries.empty()) { m_palette_selection = (m_palette_selection - 1 + static_cast(entries.size())) % static_cast(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(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(entries.size()) + 4.0f))); for (int index = 0; index < static_cast(entries.size()); ++index) { const Entry& entry = entries[static_cast(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)); if (m_console_lines.size() > MAX_CONSOLE_LINES) m_console_lines.erase(m_console_lines.begin()); m_console_scroll_pending = true; } auto Ui::report(const Result& result, std::string_view action) -> void { if (result) return; push_console(std::format("{} failed: {}", action, result.error())); } auto Ui::load_fonts() -> void { ImGuiIO& io = ImGui::GetIO(); const std::filesystem::path dir = std::filesystem::path(HSDBG_ASSET_DIR) / "fonts"; // the icon glyphs sit in the private-use area and are merged over each ui // face, so ICON_PH_* strings render inline with ordinary text static const ImWchar icon_range[] = { ICON_MIN_PH, ICON_MAX_PH, 0 }; const auto load = [&](const char* file, float size, bool merge_icons) -> ImFont* { const std::string path = (dir / file).string(); // a hair of extra coverage so the text holds up on the dark ground ImFontConfig base; base.RasterizerMultiply = 1.03f; ImFont* font = io.Fonts->AddFontFromFileTTF(path.c_str(), size, &base); if (font == nullptr) { Log::warn("fonts: could not load '{}'", path); return nullptr; } if (merge_icons) { ImFontConfig cfg; cfg.MergeMode = true; cfg.PixelSnapH = true; cfg.GlyphMinAdvanceX = size; // one cell per icon, so they align cfg.GlyphOffset = ImVec2(0.0f, 1.0f); // drop onto the text baseline io.Fonts->AddFontFromFileTTF((dir / "Phosphor.ttf").string().c_str(), size, &cfg, icon_range); } return font; }; constexpr float UI_SIZE = 16.0f; // the first face added is the default and fixes the base size the rest of // the ui scales from m_font_ui = load("Nunito-Medium.ttf", UI_SIZE, true); m_font_strong = load("Nunito-Bold.ttf", UI_SIZE, true); m_font_mono = load("JetBrainsMono-Regular.ttf", UI_SIZE, false); // never come up unstyled: fall back to the built-in vector face if (m_font_ui == nullptr) { Log::warn("fonts: bundled faces missing, using the built-in font"); m_font_ui = io.Fonts->AddFontDefaultVector(); } if (m_font_strong == nullptr) m_font_strong = m_font_ui; if (m_font_mono == nullptr) m_font_mono = m_font_ui; io.FontDefault = m_font_ui; Widgets::set_fonts(m_font_ui, m_font_strong, m_font_mono); } auto Ui::themes_directory() -> std::filesystem::path { return std::filesystem::path(HSDBG_ASSET_DIR) / "themes"; } auto Ui::seed_preferences_from_theme() -> void { const auto copy = [](const ImVec4& from, float (&to)[3]) { to[0] = from.x; to[1] = from.y; to[2] = from.z; }; copy(m_theme.accent, m_preferences.accent); m_preferences.rounding = m_theme.rounding; copy(m_theme.syntax_keyword, m_preferences.color_keyword); copy(m_theme.syntax_type, m_preferences.color_type); copy(m_theme.syntax_string, m_preferences.color_string); copy(m_theme.syntax_number, m_preferences.color_number); copy(m_theme.syntax_comment, m_preferences.color_comment); copy(m_theme.syntax_preprocessor, m_preferences.color_preprocessor); copy(m_theme.current_line, m_preferences.color_current_line); } auto Ui::load_selected_theme(bool seed_preferences) -> void { m_themes = list_themes(themes_directory()); // resolve the saved id to a file; fall back to the first theme found, // then to the baked-in default, so the ui is never left unstyled std::filesystem::path path; for (const auto& entry : m_themes) { if (entry.id == m_preferences.theme) { path = entry.path; break; } } if (path.empty() && !m_themes.empty()) { path = m_themes.front().path; m_preferences.theme = m_themes.front().id; } m_theme = path.empty() ? default_theme() : load_theme(path); if (seed_preferences) seed_preferences_from_theme(); } auto Ui::select_theme(const ThemeEntry& entry) -> void { m_preferences.theme = entry.id; m_theme = load_theme(entry.path); // picking a theme resets the tweakable accent / rounding / syntax to its // values; the source view picks up the new syntax next frame seed_preferences_from_theme(); m_restyle_pending = true; save_preferences(m_preferences_path, m_preferences); } auto Ui::apply_style() -> void { ImGuiStyle& style = ImGui::GetStyle(); const float radius = m_preferences.rounding; style.WindowRounding = radius; style.ChildRounding = radius; style.FrameRounding = radius; style.PopupRounding = radius; style.ScrollbarRounding = radius + 2.0f; style.GrabRounding = radius; style.TabRounding = radius; style.WindowBorderSize = m_theme.window_border; style.FrameBorderSize = 0.0f; style.WindowPadding = ImVec2(9.0f, 7.0f); style.FramePadding = ImVec2(9.0f, 5.0f); style.ItemSpacing = ImVec2(7.0f, 5.0f); style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); style.IndentSpacing = 16.0f; style.CellPadding = ImVec2(7.0f, 4.0f); style.ScrollbarSize = 11.0f; style.GrabMinSize = 10.0f; style.TabBarBorderSize = 0.0f; style.DockingSeparatorSize = 7.0f; // widen the splitter into a visible gap style.WindowTitleAlign = ImVec2(0.0f, 0.5f); style.SeparatorTextBorderSize = 1.0f; // neutral levels come from the theme; interactive states are derived, so a // theme names only a handful of colours. "emphasis" nudges a level toward // the text colour, so one rule brightens dark themes and darkens light ones const ImVec4 text = m_theme.text; const ImVec4 bg = m_theme.bg; const ImVec4 bg_low = m_theme.bg_low; const ImVec4 bg_high = m_theme.bg_high; const ImVec4 surface = m_theme.surface; // the accent is user-chosen (seeded from the theme); its hover/active // shades blend from the button base toward it so it stays coherent const ImVec4 accent(m_preferences.accent[0], m_preferences.accent[1], m_preferences.accent[2], 1.0f); const auto mix = [](ImVec4 from, ImVec4 to, float t) { return ImVec4(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t, from.z + (to.z - from.z) * t, 1.0f); }; const auto fade = [](ImVec4 colour, float alpha) { return ImVec4(colour.x, colour.y, colour.z, alpha); }; const auto emphasis = [&](ImVec4 colour, float t) { return mix(colour, text, t); }; const ImVec4 accent_bright = mix(accent, ImVec4(1.0f, 1.0f, 1.0f, 1.0f), 0.14f); const ImVec4 accent_hover = mix(surface, accent, 0.42f); const ImVec4 accent_active = mix(surface, accent, 0.68f); const ImVec4 grab = emphasis(bg, 0.18f); ImVec4* colors = style.Colors; colors[ImGuiCol_Text] = text; colors[ImGuiCol_TextDisabled] = m_theme.text_dim; colors[ImGuiCol_WindowBg] = bg; colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_PopupBg] = bg_high; colors[ImGuiCol_Border] = m_theme.border; colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_FrameBg] = bg_high; colors[ImGuiCol_FrameBgHovered] = emphasis(bg_high, 0.06f); colors[ImGuiCol_FrameBgActive] = emphasis(bg_high, 0.10f); colors[ImGuiCol_TitleBg] = bg_low; colors[ImGuiCol_TitleBgActive] = emphasis(bg_low, 0.05f); colors[ImGuiCol_TitleBgCollapsed] = bg_low; colors[ImGuiCol_MenuBarBg] = emphasis(bg_low, 0.04f); colors[ImGuiCol_ScrollbarBg] = bg; colors[ImGuiCol_ScrollbarGrab] = grab; colors[ImGuiCol_ScrollbarGrabHovered] = emphasis(bg, 0.28f); colors[ImGuiCol_ScrollbarGrabActive] = emphasis(bg, 0.38f); colors[ImGuiCol_CheckMark] = accent; colors[ImGuiCol_SliderGrab] = accent; colors[ImGuiCol_SliderGrabActive] = accent_bright; colors[ImGuiCol_Button] = surface; colors[ImGuiCol_ButtonHovered] = accent_hover; colors[ImGuiCol_ButtonActive] = accent_active; colors[ImGuiCol_Header] = surface; colors[ImGuiCol_HeaderHovered] = accent_hover; colors[ImGuiCol_HeaderActive] = accent_active; colors[ImGuiCol_Separator] = m_theme.border; colors[ImGuiCol_SeparatorHovered] = fade(accent, 0.60f); colors[ImGuiCol_SeparatorActive] = accent; colors[ImGuiCol_ResizeGrip] = grab; colors[ImGuiCol_ResizeGripHovered] = fade(accent, 0.60f); colors[ImGuiCol_ResizeGripActive] = accent; // the selected tab takes the panel-body colour so it reads as connected to // its content, not a floating chip; the accent overline marks it colors[ImGuiCol_Tab] = bg_low; colors[ImGuiCol_TabHovered] = emphasis(bg_low, 0.10f); colors[ImGuiCol_TabSelected] = bg; colors[ImGuiCol_TabSelectedOverline] = accent; colors[ImGuiCol_TabDimmed] = bg_low; colors[ImGuiCol_TabDimmedSelected] = bg; colors[ImGuiCol_DockingPreview] = fade(accent, 0.50f); colors[ImGuiCol_DockingEmptyBg] = bg_low; colors[ImGuiCol_TableHeaderBg] = bg_high; colors[ImGuiCol_TableBorderStrong] = m_theme.border; colors[ImGuiCol_TableBorderLight] = mix(bg, m_theme.border, 0.5f); colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_TableRowBgAlt] = fade(text, 0.03f); colors[ImGuiCol_TextSelectedBg] = fade(accent, 0.35f); colors[ImGuiCol_NavCursor] = accent; } }