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.
This commit is contained in:
@@ -389,7 +389,10 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Plugin>(),
|
sp.GetRequiredService<Plugin>(),
|
||||||
sp.GetRequiredService<PayloadHandler>()
|
sp.GetRequiredService<PayloadHandler>()
|
||||||
));
|
));
|
||||||
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
|
services.AddSingleton(sp => new FirstRunWizard(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<Ui.StyleEngine.SurfaceBackdrop>()
|
||||||
|
));
|
||||||
|
|
||||||
// Hosted-service adapters: thin wrappers around the existing init
|
// Hosted-service adapters: thin wrappers around the existing init
|
||||||
// methods so the service class bodies stay unchanged. FontManager
|
// methods so the service class bodies stay unchanged. FontManager
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using HellionChat.Code;
|
|||||||
using HellionChat.Privacy;
|
using HellionChat.Privacy;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Ui.StyleEngine;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
@@ -19,28 +20,25 @@ namespace HellionChat.Ui;
|
|||||||
// nested WizardState record; every step writes nullable Pending*
|
// nested WizardState record; every step writes nullable Pending*
|
||||||
// fields, and CommitPending() applies only the non-null ones so
|
// fields, and CommitPending() applies only the non-null ones so
|
||||||
// users who skip a step never get their existing config overwritten.
|
// 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
|
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 const int TotalSteps = 4;
|
||||||
|
|
||||||
private readonly Plugin Plugin;
|
private readonly Plugin Plugin;
|
||||||
|
private readonly StyleEngine.SurfaceBackdrop _backdrop;
|
||||||
private readonly WizardState _state = new();
|
private readonly WizardState _state = new();
|
||||||
|
|
||||||
internal FirstRunWizard(Plugin plugin)
|
internal FirstRunWizard(Plugin plugin, StyleEngine.SurfaceBackdrop backdrop)
|
||||||
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_backdrop = backdrop;
|
||||||
|
|
||||||
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
||||||
SizeCondition = ImGuiCond.Appearing;
|
SizeCondition = ImGuiCond.Appearing;
|
||||||
@@ -63,10 +61,12 @@ public sealed class FirstRunWizard : Window
|
|||||||
|
|
||||||
public override void Draw()
|
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();
|
DrawPagination();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
switch (_state.CurrentStep)
|
switch (_state.CurrentStep)
|
||||||
{
|
{
|
||||||
@@ -94,26 +94,149 @@ public sealed class FirstRunWizard : Window
|
|||||||
var draw = ImGui.GetWindowDrawList();
|
var draw = ImGui.GetWindowDrawList();
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
var cursor = ImGui.GetCursorScreenPos();
|
var cursor = ImGui.GetCursorScreenPos();
|
||||||
const float radius = 5f;
|
var scale = StyleEngine.Metrics.Scale;
|
||||||
const float spacing = 16f;
|
var radius = 4f * scale;
|
||||||
|
var spacing = 18f * scale;
|
||||||
var totalWidth = (TotalSteps - 1) * spacing;
|
var totalWidth = (TotalSteps - 1) * spacing;
|
||||||
var startX = cursor.X + avail.X - totalWidth - radius;
|
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++)
|
for (var i = 0; i < TotalSteps; i++)
|
||||||
{
|
{
|
||||||
var color = (i + 1) == _state.CurrentStep ? ForgeBronze : ForgeBronzeDim;
|
var centre = new Vector2(startX + i * spacing, cursor.Y + radius);
|
||||||
var packed = ImGui.GetColorU32(color);
|
if ((i + 1) == _state.CurrentStep)
|
||||||
draw.AddCircleFilled(
|
draw.AddCircleFilled(centre, radius, accent);
|
||||||
new Vector2(startX + i * spacing, cursor.Y + radius),
|
else
|
||||||
radius,
|
draw.AddCircle(centre, radius - 1f, dim, 0, MathF.Max(1f, scale));
|
||||||
packed
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve vertical space the circles consumed so the next widget starts below them.
|
// Reserve vertical space the circles consumed so the next widget starts below them.
|
||||||
ImGui.Dummy(new Vector2(0, radius * 2));
|
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)
|
private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary)
|
||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
||||||
@@ -133,14 +256,14 @@ public sealed class FirstRunWizard : Window
|
|||||||
|
|
||||||
if (showBack)
|
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);
|
_state.CurrentStep = Math.Max(1, _state.CurrentStep - 1);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSkip)
|
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
|
// Skip path = matches today's Cancel path: mark first-run
|
||||||
// complete, save, close. No CommitPending — the user said
|
// complete, save, close. No CommitPending — the user said
|
||||||
@@ -158,13 +281,8 @@ public sealed class FirstRunWizard : Window
|
|||||||
if (rightX > ImGui.GetCursorPosX())
|
if (rightX > ImGui.GetCursorPosX())
|
||||||
ImGui.SameLine(rightX);
|
ImGui.SameLine(rightX);
|
||||||
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Button, ForgeBronze))
|
if (DrawPrimaryPill(primaryLabel))
|
||||||
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ForgeBronze))
|
onPrimary();
|
||||||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ForgeBronze))
|
|
||||||
{
|
|
||||||
if (ImGui.Button($"{primaryLabel}##wizard-primary"))
|
|
||||||
onPrimary();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawStepWelcome()
|
private void DrawStepWelcome()
|
||||||
@@ -306,7 +424,13 @@ public sealed class FirstRunWizard : Window
|
|||||||
// packed-colour overload of PushColor for the default branch so we
|
// packed-colour overload of PushColor for the default branch so we
|
||||||
// can stay in safe code while still matching the current border.
|
// can stay in safe code while still matching the current border.
|
||||||
var borderColor = isSelected
|
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);
|
: ImGui.GetColorU32(ImGuiCol.Border);
|
||||||
|
|
||||||
using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor);
|
using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor);
|
||||||
@@ -357,8 +481,7 @@ public sealed class FirstRunWizard : Window
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
// History section.
|
// History section.
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
DrawHeading(HellionStrings.Wizard_Step3_Section_History);
|
||||||
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_History);
|
|
||||||
|
|
||||||
// One checkbox, not two. LoadPreviousSession was asked for here, shown
|
// One checkbox, not two. LoadPreviousSession was asked for here, shown
|
||||||
// as applied in the summary and written to the config, and no code in
|
// 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();
|
ImGui.Spacing();
|
||||||
|
|
||||||
// Tell-Tabs section.
|
// Tell-Tabs section.
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
DrawHeading(HellionStrings.Wizard_Step3_Section_TellTabs);
|
||||||
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_TellTabs);
|
|
||||||
|
|
||||||
var preload =
|
var preload =
|
||||||
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
@@ -397,8 +519,7 @@ public sealed class FirstRunWizard : Window
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
// Visual section.
|
// Visual section.
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
DrawHeading(HellionStrings.Wizard_Step3_Section_Visual);
|
||||||
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_Visual);
|
|
||||||
|
|
||||||
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
||||||
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact))
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact))
|
||||||
@@ -453,7 +574,7 @@ public sealed class FirstRunWizard : Window
|
|||||||
var avail = ImGui.GetContentRegionAvail();
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0));
|
ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0));
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
using (ImRaii.PushColor(ImGuiCol.Text, AccentVec4()))
|
||||||
ImGui.TextUnformatted(checkmark);
|
ImGui.TextUnformatted(checkmark);
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
@@ -525,7 +646,7 @@ public sealed class FirstRunWizard : Window
|
|||||||
// Inline FR-3 hint with placeholder for preload count.
|
// Inline FR-3 hint with placeholder for preload count.
|
||||||
var preloadForHint =
|
var preloadForHint =
|
||||||
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
_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.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint));
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|||||||
Reference in New Issue
Block a user