From 5958bceb1c5e6c190ab0de5f0374dcfdc49471e3 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:44:39 +0200 Subject: [PATCH] feat(wizard): the chrome speaks the window's language (block A of v1.15.0) The wizard is the first thing a tester ever sees and it was the last surface still drawn in ImGui defaults -- stock buttons under a hardcoded forge bronze that ignored the active theme entirely, since v1.5.2. Block A is the frame: the settings backdrop at full strength, pagination dots on the contrast-checked theme accent (current step filled, the rest rings, so the position reads by shape), step-3 section headers in the tracked-caps-and-fading- rule language every finished window uses, the primary action as the chamfered accent pill, and back/skip as ghost links. The selected profile card's border and the step-4 accents follow the theme now too. Still open, deliberately: block B gives the profile cards their real shape -- including replacing the emoji icons, which have stood against the global no-emoji-as-UI-icon rule since the cards were built -- and block C moves the step-3 checkboxes onto ToggleSwitch rows and the theme list onto PopupRow. --- HellionChat/PluginHostFactory.cs | 5 +- HellionChat/Ui/FirstRunWizard.cs | 203 ++++++++++++++++++++++++------- 2 files changed, 166 insertions(+), 42 deletions(-) diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index c8fa192..82938c9 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -389,7 +389,10 @@ internal static class PluginHostFactory sp.GetRequiredService(), sp.GetRequiredService() )); - services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService())); + services.AddSingleton(sp => new FirstRunWizard( + sp.GetRequiredService(), + sp.GetRequiredService() + )); // Hosted-service adapters: thin wrappers around the existing init // methods so the service class bodies stay unchanged. FontManager diff --git a/HellionChat/Ui/FirstRunWizard.cs b/HellionChat/Ui/FirstRunWizard.cs index ea2017d..061e4ef 100644 --- a/HellionChat/Ui/FirstRunWizard.cs +++ b/HellionChat/Ui/FirstRunWizard.cs @@ -9,6 +9,7 @@ using HellionChat.Code; using HellionChat.Privacy; using HellionChat.Resources; using HellionChat.Themes; +using HellionChat.Ui.StyleEngine; using HellionChat.Util; namespace HellionChat.Ui; @@ -19,28 +20,25 @@ namespace HellionChat.Ui; // nested WizardState record; every step writes nullable Pending* // fields, and CommitPending() applies only the non-null ones so // users who skip a step never get their existing config overwritten. +// +// This is the first thing a tester ever sees, and until v1.15.0 it was the last +// surface still drawn in ImGui defaults -- stock buttons under a hardcoded +// bronze that ignored the theme. It speaks the window's language now: the +// settings backdrop, tracked-caps headings, the chamfered accent pill for the +// one action that matters, and ghosts for the ones that do not. public sealed class FirstRunWizard : Window { - // Forge-Bronze (#C2410C). The same constant lives in ThemeRegistry - // and the forge-announce workflow; pinning it locally keeps the - // wizard render path free of registry lookups during draw. - private static readonly Vector4 ForgeBronze = new(0xC2 / 255f, 0x41 / 255f, 0x0C / 255f, 1f); - private static readonly Vector4 ForgeBronzeDim = new( - 0xC2 / 255f, - 0x41 / 255f, - 0x0C / 255f, - 0.3f - ); - private const int TotalSteps = 4; private readonly Plugin Plugin; + private readonly StyleEngine.SurfaceBackdrop _backdrop; private readonly WizardState _state = new(); - internal FirstRunWizard(Plugin plugin) + internal FirstRunWizard(Plugin plugin, StyleEngine.SurfaceBackdrop backdrop) : base($"{HellionStrings.Wizard_Title}###hellion-firstrun") { Plugin = plugin; + _backdrop = backdrop; Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking; SizeCondition = ImGuiCond.Appearing; @@ -63,10 +61,12 @@ public sealed class FirstRunWizard : Window public override void Draw() { + // Same floor as the settings window, full strength: the wizard is read + // in glances, not line by line, and it is the first impression. + _backdrop.Draw(accentWashHeight: 46f, moteIntensity: 1f, strength: 1f); + DrawPagination(); ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); switch (_state.CurrentStep) { @@ -94,26 +94,149 @@ public sealed class FirstRunWizard : Window var draw = ImGui.GetWindowDrawList(); var avail = ImGui.GetContentRegionAvail(); var cursor = ImGui.GetCursorScreenPos(); - const float radius = 5f; - const float spacing = 16f; + var scale = StyleEngine.Metrics.Scale; + var radius = 4f * scale; + var spacing = 18f * scale; var totalWidth = (TotalSteps - 1) * spacing; var startX = cursor.X + avail.X - totalWidth - radius; + // Theme accent, contrast-checked, instead of the hardcoded forge bronze + // the wizard carried since v1.5.2 -- it was the one surface that ignored + // the theme entirely. The current dot is filled, the rest are rings, so + // the step reads by shape as well as by brightness. + var c = Plugin.ThemeRegistry.Active.Colors; + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.WindowBg), + 3f + ); + var dim = ColourUtil.ApplyAlpha(accent, 0.35f); + for (var i = 0; i < TotalSteps; i++) { - var color = (i + 1) == _state.CurrentStep ? ForgeBronze : ForgeBronzeDim; - var packed = ImGui.GetColorU32(color); - draw.AddCircleFilled( - new Vector2(startX + i * spacing, cursor.Y + radius), - radius, - packed - ); + var centre = new Vector2(startX + i * spacing, cursor.Y + radius); + if ((i + 1) == _state.CurrentStep) + draw.AddCircleFilled(centre, radius, accent); + else + draw.AddCircle(centre, radius - 1f, dim, 0, MathF.Max(1f, scale)); } // Reserve vertical space the circles consumed so the next widget starts below them. ImGui.Dummy(new Vector2(0, radius * 2)); } + // The heading language every finished window speaks: tracked small caps with + // a rule that fades out. Shape, not hue, so it reads in every palette. + // Contrast-checked theme accent as a Vector4, for the text pushes that used + // to hardcode forge bronze. The double swap is deliberate: RgbaToAbgr is an + // involution, so it converts ABGR back to the RGBA that RgbaToVector4 reads. + private Vector4 AccentVec4() + { + var c = Plugin.ThemeRegistry.Active.Colors; + var abgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + return ColourUtil.RgbaToVector4(ColourUtil.RgbaToAbgr(abgr)); + } + + private void DrawHeading(string text) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + var pos = ImGui.GetCursorScreenPos(); + + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + var width = dl.DrawTrackedText(pos, text.ToUpperInvariant(), tint, 1.8f * scale); + + var ruleX = pos.X + width + 10f * scale; + var availX = ImGui.GetContentRegionAvail().X; + if (pos.X + availX > ruleX) + dl.DrawFadeRule( + new Vector2(ruleX, pos.Y + ImGui.GetTextLineHeight() * 0.5f), + pos.X + availX - ruleX, + ColourUtil.RgbaToAbgr(c.Border), + MathF.Max(1f, scale) + ); + + ImGui.Dummy(new Vector2(0f, ImGui.GetTextLineHeight() + 6f * scale)); + } + + // The primary action is the channel pill's shape -- chamfered accent with a + // white depth gradient -- because it is the one thing on the page the user + // is meant to press. Everything secondary is a ghost. + private bool DrawPrimaryPill(string label) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var size = new Vector2(ImGui.CalcTextSize(label).X + 36f * scale, ImGui.GetFrameHeight()); + var origin = ImGui.GetCursorScreenPos(); + + var clicked = ImGui.InvisibleButton($"##wiz-primary-{label}", size); + var hovered = ImGui.IsItemHovered(); + var amount = StyleEngine.HoverState.Query(ImGui.GetID($"##wiz-primary-{label}"), hovered); + + var fill = ColourUtil.RgbaToAbgr(c.Accent); + if (amount > 0f) + fill = ColourUtil.LerpTowardWhite(fill, amount * 0.12f); + + dl.DrawSlipPolygon(origin, origin + size, ColourUtil.RgbaToAbgr(fill), 6f * scale); + dl.DrawVerticalGradient(origin, origin + size, 0x28FFFFFFu, 0u); + + var ink = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.WindowBg), + ColourUtil.RgbaToAbgr(c.Accent), + 4.5f + ); + var textSize = ImGui.CalcTextSize(label); + dl.AddText(origin + (size - textSize) * 0.5f, ink, label); + + return clicked; + } + + private bool DrawGhostLink(string id, string label) + { + var scale = StyleEngine.Metrics.Scale; + var c = Plugin.ThemeRegistry.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + var size = new Vector2(ImGui.CalcTextSize(label).X + 16f * scale, ImGui.GetFrameHeight()); + var origin = ImGui.GetCursorScreenPos(); + + var clicked = ImGui.InvisibleButton(id, size); + var hovered = ImGui.IsItemHovered(); + var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), hovered); + + if (amount > 0f) + dl.AddRectFilled( + origin, + origin + size, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + 3f * scale + ); + + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ); + if (amount > 0f) + tint = ColourUtil.Lerp(tint, ColourUtil.RgbaToAbgr(c.Accent), amount * 0.6f); + + var textSize = ImGui.CalcTextSize(label); + dl.AddText(origin + (size - textSize) * 0.5f, tint, label); + + return clicked; + } + private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary) { var spacing = ImGui.GetStyle().ItemSpacing.Y; @@ -133,14 +256,14 @@ public sealed class FirstRunWizard : Window if (showBack) { - if (ImGui.Button(HellionStrings.Wizard_Nav_Back)) + if (DrawGhostLink("##wiz-back", HellionStrings.Wizard_Nav_Back)) _state.CurrentStep = Math.Max(1, _state.CurrentStep - 1); ImGui.SameLine(); } if (showSkip) { - if (ImGui.Button(HellionStrings.Wizard_Step1_Skip_Label)) + if (DrawGhostLink("##wiz-skip", HellionStrings.Wizard_Step1_Skip_Label)) { // Skip path = matches today's Cancel path: mark first-run // complete, save, close. No CommitPending — the user said @@ -158,13 +281,8 @@ public sealed class FirstRunWizard : Window if (rightX > ImGui.GetCursorPosX()) ImGui.SameLine(rightX); - using (ImRaii.PushColor(ImGuiCol.Button, ForgeBronze)) - using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ForgeBronze)) - using (ImRaii.PushColor(ImGuiCol.ButtonActive, ForgeBronze)) - { - if (ImGui.Button($"{primaryLabel}##wizard-primary")) - onPrimary(); - } + if (DrawPrimaryPill(primaryLabel)) + onPrimary(); } private void DrawStepWelcome() @@ -306,7 +424,13 @@ public sealed class FirstRunWizard : Window // packed-colour overload of PushColor for the default branch so we // can stay in safe code while still matching the current border. var borderColor = isSelected - ? ImGui.GetColorU32(ForgeBronze) + ? ColourUtil.RgbaToAbgr( + ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(Plugin.ThemeRegistry.Active.Colors.Accent), + ColourUtil.RgbaToAbgr(Plugin.ThemeRegistry.Active.Colors.ChildBg), + 3f + ) + ) : ImGui.GetColorU32(ImGuiCol.Border); using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor); @@ -357,8 +481,7 @@ public sealed class FirstRunWizard : Window ImGui.Spacing(); // History section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_History); + DrawHeading(HellionStrings.Wizard_Step3_Section_History); // One checkbox, not two. LoadPreviousSession was asked for here, shown // as applied in the summary and written to the config, and no code in @@ -379,8 +502,7 @@ public sealed class FirstRunWizard : Window ImGui.Spacing(); // Tell-Tabs section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_TellTabs); + DrawHeading(HellionStrings.Wizard_Step3_Section_TellTabs); var preload = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; @@ -397,8 +519,7 @@ public sealed class FirstRunWizard : Window ImGui.Spacing(); // Visual section. - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) - ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_Visual); + DrawHeading(HellionStrings.Wizard_Step3_Section_Visual); var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity; if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact)) @@ -453,7 +574,7 @@ public sealed class FirstRunWizard : Window var avail = ImGui.GetContentRegionAvail(); ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0)); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + using (ImRaii.PushColor(ImGuiCol.Text, AccentVec4())) ImGui.TextUnformatted(checkmark); ImGui.Spacing(); @@ -525,7 +646,7 @@ public sealed class FirstRunWizard : Window // Inline FR-3 hint with placeholder for preload count. var preloadForHint = _state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload; - using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze)) + using (ImRaii.PushColor(ImGuiCol.Text, AccentVec4())) ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint)); ImGui.Spacing();