diff options
| author | hachem <im@hachem.wtf> | 2026-09-18 12:25:58 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-18 12:25:58 +0200 |
| commit | 4f74d1400180ca4ad7cfcd1903386fda8779818d (patch) | |
| tree | 258a332c3076045461b5d6f8592a52140225abd1 /src/ui/source_view.cpp | |
| parent | 0846724177d823b632f2a4deffdcb5b6e226cd56 (diff) | |
feat: improve layout and themes
Diffstat (limited to 'src/ui/source_view.cpp')
| -rw-r--r-- | src/ui/source_view.cpp | 288 |
1 files changed, 255 insertions, 33 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp index 205fbca..af389f8 100644 --- a/src/ui/source_view.cpp +++ b/src/ui/source_view.cpp @@ -2,6 +2,8 @@ #include "core/log.h" #include "debugger/debugger.h" +#include "ui/icons.h" +#include "ui/widgets.h" #include <imgui.h> #include <imgui_internal.h> @@ -9,10 +11,14 @@ #include <algorithm> #include <cctype> +#include <cfloat> +#include <format> #include <fstream> +#include <iterator> #include <optional> #include <span> #include <string_view> +#include <unordered_map> #include <unordered_set> namespace Hsdbg @@ -21,6 +27,9 @@ namespace Hsdbg { constexpr float BREAKPOINT_RADIUS = 5.0f; + constexpr float MIN_FONT_SIZE = 9.0f; + constexpr float MAX_FONT_SIZE = 32.0f; + constexpr float DEFAULT_FONT_SIZE = 16.0f; enum class Language : uint8_t { @@ -325,6 +334,24 @@ namespace Hsdbg // green of comments so they do not read as part of the code const ImU32 INLINE_VALUE_COLOR = IM_COL32(122, 140, 170, 235); + // the accent bar down the left edge of the line execution is on + constexpr float EXECUTION_BAR_WIDTH = 3.0f; + + // the directory of a path cut to its last two components, so a deep path + // leaves room for the file name in the location bar + auto short_directory(const std::filesystem::path& path) -> std::string + { + const std::filesystem::path parent = path.parent_path(); + std::vector<std::string> parts; + std::ranges::transform(parent, std::back_inserter(parts), + [](const std::filesystem::path& part) { return part.string(); }); + + if (parts.size() <= 3) + return parent.generic_string() + "/"; + + return "…/" + parts[parts.size() - 2] + "/" + parts.back() + "/"; + } + auto line_number_width(size_t line_count) -> float { return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x; @@ -361,6 +388,22 @@ namespace Hsdbg return false; } + // whether the line holds `name` as an identifier: by its spans when it + // has them (so a mention inside a comment or string does not count), + // else by the raw text + auto line_mentions(const std::string& line, const std::vector<SourceSpan>& spans, + const std::string& name) -> bool + { + if (spans.empty()) + return contains_word(line, name); + + return std::ranges::any_of(spans, [&](const SourceSpan& span) + { + return span.kind == SyntaxKind::Default && span.length == name.size() && + line.compare(span.start, span.length, name) == 0; + }); + } + // a single-line, length-capped rendering of a value for the inline annotation auto inline_value(const Variable& local) -> std::string { @@ -491,51 +534,153 @@ namespace Hsdbg m_scroll_to_highlight = line != 0; } + auto SourceView::set_frame_extent(uint32_t first, uint32_t last) -> void + { + m_frame_first_line = first; + m_frame_last_line = last; + } + auto SourceView::draw(Debugger& debugger) -> void { draw_open_bar(); - ImGui::Separator(); - if (m_lines.empty()) { - ImGui::TextDisabled("No source file open"); + Widgets::empty_state(ICON_PH_FILE_CODE, "No source file open"); return; } - // the code and its line-number gutter render in the monospace face; the - // open bar above stays in the ui font - if (m_mono_font != nullptr) - ImGui::PushFont(m_mono_font, 0.0f); - + // the code and its line-number gutter render in the monospace face at the + // zoomable size; the location bar above stays in the ui font + ImGui::PushFont(m_mono_font, m_font_size); draw_lines(debugger); + ImGui::PopFont(); + } - if (m_mono_font != nullptr) - ImGui::PopFont(); + auto SourceView::handle_zoom() -> void + { + // cmd/ctrl + wheel over the code resizes it, and cmd/ctrl + 0 resets it. + // owning the wheel while the modifier is down keeps the view from + // scrolling too (imgui only holds off on its own for the real ctrl key) + const ImGuiIO& io = ImGui::GetIO(); + if (!ImGui::IsWindowHovered() || !(io.KeyCtrl || io.KeySuper)) + { + m_zoom_accumulator = 0.0f; + return; + } + + ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, ImGui::GetID("##zoom")); + + // a mouse notch is a whole step; a trackpad gets there in small increments + float size = m_font_size; + m_zoom_accumulator += io.MouseWheel; + while (m_zoom_accumulator >= 1.0f) + { + size += 1.0f; + m_zoom_accumulator -= 1.0f; + } + while (m_zoom_accumulator <= -1.0f) + { + size -= 1.0f; + m_zoom_accumulator += 1.0f; + } + + if (ImGui::IsKeyPressed(ImGuiKey_0)) + size = DEFAULT_FONT_SIZE; + + size = std::clamp(size, MIN_FONT_SIZE, MAX_FONT_SIZE); + if (size != m_font_size) + { + m_font_size = size; + m_zoom_badge_until = ImGui::GetTime() + 1.2; + } } auto SourceView::draw_open_bar() -> void { - ImGui::SetNextItemWidth(-ImGui::CalcTextSize("Open").x - ImGui::GetStyle().FramePadding.x * 4.0f); - - const bool submitted = ImGui::InputTextWithHint("##source_path", - "Path to a source file", - &m_path_input, - ImGuiInputTextFlags_EnterReturnsTrue); + // with a file open the bar is a breadcrumb (dim directory, bright name) + // that a click turns into the path field; with none, the field is there + // from the start + if (m_editing_path || m_path.empty()) + { + if (m_focus_path) + { + ImGui::SetKeyboardFocusHere(); + m_focus_path = false; + } - ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + const bool submitted = Widgets::input_with_icon("##source_path", ICON_PH_FILE_CODE, + "Path to a source file, Enter to open", + &m_path_input, + ImGuiInputTextFlags_EnterReturnsTrue); - if (ImGui::Button("Open") || submitted) + if (submitted) + { + if (const auto result = open(m_path_input); !result) + { + m_error = result.error(); + Log::error("{}", m_error); + } + else + { + m_editing_path = false; + } + } + else if (m_editing_path && (ImGui::IsItemDeactivated() || ImGui::IsKeyPressed(ImGuiKey_Escape))) + { + m_editing_path = false; + m_path_input = m_path.string(); + } + } + else { - if (const auto result = open(m_path_input); !result) + const ImGuiStyle& style = ImGui::GetStyle(); + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const float height = ImGui::GetFrameHeight(); + const float width = ImGui::GetContentRegionAvail().x; + + ImGui::InvisibleButton("##path_row", ImVec2(width, height)); + const bool hovered = ImGui::IsItemHovered(); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + if (hovered) + { + draw_list->AddRectFilled(pos, ImVec2(pos.x + width, pos.y + height), + ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s\nClick to type a path", m_path.string().c_str()); + } + + const ImU32 dim = ImGui::GetColorU32(ImGuiCol_TextDisabled); + const ImU32 bright = ImGui::GetColorU32(ImGuiCol_Text); + const std::string directory = short_directory(m_path); + const std::string name = m_path.filename().string(); + + float x = pos.x + style.FramePadding.x; + const float y = pos.y + style.FramePadding.y; + draw_list->AddText(ImVec2(x, y), dim, ICON_PH_FILE_CODE); + x += ImGui::CalcTextSize(ICON_PH_FILE_CODE).x + 6.0f; + draw_list->AddText(ImVec2(x, y), dim, directory.c_str()); + x += ImGui::CalcTextSize(directory.c_str()).x; + draw_list->AddText(ImVec2(x, y), bright, name.c_str()); + + if (hovered) + { + const float pencil = ImGui::CalcTextSize(ICON_PH_PENCIL).x; + draw_list->AddText(ImVec2(pos.x + width - pencil - style.FramePadding.x, y), dim, ICON_PH_PENCIL); + } + + if (ImGui::IsItemClicked()) { - m_error = result.error(); - Log::error("{}", m_error); + m_editing_path = true; + m_focus_path = true; + m_path_input = m_path.string(); } } if (!m_error.empty()) - ImGui::TextColored(ImVec4(0.89f, 0.33f, 0.33f, 1.0f), "%s", m_error.c_str()); + ImGui::TextColored(ImVec4(0.89f, 0.33f, 0.33f, 1.0f), ICON_PH_WARNING " %s", m_error.c_str()); } auto SourceView::draw_lines(Debugger& debugger) -> void @@ -551,6 +696,8 @@ namespace Hsdbg return; } + handle_zoom(); + if (m_scroll_to_highlight && m_highlighted_line != 0) { ImGui::SetScrollY(static_cast<float>(m_highlighted_line - 1) * text_height - @@ -564,6 +711,40 @@ namespace Hsdbg ? debugger.locals() : std::span<const Variable>{}; + // each local is annotated once, on the line that declares it: the + // declaration the debug info names (a parameter's is the signature), else + // its first mention inside the current function. never past the pc, as + // anything there has not run and would read stale + std::unordered_map<std::string, uint32_t> pinned; + if (!locals.empty()) + { + for (const Variable& local : locals) + { + if (!local.value.empty() && local.declaration_line != 0 && + local.declaration_line <= m_highlighted_line && local.declaration_line <= m_lines.size()) + pinned.emplace(local.name, local.declaration_line); + } + + const uint32_t first = std::max(m_frame_first_line, 1u); + const uint32_t last = std::min({ m_highlighted_line, + m_frame_last_line != 0 ? m_frame_last_line : m_highlighted_line, + static_cast<uint32_t>(m_lines.size()) }); + + for (uint32_t line = first; line <= last; ++line) + { + for (const Variable& local : locals) + { + if (local.value.empty() || pinned.contains(local.name)) + continue; + + const size_t at = line - 1; + const std::vector<SourceSpan>& spans = at < m_spans.size() ? m_spans[at] : std::vector<SourceSpan>{}; + if (line_mentions(m_lines[at], spans, local.name)) + pinned.emplace(local.name, line); + } + } + } + ImGuiListClipper clipper; clipper.Begin(static_cast<int>(m_lines.size()), text_height); @@ -574,12 +755,18 @@ namespace Hsdbg const uint32_t line_number = static_cast<uint32_t>(index) + 1; const ImVec2 row_start = ImGui::GetCursorScreenPos(); + // the line execution sits on: a full-bleed tint plus an accent bar + // down the panel's left edge if (line_number == m_highlighted_line && m_highlight_current_line) { - draw_list->AddRectFilled(row_start, - ImVec2(row_start.x + ImGui::GetContentRegionAvail().x, - row_start.y + text_height), + const float left = ImGui::GetWindowPos().x + 1.0f; + const float right = ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 1.0f; + draw_list->AddRectFilled(ImVec2(left, row_start.y), + ImVec2(right, row_start.y + text_height), m_current_line_color); + draw_list->AddRectFilled(ImVec2(left, row_start.y), + ImVec2(left + EXECUTION_BAR_WIDTH, row_start.y + text_height), + ImGui::ColorConvertFloat4ToU32(Widgets::accent())); } ImGui::PushID(index); @@ -636,7 +823,11 @@ namespace Hsdbg ImGui::SameLine(0.0f, 0.0f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + number_width - ImGui::CalcTextSize(number_text.c_str()).x); - ImGui::TextDisabled("%s", number_text.c_str()); + + if (line_number == m_highlighted_line) + ImGui::TextUnformatted(number_text.c_str()); + else + ImGui::TextDisabled("%s", number_text.c_str()); } ImGui::SameLine(0.0f, GUTTER_MARGIN * 2.0f); @@ -697,9 +888,7 @@ namespace Hsdbg } } - // live values for locals on this line, shown only up to the pc — - // anything past it has not run yet and would read stale - if (!locals.empty() && line_number <= m_highlighted_line) + if (!pinned.empty()) { int shown = 0; @@ -708,18 +897,37 @@ namespace Hsdbg if (shown >= 4) break; - if (local.value.empty() || !contains_word(text, local.name)) + const auto pin = pinned.find(local.name); + if (pin == pinned.end() || pin->second != line_number) 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()); + const bool changed = m_changed_locals != nullptr && m_changed_locals->contains(local.name); + + // set in a dim pill so it reads as the debugger's note on + // the line rather than as more code + const std::string label = inline_value(local); + ImGui::SameLine(0.0f, shown == 0 ? GUTTER_MARGIN * 4.0f : GUTTER_MARGIN); + + constexpr float pad = 5.0f; + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 size = ImGui::CalcTextSize(label.c_str()); + draw_list->AddRectFilled(ImVec2(pos.x, pos.y - 1.0f), + ImVec2(pos.x + size.x + pad * 2.0f, pos.y + size.y + 1.0f), + ImGui::GetColorU32(ImGuiCol_FrameBg, 0.85f), 4.0f); + + ImGui::SetCursorScreenPos(ImVec2(pos.x + pad, pos.y)); + ImGui::PushStyleColor(ImGuiCol_Text, changed ? ImGui::ColorConvertFloat4ToU32(Widgets::accent()) + : INLINE_VALUE_COLOR); + ImGui::TextUnformatted(label.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()); + ImGui::SameLine(0.0f, 0.0f); + ImGui::Dummy(ImVec2(pad, 0.0f)); + ++shown; } } @@ -728,6 +936,20 @@ namespace Hsdbg clipper.End(); + // a passing badge with the new size, top right, after a zoom + if (ImGui::GetTime() < m_zoom_badge_until) + { + const std::string label = std::format("{:.0f} px", m_font_size); + const ImVec2 ts = ImGui::CalcTextSize(label.c_str()); + const ImVec2 win = ImGui::GetWindowPos(); + const ImVec2 end(win.x + ImGui::GetWindowWidth() - 16.0f, win.y + 10.0f + ts.y + 10.0f); + const ImVec2 start(end.x - ts.x - 20.0f, win.y + 10.0f); + ImDrawList* overlay = ImGui::GetWindowDrawList(); + overlay->AddRectFilled(start, end, ImGui::GetColorU32(ImGuiCol_FrameBg), 6.0f); + overlay->AddRect(start, end, ImGui::GetColorU32(ImGuiCol_Border), 6.0f); + overlay->AddText(ImVec2(start.x + 10.0f, start.y + 5.0f), ImGui::GetColorU32(ImGuiCol_Text), label.c_str()); + } + ImGui::EndChild(); } |
