hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-08 12:54:05 +0200
committerhachem <im@hachem.wtf>2026-09-08 12:54:05 +0200
commit1fa69b34a0968cf79461f37b8d9fdadba745b4d3 (patch)
tree639cb5b89c8eeebcb8b83b6f3fd114d19c4f5b81
parentff58bb0cda2f183836f8e016db42caf055041744 (diff)
feat: source view zoom and line mentions
-rw-r--r--src/ui/source_view.cpp260
-rw-r--r--src/ui/source_view.h19
2 files changed, 250 insertions, 29 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index f9a3f85..00487c7 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
{
@@ -313,6 +322,21 @@ namespace Hsdbg
const ImU32 INLINE_VALUE_COLOR = IM_COL32(122, 140, 170, 235);
+ constexpr float EXECUTION_BAR_WIDTH = 3.0f;
+
+ 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;
@@ -345,6 +369,19 @@ namespace Hsdbg
return false;
}
+ 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;
+ });
+ }
+
auto inline_value(const Variable& local) -> std::string
{
std::string text = local.name + " = " + local.value;
@@ -470,49 +507,144 @@ 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;
}
- if (m_mono_font != nullptr)
- ImGui::PushFont(m_mono_font, 0.0f);
-
+ 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
+ {
+ 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"));
+
+ 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);
+ 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
@@ -528,6 +660,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 -
@@ -539,6 +673,36 @@ namespace Hsdbg
? debugger.locals()
: std::span<const Variable>{};
+ 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);
@@ -551,10 +715,14 @@ namespace Hsdbg
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);
@@ -610,7 +778,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);
@@ -669,7 +841,7 @@ namespace Hsdbg
}
}
- if (!locals.empty() && line_number <= m_highlighted_line)
+ if (!pinned.empty())
{
int shown = 0;
@@ -678,18 +850,35 @@ 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);
+
+ 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;
}
}
@@ -698,6 +887,19 @@ namespace Hsdbg
clipper.End();
+ 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();
}
diff --git a/src/ui/source_view.h b/src/ui/source_view.h
index 5673d69..b38772b 100644
--- a/src/ui/source_view.h
+++ b/src/ui/source_view.h
@@ -7,6 +7,7 @@
#include <filesystem>
#include <optional>
#include <string>
+#include <unordered_set>
#include <vector>
struct ImDrawList;
@@ -44,6 +45,10 @@ namespace Hsdbg
auto draw(Debugger& debugger) -> void;
auto set_highlighted_line(uint32_t line) -> void;
+
+ auto set_frame_extent(uint32_t first, uint32_t last) -> void;
+
+ auto set_changed_locals(const std::unordered_set<std::string>* names) -> void { m_changed_locals = names; }
auto set_highlighting(bool enabled) -> void { m_highlighting_enabled = enabled; }
auto set_line_numbers(bool enabled) -> void { m_line_numbers = enabled; }
auto set_highlight_current_line(bool enabled) -> void { m_highlight_current_line = enabled; }
@@ -52,6 +57,8 @@ namespace Hsdbg
auto set_current_line_color(unsigned int color) -> void { m_current_line_color = color; }
auto set_mono_font(ImFont* font) -> void { m_mono_font = font; }
+ auto set_font_size(float size) -> void { m_font_size = size; }
+ auto font_size() const -> float { return m_font_size; }
auto path() const -> const std::filesystem::path& { return m_path; }
auto line_count() const -> size_t { return m_lines.size(); }
@@ -66,6 +73,7 @@ namespace Hsdbg
private:
auto draw_open_bar() -> void;
auto draw_lines(Debugger& debugger) -> void;
+ auto handle_zoom() -> void;
auto draw_macro_span(ImDrawList* draw_list, const std::string& line,
const SourceSpan& span) -> void;
@@ -91,5 +99,16 @@ namespace Hsdbg
std::string m_error;
uint32_t m_highlighted_line = 0;
bool m_scroll_to_highlight = false;
+
+ bool m_editing_path = false;
+ bool m_focus_path = false;
+
+ float m_font_size = 16.0f;
+ float m_zoom_accumulator = 0.0f;
+ double m_zoom_badge_until = 0.0;
+
+ uint32_t m_frame_first_line = 0;
+ uint32_t m_frame_last_line = 0;
+ const std::unordered_set<std::string>* m_changed_locals = nullptr;
};
}