chore(style): add a widget gallery for visual verification

Every widget in its states, reachable via /hellion widgets. The point is to
check them one at a time before they land in real components: the v2.x style
engine grew three primitives that were never wired to a call site
(DrawGlowBorder, DrawSlipPolygon, DrawHonorificHeader), and this is the cheap
way to notice that before a cycle closes.

DEBUG-only, like SeStringDebugger. It is a verification aid, not a feature, so
it never reaches a release build -- verified against a Release compile.

The header line also shows the live GlobalScale and the hover registry size,
which makes both the scaling work and the eviction contract observable while
dragging the Dalamud scale slider.
This commit is contained in:
2026-08-17 23:42:05 +02:00
parent d6ad81e8ca
commit 460217459c
4 changed files with 218 additions and 0 deletions
+13
View File
@@ -109,6 +109,9 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal static InputPreview InputPreview { get; private set; } = null!;
internal CommandHelpWindow CommandHelpWindow { get; private set; } = null!;
public SeStringDebugger SeStringDebugger { get; private set; } = null!;
#if DEBUG
internal Ui.Windows.WidgetGalleryWindow WidgetGallery { get; private set; } = null!;
#endif
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
internal DebuggerWindow DebuggerWindow { get; private set; } = null!;
@@ -365,6 +368,9 @@ public sealed class Plugin : IAsyncDalamudPlugin
InputPreview = _host.Services.GetRequiredService<InputPreview>();
CommandHelpWindow = _host.Services.GetRequiredService<CommandHelpWindow>();
SeStringDebugger = _host.Services.GetRequiredService<SeStringDebugger>();
#if DEBUG
WidgetGallery = _host.Services.GetRequiredService<Ui.Windows.WidgetGalleryWindow>();
#endif
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
@@ -925,6 +931,13 @@ public sealed class Plugin : IAsyncDalamudPlugin
SettingsWindow.Toggle();
return;
}
#if DEBUG
if (arg.Equals("widgets", StringComparison.OrdinalIgnoreCase))
{
WidgetGallery.Toggle();
return;
}
#endif
if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase))
{
// Recovery path documented in the v2.x master spec — drops a
+6
View File
@@ -340,6 +340,12 @@ internal static class PluginHostFactory
sp.GetRequiredService<ILogger<CommandHelpWindow>>()
));
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
#if DEBUG
services.AddSingleton(sp => new Ui.Windows.WidgetGalleryWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
#endif
services.AddSingleton(sp => new DebuggerWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<PayloadHandler>()
+3
View File
@@ -64,6 +64,9 @@ internal sealed class PluginLifecycle : IAsyncDisposable
plugin.WindowSystem.AddWindow(Plugin.InputPreview);
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
#if DEBUG
plugin.WindowSystem.AddWindow(plugin.WidgetGallery);
#endif
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
@@ -0,0 +1,196 @@
#if DEBUG
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Ui.StyleEngine.Widgets;
namespace HellionChat.Ui.Windows;
// Every widget in Ui/StyleEngine/Widgets shown in its states, so they can be
// checked one at a time before they land in real components. The v2.x style
// engine grew three primitives that were never wired to a call site
// (DrawGlowBorder, DrawSlipPolygon, DrawHonorificHeader) -- this is the cheap
// way to notice that before a cycle closes.
//
// DEBUG-only, like SeStringDebugger: it is a verification aid, not a feature.
internal sealed class WidgetGalleryWindow : Window
{
private readonly Plugin _plugin;
private readonly WidgetPalette _palette;
private int _badgeCount = 3;
private bool _rowActive = true;
internal WidgetGalleryWindow(Plugin plugin, TokenResolver resolver)
: base("Widget Gallery###hellion-widget-gallery")
{
_plugin = plugin;
_palette = new WidgetPalette(resolver);
Size = new Vector2(460, 560);
SizeCondition = ImGuiCond.FirstUseEver;
}
public override void Draw()
{
var c = _plugin.ThemeRegistry.Active.Colors;
ImGui.TextDisabled(
$"GlobalScale {Metrics.Scale:0.00} · hover entries {HoverState.TrackedCount}"
);
ImGui.Separator();
DrawRowSection(c);
DrawBadgeSection(c);
DrawPillSection(c);
DrawIconButtonSection(c);
DrawDividerSection(c);
}
private void DrawRowSection(ThemeColors c)
{
ImGui.TextUnformatted("Row");
ImGui.Checkbox("active##row", ref _rowActive);
var width = ImGui.GetContentRegionAvail().X;
var height = Metrics.SidebarRowHeight;
for (var i = 0; i < 3; i++)
{
var origin = ImGui.GetCursorScreenPos();
var id = ImGui.GetID($"gallery.row.{i}");
ImGui.InvisibleButton($"##gallery-row-{i}", new Vector2(width, height));
var hovered = ImGui.IsItemHovered();
Row.Draw(
origin,
new Vector2(width, height),
new RowVisualState
{
IsActive = _rowActive && i == 1,
HoverAmount = HoverState.Query(id, hovered),
SurfaceAbgr = _palette.Abgr(Token.SurfaceBase, c),
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
SurfaceActiveAbgr = _palette.Abgr(Token.SurfaceActive, c),
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
}
);
ImGui
.GetWindowDrawList()
.AddText(
origin + new Vector2(12f * Metrics.Scale, Metrics.CenterY(height)),
_palette.Abgr(Token.Text, c),
$"Row {i}"
);
}
ImGui.Spacing();
}
private void DrawBadgeSection(ThemeColors c)
{
ImGui.TextUnformatted("Badge");
ImGui.SliderInt("count##badge", ref _badgeCount, 0, 150);
int[] samples = [1, 9, _badgeCount, 120];
var origin = ImGui.GetCursorScreenPos();
var x = origin.X;
foreach (var n in samples)
{
Badge.Draw(
new Vector2(x, origin.Y),
n,
_palette.Abgr(Token.AccentEmber, c),
_palette.Abgr(Token.Text, c)
);
x += Badge.CalcSize(n).X + 8f * Metrics.Scale;
}
ImGui.Dummy(new Vector2(0, Badge.CalcSize(1).Y + 8f * Metrics.Scale));
ImGui.Spacing();
}
private void DrawPillSection(ThemeColors c)
{
ImGui.TextUnformatted("Pill");
var origin = ImGui.GetCursorScreenPos();
var x = origin.X;
Pill.Draw(
new Vector2(x, origin.Y),
"filled",
_palette.Abgr(Token.SurfaceRaised, c),
_palette.Abgr(Token.Text, c)
);
x += Pill.CalcSize("filled", withDot: false).X + 8f * Metrics.Scale;
Pill.Draw(
new Vector2(x, origin.Y),
"with dot",
_palette.Abgr(Token.SurfaceRaised, c),
_palette.Abgr(Token.Text, c),
_palette.Abgr(Token.AccentPrimary, c)
);
x += Pill.CalcSize("with dot", withDot: true).X + 8f * Metrics.Scale;
Pill.Draw(
new Vector2(x, origin.Y),
"outlined",
_palette.Abgr(Token.AccentPrimary, c),
_palette.Abgr(Token.TextMuted, c),
styleOverride: new PillStyle { Outlined = true }
);
ImGui.Dummy(new Vector2(0, Pill.CalcSize("x", false).Y + 8f * Metrics.Scale));
ImGui.Spacing();
}
private void DrawIconButtonSection(ThemeColors c)
{
ImGui.TextUnformatted("IconButton");
var size = new Vector2(Metrics.SidebarPopOutHitWidth, Metrics.SidebarRowHeight);
FontAwesomeIcon[] glyphs =
[
FontAwesomeIcon.ArrowUpRightFromSquare,
FontAwesomeIcon.Check,
FontAwesomeIcon.CheckCircle,
];
for (var i = 0; i < glyphs.Length; i++)
{
if (i > 0)
ImGui.SameLine();
IconButton.Draw(
ImGui.GetID($"gallery.icon.{i}"),
size,
glyphs[i],
_palette.Abgr(Token.TextMuted, c),
_palette.Abgr(Token.SurfaceHover, c),
_plugin.FontManager.FontAwesome
);
}
ImGui.Spacing();
}
private void DrawDividerSection(ThemeColors c)
{
ImGui.TextUnformatted("LineDivider");
LineDivider.Draw(
"Section (2)",
_palette.Abgr(Token.Border, c),
_palette.Abgr(Token.TextMuted, c)
);
LineDivider.Draw(null, _palette.Abgr(Token.Border, c), _palette.Abgr(Token.TextMuted, c));
}
}
#endif