aboutsummaryrefslogtreecommitdiff
path: root/src/ui/widgets.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/widgets.cpp
parent0846724177d823b632f2a4deffdcb5b6e226cd56 (diff)
feat: improve layout and themes
Diffstat (limited to 'src/ui/widgets.cpp')
-rw-r--r--src/ui/widgets.cpp178
1 files changed, 142 insertions, 36 deletions
diff --git a/src/ui/widgets.cpp b/src/ui/widgets.cpp
index 68f4875..7172f88 100644
--- a/src/ui/widgets.cpp
+++ b/src/ui/widgets.cpp
@@ -1,7 +1,9 @@
#include "ui/widgets.h"
#include <imgui_internal.h>
+#include <misc/cpp/imgui_stdlib.h>
+#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
@@ -15,16 +17,11 @@ namespace Hsdbg::Widgets
ImFont* g_strong = nullptr;
[[maybe_unused]] ImFont* g_mono = nullptr;
- auto accent_color() -> ImVec4
- {
- return ImGui::GetStyle().Colors[ImGuiCol_CheckMark];
- }
-
// a colour that reads on top of the accent: dark ink on a light accent,
// light ink on a dark one, so on-accent labels stay legible in any theme
auto accent_ink() -> ImU32
{
- const ImVec4 a = accent_color();
+ const ImVec4 a = accent();
const float luma = 0.299f * a.x + 0.587f * a.y + 0.114f * a.z;
return luma > 0.6f ? IM_COL32(12, 16, 20, 255) : IM_COL32(245, 247, 250, 255);
}
@@ -34,6 +31,14 @@ namespace Hsdbg::Widgets
colour.w = alpha;
return ImGui::ColorConvertFloat4ToU32(colour);
}
+
+ // a track's resting fill: the frame colour plus a whisper of the text
+ // colour, so it still reads when drawn over a card that is frame-coloured
+ auto track_fill(ImDrawList* draw, ImVec2 min, ImVec2 max, float rounding) -> void
+ {
+ draw->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_FrameBg), rounding);
+ draw->AddRectFilled(min, max, with_alpha(ImGui::GetStyle().Colors[ImGuiCol_Text], 0.07f), rounding);
+ }
}
auto set_fonts(ImFont* ui, ImFont* strong, ImFont* mono) -> void
@@ -43,7 +48,12 @@ namespace Hsdbg::Widgets
g_mono = mono;
}
- auto section_header(const char* label) -> void
+ auto accent() -> ImVec4
+ {
+ return ImGui::GetStyle().Colors[ImGuiCol_CheckMark];
+ }
+
+ auto section_header(const char* label, bool rule) -> void
{
std::string caps;
caps.reserve(24);
@@ -65,7 +75,7 @@ namespace Hsdbg::Widgets
const ImVec2 hi = ImGui::GetItemRectMax();
const float cy = (lo.y + hi.y) * 0.5f;
const float right = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
- if (right > hi.x + 10.0f)
+ if (rule && right > hi.x + 10.0f)
ImGui::GetWindowDrawList()->AddLine(ImVec2(hi.x + 8.0f, cy), ImVec2(right, cy),
ImGui::GetColorU32(ImGuiCol_Border), 1.0f);
}
@@ -74,7 +84,15 @@ namespace Hsdbg::Widgets
{
ImGui::PushID(label);
- const float width = ImGui::CalcItemWidth();
+ // the value column is sized for either end of the range, so the grab
+ // never sits on top of the number
+ char lo[64];
+ char hi[64];
+ std::snprintf(lo, sizeof(lo), fmt, static_cast<double>(min));
+ std::snprintf(hi, sizeof(hi), fmt, static_cast<double>(max));
+ const float value_w = std::max(ImGui::CalcTextSize(lo).x, ImGui::CalcTextSize(hi).x);
+ const float gap = 10.0f;
+ const float width = std::max(ImGui::CalcItemWidth() - value_w - gap, 40.0f);
const float height = ImGui::GetFrameHeight();
const ImVec2 p = ImGui::GetCursorScreenPos();
@@ -94,33 +112,38 @@ namespace Hsdbg::Widgets
const float ty = p.y + (height - th) * 0.5f;
const float frac = (max > min) ? ImClamp((*value - min) / (max - min), 0.0f, 1.0f) : 0.0f;
const float gx = p.x + frac * width;
- const ImU32 accent = ImGui::ColorConvertFloat4ToU32(accent_color());
+ const ImU32 fill = ImGui::ColorConvertFloat4ToU32(accent());
- draw->AddRectFilled(ImVec2(p.x, ty), ImVec2(p.x + width, ty + th),
- ImGui::GetColorU32(ImGuiCol_FrameBg), th * 0.5f);
- draw->AddRectFilled(ImVec2(p.x, ty), ImVec2(gx, ty + th), accent, th * 0.5f);
+ track_fill(draw, ImVec2(p.x, ty), ImVec2(p.x + width, ty + th), th * 0.5f);
+ draw->AddRectFilled(ImVec2(p.x, ty), ImVec2(gx, ty + th), fill, th * 0.5f);
const float r = (active || hovered) ? height * 0.30f : height * 0.26f;
- draw->AddCircleFilled(ImVec2(gx, p.y + height * 0.5f), r, accent);
+ draw->AddCircleFilled(ImVec2(gx, p.y + height * 0.5f), r, fill);
draw->AddCircleFilled(ImVec2(gx, p.y + height * 0.5f), r * 0.42f, accent_ink());
char buf[64];
std::snprintf(buf, sizeof(buf), fmt, static_cast<double>(*value));
const ImVec2 ts = ImGui::CalcTextSize(buf);
- draw->AddText(ImVec2(p.x + (width - ts.x) * 0.5f, p.y + (height - ts.y) * 0.5f),
+ draw->AddText(ImVec2(p.x + width + gap + value_w - ts.x, p.y + (height - ts.y) * 0.5f),
ImGui::GetColorU32(ImGuiCol_Text), buf);
+ ImGui::SameLine(0.0f, 0.0f);
+ ImGui::Dummy(ImVec2(gap + value_w, height));
+
ImGui::PopID();
- ImGui::SameLine();
- ImGui::AlignTextToFramePadding();
- ImGui::TextUnformatted(label);
+ if (label[0] != '#' || label[1] != '#')
+ {
+ ImGui::SameLine();
+ ImGui::AlignTextToFramePadding();
+ ImGui::TextUnformatted(label);
+ }
return *value != before;
}
auto icon_button(const char* id, const char* icon, bool active, const char* tooltip,
- bool disabled, float size) -> bool
+ bool disabled, float size, ImVec4 glyph) -> bool
{
if (disabled)
ImGui::BeginDisabled();
@@ -129,7 +152,7 @@ namespace Hsdbg::Widgets
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 10.0f);
if (active)
- ImGui::PushStyleColor(ImGuiCol_Button, accent_color());
+ ImGui::PushStyleColor(ImGuiCol_Button, accent());
// Button draws the bg + handles the click; the glyph is drawn by hand
const ImVec2 origin = ImGui::GetCursorScreenPos();
@@ -141,7 +164,9 @@ namespace Hsdbg::Widgets
// centre on the glyph's visual bounds so an asymmetric glyph (the play
// triangle) sits dead-centre, not offset by its advance box or line-gap
- const ImU32 glyph_col = active ? accent_ink() : ImGui::GetColorU32(ImGuiCol_Text);
+ const bool tinted = glyph.w > 0.0f && !disabled && !active;
+ const ImU32 glyph_col = active ? accent_ink()
+ : ImGui::GetColorU32(tinted ? glyph : ImGui::GetStyleColorVec4(ImGuiCol_Text));
const float cx = origin.x + size * 0.5f;
const float cy = origin.y + size * 0.5f;
@@ -186,12 +211,15 @@ namespace Hsdbg::Widgets
const float radius = height * 0.5f;
const ImVec2 end(pos.x + width, pos.y + height);
- const ImU32 track = *value ? ImGui::ColorConvertFloat4ToU32(accent_color())
- : ImGui::GetColorU32(ImGuiCol_FrameBg);
- draw->AddRectFilled(pos, end, track, radius);
- if (!*value)
- draw->AddRect(pos, end, ImGui::GetColorU32(hovered ? ImGuiCol_Border : ImGuiCol_FrameBg),
- radius);
+ if (*value)
+ {
+ draw->AddRectFilled(pos, end, ImGui::ColorConvertFloat4ToU32(accent()), radius);
+ }
+ else
+ {
+ track_fill(draw, pos, end, radius);
+ draw->AddRect(pos, end, ImGui::GetColorU32(hovered ? ImGuiCol_TextDisabled : ImGuiCol_Border), radius);
+ }
const float knob = radius - 2.5f;
const float knob_x = *value ? (end.x - radius) : (pos.x + radius);
@@ -201,6 +229,14 @@ namespace Hsdbg::Widgets
return clicked;
}
+ auto segmented_width(const char* const* labels, int count) -> float
+ {
+ float total = 4.0f;
+ for (int i = 0; i < count; ++i)
+ total += ImGui::CalcTextSize(labels[i]).x + 8.0f * 2.0f + 2.0f;
+ return total;
+ }
+
auto segmented(const char* id, const char* const* labels, int count, int current) -> int
{
const ImGuiStyle& style = ImGui::GetStyle();
@@ -209,14 +245,9 @@ namespace Hsdbg::Widgets
const float height = ImGui::GetFrameHeight();
const float pad = 8.0f;
const ImVec2 origin = ImGui::GetCursorScreenPos();
+ const float total = segmented_width(labels, count);
- // measure so the track wraps the labels exactly
- float total = 4.0f;
- for (int i = 0; i < count; ++i)
- total += ImGui::CalcTextSize(labels[i]).x + pad * 2.0f + 2.0f;
-
- const ImU32 track = ImGui::GetColorU32(ImGuiCol_FrameBg);
- draw->AddRectFilled(origin, ImVec2(origin.x + total, origin.y + height), track, height * 0.5f);
+ track_fill(draw, origin, ImVec2(origin.x + total, origin.y + height), height * 0.5f);
int result = current;
ImGui::PushID(id);
@@ -276,12 +307,12 @@ namespace Hsdbg::Widgets
ImVec2 max = ImGui::GetItemRectMax();
min.x += 2.0f;
max.x -= 2.0f;
- const ImU32 fill = selected ? with_alpha(accent_color(), 0.18f)
+ const ImU32 fill = selected ? with_alpha(accent(), 0.18f)
: with_alpha(ImGui::GetStyle().Colors[ImGuiCol_Text], 0.055f);
split.SetCurrentChannel(draw, 0);
draw->AddRectFilled(min, max, fill, 7.0f);
if (selected)
- draw->AddRect(min, max, with_alpha(accent_color(), 0.40f), 7.0f);
+ draw->AddRect(min, max, with_alpha(accent(), 0.40f), 7.0f);
}
split.Merge(draw);
@@ -358,4 +389,79 @@ namespace Hsdbg::Widgets
return clicked;
}
+
+ auto input_with_icon(const char* id, const char* icon, const char* hint, std::string* text,
+ ImGuiInputTextFlags flags) -> bool
+ {
+ const ImGuiStyle& style = ImGui::GetStyle();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const float pad_x = style.FramePadding.x;
+ const float pad_y = style.FramePadding.y;
+
+ // widen the left padding to leave room for the glyph, then set it over
+ // the frame in the dim text colour
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
+ ImVec2(pad_x * 2.0f + ImGui::CalcTextSize(icon).x, pad_y));
+ const bool changed = ImGui::InputTextWithHint(id, hint, text, flags);
+ ImGui::PopStyleVar();
+
+ ImGui::GetWindowDrawList()->AddText(ImVec2(pos.x + pad_x, pos.y + pad_y),
+ ImGui::GetColorU32(ImGuiCol_TextDisabled), icon);
+ return changed;
+ }
+
+ auto dot_toggle(const char* id, bool* value, ImU32 on, ImU32 off, bool pending) -> bool
+ {
+ const float s = ImGui::GetFrameHeight();
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+
+ ImGui::InvisibleButton(id, ImVec2(s, s));
+ const bool clicked = ImGui::IsItemClicked();
+ if (clicked)
+ *value = !*value;
+
+ const ImVec2 centre(p.x + s * 0.5f, p.y + s * 0.5f);
+ const ImU32 colour = *value ? on : off;
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ if (pending)
+ draw->AddCircle(centre, 5.0f, colour, 0, 1.5f);
+ else
+ draw->AddCircleFilled(centre, 5.0f, colour);
+
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("%s", *value ? "Disable" : "Enable");
+
+ return clicked;
+ }
+
+ auto hex_value(unsigned long long value, int digits) -> void
+ {
+ char buf[32];
+ std::snprintf(buf, sizeof(buf), "%0*llx", digits, value);
+
+ int zeros = 0;
+ while (zeros < digits - 1 && buf[zeros] == '0')
+ ++zeros;
+
+ ImGui::TextDisabled("0x%.*s", zeros, buf);
+ ImGui::SameLine(0.0f, 0.0f);
+ ImGui::TextUnformatted(buf + zeros);
+ }
+
+ auto link_text(const char* text) -> bool
+ {
+ ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextLink));
+ ImGui::TextUnformatted(text);
+ ImGui::PopStyleColor();
+
+ if (!ImGui::IsItemHovered())
+ return false;
+
+ ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
+ const ImVec2 lo = ImGui::GetItemRectMin();
+ const ImVec2 hi = ImGui::GetItemRectMax();
+ ImGui::GetWindowDrawList()->AddLine(ImVec2(lo.x, hi.y - 1.0f), ImVec2(hi.x, hi.y - 1.0f),
+ ImGui::GetColorU32(ImGuiCol_TextLink));
+ return ImGui::IsMouseClicked(ImGuiMouseButton_Left);
+ }
}