aboutsummaryrefslogtreecommitdiff
path: root/src/ui/ui_preferences.cpp
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-18 12:25:58 +0200
committerhachem <im@hachem.wtf>2026-09-18 12:25:58 +0200
commit4f74d1400180ca4ad7cfcd1903386fda8779818d (patch)
tree258a332c3076045461b5d6f8592a52140225abd1 /src/ui/ui_preferences.cpp
parent0846724177d823b632f2a4deffdcb5b6e226cd56 (diff)
feat: improve layout and themes
Diffstat (limited to 'src/ui/ui_preferences.cpp')
-rw-r--r--src/ui/ui_preferences.cpp667
1 files changed, 667 insertions, 0 deletions
diff --git a/src/ui/ui_preferences.cpp b/src/ui/ui_preferences.cpp
new file mode 100644
index 0000000..a1768a9
--- /dev/null
+++ b/src/ui/ui_preferences.cpp
@@ -0,0 +1,667 @@
+#include "ui/ui.h"
+
+#include "debugger/debugger.h"
+#include "ui/icons.h"
+#include "ui/widgets.h"
+
+#include <glad/gl.h>
+#include <imgui.h>
+#include <imgui_internal.h>
+
+#include <algorithm>
+#include <cfloat>
+#include <cmath>
+#include <format>
+#include <iterator>
+#include <string>
+
+#ifndef HSDBG_VERSION
+ #define HSDBG_VERSION "dev"
+#endif
+
+#ifndef HSDBG_ASSET_DIR
+ #define HSDBG_ASSET_DIR ""
+#endif
+
+// the preferences window: a category rail on the left and, on the right, that
+// category's settings grouped into raised cards of title / description / control
+// rows, with theme and accent pickers that show what they pick
+namespace Hsdbg
+{
+ namespace
+ {
+ constexpr const char* CATEGORIES[] = { "Appearance", "Editor", "Debugger", "About" };
+ constexpr const char* CATEGORY_ICONS[] = { ICON_PH_SLIDERS, ICON_PH_FILE_CODE, ICON_PH_BUG, ICON_PH_INFO };
+ constexpr const char* CATEGORY_BLURBS[] = {
+ "Theme, accent and the shape of the interface",
+ "How source is drawn and coloured",
+ "How targets launch and step, and the keys that drive them",
+ "What this build is made of",
+ };
+
+ struct AccentPreset
+ {
+ const char* name;
+ float rgb[3];
+ };
+
+ constexpr AccentPreset ACCENTS[] = {
+ { "Teal", { 0.302f, 0.714f, 0.769f } }, { "Sky", { 0.357f, 0.659f, 0.961f } },
+ { "Indigo", { 0.486f, 0.486f, 0.941f } }, { "Violet", { 0.690f, 0.518f, 0.902f } },
+ { "Rose", { 0.886f, 0.478f, 0.604f } }, { "Coral", { 0.941f, 0.541f, 0.365f } },
+ { "Amber", { 0.910f, 0.690f, 0.290f } }, { "Mint", { 0.373f, 0.788f, 0.561f } },
+ };
+
+ constexpr float CARD_PAD_X = 14.0f;
+ constexpr float CARD_PAD_Y = 4.0f;
+ constexpr float ROW_PAD_Y = 7.0f;
+
+ // the window, rail and controls grow with the ui scale, since imgui only
+ // scales the fonts and would otherwise leave them cramped at 1.5x
+ auto ui_scale() -> float
+ {
+ return ImGui::GetStyle().FontScaleMain;
+ }
+
+ auto slider_width() -> float
+ {
+ return 190.0f * ui_scale();
+ }
+
+ auto rgb(const float (&c)[3]) -> ImVec4
+ {
+ return ImVec4(c[0], c[1], c[2], 1.0f);
+ }
+
+ auto u32(const ImVec4& colour) -> ImU32
+ {
+ return ImGui::ColorConvertFloat4ToU32(colour);
+ }
+
+ auto card_rounding() -> float
+ {
+ return ImGui::GetStyle().FrameRounding + 3.0f;
+ }
+
+ // a card is drawn after its rows so it can wrap them: rows go to the upper
+ // channel, the card body to the lower one once the height is known
+ struct Card
+ {
+ ImVec2 origin;
+ float width = 0.0f;
+ int rows = 0;
+ ImDrawListSplitter splitter;
+ };
+
+ Card g_card;
+
+ auto begin_card() -> void
+ {
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ g_card.origin = ImGui::GetCursorScreenPos();
+ g_card.width = ImGui::GetContentRegionAvail().x;
+ g_card.rows = 0;
+ g_card.splitter.Split(draw, 2);
+ g_card.splitter.SetCurrentChannel(draw, 1);
+ ImGui::SetCursorScreenPos(ImVec2(g_card.origin.x + CARD_PAD_X, g_card.origin.y + CARD_PAD_Y));
+ }
+
+ auto end_card() -> void
+ {
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ const ImVec2 end(g_card.origin.x + g_card.width, ImGui::GetCursorScreenPos().y + CARD_PAD_Y);
+
+ g_card.splitter.SetCurrentChannel(draw, 0);
+ draw->AddRectFilled(g_card.origin, end, ImGui::GetColorU32(ImGuiCol_FrameBg), card_rounding());
+ draw->AddRect(g_card.origin, end, ImGui::GetColorU32(ImGuiCol_Border), card_rounding());
+ g_card.splitter.Merge(draw);
+
+ ImGui::SetCursorScreenPos(ImVec2(g_card.origin.x, end.y));
+ ImGui::Dummy(ImVec2(g_card.width, 0.0f));
+ ImGui::Spacing();
+ }
+
+ // one setting: title and dim description on the left, the control pinned
+ // to the right edge, both centred on the row; a hairline rules rows apart
+ template <typename Control>
+ auto setting_row(const char* title, const char* description, float control_width,
+ float control_height, Control&& control) -> void
+ {
+ const float base = ImGui::GetStyle().FontSizeBase;
+ const float title_h = ImGui::GetTextLineHeight();
+ const float desc_h = description != nullptr ? std::floor(base * 0.88f) + 3.0f : 0.0f;
+ const float row_h = std::max(title_h + desc_h, control_height) + ROW_PAD_Y * 2.0f;
+ const float inner = g_card.width - CARD_PAD_X * 2.0f;
+ const ImVec2 top(g_card.origin.x + CARD_PAD_X, ImGui::GetCursorScreenPos().y);
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+
+ if (g_card.rows > 0)
+ draw->AddLine(ImVec2(top.x, top.y - 0.5f), ImVec2(top.x + inner, top.y - 0.5f),
+ ImGui::GetColorU32(ImGuiCol_Border, 0.6f));
+
+ ImGui::SetCursorScreenPos(ImVec2(top.x, top.y + (row_h - title_h - desc_h) * 0.5f));
+ ImGui::TextUnformatted(title);
+ if (description != nullptr)
+ {
+ ImGui::SetCursorScreenPos(ImVec2(top.x, ImGui::GetCursorScreenPos().y));
+ ImGui::PushFont(nullptr, base * 0.88f);
+ ImGui::TextDisabled("%s", description);
+ ImGui::PopFont();
+ }
+
+ ImGui::SetCursorScreenPos(ImVec2(top.x + inner - control_width, top.y + (row_h - control_height) * 0.5f));
+ ImGui::PushID(title);
+ control();
+ ImGui::PopID();
+
+ ImGui::SetCursorScreenPos(ImVec2(top.x, top.y + row_h));
+ ImGui::Dummy(ImVec2(inner, 0.0f));
+ ++g_card.rows;
+ }
+
+ auto toggle_row(const char* title, const char* description, bool* value) -> bool
+ {
+ bool changed = false;
+ const float h = ImGui::GetFrameHeight() * 0.78f;
+ setting_row(title, description, h * 1.8f, h, [&] { changed = Widgets::toggle("##toggle", value); });
+ return changed;
+ }
+
+ auto slider_row(const char* title, const char* description, float* value, float min, float max,
+ const char* fmt) -> bool
+ {
+ bool changed = false;
+ setting_row(title, description, slider_width(), ImGui::GetFrameHeight(), [&]
+ {
+ ImGui::SetNextItemWidth(slider_width());
+ changed = Widgets::slider_float("##slider", value, min, max, fmt);
+ });
+ return changed;
+ }
+
+ auto color_row(const char* title, const char* description, float (&value)[3]) -> bool
+ {
+ bool changed = false;
+ const float h = ImGui::GetFrameHeight();
+ setting_row(title, description, h, h, [&]
+ {
+ changed = ImGui::ColorEdit3("##colour", value, ImGuiColorEditFlags_NoInputs |
+ ImGuiColorEditFlags_NoAlpha |
+ ImGuiColorEditFlags_NoLabel);
+ });
+ return changed;
+ }
+
+ // a keycap: the key's name in a small sunken box, as on a keyboard legend
+ auto key_cap(const char* text) -> void
+ {
+ const ImGuiStyle& style = ImGui::GetStyle();
+ const ImVec2 size = ImGui::CalcTextSize(text);
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+ const ImVec2 end(p.x + size.x + 16.0f, p.y + ImGui::GetFrameHeight());
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ draw->AddRectFilled(p, end, ImGui::GetColorU32(ImGuiCol_WindowBg), style.FrameRounding);
+ draw->AddRect(p, end, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding);
+ draw->AddText(ImVec2(p.x + 8.0f, p.y + style.FramePadding.y), ImGui::GetColorU32(ImGuiCol_Text), text);
+ ImGui::Dummy(ImVec2(end.x - p.x, end.y - p.y));
+ }
+
+ auto key_row(const char* title, const char* description, const char* key) -> void
+ {
+ const float width = ImGui::CalcTextSize(key).x + 16.0f;
+ setting_row(title, description, width, ImGui::GetFrameHeight(), [&] { key_cap(key); });
+ }
+
+ // a read-only fact; a long value is cut from the left and shown whole on hover
+ auto value_row(const char* title, std::string value) -> void
+ {
+ const float inner = g_card.width - CARD_PAD_X * 2.0f;
+ const float limit = inner * 0.58f;
+ const std::string full = value;
+ while (value.size() > 4 && ImGui::CalcTextSize(value.c_str()).x > limit)
+ value = "…" + value.substr(4);
+
+ setting_row(title, nullptr, ImGui::CalcTextSize(value.c_str()).x, ImGui::GetTextLineHeight(), [&]
+ {
+ ImGui::TextDisabled("%s", value.c_str());
+ if (value != full && ImGui::IsItemHovered())
+ ImGui::SetTooltip("%s", full.c_str());
+ });
+ }
+
+ // a theme card: a tiny window in the theme's own palette over its name; the
+ // chosen one is ringed in the accent. returns true on click.
+ auto theme_card(const ThemeEntry& entry, const Theme& theme, bool selected, ImVec2 size) -> bool
+ {
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+ ImGui::InvisibleButton(entry.id.c_str(), size);
+ const bool hovered = ImGui::IsItemHovered();
+ const bool clicked = ImGui::IsItemClicked();
+
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ const float r = card_rounding();
+ const ImVec2 end(p.x + size.x, p.y + size.y);
+ draw->AddRectFilled(p, end, ImGui::GetColorU32(ImGuiCol_FrameBg), r);
+
+ // the mock: title stripe, sidebar, a few lines of "code", an accent pill
+ const ImVec2 m0(p.x + 6.0f, p.y + 6.0f);
+ const ImVec2 m1(end.x - 6.0f, end.y - 24.0f);
+ draw->AddRectFilled(m0, m1, u32(theme.bg), 4.0f);
+ draw->AddRectFilled(m0, ImVec2(m1.x, m0.y + 7.0f), u32(theme.bg_low), 4.0f, ImDrawFlags_RoundCornersTop);
+ draw->AddRectFilled(ImVec2(m0.x, m0.y + 7.0f), ImVec2(m0.x + 26.0f, m1.y), u32(theme.bg_high), 4.0f,
+ ImDrawFlags_RoundCornersBottomLeft);
+ for (int i = 0; i < 3; ++i)
+ draw->AddRectFilled(ImVec2(m0.x + 5.0f, m0.y + 13.0f + static_cast<float>(i) * 6.0f),
+ ImVec2(m0.x + 17.0f + static_cast<float>(i % 2) * 4.0f,
+ m0.y + 15.0f + static_cast<float>(i) * 6.0f),
+ u32(theme.text_dim));
+
+ struct Line { float indent, width; ImVec4 colour; };
+ const Line lines[] = {
+ { 0.0f, 22.0f, theme.syntax_preprocessor }, { 0.0f, 40.0f, theme.syntax_comment },
+ { 0.0f, 18.0f, theme.syntax_keyword }, { 8.0f, 36.0f, theme.text },
+ { 8.0f, 26.0f, theme.syntax_string }, { 0.0f, 12.0f, theme.text },
+ };
+ float y = m0.y + 13.0f;
+ for (const Line& line : lines)
+ {
+ if (y + 2.0f > m1.y - 4.0f)
+ break;
+ draw->AddRectFilled(ImVec2(m0.x + 32.0f + line.indent, y),
+ ImVec2(m0.x + 32.0f + line.indent + line.width, y + 2.0f), u32(line.colour));
+ y += 6.0f;
+ }
+ draw->AddRectFilled(ImVec2(m1.x - 24.0f, m1.y - 10.0f), ImVec2(m1.x - 5.0f, m1.y - 4.0f),
+ u32(theme.accent), 3.0f);
+
+ const ImVec2 ts = ImGui::CalcTextSize(entry.name.c_str());
+ draw->PushClipRect(ImVec2(p.x + 4.0f, m1.y), ImVec2(end.x - 4.0f, end.y), true);
+ draw->AddText(ImVec2(p.x + std::max((size.x - ts.x) * 0.5f, 4.0f), m1.y + (24.0f - ts.y) * 0.5f),
+ ImGui::GetColorU32(selected ? ImGuiCol_Text : ImGuiCol_TextDisabled), entry.name.c_str());
+ draw->PopClipRect();
+
+ if (selected)
+ draw->AddRect(p, end, u32(Widgets::accent()), r, 0, 2.0f);
+ else
+ draw->AddRect(p, end, ImGui::GetColorU32(hovered ? ImGuiCol_TextDisabled : ImGuiCol_Border), r);
+
+ return clicked;
+ }
+
+ auto same_colour(const float (&a)[3], const float (&b)[3]) -> bool
+ {
+ return std::abs(a[0] - b[0]) < 0.02f && std::abs(a[1] - b[1]) < 0.02f && std::abs(a[2] - b[2]) < 0.02f;
+ }
+
+ auto gl_renderer() -> std::string
+ {
+ const GLubyte* text = glGetString(GL_RENDERER);
+ return text != nullptr ? reinterpret_cast<const char*>(text) : "unknown";
+ }
+ }
+
+ 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, and load each palette for its card
+ if (!m_prefs_open_prev)
+ {
+ m_themes = list_themes(themes_directory());
+ m_theme_previews.clear();
+ std::ranges::transform(m_themes, std::back_inserter(m_theme_previews),
+ [](const ThemeEntry& entry) { return load_theme(entry.path); });
+ }
+ 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);
+
+ const float scale = ui_scale();
+ ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
+ ImGui::SetNextWindowSize(ImVec2(860.0f * scale, 600.0f * scale), ImGuiCond_Appearing);
+ ImGui::SetNextWindowSizeConstraints(ImVec2(660.0f * scale, 440.0f * scale), ImVec2(FLT_MAX, FLT_MAX));
+
+ if (!ImGui::Begin(ICON_PH_GEAR " Preferences###preferences", &m_show_preferences,
+ ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse))
+ {
+ ImGui::End();
+ return;
+ }
+
+ const float base = ImGui::GetStyle().FontSizeBase;
+ bool changed = false;
+ bool restyle = false;
+
+ const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
+
+ // the rail: one roomy row per category on the deepest ground
+ ImGui::PushStyleColor(ImGuiCol_ChildBg, m_theme.bg_low);
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(7.0f, 12.0f));
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 8.0f));
+ ImGui::BeginChild("##pref_categories", ImVec2(180.0f * scale, -footer), ImGuiChildFlags_AlwaysUseWindowPadding);
+ 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::PopStyleVar(2);
+ ImGui::PopStyleColor();
+
+ ImGui::SameLine();
+
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 12.0f));
+ ImGui::BeginChild("##pref_content", ImVec2(0.0f, -footer), ImGuiChildFlags_AlwaysUseWindowPadding);
+ ImGui::PopStyleVar();
+
+ const int tab = std::clamp(m_preferences_tab, 0, IM_ARRAYSIZE(CATEGORIES) - 1);
+ ImGui::PushFont(m_font_strong, base * 1.4f);
+ ImGui::TextUnformatted(CATEGORIES[tab]);
+ ImGui::PopFont();
+ ImGui::TextDisabled("%s", CATEGORY_BLURBS[tab]);
+ ImGui::Spacing();
+
+ if (tab == 0)
+ {
+ Widgets::section_header("Theme", false);
+
+ // dark and light themes in their own rows, four cards across; picking
+ // one seeds the accent and syntax colours below and saves
+ constexpr int columns = 4;
+ const float gap = 8.0f;
+ const float card_w = std::floor((ImGui::GetContentRegionAvail().x - gap * (columns - 1)) / columns);
+ const ImVec2 card_size(card_w, 72.0f + 20.0f * scale);
+
+ const auto is_light = [](const Theme& theme)
+ {
+ return 0.299f * theme.bg.x + 0.587f * theme.bg.y + 0.114f * theme.bg.z > 0.5f;
+ };
+
+ for (const bool light : { false, true })
+ {
+ int shown = 0;
+ for (size_t index = 0; index < m_themes.size(); ++index)
+ {
+ if (is_light(m_theme_previews[index]) != light)
+ continue;
+
+ if (shown == 0)
+ {
+ ImGui::PushFont(nullptr, base * 0.85f);
+ ImGui::TextDisabled("%s", light ? "Light" : "Dark");
+ ImGui::PopFont();
+ }
+ else if (shown % columns != 0)
+ {
+ ImGui::SameLine(0.0f, gap);
+ }
+
+ const ThemeEntry& entry = m_themes[index];
+ const bool selected = entry.id == m_preferences.theme;
+ if (theme_card(entry, m_theme_previews[index], selected, card_size) && !selected)
+ select_theme(entry);
+ ++shown;
+ }
+
+ if (shown > 0)
+ ImGui::Spacing();
+ }
+ if (m_themes.empty())
+ ImGui::TextDisabled("No themes found in assets/themes");
+
+ ImGui::Spacing();
+ Widgets::section_header("Accent", false);
+
+ for (const AccentPreset& preset : ACCENTS)
+ {
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+ ImGui::InvisibleButton(preset.name, ImVec2(26.0f, 26.0f));
+ const ImVec2 c(p.x + 13.0f, p.y + 13.0f);
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ draw_list->AddCircleFilled(c, ImGui::IsItemHovered() ? 11.0f : 10.0f, u32(rgb(preset.rgb)));
+ if (same_colour(preset.rgb, m_preferences.accent))
+ draw_list->AddCircle(c, 12.5f, ImGui::GetColorU32(ImGuiCol_Text), 0, 2.0f);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("%s", preset.name);
+ if (ImGui::IsItemClicked())
+ {
+ std::copy(std::begin(preset.rgb), std::end(preset.rgb), std::begin(m_preferences.accent));
+ changed = true;
+ restyle = true;
+ }
+ ImGui::SameLine(0.0f, 8.0f);
+ }
+
+ if (ImGui::ColorEdit3("##accent_custom", m_preferences.accent,
+ ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha |
+ ImGuiColorEditFlags_NoLabel))
+ {
+ changed = true;
+ restyle = true;
+ }
+ ImGui::SameLine();
+ ImGui::TextDisabled("Custom");
+
+ ImGui::Spacing();
+ Widgets::section_header("Interface", false);
+ begin_card();
+ changed |= slider_row("UI scale", "Scales every font; text is re-rasterised, so it stays crisp",
+ &m_preferences.ui_scale, 0.75f, 2.0f, "%.2fx");
+ if (slider_row("Corner rounding", "Radius on frames, windows, tabs and buttons",
+ &m_preferences.rounding, 0.0f, 12.0f, "%.0f px"))
+ {
+ changed = true;
+ restyle = true;
+ }
+ changed |= toggle_row("Show FPS in the status bar", "hsdbg's own frame rate, bottom right",
+ &m_preferences.show_fps);
+ end_card();
+
+ Widgets::section_header("Layout", false);
+ begin_card();
+ {
+ const float w = ImGui::CalcTextSize("Reset layout").x + ImGui::GetStyle().FramePadding.x * 2.0f;
+ setting_row("Window layout", "Put every panel back where a fresh install has it", w,
+ ImGui::GetFrameHeight(), [&]
+ {
+ if (ImGui::Button("Reset layout"))
+ m_layout_built = false;
+ });
+ }
+ end_card();
+
+ Widgets::section_header("Mascot", false);
+ begin_card();
+ changed |= toggle_row("Show the crying pepe", "He lives in the toolbar and means well",
+ &m_preferences.show_mascot);
+ ImGui::BeginDisabled(!m_preferences.show_mascot);
+ changed |= slider_row("Pepe size", "As a multiple of a toolbar button", &m_preferences.mascot_scale,
+ 1.0f, 3.0f, "%.1fx");
+ ImGui::EndDisabled();
+ end_card();
+ }
+ else if (tab == 1)
+ {
+ Widgets::section_header("Font", false);
+ begin_card();
+ changed |= slider_row("Source font size", "Cmd/Ctrl + scroll over the source zooms it too; Cmd/Ctrl + 0 resets",
+ &m_preferences.source_font_size, 9.0f, 32.0f, "%.0f px");
+ end_card();
+
+ Widgets::section_header("Source view", false);
+ begin_card();
+ changed |= toggle_row("Syntax highlighting", "Colour keywords, types, strings and the rest",
+ &m_preferences.syntax_highlighting);
+ changed |= toggle_row("Show line numbers", nullptr, &m_preferences.show_line_numbers);
+ changed |= toggle_row("Highlight the current line", "Tint the line execution has stopped on",
+ &m_preferences.highlight_current_line);
+ end_card();
+
+ Widgets::section_header("Syntax colours", false);
+
+ // a live sample in the source face and size, coloured as the source
+ // view would colour it right now
+ {
+ struct Token { const char* text; int kind; };
+ const Token sample[][6] = {
+ { { "#include", 6 }, { " ", 0 }, { "<cstdio>", 3 }, { nullptr, 0 } },
+ { { "// the answer, as a function", 5 }, { nullptr, 0 } },
+ { { "auto", 2 }, { " answer(", 0 }, { "int", 2 }, { " base) -> ", 0 }, { "int", 2 }, { nullptr, 0 } },
+ { { "{", 0 }, { nullptr, 0 } },
+ { { " ", 0 }, { "const", 1 }, { " ", 0 }, { "char", 2 }, { "* text = ", 0 }, { "\"hello\";", 3 } },
+ { { " ", 0 }, { "return", 1 }, { " base * ", 0 }, { "42", 4 }, { ";", 0 }, { nullptr, 0 } },
+ { { "}", 0 }, { nullptr, 0 } },
+ };
+ const float (*colours[])[3] = {
+ nullptr, &m_preferences.color_keyword, &m_preferences.color_type, &m_preferences.color_string,
+ &m_preferences.color_number, &m_preferences.color_comment, &m_preferences.color_preprocessor,
+ };
+
+ ImGui::PushFont(m_font_mono, m_preferences.source_font_size);
+ const float line_h = ImGui::GetTextLineHeight() + 4.0f;
+ const float pad = 12.0f;
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+ const ImVec2 size(ImGui::GetContentRegionAvail().x, line_h * IM_ARRAYSIZE(sample) + pad * 2.0f);
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ draw_list->AddRectFilled(p, ImVec2(p.x + size.x, p.y + size.y), u32(m_theme.bg_low), card_rounding());
+ draw_list->AddRect(p, ImVec2(p.x + size.x, p.y + size.y), ImGui::GetColorU32(ImGuiCol_Border), card_rounding());
+
+ const int current = 5;
+ if (m_preferences.highlight_current_line)
+ {
+ const float y = p.y + pad + line_h * current - 2.0f;
+ draw_list->AddRectFilled(ImVec2(p.x + 1.0f, y), ImVec2(p.x + size.x - 1.0f, y + line_h),
+ u32(rgb(m_preferences.color_current_line)));
+ draw_list->AddRectFilled(ImVec2(p.x + 1.0f, y), ImVec2(p.x + 4.0f, y + line_h), u32(Widgets::accent()));
+ }
+
+ for (int line = 0; line < IM_ARRAYSIZE(sample); ++line)
+ {
+ float x = p.x + pad + 8.0f;
+ const float y = p.y + pad + line_h * static_cast<float>(line);
+ for (const Token& token : sample[line])
+ {
+ if (token.text == nullptr)
+ break;
+ const bool plain = token.kind == 0 || !m_preferences.syntax_highlighting;
+ const ImU32 colour = plain ? ImGui::GetColorU32(ImGuiCol_Text) : u32(rgb(*colours[token.kind]));
+ draw_list->AddText(ImVec2(x, y), colour, token.text);
+ x += ImGui::CalcTextSize(token.text).x;
+ }
+ }
+ ImGui::PopFont();
+ ImGui::Dummy(size);
+ ImGui::Spacing();
+ }
+
+ begin_card();
+ ImGui::BeginDisabled(!m_preferences.syntax_highlighting);
+ changed |= color_row("Keyword", "const, return, if, while…", m_preferences.color_keyword);
+ changed |= color_row("Type", "int, auto, char, size_t…", m_preferences.color_type);
+ changed |= color_row("String", "String and character literals, includes", m_preferences.color_string);
+ changed |= color_row("Number", nullptr, m_preferences.color_number);
+ changed |= color_row("Comment", nullptr, m_preferences.color_comment);
+ changed |= color_row("Preprocessor", "#include, #define and friends", m_preferences.color_preprocessor);
+ ImGui::EndDisabled();
+ changed |= color_row("Current line", "Behind the line execution has stopped on", m_preferences.color_current_line);
+ end_card();
+ }
+ else if (tab == 2)
+ {
+ Widgets::section_header("Launching", false);
+ begin_card();
+ changed |= toggle_row("Break at entry point", "Stop on the very first instruction instead of running to your breakpoints",
+ &m_preferences.stop_at_entry);
+ end_card();
+
+ Widgets::section_header("Stepping", false);
+ begin_card();
+ {
+ static const char* const step_modes[] = { "Line", "Instruction" };
+ const int current_mode = m_preferences.step_by_instruction ? 1 : 0;
+ int next_mode = current_mode;
+ setting_row("Step granularity", "What Step Over and Step Into advance by",
+ Widgets::segmented_width(step_modes, 2), ImGui::GetFrameHeight(), [&]
+ {
+ next_mode = Widgets::segmented("##step_mode", step_modes, 2, current_mode);
+ });
+ if (next_mode != current_mode)
+ {
+ m_preferences.step_by_instruction = next_mode == 1;
+ changed = true;
+ }
+ }
+ end_card();
+
+ Widgets::section_header("Keys", false);
+ begin_card();
+ key_row("Continue, or run a loaded target", nullptr, "F5");
+ key_row("Step Over", nullptr, "F10");
+ key_row("Step Into", nullptr, "F11");
+ key_row("Step Out", nullptr, "Shift + F11");
+ key_row("Command palette", "Files, symbols and every command, fuzzy-matched", "Cmd/Ctrl + K");
+ key_row("Zoom the source", "Over the source view", "Cmd/Ctrl + scroll");
+ end_card();
+ }
+ else
+ {
+ ImGui::PushFont(m_font_strong, base * 2.0f);
+ ImGui::TextUnformatted("hsdbg");
+ ImGui::PopFont();
+ ImGui::SameLine(0.0f, 10.0f);
+ ImGui::AlignTextToFramePadding();
+ Widgets::chip(HSDBG_VERSION, u32(Widgets::accent()));
+ ImGui::TextDisabled("A GUI debugger for C and C++. LLDB does the debugging; this is the ui.");
+ ImGui::Spacing();
+
+ Widgets::section_header("Build", false);
+ begin_card();
+ value_row("LLDB", Debugger::backend_version());
+ value_row("Dear ImGui", IMGUI_VERSION);
+ value_row("OpenGL", gl_renderer());
+ value_row("Fonts", "Nunito, JetBrains Mono, Phosphor");
+ end_card();
+
+ Widgets::section_header("Paths", false);
+ begin_card();
+ value_row("Assets", HSDBG_ASSET_DIR);
+ value_row("Preferences", m_preferences_path.string());
+ value_row("Layout", ImGui::GetIO().IniFilename != nullptr ? ImGui::GetIO().IniFilename : "none");
+ end_card();
+ }
+
+ 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::AlignTextToFramePadding();
+ ImGui::TextDisabled("Saved to %s as you change things", m_preferences_path.filename().string().c_str());
+
+ if (changed)
+ save_preferences(m_preferences_path, m_preferences);
+
+ if (restyle)
+ m_restyle_pending = true;
+
+ ImGui::End();
+ }
+}