From 0abbbc8f8a7ec84336c9c95149fa2ce6fff6b112 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 17:48:39 +0200 Subject: [PATCH 01/11] docs(roadmap): the sidebar comes off the list, the input bar goes on Both reasons the channel-rows idea existed are gone. It was written when the sidebar was flat and unstyled; v1.10.0 gave it row surfaces and it already groups pinned and auto-tell tabs under headings. What was left was a change to how the plugin is operated rather than how it looks -- and tabs are what users have learned to use. Next two are the input bar and the first-run wizard, the last surfaces still drawn in ImGui defaults. --- docs/ROADMAP.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2fddac7..c918a00 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -35,8 +35,14 @@ Where it stands: - **v1.13.0** — typography. Named type roles instead of one size, a channel header above the conversation, timestamps in a column of their own, and rows with a surface. Screenshot mode reached one of four surfaces that draw a tab name and now reaches all of them. -- **v1.14.0 onwards** — the sidebar moving from tab rows to channel rows, then the first-run - wizard and the input bar, which are the last two surfaces still drawn in ImGui defaults. +- **v1.14.0 onwards** — the input bar, then the first-run wizard: the last two surfaces still + drawn in ImGui defaults. + +The sidebar is no longer on this list. Earlier drafts had it moving from tab rows to channel +rows, but both reasons for that are gone: it was flat and unstyled when the idea was written, +and it is neither now — it draws its own row surfaces and already groups pinned and auto-tell +tabs under headings. What remained was a change to how the plugin is operated rather than how +it looks, and tabs are what users have learned. They stay. A tester beta follows once the visual pass is complete. From 6449a51daa6027abf37a43c6c86446f142bc2f06 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 18:08:37 +0200 Subject: [PATCH 02/11] fix(chat): the channel header had a plate the settings window gave up weeks ago MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mockup draws this band as a filled surface, and I built it that way. The settings window used to draw its section headings the same way -- and dropped it in v1.11.0, with the reason written into SectionHeader: a fill reads fine against blue themes and vanishes against violet ones, because its only distinction from its surroundings is hue. Tracking is shape, and shape survives every palette. Side by side with yesterday's build the difference was obvious: the settings window sets VERHALTEN and TASTENKÜRZEL as tracked caps with a rule that fades out, while the chat window right next to it carried a lit bar. One window was three weeks ahead of the other. So the plate is gone and a fading rule runs between the channel name and the world, stopping short of it. Same shape the settings headings have had since ced7ea0. Worth writing down because the mockups are where this came from, and they are from before that lesson. They are the starting point of the style track, not its target -- the target is how far ImGui can be pushed, and the plugin has been past these drawings since v1.11.0. --- .../Components/Settings/LivePreviewPanel.cs | 26 +++++------ .../Ui/StyleEngine/Widgets/ChannelHeader.cs | 45 ++++++++++++------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs index c11cb1d..32cf0a6 100644 --- a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs +++ b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs @@ -269,20 +269,10 @@ internal sealed class LivePreviewPanel : IDisposable draw.AddRectFilled(listOrigin, max, ColourUtil.RgbaToAbgr(theme.Colors.WindowBg)); - // The channel header the real window now carries above its log. Tracked - // caps, same as the widget draws them. + // The channel header the real window carries above its log: tracked caps + // and a rule that fades out, no plate. Same reasoning as the section + // headings in this very window. var headerHeight = ImGui.GetTextLineHeight() + 8f; - draw.AddRectFilled( - listOrigin, - new Vector2(max.X, listOrigin.Y + headerHeight), - ColourUtil.RgbaToAbgr(theme.Colors.Surface) - ); - draw.AddLine( - new Vector2(listOrigin.X, listOrigin.Y + headerHeight), - new Vector2(max.X, listOrigin.Y + headerHeight), - ColourUtil.RgbaToAbgr(theme.Colors.Border), - 1f - ); draw.DrawTrackedText( new Vector2(listOrigin.X + 6f, listOrigin.Y + 4f), MockChannel, @@ -295,6 +285,16 @@ internal sealed class LivePreviewPanel : IDisposable 1.8f ); + draw.DrawFadeRule( + new Vector2( + listOrigin.X + 6f + 68f, + listOrigin.Y + 4f + ImGui.GetTextLineHeight() * 0.5f + ), + listWidth - 6f - 68f - 6f, + ColourUtil.RgbaToAbgr(theme.Colors.Border), + 1f + ); + var padMin = new Vector2(listOrigin.X + 2f, max.Y - 6f); draw.AddRectFilled( padMin, diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs index 8668c7c..6dc720e 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs @@ -9,11 +9,15 @@ namespace HellionChat.Ui.StyleEngine.Widgets; // The band above the conversation: which channel you are in on the left, where // you are and what time it is on the right. // -// Set apart by small caps with wide tracking rather than by size. That is a -// deliberate departure from the mockup, which asks for one pixel smaller than -// body text: one pixel would cost a whole additional font handle at full glyph -// range, because tab names are free user input and can be CJK. Tracking carries -// the same weight in every palette and costs nothing. +// Set apart by small caps with wide tracking, and by nothing else. The mockup +// draws this as a filled bar; the settings window used to draw its section +// headings that way too, and it was dropped in v1.11.0 for a reason worth +// repeating -- a fill reads fine against blue themes and vanishes against violet +// ones, because its only distinction from its surroundings is hue. Tracking is +// shape, and shape survives every palette. +// +// So: no plate, and a rule that fades out between the name and the world, the +// same way SectionHeader runs one between its title and the edge. // // Which face draws what is not a style choice here, it is a constraint. The meta // face has a glyph range of ASCII plus a middle dot, so only the world name and @@ -26,6 +30,7 @@ internal static class ChannelHeader private const float TrackRaw = 1.8f; private const float DetailTrackRaw = 0.9f; private const float IconGapRaw = 8f; + private const float RuleGapRaw = 10f; // Measured, never a fixed 32px: the band has to hold a line of text, and the // font comes from Config, which display scaling does not feed into. @@ -68,8 +73,10 @@ internal static class ChannelHeader var origin = ImGui.GetCursorScreenPos(); var theme = Plugin.Instance.ThemeRegistry.Active; - var surface = theme.Colors.Surface; - var surfaceAbgr = ColourUtil.RgbaToAbgr(surface); + // Contrast is still measured against the surface the header sits on, even + // though it no longer paints one -- the chat log's own floor is what the + // text lands on. + var surfaceAbgr = ColourUtil.RgbaToAbgr(theme.Colors.ChildBg); var body = BodyFace(fonts); var meta = MetaFace(fonts); @@ -152,13 +159,6 @@ internal static class ChannelHeader var dl = ImGui.GetWindowDrawList(); var bottomRight = origin + new Vector2(width, height); - dl.AddRectFilled(origin, bottomRight, surfaceAbgr); - dl.AddLine( - new Vector2(origin.X, bottomRight.Y - 1f), - new Vector2(bottomRight.X, bottomRight.Y - 1f), - ColourUtil.RgbaToAbgr(theme.Colors.Border), - MathF.Max(1f, scale) - ); var textY = origin.Y + MathF.Round(PadYRaw * scale); @@ -188,13 +188,28 @@ internal static class ChannelHeader x += iconSize.X + IconGapRaw * scale; + float drawn; using (meta.Push()) - dl.DrawTrackedText( + drawn = dl.DrawTrackedText( new Vector2(x, textY + DropFor(body, meta, scale)), name, accent, track ); + + // Starts where the name ends and fades into nothing, so it reads as a + // continuation of the heading rather than as a box lid. Stops short of + // the trailing detail when there is one. + var ruleX = x + drawn + RuleGapRaw * scale; + var ruleEnd = + bottomRight.X - inset - (plan.ShowDetail ? detailRun + RuleGapRaw * scale : 0f); + if (ruleEnd > ruleX) + dl.DrawFadeRule( + new Vector2(ruleX, textY + ImGui.GetTextLineHeight() * 0.5f), + ruleEnd - ruleX, + ColourUtil.RgbaToAbgr(theme.Colors.Border), + MathF.Max(1f, scale) + ); } if (plan.ShowDetail && detail.Where.Length > 0) From df6296024b63b31b98b5f2f70fa19f400c8ff4b2 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 18:22:34 +0200 Subject: [PATCH 03/11] feat(style): a lab window for the input-row decision Four elements, three variants each, drawn side by side in real ImGui against the live theme. Reachable with /hellion lab. The alternative was drawing mockups, and mockups are what sent this cycle wrong once already -- they predate the settings window learning that structure is typography and only controls get a fill. A picture also cannot show the part that matters most here: switching themes switches which variant works. Everything it draws comes from helpers that were already in DrawListExtensions and had almost no callers. DrawSlipPolygon and DrawVerticalGradient had one between them, in SegmentedControl. DrawGlowBorder had none at all. Temporary, and deliberately not behind DEBUG: the decision happens in the build Flo runs. It comes out once the variants are picked. --- HellionChat/Plugin.cs | 10 + HellionChat/PluginHostFactory.cs | 7 + HellionChat/PluginLifecycle.cs | 1 + HellionChat/Ui/Windows/InputBarLabWindow.cs | 406 ++++++++++++++++++++ 4 files changed, 424 insertions(+) create mode 100644 HellionChat/Ui/Windows/InputBarLabWindow.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 1e01812..99aadc6 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -112,6 +112,7 @@ public sealed class Plugin : IAsyncDalamudPlugin #if DEBUG internal Ui.Windows.WidgetGalleryWindow WidgetGallery { get; private set; } = null!; #endif + internal Ui.Windows.InputBarLabWindow InputBarLab { get; private set; } = null!; public FirstRunWizard FirstRunWizard { get; private set; } = null!; internal DebuggerWindow DebuggerWindow { get; private set; } = null!; @@ -456,6 +457,7 @@ public sealed class Plugin : IAsyncDalamudPlugin #if DEBUG WidgetGallery = _host.Services.GetRequiredService(); #endif + InputBarLab = _host.Services.GetRequiredService(); DebuggerWindow = _host.Services.GetRequiredService(); FirstRunWizard = _host.Services.GetRequiredService(); ChannelPopoutPool = _host.Services.GetRequiredService(); @@ -1042,6 +1044,14 @@ public sealed class Plugin : IAsyncDalamudPlugin WidgetGallery.Toggle(); return; } +#endif + + if (arg.Equals("lab", StringComparison.OrdinalIgnoreCase)) + { + InputBarLab.Toggle(); + return; + } +#if DEBUG #endif if (arg.Equals("reset", StringComparison.OrdinalIgnoreCase)) { diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index eee2b8d..a039011 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -379,6 +379,13 @@ internal static class PluginHostFactory sp.GetRequiredService() )); #endif + // Temporary: a side-by-side comparison of input-row variants, so the + // v1.14.0 style decision is made by looking rather than by imagining. + // Deliberately NOT behind DEBUG -- the decision happens in the build Flo + // actually runs. Comes out once the variants are picked. + services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow( + sp.GetRequiredService() + )); services.AddSingleton(sp => new DebuggerWindow( sp.GetRequiredService(), sp.GetRequiredService() diff --git a/HellionChat/PluginLifecycle.cs b/HellionChat/PluginLifecycle.cs index e0a27df..30c7966 100644 --- a/HellionChat/PluginLifecycle.cs +++ b/HellionChat/PluginLifecycle.cs @@ -67,6 +67,7 @@ internal sealed class PluginLifecycle : IAsyncDisposable #if DEBUG plugin.WindowSystem.AddWindow(plugin.WidgetGallery); #endif + plugin.WindowSystem.AddWindow(plugin.InputBarLab); plugin.WindowSystem.AddWindow(plugin.DebuggerWindow); plugin.WindowSystem.AddWindow(plugin.FirstRunWizard); diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs new file mode 100644 index 0000000..5f3fc07 --- /dev/null +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -0,0 +1,406 @@ +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; +using HellionChat.Util; + +namespace HellionChat.Ui.Windows; + +// Variants of the input row, drawn side by side so a decision can be made by +// looking rather than by imagining. Reachable with /hellion lab. +// +// Temporary. It comes out once the variants are picked, which is also why it is +// not behind DEBUG -- the decision happens in the build that actually runs. +// +// It exists because the alternative was drawing mockups, and mockups are what +// sent this cycle down the wrong path once already: they are from before the +// settings window learned that structure is typography and only controls get a +// fill. Everything here is real ImGui against the live theme, so switching +// themes switches the answer too -- which is the part a picture cannot show. +// +// The tools were all sitting in DrawListExtensions unused: DrawSlipPolygon and +// DrawVerticalGradient had one caller between them (SegmentedControl), +// DrawGlowBorder had none at all. +internal sealed class InputBarLabWindow : Window +{ + private readonly Plugin _plugin; + + private string _sample = string.Empty; + private int _buttonVariant; + private int _fieldVariant; + private int _pillVariant; + private int _headerVariant; + + internal InputBarLabWindow(Plugin plugin) + : base("Input Bar Lab###hellion-input-lab") + { + _plugin = plugin; + Size = new Vector2(560, 720); + SizeCondition = ImGuiCond.FirstUseEver; + } + + public override void Draw() + { + var fonts = _plugin.FontManager; + if (fonts is null || !fonts.FontsReady) + { + ImGui.TextDisabled("fonts not ready"); + return; + } + + var c = _plugin.ThemeRegistry.Active.Colors; + + ImGui.TextDisabled( + $"theme {_plugin.ThemeRegistry.Active.Slug} · scale {Metrics.Scale:0.00}" + ); + ImGui.Separator(); + + DrawButtonSection(c, fonts); + DrawFieldSection(c); + DrawPillSection(c, fonts); + DrawHeaderSection(c, fonts); + } + + // ---- icon buttons ------------------------------------------------------- + + private void DrawButtonSection(ThemeColors c, FontManager fonts) + { + Heading("ICON BUTTONS", c); + ImGui.RadioButton("plain hover##b", ref _buttonVariant, 0); + ImGui.SameLine(); + ImGui.RadioButton("chamfer + gradient##b", ref _buttonVariant, 1); + ImGui.SameLine(); + ImGui.RadioButton("glow on hover##b", ref _buttonVariant, 2); + + ImGui.Spacing(); + + var icons = new[] + { + FontAwesomeIcon.SmileBeam, + FontAwesomeIcon.Palette, + FontAwesomeIcon.Cog, + FontAwesomeIcon.Camera, + FontAwesomeIcon.EyeSlash, + }; + + var scale = Metrics.Scale; + var side = MathF.Round(26f * scale); + var gap = MathF.Round(4f * scale); + var origin = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + + for (var i = 0; i < icons.Length; i++) + { + var min = new Vector2(origin.X + i * (side + gap), origin.Y); + var max = min + new Vector2(side, side); + + ImGui.SetCursorScreenPos(min); + ImGui.InvisibleButton($"##lab-btn-{i}", new Vector2(side, side)); + var hovered = ImGui.IsItemHovered(); + var amount = HoverState.Query((uint)(0x1B000 + i), hovered); + + DrawIconButtonVariant(dl, min, max, icons[i], amount, c, fonts, scale); + } + + ImGui.SetCursorScreenPos(origin); + ImGui.Dummy(new Vector2(0f, side + 10f * scale)); + ImGui.Spacing(); + } + + private void DrawIconButtonVariant( + ImDrawListPtr dl, + Vector2 min, + Vector2 max, + FontAwesomeIcon icon, + float amount, + ThemeColors c, + FontManager fonts, + float scale + ) + { + var accent = ColourUtil.RgbaToAbgr(c.Accent); + + switch (_buttonVariant) + { + case 0: + // What the sidebar does: nothing at rest, a soft fill on hover. + if (amount > 0f) + dl.AddRectFilled( + min, + max, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + 3f * scale + ); + break; + + case 1: + // Chamfered plate with a vertical gradient, the shape the + // segmented control uses for its selected segment. + dl.DrawSlipPolygon(min, max, c.Surface, 4f * scale); + dl.DrawVerticalGradient( + min, + max, + ColourUtil.ApplyAlpha( + ColourUtil.RgbaToAbgr(c.SurfaceHover), + 0.35f + amount * 0.4f + ), + 0u + ); + break; + + case 2: + // Flat at rest, and the glow border -- unused until now -- comes + // up with the hover instead of a fill. + if (amount > 0f) + { + dl.AddRectFilled( + min, + max, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.Surface), amount * 0.8f), + 3f * scale + ); + dl.DrawGlowBorder( + min, + max, + ColourUtil.ApplyAlpha(c.Accent, amount * 0.7f), + 1f, + 4 + ); + } + break; + } + + using (fonts.FontAwesome.Push()) + { + var glyph = icon.ToIconString(); + var size = ImGui.CalcTextSize(glyph); + var tint = ColourUtil.RgbaToAbgr( + ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + ); + if (amount > 0f) + tint = ColourUtil.Lerp(tint, accent, amount); + + dl.AddText(min + (max - min - size) * 0.5f, tint, glyph); + } + } + + // ---- input field -------------------------------------------------------- + + private void DrawFieldSection(ThemeColors c) + { + Heading("INPUT FIELD", c); + ImGui.RadioButton("imgui default##f", ref _fieldVariant, 0); + ImGui.SameLine(); + ImGui.RadioButton("own surface##f", ref _fieldVariant, 1); + ImGui.SameLine(); + ImGui.RadioButton("own surface + focus rail##f", ref _fieldVariant, 2); + + ImGui.Spacing(); + + var scale = Metrics.Scale; + var height = MathF.Round(24f * scale); + var width = ImGui.GetContentRegionAvail().X - 8f; + var origin = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + var max = origin + new Vector2(width, height); + + if (_fieldVariant > 0) + { + // The technique from Boutique.Inputs: paint the surface, then hand + // ImGui a transparent frame so the widget draws only text and caret. + dl.AddRectFilled(origin, max, ColourUtil.RgbaToAbgr(c.FrameBg), 6f * scale); + + if (_fieldVariant == 2 && ImGui.IsItemActive()) + dl.AddRectFilled( + origin, + new Vector2(origin.X + 2f * scale, max.Y), + ColourUtil.RgbaToAbgr(c.Accent) + ); + } + + using (ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero, _fieldVariant > 0)) + using (ImRaii.PushColor(ImGuiCol.FrameBgHovered, Vector4.Zero, _fieldVariant > 0)) + using (ImRaii.PushColor(ImGuiCol.FrameBgActive, Vector4.Zero, _fieldVariant > 0)) + using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(10f * scale, 4f * scale))) + { + ImGui.SetNextItemWidth(width); + ImGui.InputTextWithHint("##lab-field", "Type a message...", ref _sample, 256); + } + + // Focus rail after the widget: IsItemActive only answers afterwards. + if (_fieldVariant == 2 && ImGui.IsItemActive()) + dl.AddRectFilled( + origin, + new Vector2(origin.X + 2f * scale, max.Y), + ColourUtil.RgbaToAbgr(c.Accent) + ); + + ImGui.Spacing(); + } + + // ---- channel pill ------------------------------------------------------- + + private void DrawPillSection(ThemeColors c, FontManager fonts) + { + Heading("CHANNEL PILL", c); + ImGui.RadioButton("current##p", ref _pillVariant, 0); + ImGui.SameLine(); + ImGui.RadioButton("gradient + top light##p", ref _pillVariant, 1); + ImGui.SameLine(); + ImGui.RadioButton("chamfered##p", ref _pillVariant, 2); + + ImGui.Spacing(); + + var scale = Metrics.Scale; + var origin = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + const string label = "Jingliu Moon"; + var textWidth = ImGui.CalcTextSize(label).X; + var size = new Vector2(textWidth + 34f * scale, MathF.Round(22f * scale)); + var max = origin + size; + var fill = ColourUtil.RgbaToAbgr(c.Accent); + + switch (_pillVariant) + { + case 0: + dl.AddRectFilled(origin, max, fill, 6f * scale); + break; + + case 1: + dl.AddRectFilled(origin, max, fill, 6f * scale); + dl.DrawVerticalGradient(origin, max, 0x30FFFFFFu, 0u); + dl.AddRectFilled( + new Vector2(origin.X + 6f * scale, origin.Y), + new Vector2(max.X - 6f * scale, origin.Y + 1f * scale), + 0x60FFFFFFu + ); + break; + + case 2: + dl.DrawSlipPolygon(origin, max, c.Accent, 6f * scale); + dl.DrawVerticalGradient(origin, max, 0x28FFFFFFu, 0u); + break; + } + + var ink = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.WindowBg, c.Accent, 4.5f)); + using (fonts.FontAwesome.Push()) + { + var arrow = FontAwesomeIcon.ArrowRight.ToIconString(); + dl.AddText( + new Vector2( + origin.X + 8f * scale, + origin.Y + (size.Y - ImGui.GetFontSize()) * 0.5f + ), + ink, + arrow + ); + } + + dl.AddText( + new Vector2( + origin.X + 24f * scale, + origin.Y + (size.Y - ImGui.GetTextLineHeight()) * 0.5f + ), + ink, + label + ); + + ImGui.Dummy(new Vector2(0f, size.Y + 10f * scale)); + ImGui.Spacing(); + } + + // ---- channel header ----------------------------------------------------- + + private void DrawHeaderSection(ThemeColors c, FontManager fonts) + { + Heading("CHANNEL HEADER", c); + ImGui.RadioButton("rule only (current)##h", ref _headerVariant, 0); + ImGui.SameLine(); + ImGui.RadioButton("underline##h", ref _headerVariant, 1); + ImGui.SameLine(); + ImGui.RadioButton("tinted band##h", ref _headerVariant, 2); + + ImGui.Spacing(); + + var scale = Metrics.Scale; + var width = ImGui.GetContentRegionAvail().X - 8f; + var height = ImGui.GetTextLineHeight() + 14f * scale; + var origin = ImGui.GetCursorScreenPos(); + var max = origin + new Vector2(width, height); + var dl = ImGui.GetWindowDrawList(); + + var accent = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.Accent, c.ChildBg, 4.5f)); + + // The band variant is the one the mockup drew and v1.11.0 abandoned. + // Here at a fraction of the strength, as a tint rather than a plate -- + // included so the comparison is fair rather than rhetorical. + if (_headerVariant == 2) + dl.DrawEdgeTint(origin, max, ColourUtil.ApplyAlpha(accent, 0.10f), height); + + var textY = origin.Y + 7f * scale; + const string name = "FREE COMPANY"; + + float drawn; + using (fonts.MetaFont!.Push()) + drawn = dl.DrawTrackedText( + new Vector2(origin.X + 14f * scale, textY), + name, + accent, + 1.8f * scale + ); + + if (_headerVariant == 0) + { + var ruleX = origin.X + 14f * scale + drawn + 10f * scale; + if (max.X - 14f * scale > ruleX) + dl.DrawFadeRule( + new Vector2(ruleX, textY + ImGui.GetTextLineHeight() * 0.5f), + max.X - 14f * scale - ruleX, + ColourUtil.RgbaToAbgr(c.Border), + MathF.Max(1f, scale) + ); + } + else if (_headerVariant == 1) + { + // Underline under the name only, the way the top tab strip marks its + // active tab -- a shape the plugin already uses elsewhere. + dl.AddRectFilled( + new Vector2(origin.X + 14f * scale, max.Y - 2f * scale), + new Vector2(origin.X + 14f * scale + drawn, max.Y), + accent + ); + } + + ImGui.Dummy(new Vector2(0f, height + 6f * scale)); + } + + // ---- shared ------------------------------------------------------------- + + private static void Heading(string text, ThemeColors c) + { + ImGui.Spacing(); + var dl = ImGui.GetWindowDrawList(); + var pos = ImGui.GetCursorScreenPos(); + var width = dl.DrawTrackedText( + pos, + text, + ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.WindowBg, 4.5f)), + 1.6f * Metrics.Scale + ); + + var ruleX = pos.X + width + 10f; + var avail = ImGui.GetContentRegionAvail().X; + dl.DrawFadeRule( + new Vector2(ruleX, pos.Y + ImGui.GetTextLineHeight() * 0.5f), + pos.X + avail - ruleX, + ColourUtil.RgbaToAbgr(c.Border), + 1f + ); + + ImGui.Dummy(new Vector2(0f, ImGui.GetTextLineHeight() + 4f)); + } +} From 6bdfecb1df9f2cc01a2299388d118f225e72a28a Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 18:30:15 +0200 Subject: [PATCH 04/11] feat(chat): the input row learns the window's own language Five filled plates become two ghost buttons and a menu. The plates were ImGui defaults sitting between a drawn pill and a drawn status bar -- three shape languages in the one row a user works in, which is what the tester's screenshot made obvious. The ghosts follow the sidebar's icon buttons: nothing at rest, a held hover fill, the glyph lifting toward the accent. Symbols stay outside the menu because they are used mid-sentence. Theme, settings, screenshot and hide move in; the screenshot toggle was allowed in only because its state moved to the status bar first, as an accent pill that shows while the mode is on. A privacy state behind a closed menu answers nothing. The input field paints its own rounded surface and hands ImGui a transparent frame, so the widget draws only text and caret -- the Boutique.Inputs technique from Character Select+, no rebuild of the widget itself. Focus is a two-pixel accent rail on the left edge: the same mark the rows, the popups and the message list already use for "this is where you are". The channel pill gets the CS+ treatment, a faint white gradient and a one-pixel light along the top edge. White over the fill rather than a second hue, so it reads as depth in every palette. And the popups that open from the row are made of PopupRow now -- the sidebar row's shape, which is also exactly what CS+ draws for its own popup entries. The channel picker was the reported case: a drawn pill opening a list of naked selectables, the style breaking mid-click. A hundred-odd pixels of button reserve go back to the input field. --- .../Resources/HellionStrings.Designer.cs | 1 + HellionChat/Resources/HellionStrings.ca.resx | 3 + HellionChat/Resources/HellionStrings.cs.resx | 3 + HellionChat/Resources/HellionStrings.da.resx | 3 + HellionChat/Resources/HellionStrings.de.resx | 3 + HellionChat/Resources/HellionStrings.el.resx | 3 + HellionChat/Resources/HellionStrings.es.resx | 3 + HellionChat/Resources/HellionStrings.fi.resx | 3 + HellionChat/Resources/HellionStrings.fr.resx | 3 + HellionChat/Resources/HellionStrings.hu.resx | 3 + HellionChat/Resources/HellionStrings.it.resx | 3 + HellionChat/Resources/HellionStrings.ja.resx | 3 + HellionChat/Resources/HellionStrings.ko.resx | 3 + HellionChat/Resources/HellionStrings.nb.resx | 3 + HellionChat/Resources/HellionStrings.nl.resx | 3 + HellionChat/Resources/HellionStrings.pl.resx | 3 + .../Resources/HellionStrings.pt-BR.resx | 3 + .../Resources/HellionStrings.pt-PT.resx | 3 + HellionChat/Resources/HellionStrings.resx | 3 + HellionChat/Resources/HellionStrings.ro.resx | 3 + HellionChat/Resources/HellionStrings.ru.resx | 3 + HellionChat/Resources/HellionStrings.sv.resx | 3 + HellionChat/Resources/HellionStrings.tr.resx | 3 + HellionChat/Resources/HellionStrings.uk.resx | 3 + .../Resources/HellionStrings.zh-Hans.resx | 3 + .../Resources/HellionStrings.zh-Hant.resx | 3 + HellionChat/Ui/Components/InputBar.cs | 310 +++++++++++++----- HellionChat/Ui/Components/StatusBar.cs | 24 ++ HellionChat/Ui/StyleEngine/Metrics.cs | 8 +- .../Ui/StyleEngine/Widgets/PopupRow.cs | 94 ++++++ 30 files changed, 418 insertions(+), 94 deletions(-) create mode 100644 HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index f72737d..9bbfa0c 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -389,6 +389,7 @@ internal class HellionStrings internal static string Settings_Preview_TitleMock => Get(nameof(Settings_Preview_TitleMock)); internal static string Settings_Preview_StatusOpen => Get(nameof(Settings_Preview_StatusOpen)); internal static string ChannelHeader_NotLoggedIn => Get(nameof(ChannelHeader_NotLoggedIn)); + internal static string InputBar_More_Tooltip => Get(nameof(InputBar_More_Tooltip)); internal static string Settings_Section_Links => Get(nameof(Settings_Section_Links)); internal static string Settings_Section_Behaviour => Get(nameof(Settings_Section_Behaviour)); internal static string Settings_Section_Keybinds => Get(nameof(Settings_Section_Keybinds)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index bc4c079..c843d03 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -1361,4 +1361,7 @@ Sessió no iniciada + + Més accions + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index ca16c67..048653e 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -1360,4 +1360,7 @@ Nepřihlášen + + Další akce + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index b0c3a86..2c9743c 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -1360,4 +1360,7 @@ Ikke logget ind + + Flere handlinger + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index d4a2e8c..0ebcdee 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -1355,4 +1355,7 @@ Nicht eingeloggt + + Weitere Aktionen + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index 7aad3ea..33afe56 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -1360,4 +1360,7 @@ Εκτός σύνδεσης + + Περισσότερες ενέργειες + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index e274935..81d99d8 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -1361,4 +1361,7 @@ Sesión no iniciada + + Más acciones + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index ec8548c..e9641cf 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -1360,4 +1360,7 @@ Ei kirjautuneena + + Lisää toimintoja + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index c1679c2..a1baa48 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -1361,4 +1361,7 @@ Non connecté + + Plus d'actions + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index edcab59..f00d6b3 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -1360,4 +1360,7 @@ Nincs bejelentkezve + + További műveletek + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index f821293..ef3f54e 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -1361,4 +1361,7 @@ Non connesso + + Altre azioni + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 67029bb..9d700ef 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -1361,4 +1361,7 @@ 未ログイン + + その他の操作 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index 8bf133b..eb431f0 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -1361,4 +1361,7 @@ 로그인되지 않음 + + 추가 작업 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index bd4eba9..0c5073e 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -1360,4 +1360,7 @@ Ikke innlogget + + Flere handlinger + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index a98ec25..a44f9b3 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -1361,4 +1361,7 @@ Niet ingelogd + + Meer acties + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 1002fb9..567421d 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -1360,4 +1360,7 @@ Niezalogowany + + Więcej działań + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index 3c17c35..fe29447 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -1361,4 +1361,7 @@ Não conectado + + Mais ações + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 708b673..8a0b209 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -1360,4 +1360,7 @@ Sem sessão iniciada + + Mais ações + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index c0dfed0..4d12f3d 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1372,4 +1372,7 @@ Not logged in + + More actions + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index adae878..2e97185 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -1361,4 +1361,7 @@ Neconectat + + Mai multe acțiuni + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index d7da149..d3e84fd 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -1361,4 +1361,7 @@ Вход не выполнен + + Другие действия + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index b175ce6..62059bc 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -1361,4 +1361,7 @@ Inte inloggad + + Fler åtgärder + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index b99d3e7..5d8b520 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -1360,4 +1360,7 @@ Oturum açılmadı + + Diğer işlemler + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 6942e83..4274ec3 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -1360,4 +1360,7 @@ Вхід не виконано + + Інші дії + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index ea43134..5605975 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -1361,4 +1361,7 @@ 未登录 + + 更多操作 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index d06e35e..337e273 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -1361,4 +1361,7 @@ 未登入 + + 更多操作 + \ No newline at end of file diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index 545c741..031df50 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -273,6 +273,18 @@ internal sealed class InputBar StyleEngine.Widgets.Pill.Draw(origin, label, pillAbgr, textAbgr); + // The CS+ pill treatment: a faint white gradient from the top and a + // one-pixel light along the upper edge. Reads as depth in every palette + // because it is white over the fill, not a second hue. + var pillDl = ImGui.GetWindowDrawList(); + var pillScale = StyleEngine.Metrics.Scale; + pillDl.DrawVerticalGradient(origin, origin + size, 0x28FFFFFFu, 0u); + pillDl.AddRectFilled( + new Vector2(origin.X + 6f * pillScale, origin.Y), + new Vector2(origin.X + size.X - 6f * pillScale, origin.Y + 1f * pillScale), + 0x50FFFFFFu + ); + // Hit area over the rendered pill so a click opens the channel // picker. InvisibleButton both reserves the layout slot and gives // the popup a stable anchor item. @@ -283,7 +295,7 @@ internal sealed class InputBar DrawChannelPickerPopup(tab); } - private static void DrawChannelPickerPopup(Tab? tab) + private void DrawChannelPickerPopup(Tab? tab) { if (!ImGui.BeginPopup("##hellion-channel-picker")) return; @@ -296,14 +308,27 @@ internal sealed class InputBar return; } + ImGui.Dummy(new Vector2(150f * StyleEngine.Metrics.Scale, 0f)); + + var i = 0; foreach (var chatType in tab.SelectedChannels.Keys) { if (chatType.ToInputChannel() is not { } input) continue; var isCurrent = tab.CurrentChannel.Channel == input; - if (ImGui.Selectable(input.ToChatType().Name(), isCurrent)) + if ( + StyleEngine.Widgets.PopupRow.Draw( + $"##ch-{i++}", + input.ToChatType().Name(), + isCurrent, + _fonts + ) + ) + { tab.CurrentChannel.SetChannel(input); + ImGui.CloseCurrentPopup(); + } } } finally @@ -325,6 +350,33 @@ internal sealed class InputBar Activate = false; } + // Boutique.Inputs technique: paint the surface, then hand ImGui a + // transparent frame so the widget draws only text and caret. The focus + // rail is the same two-pixel accent bar the rows and popups carry -- + // one vocabulary for "this is where you are". + var scale = StyleEngine.Metrics.Scale; + var fieldOrigin = ImGui.GetCursorScreenPos(); + var fieldSize = new Vector2( + ImGui.GetContentRegionAvail().X - QuickButtonsReserve, + ImGui.GetFrameHeight() + ); + ImGui + .GetWindowDrawList() + .AddRectFilled( + fieldOrigin, + fieldOrigin + fieldSize, + ColourUtil.RgbaToAbgr(_themes.Active.Colors.FrameBg), + 6f * scale + ); + + using var frameBg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero); + using var frameBgHovered = ImRaii.PushColor(ImGuiCol.FrameBgHovered, Vector4.Zero); + using var frameBgActive = ImRaii.PushColor(ImGuiCol.FrameBgActive, Vector4.Zero); + using var framePad = ImRaii.PushStyle( + ImGuiStyleVar.FramePadding, + new Vector2(10f * scale, ImGui.GetStyle().FramePadding.Y) + ); + ImGui.SetNextItemWidth(-QuickButtonsReserve); if ( ImGui.InputText( @@ -343,6 +395,15 @@ internal sealed class InputBar _commandHelpWindow.IsOpen = false; TrySend(activeTab); } + + if (ImGui.IsItemActive()) + ImGui + .GetWindowDrawList() + .AddRectFilled( + fieldOrigin, + new Vector2(fieldOrigin.X + 2f * scale, fieldOrigin.Y + fieldSize.Y), + ColourUtil.RgbaToAbgr(_themes.Active.Colors.Accent) + ); DrawInputContextMenu(); _isFocused = ImGui.IsItemFocused(); @@ -627,106 +688,175 @@ internal sealed class InputBar return (current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}", false); } + // Two ghost buttons and a menu, where five filled plates used to sit. The + // plates were ImGui defaults in a row whose pill and status bar are drawn; + // the ghosts follow the sidebar's icon buttons instead -- nothing at rest, a + // held hover fill, the glyph lifting toward the accent. + // + // Symbols stay outside the menu because they are used mid-sentence; a click + // through a drawer for that would be a regression. The screenshot toggle may + // live in the menu only because its state moved to the status bar first -- + // the whole point of that mode is knowing what your screen shows BEFORE the + // screenshot key, and a state behind a closed menu answers nothing. private void DrawQuickButtons() { - // Collected here and drawn after the icon font is popped. Inside the push - // it rendered against the FontAwesome atlas, which has no ASCII glyphs, so - // every tooltip came out as an empty box. string? tooltip = null; - using (_fonts.FontAwesome.Push()) - { - if (ImGui.Button(FontAwesomeIcon.SmileBeam.ToIconString())) - _symbolPicker.OpenPopup(); - if (ImGui.IsItemHovered()) - tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip; + if (DrawGhostButton("##qb-symbols", FontAwesomeIcon.SmileBeam)) + _symbolPicker.OpenPopup(); + if (ImGui.IsItemHovered()) + tooltip = HellionStrings.InputBar_InsertSymbol_Tooltip; - if (_themeQuickPicker is not null) - { - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Palette.ToIconString())) - _themeQuickPicker.OpenPopup(); - if (ImGui.IsItemHovered()) - tooltip = HellionStrings.Settings_QuickPicker_Tooltip; - } + ImGui.SameLine(0f, 4f * StyleEngine.Metrics.Scale); - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString())) - { - _onOpenSettings(); - } - if (ImGui.IsItemHovered()) - tooltip = HellionStrings.InputBar_Settings_Tooltip; + if (DrawGhostButton("##qb-more", FontAwesomeIcon.EllipsisH)) + ImGui.OpenPopup("##hellion-more-menu"); + if (ImGui.IsItemHovered()) + tooltip = HellionStrings.InputBar_More_Tooltip; - // Screenshot mode. It lived only in the right-click menu on a player - // name, which for a privacy feature is the same as not existing -- - // reported as missing by a tester who had been using the plugin for - // months. Lit when active, because a mode you cannot see the state of - // is worse than no mode. - ImGui.SameLine(); - var shooting = Plugin.Config.ScreenshotMode; - using ( - ImRaii.PushColor( - ImGuiCol.Text, - ColourUtil.RgbaToVector4( - _resolver.Resolve(Token.AccentEmber, _themes.Active.Colors) - ), - shooting - ) - ) - { - if (ImGui.Button(FontAwesomeIcon.Camera.ToIconString())) - Plugin.Config.ScreenshotMode = !Plugin.Config.ScreenshotMode; - } - if (ImGui.IsItemHovered()) - tooltip = Language.Context_ScreenshotMode; - - // Hides the window (1.5.6 UserHide). One-way — Enter brings it back. - // Main window only; last in the row there. - if (Plugin.Config.ShowHideButton && _onHideWindow is not null) - { - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.EyeSlash.ToIconString())) - _onHideWindow(); - if (ImGui.IsItemHovered()) - tooltip = HellionStrings.InputBar_HideChat_Tooltip; - } - - // Pop-in, in the pop-out windows only. It sits here rather than in a - // header row because a pop-out with its title bar on had no header at - // all -- and the title bar carries no close button, since closing has - // to go through the pool to release the slot. - if (OnPopIn is not null) - { - ImGui.SameLine(); - // Red, and contrast-checked against the button plate it sits on - // rather than taken raw: several themes ship a danger colour that - // is nearly invisible on their own button fill. - var danger = ColourUtil.RgbaToAbgr( - _resolver.Resolve(Token.StatusDanger, _themes.Active.Colors) - ); - var plate = ColourUtil.Vector4ToAbgr(ImGui.GetStyle().Colors[(int)ImGuiCol.Button]); - using ( - ImRaii.PushColor( - ImGuiCol.Text, - ColourUtil.RgbaToVector4( - ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(danger, plate, 3f)) - ) - ) - ) - { - if (ImGui.Button(FontAwesomeIcon.Times.ToIconString())) - OnPopIn(); - } - if (ImGui.IsItemHovered()) - tooltip = HellionStrings.InputBar_PopIn_Tooltip; - } - } + DrawMoreMenu(); if (tooltip is not null) ImGui.SetTooltip(tooltip); } + // Sidebar language: no plate at rest, held hover fill, glyph toward accent. + private bool DrawGhostButton(string id, FontAwesomeIcon icon, bool lit = false) + { + var scale = StyleEngine.Metrics.Scale; + var side = MathF.Round(24f * scale); + var origin = ImGui.GetCursorScreenPos(); + var max = origin + new Vector2(side, side); + + var clicked = ImGui.InvisibleButton(id, new Vector2(side, side)); + var hovered = ImGui.IsItemHovered(); + var amount = StyleEngine.HoverState.Query(ImGui.GetID(id), hovered); + + var c = _themes.Active.Colors; + var dl = ImGui.GetWindowDrawList(); + + if (amount > 0f) + dl.AddRectFilled( + origin, + max, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + 3f * scale + ); + + var accent = ColourUtil.RgbaToAbgr(c.Accent); + var tint = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.ChildBg, 4.5f)); + if (lit) + tint = accent; + else if (amount > 0f) + tint = ColourUtil.Lerp(tint, accent, amount * 0.6f); + + using (_fonts.FontAwesome.Push()) + { + var glyph = icon.ToIconString(); + var size = ImGui.CalcTextSize(glyph); + dl.AddText(origin + (max - origin - size) * 0.5f, tint, glyph); + } + + return clicked; + } + + private void DrawMoreMenu() + { + if (!ImGui.BeginPopup("##hellion-more-menu")) + return; + + // Actions run after EndPopup: the theme picker opens its own popup at + // this component's ID scope, not inside the menu's. + var openTheme = false; + + try + { + ImGui.Dummy(new Vector2(160f * StyleEngine.Metrics.Scale, 0f)); + + if ( + _themeQuickPicker is not null + && StyleEngine.Widgets.PopupRow.Draw( + "##mm-theme", + HellionStrings.Settings_QuickPicker_Tooltip, + active: false, + _fonts, + FontAwesomeIcon.Palette + ) + ) + { + openTheme = true; + ImGui.CloseCurrentPopup(); + } + + if ( + StyleEngine.Widgets.PopupRow.Draw( + "##mm-settings", + HellionStrings.InputBar_Settings_Tooltip, + active: false, + _fonts, + FontAwesomeIcon.Cog + ) + ) + { + _onOpenSettings(); + ImGui.CloseCurrentPopup(); + } + + // Shows its state right here as the active row, and again in the + // status bar once on -- the menu closes, the awareness must not. + if ( + StyleEngine.Widgets.PopupRow.Draw( + "##mm-screenshot", + Language.Context_ScreenshotMode, + active: Plugin.Config.ScreenshotMode, + _fonts, + FontAwesomeIcon.Camera + ) + ) + { + Plugin.Config.ScreenshotMode = !Plugin.Config.ScreenshotMode; + } + + if ( + Plugin.Config.ShowHideButton + && _onHideWindow is not null + && StyleEngine.Widgets.PopupRow.Draw( + "##mm-hide", + HellionStrings.InputBar_HideChat_Tooltip, + active: false, + _fonts, + FontAwesomeIcon.EyeSlash + ) + ) + { + _onHideWindow(); + ImGui.CloseCurrentPopup(); + } + + if ( + OnPopIn is not null + && StyleEngine.Widgets.PopupRow.Draw( + "##mm-popin", + HellionStrings.InputBar_PopIn_Tooltip, + active: false, + _fonts, + FontAwesomeIcon.Times + ) + ) + { + OnPopIn(); + ImGui.CloseCurrentPopup(); + } + } + finally + { + ImGui.EndPopup(); + } + + if (openTheme) + _themeQuickPicker!.OpenPopup(); + } + // Test-only hook; do not call from production code. internal void TestSetPendingMessageForSelfTest(string value) => _pendingMessage = value; diff --git a/HellionChat/Ui/Components/StatusBar.cs b/HellionChat/Ui/Components/StatusBar.cs index b7c17b1..e3fe143 100644 --- a/HellionChat/Ui/Components/StatusBar.cs +++ b/HellionChat/Ui/Components/StatusBar.cs @@ -203,6 +203,30 @@ internal sealed class StatusBar FontAwesomeIcon.Lock, _fonts.FontAwesome ); + // Screenshot mode announces itself here while it is on. The toggle sits + // in the input row's menu, and a closed menu answers nothing -- the whole + // point of the mode is knowing what the screen shows before the + // screenshot key is pressed. Accent text, because this one is a state + // the user chose, not ambient information. + if (Plugin.Config.ScreenshotMode) + { + var camWidth = StyleEngine.Widgets.Pill.MeasureIcon( + FontAwesomeIcon.Camera, + _fonts.FontAwesome + ); + var camLabel = Language.Context_ScreenshotMode; + if (Fits(camLabel, withDot: false, camWidth)) + x += + DrawSlot( + new Vector2(x, top), + camLabel, + pillFill, + palette.Abgr(Token.AccentPrimary, colors), + null, + (FontAwesomeIcon.Camera, _fonts.FontAwesome) + ) + gap; + } + if (Fits(privacyLabel, withDot: false, lockWidth)) x += DrawSlot( diff --git a/HellionChat/Ui/StyleEngine/Metrics.cs b/HellionChat/Ui/StyleEngine/Metrics.cs index 2535881..a836dff 100644 --- a/HellionChat/Ui/StyleEngine/Metrics.cs +++ b/HellionChat/Ui/StyleEngine/Metrics.cs @@ -27,10 +27,10 @@ internal static class Metrics // --- Input bar --- internal const float InputBarHeightRaw = 32f; - // Raised from 130 in v1.13.0 for the screenshot-mode button. Five buttons in - // the main window (symbols, theme, screenshot, settings, hide) and five in a - // pop-out, where pop-in takes the place of hide. - internal const float InputQuickButtonsReserveRaw = 156f; + // Two ghost buttons since v1.14.0 -- symbols and the more-menu. Everything + // else moved into the menu, and the hundred-odd pixels went back to the + // input field. + internal const float InputQuickButtonsReserveRaw = 62f; // --- Honorific header --- internal const float HonorificHeightRaw = 30f; diff --git a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs new file mode 100644 index 0000000..c502462 --- /dev/null +++ b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs @@ -0,0 +1,94 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using HellionChat.Util; + +namespace HellionChat.Ui.StyleEngine.Widgets; + +// The row a popup is made of. Replaces ImGui.Selectable inside the plugin's +// popups, which carried the right colours through GlobalStyleScope but none of +// the shape the rest of the window speaks: no held hover, no accent bar on the +// active entry. +// +// The shape is the sidebar row's, and it is also what Character Select+ draws +// for its popup entries (Boutique.Dropdowns, DrawPopupRow): a soft fill on +// hover, a two-pixel accent bar when active. One conversation, wherever a list +// appears. +internal static class PopupRow +{ + private const float PadXRaw = 10f; + private const float PadYRaw = 5f; + private const float IconGapRaw = 8f; + private const float AccentBarRaw = 2f; + + internal static bool Draw( + string id, + string label, + bool active, + FontManager fonts, + FontAwesomeIcon? icon = null + ) + { + var scale = Metrics.Scale; + var theme = Plugin.Instance.ThemeRegistry.Active; + var c = theme.Colors; + + var height = ImGui.GetTextLineHeight() + MathF.Round(PadYRaw * 2f * scale); + var width = ImGui.GetContentRegionAvail().X; + var origin = ImGui.GetCursorScreenPos(); + var max = origin + new Vector2(width, height); + + var clicked = ImGui.InvisibleButton(id, new Vector2(width, height)); + var hovered = ImGui.IsItemHovered(); + var amount = HoverState.Query(ImGui.GetID(id), hovered); + + var dl = ImGui.GetWindowDrawList(); + var accentAbgr = ColourUtil.RgbaToAbgr(c.Accent); + + // Active first, hover on top -- an active entry still answers the mouse. + if (active) + dl.AddRectFilled(origin, max, ColourUtil.ApplyAlpha(accentAbgr, 0.10f), 3f * scale); + + if (amount > 0f) + dl.AddRectFilled( + origin, + max, + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + 3f * scale + ); + + if (active) + dl.AddRectFilled( + origin, + new Vector2(origin.X + AccentBarRaw * scale, max.Y), + accentAbgr + ); + + var textAbgr = ColourUtil.RgbaToAbgr( + ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + ); + if (active || amount > 0f) + textAbgr = ColourUtil.Lerp(textAbgr, accentAbgr, active ? 0.6f : amount * 0.4f); + + var x = origin.X + PadXRaw * scale; + + if (icon is { } glyph) + { + using (fonts.FontAwesome.Push()) + { + var s = glyph.ToIconString(); + var size = ImGui.CalcTextSize(s); + dl.AddText(new Vector2(x, origin.Y + (height - size.Y) * 0.5f), textAbgr, s); + x += size.X + IconGapRaw * scale; + } + } + + dl.AddText( + new Vector2(x, origin.Y + (height - ImGui.GetTextLineHeight()) * 0.5f), + textAbgr, + label + ); + + return clicked; + } +} From d7a308b522e9f0ec487121c993aca4063a182ed7 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:04:05 +0200 Subject: [PATCH 05/11] feat(style): option three across the board, every colour contrast-bound Flo picked the third variant in all four lab sections, with one warning attached: bind the glyph and text colours to the contrast helper or the theme, or they drown. The warning was well aimed -- the ghost buttons from the previous commit were feeding RGBA into EnsureContrast, the same channel-order mistake the header made this morning, and the pill text was raw TextPrimary on an accent fill with no check at all. Every colour in the row now goes through EnsureContrast against the surface it actually lands on. What changed shape: The icon buttons glow. Flat at rest, and on hover a soft fill with an accent glow border rising on the held hover value -- DrawGlowBorder's first caller ever. The lab version of that glow had its alpha in the wrong byte (DrawGlowBorder reads RGBA, ApplyAlpha writes ABGR), so what Flo approved was a full-alpha glow with a dimmed red channel. Fixed in both places, with the alpha byte set by hand. The channel pill is chamfered, the segmented control's corner language, with the white depth gradient kept. The rounded Pill widget stays untouched for the status bar. The channel header trades its fading rule for a tenth-opacity accent wash from the top edge. Colour as atmosphere rather than as a box -- at this strength it survives the violet themes that killed the filled bar in v1.11.0. The lab stays in permanently, by Flo's call: a dev playground for seeing ideas in-game against the live theme. Its radios now default to what shipped, so the window doubles as a record of which variant won. --- HellionChat/PluginHostFactory.cs | 7 +-- HellionChat/Ui/Components/InputBar.cs | 58 ++++++++++++++----- .../Ui/StyleEngine/Widgets/ChannelHeader.cs | 45 ++++++-------- HellionChat/Ui/Windows/InputBarLabWindow.cs | 30 +++++----- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index a039011..c8fa192 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -379,10 +379,9 @@ internal static class PluginHostFactory sp.GetRequiredService() )); #endif - // Temporary: a side-by-side comparison of input-row variants, so the - // v1.14.0 style decision is made by looking rather than by imagining. - // Deliberately NOT behind DEBUG -- the decision happens in the build Flo - // actually runs. Comes out once the variants are picked. + // The style lab: variants side by side, in-game, against the live theme. + // Permanent by Flo's call, and deliberately not behind DEBUG -- style + // decisions happen in the build he actually runs. services.AddSingleton(sp => new Ui.Windows.InputBarLabWindow( sp.GetRequiredService() )); diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index 031df50..377c30d 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -182,7 +182,13 @@ internal sealed class InputBar var pillToken = isTell ? Token.AccentEmber : Token.AccentPrimary; var pillRgba = _resolver.Resolve(pillToken, theme.Colors); var pillAbgr = ColourUtil.RgbaToAbgr(pillRgba); - var pillTextAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + // Measured against the pill fill it lands on, not taken from the theme + // raw -- an accent fill can swallow the theme's text colour whole. + var pillTextAbgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), + pillAbgr, + 4.5f + ); DrawChannelPill(activeTab, isTell, pillAbgr, pillTextAbgr); ImGui.SameLine(); @@ -271,19 +277,18 @@ internal sealed class InputBar var size = StyleEngine.Widgets.Pill.CalcSize(label, withDot: false); var origin = ImGui.GetCursorScreenPos(); - StyleEngine.Widgets.Pill.Draw(origin, label, pillAbgr, textAbgr); - - // The CS+ pill treatment: a faint white gradient from the top and a - // one-pixel light along the upper edge. Reads as depth in every palette - // because it is white over the fill, not a second hue. + // Chamfered, not rounded: the shape the segmented control already uses + // for its selected segment, with the CS+ white gradient for depth. The + // slip corner is the one piece of the Boutique geometry the plugin had + // built and barely used. var pillDl = ImGui.GetWindowDrawList(); var pillScale = StyleEngine.Metrics.Scale; - pillDl.DrawVerticalGradient(origin, origin + size, 0x28FFFFFFu, 0u); - pillDl.AddRectFilled( - new Vector2(origin.X + 6f * pillScale, origin.Y), - new Vector2(origin.X + size.X - 6f * pillScale, origin.Y + 1f * pillScale), - 0x50FFFFFFu - ); + var pillMax = origin + size; + pillDl.DrawSlipPolygon(origin, pillMax, ColourUtil.RgbaToAbgr(pillAbgr), 6f * pillScale); + pillDl.DrawVerticalGradient(origin, pillMax, 0x28FFFFFFu, 0u); + + var labelSize = ImGui.CalcTextSize(label); + pillDl.AddText(origin + (size - labelSize) * 0.5f, textAbgr, label); // Hit area over the rendered pill so a click opens the channel // picker. InvisibleButton both reserves the layout slot and gives @@ -735,16 +740,39 @@ internal sealed class InputBar var c = _themes.Active.Colors; var dl = ImGui.GetWindowDrawList(); + // Every colour here is measured against what it actually lands on. The + // glyph sits on the window floor, the glow hugs the fill -- a raw theme + // accent can vanish on either, which is exactly the warning that came + // back from the smoke test. + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.ChildBg), + 3f + ); + if (amount > 0f) + { dl.AddRectFilled( origin, max, - ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.SurfaceHover), amount), + ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.Surface), amount * 0.8f), 3f * scale ); - var accent = ColourUtil.RgbaToAbgr(c.Accent); - var tint = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.ChildBg, 4.5f)); + // DrawGlowBorder reads RGBA with alpha in the low byte; ApplyAlpha + // writes the high byte (ABGR). Mixing them is the channel-order trap + // this cycle already fell into once, so the alpha byte is set by hand. + var glowRgba = + (ColourUtil.RgbaToAbgr(accent) & 0xFFFFFF00u) + | (uint)(byte)Math.Round(0xB4 * amount); + dl.DrawGlowBorder(min: origin, max: max, glowRgba, 1f, 4); + } + + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f + ); if (lit) tint = accent; else if (amount > 0f) diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs index 6dc720e..c738784 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs @@ -16,8 +16,10 @@ namespace HellionChat.Ui.StyleEngine.Widgets; // ones, because its only distinction from its surroundings is hue. Tracking is // shape, and shape survives every palette. // -// So: no plate, and a rule that fades out between the name and the world, the -// same way SectionHeader runs one between its title and the edge. +// So: no plate, but a tint. A tenth-opacity accent wash falling from the top +// edge -- colour as atmosphere rather than as a box, which is what the plate got +// wrong. Flo picked it from the lab over the rule-only variant; at this strength +// it survives the violet themes that killed the filled bar. // // Which face draws what is not a style choice here, it is a constraint. The meta // face has a glyph range of ASCII plus a middle dot, so only the world name and @@ -30,7 +32,6 @@ internal static class ChannelHeader private const float TrackRaw = 1.8f; private const float DetailTrackRaw = 0.9f; private const float IconGapRaw = 8f; - private const float RuleGapRaw = 10f; // Measured, never a fixed 32px: the band has to hold a line of text, and the // font comes from Config, which display scaling does not feed into. @@ -160,19 +161,22 @@ internal static class ChannelHeader var dl = ImGui.GetWindowDrawList(); var bottomRight = origin + new Vector2(width, height); + // Converted first, then measured. EnsureContrast works in ABGR -- + // handing it the theme's RGBA swaps red and blue on both arguments, + // so it measures a contrast that has nothing to do with what ends up + // on screen and returns a colour in the wrong order on top. + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(theme.Colors.Accent), + surfaceAbgr, + 4.5f + ); + + dl.DrawEdgeTint(origin, bottomRight, ColourUtil.ApplyAlpha(accent, 0.10f), height); + var textY = origin.Y + MathF.Round(PadYRaw * scale); if (plan.ShowName) { - // Converted first, then measured. EnsureContrast works in ABGR -- - // handing it the theme's RGBA swaps red and blue on both arguments, - // so it measures a contrast that has nothing to do with what ends up - // on screen and returns a colour in the wrong order on top. - var accent = ColourUtil.EnsureContrast( - ColourUtil.RgbaToAbgr(theme.Colors.Accent), - surfaceAbgr, - 4.5f - ); var x = origin.X + inset; // Centred against the band, not aligned to the text baseline: @@ -188,28 +192,13 @@ internal static class ChannelHeader x += iconSize.X + IconGapRaw * scale; - float drawn; using (meta.Push()) - drawn = dl.DrawTrackedText( + dl.DrawTrackedText( new Vector2(x, textY + DropFor(body, meta, scale)), name, accent, track ); - - // Starts where the name ends and fades into nothing, so it reads as a - // continuation of the heading rather than as a box lid. Stops short of - // the trailing detail when there is one. - var ruleX = x + drawn + RuleGapRaw * scale; - var ruleEnd = - bottomRight.X - inset - (plan.ShowDetail ? detailRun + RuleGapRaw * scale : 0f); - if (ruleEnd > ruleX) - dl.DrawFadeRule( - new Vector2(ruleX, textY + ImGui.GetTextLineHeight() * 0.5f), - ruleEnd - ruleX, - ColourUtil.RgbaToAbgr(theme.Colors.Border), - MathF.Max(1f, scale) - ); } if (plan.ShowDetail && detail.Where.Length > 0) diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs index 5f3fc07..b9a8f4c 100644 --- a/HellionChat/Ui/Windows/InputBarLabWindow.cs +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -10,11 +10,12 @@ using HellionChat.Util; namespace HellionChat.Ui.Windows; -// Variants of the input row, drawn side by side so a decision can be made by -// looking rather than by imagining. Reachable with /hellion lab. +// Variants of UI elements, drawn side by side so decisions are made by looking +// rather than by imagining. Reachable with /hellion lab. // -// Temporary. It comes out once the variants are picked, which is also why it is -// not behind DEBUG -- the decision happens in the build that actually runs. +// Permanent, by Flo's call: a dev playground for seeing ideas in-game against +// the live theme. The radios default to whatever shipped, so the window also +// documents which variant won and what it beat. // // It exists because the alternative was drawing mockups, and mockups are what // sent this cycle down the wrong path once already: they are from before the @@ -30,10 +31,10 @@ internal sealed class InputBarLabWindow : Window private readonly Plugin _plugin; private string _sample = string.Empty; - private int _buttonVariant; - private int _fieldVariant; - private int _pillVariant; - private int _headerVariant; + private int _buttonVariant = 2; + private int _fieldVariant = 2; + private int _pillVariant = 2; + private int _headerVariant = 2; internal InputBarLabWindow(Plugin plugin) : base("Input Bar Lab###hellion-input-lab") @@ -163,13 +164,12 @@ internal sealed class InputBarLabWindow : Window ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(c.Surface), amount * 0.8f), 3f * scale ); - dl.DrawGlowBorder( - min, - max, - ColourUtil.ApplyAlpha(c.Accent, amount * 0.7f), - 1f, - 4 - ); + // Alpha into the LOW byte: DrawGlowBorder reads RGBA, while + // ApplyAlpha writes the high byte (ABGR). The first version + // of this lab mixed them up and got a full-alpha glow with + // the red channel dimmed instead. + var glow = (c.Accent & 0xFFFFFF00u) | (uint)(byte)Math.Round(0xB4 * amount); + dl.DrawGlowBorder(min, max, glow, 1f, 4); } break; } From 3e46600fc33e1da70c593c93fcd413e9288f95c9 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:09:22 +0200 Subject: [PATCH 06/11] fix(style): the popup text fed RGBA into the contrast helper -- and a guard so this stops recurring Fifth and sixth occurrences of the same defect in one day, this time in PopupRow and three spots in the lab. The menu entries and the channel picker were unreadable at rest on the green theme and only became legible on hover, because the hover lerp pulls toward a correctly converted accent -- which is exactly the symptom the tester reported. The shape of the mistake is always identical: a raw ThemeColors member (RGBA) handed to EnsureContrast (ABGR). The compiler cannot see it, both layouts are uint, and a stray extra RgbaToAbgr around the call makes the result look plausible while measuring a contrast between two colours that are never on screen. So this commit is mostly the guard. preflight Block G runs scripts/verify-colour-channels.sh, which flags any raw theme member in either argument of EnsureContrast across the UI tree. Falsified before trusting: broken deliberately, it goes red; and on its very first real run it caught three offenders in the lab that a hand-rolled grep had missed minutes earlier. --- .../Ui/StyleEngine/Widgets/PopupRow.cs | 6 +- HellionChat/Ui/Windows/InputBarLabWindow.cs | 24 ++++++-- scripts/preflight.sh | 3 + scripts/verify-colour-channels.sh | 60 +++++++++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100755 scripts/verify-colour-channels.sh diff --git a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs index c502462..ba8a6ef 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs @@ -64,8 +64,10 @@ internal static class PopupRow accentAbgr ); - var textAbgr = ColourUtil.RgbaToAbgr( - ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + var textAbgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextPrimary), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f ); if (active || amount > 0f) textAbgr = ColourUtil.Lerp(textAbgr, accentAbgr, active ? 0.6f : amount * 0.4f); diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs index b9a8f4c..17f8f52 100644 --- a/HellionChat/Ui/Windows/InputBarLabWindow.cs +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -178,8 +178,10 @@ internal sealed class InputBarLabWindow : Window { var glyph = icon.ToIconString(); var size = ImGui.CalcTextSize(glyph); - var tint = ColourUtil.RgbaToAbgr( - ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextPrimary), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f ); if (amount > 0f) tint = ColourUtil.Lerp(tint, accent, amount); @@ -286,7 +288,11 @@ internal sealed class InputBarLabWindow : Window break; } - var ink = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.WindowBg, c.Accent, 4.5f)); + var ink = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.WindowBg), + ColourUtil.RgbaToAbgr(c.Accent), + 4.5f + ); using (fonts.FontAwesome.Push()) { var arrow = FontAwesomeIcon.ArrowRight.ToIconString(); @@ -333,7 +339,11 @@ internal sealed class InputBarLabWindow : Window var max = origin + new Vector2(width, height); var dl = ImGui.GetWindowDrawList(); - var accent = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.Accent, c.ChildBg, 4.5f)); + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f + ); // The band variant is the one the mockup drew and v1.11.0 abandoned. // Here at a fraction of the strength, as a tint rather than a plate -- @@ -388,7 +398,11 @@ internal sealed class InputBarLabWindow : Window var width = dl.DrawTrackedText( pos, text, - ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.WindowBg, 4.5f)), + ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ), 1.6f * Metrics.Scale ); diff --git a/scripts/preflight.sh b/scripts/preflight.sh index 1eb618f..af60f19 100755 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -18,6 +18,9 @@ echo "==> preflight: Block B — manifest shape" echo "==> preflight: Block C — changelog sync" ./scripts/verify-changelog-sync.sh +echo "==> preflight: Block G — colour channel audit" +./scripts/verify-colour-channels.sh + echo "==> preflight: Block D — plugin compile health" dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet diff --git a/scripts/verify-colour-channels.sh b/scripts/verify-colour-channels.sh new file mode 100755 index 0000000..9bf6b92 --- /dev/null +++ b/scripts/verify-colour-channels.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# verify-colour-channels.sh — Block G of preflight. +# +# EnsureContrast works in ABGR. ThemeColors fields are RGBA. Feeding one into +# the other swaps red and blue on both arguments, measures a contrast between +# two colours that are never on screen, and returns a value in the wrong order +# on top — and because RgbaToAbgr is an involution, a stray extra conversion +# around the call makes it LOOK plausible while being wrong. +# +# This happened four times on 2026-08-19 alone (channel header, ghost buttons, +# pill text, popup rows), each found only by a human staring at an unreadable +# window. The compiler cannot catch it — both layouts are uint. This script +# catches the one shape every occurrence shared: a raw ThemeColors member as a +# direct argument to EnsureContrast. +# +# Legitimate calls convert first (RgbaToAbgr(...)) or pass values that are +# already ABGR (fields/locals named *Abgr, palette.Abgr(...)). + +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +fail() { echo "verify-colour-channels: FAIL — $1" >&2; exit 1; } +ok() { echo "verify-colour-channels: OK — $1"; } + +HITS="$(python3 - "$ROOT/HellionChat" <<'PY' +import re +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +# A ThemeColors member (via any local alias) fed straight into EnsureContrast, +# in either argument position, spanning line breaks. Members already in ABGR +# end in "Abgr" and are exempt. +pattern = re.compile( + r"EnsureContrast\(\s*" + r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*," + r"|EnsureContrast\([^;]{0,200}?,\s*" + r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*[,)]", + re.S, +) + +hits = [] +for f in root.rglob("*.cs"): + if "obj" in f.parts or "bin" in f.parts: + continue + text = f.read_text(encoding="utf-8") + for m in pattern.finditer(text): + line = text.count("\n", 0, m.start()) + 1 + hits.append(f"{f.relative_to(root.parent)}:{line}") + +print("\n".join(hits)) +PY +)" + +if [ -n "$HITS" ]; then + fail "raw ThemeColors member fed into EnsureContrast (expects ABGR — wrap in RgbaToAbgr): +$HITS" +fi + +ok "no raw RGBA theme members reach EnsureContrast" From 8a9692a71d4f659d16ad86db47a8ac25182d7112 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:13:30 +0200 Subject: [PATCH 07/11] fix(chat): size the popups from their rows, not the rows from a guess An ImGui popup does not grow for draw-list content, so both new popups carried a fixed minimum width -- and German outran it within a day. The menu clipped Schnellauswahl and Chat ausblenden mid-word; the channel picker would have done the same to Freie Gesellschaft on a narrow theme font. PopupRow gets a CalcWidth that measures label plus icon plus padding under the faces that will draw them, and both popups take the widest visible entry as their width. Locale-proof by construction: whatever language writes the longest string sets the size. --- HellionChat/Ui/Components/InputBar.cs | 63 ++++++++++++++++++- .../Ui/StyleEngine/Widgets/PopupRow.cs | 17 +++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/HellionChat/Ui/Components/InputBar.cs b/HellionChat/Ui/Components/InputBar.cs index 377c30d..2e18f96 100644 --- a/HellionChat/Ui/Components/InputBar.cs +++ b/HellionChat/Ui/Components/InputBar.cs @@ -313,7 +313,18 @@ internal sealed class InputBar return; } - ImGui.Dummy(new Vector2(150f * StyleEngine.Metrics.Scale, 0f)); + var pickerWidth = 0f; + foreach (var chatType in tab.SelectedChannels.Keys) + { + if (chatType.ToInputChannel() is not { } ch) + continue; + pickerWidth = MathF.Max( + pickerWidth, + StyleEngine.Widgets.PopupRow.CalcWidth(ch.ToChatType().Name(), null, _fonts) + ); + } + + ImGui.Dummy(new Vector2(pickerWidth, 0f)); var i = 0; foreach (var chatType in tab.SelectedChannels.Keys) @@ -799,7 +810,55 @@ internal sealed class InputBar try { - ImGui.Dummy(new Vector2(160f * StyleEngine.Metrics.Scale, 0f)); + // Width from the widest visible entry, not a fixed minimum -- the + // popup does not grow for draw-list content, and German labels + // outran the first guess within a day. + var menuWidth = 0f; + if (_themeQuickPicker is not null) + menuWidth = MathF.Max( + menuWidth, + StyleEngine.Widgets.PopupRow.CalcWidth( + HellionStrings.Settings_QuickPicker_Tooltip, + FontAwesomeIcon.Palette, + _fonts + ) + ); + menuWidth = MathF.Max( + menuWidth, + StyleEngine.Widgets.PopupRow.CalcWidth( + HellionStrings.InputBar_Settings_Tooltip, + FontAwesomeIcon.Cog, + _fonts + ) + ); + menuWidth = MathF.Max( + menuWidth, + StyleEngine.Widgets.PopupRow.CalcWidth( + Language.Context_ScreenshotMode, + FontAwesomeIcon.Camera, + _fonts + ) + ); + if (Plugin.Config.ShowHideButton && _onHideWindow is not null) + menuWidth = MathF.Max( + menuWidth, + StyleEngine.Widgets.PopupRow.CalcWidth( + HellionStrings.InputBar_HideChat_Tooltip, + FontAwesomeIcon.EyeSlash, + _fonts + ) + ); + if (OnPopIn is not null) + menuWidth = MathF.Max( + menuWidth, + StyleEngine.Widgets.PopupRow.CalcWidth( + HellionStrings.InputBar_PopIn_Tooltip, + FontAwesomeIcon.Times, + _fonts + ) + ); + + ImGui.Dummy(new Vector2(menuWidth, 0f)); if ( _themeQuickPicker is not null diff --git a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs index ba8a6ef..a16755d 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs @@ -21,6 +21,23 @@ internal static class PopupRow private const float IconGapRaw = 8f; private const float AccentBarRaw = 2f; + // The popup must be sized from its rows, not the rows from the popup: an + // ImGui popup does not grow for draw-list content, so a fixed minimum width + // clips whichever locale writes the longest labels. German did, first day. + internal static float CalcWidth(string label, FontAwesomeIcon? icon, FontManager fonts) + { + var scale = Metrics.Scale; + var w = PadXRaw * 2f * scale + ImGui.CalcTextSize(label).X; + + if (icon is { } glyph) + { + using (fonts.FontAwesome.Push()) + w += ImGui.CalcTextSize(glyph.ToIconString()).X + IconGapRaw * scale; + } + + return w; + } + internal static bool Draw( string id, string label, From 12a445e08f84705668ad4589544c4e0a8824291d Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:20:22 +0200 Subject: [PATCH 08/11] feat(sidebar): soften the seam and let the active tab reach its content Tester feedback, relayed by Flo: the transition from the tab list to the chat field is too hard, and the tabs could present themselves better. Three causes, three changes, no structural touch -- tabs stay tabs, per the standing decision. The full-width border line under every row was a ladder of hard cuts. It is a fading rule now, starting past the icon column and dissolving before the right edge -- the same shape the section headers have used since v1.11.0, at about half the opacity. The gap between the sidebar group and the message area was a bare strip of window background with a hard edge on both sides. A faint surface wash fades across it toward the messages, turning the cut into a seam. And the selected tab bridges that gap: its active fill extends across the spacing so it touches the conversation it selects -- the classic tab metaphor, attached instead of adjacent. The bridge is a RowStyle knob (default zero), so sidebar rows opt in and nothing else inherits it. Hover fills picked up the standard three-pixel rounding on the way. --- HellionChat/Ui/Components/Sidebar.cs | 7 ++++++ HellionChat/Ui/StyleEngine/Widgets/Row.cs | 29 ++++++++++++++++++----- HellionChat/Ui/Windows/MainWindow.cs | 19 +++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 64bc5cc..4adad40 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -317,6 +317,13 @@ internal sealed class Sidebar ), AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors), BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity), + }, + new RowStyle + { + // Across the gap between the sidebar group and the message + // area, so the selected tab touches the conversation it + // selects instead of ending at the seam. + ActiveBridgeWidth = ImGui.GetStyle().ItemSpacing.X, } ); diff --git a/HellionChat/Ui/StyleEngine/Widgets/Row.cs b/HellionChat/Ui/StyleEngine/Widgets/Row.cs index f80cd93..c2dba86 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/Row.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/Row.cs @@ -25,6 +25,12 @@ internal readonly record struct RowStyle public float AccentBarWidth { get; init; } = 2f; public bool DrawSeparator { get; init; } = true; + + // Extends the active fill past the right edge by this many pixels, so the + // selected tab reads as attached to the content it selects -- the classic + // tab metaphor -- instead of ending at a hard seam. Zero for rows that do + // not sit next to the thing they select. + public float ActiveBridgeWidth { get; init; } = 0f; } internal static class Row @@ -51,13 +57,19 @@ internal static class Row // default is 0.85 -- an opaque fill per row would make the sidebar a // solid block inside a translucent window. if (state.IsActive) - dl.AddRectFilled(origin, max, state.SurfaceActiveAbgr); + { + var fillMax = max; + if (style.ActiveBridgeWidth > 0f) + fillMax.X += style.ActiveBridgeWidth; + dl.AddRectFilled(origin, fillMax, state.SurfaceActiveAbgr); + } if (state.HoverAmount > 0f) dl.AddRectFilled( origin, max, - ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount) + ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount), + 3f * Metrics.Scale ); if (state.IsActive && style.AccentBarWidth > 0f) @@ -72,12 +84,17 @@ internal static class Row // path, so an unscaled 1px offset with a scaled stroke puts half the // line below max.Y -- and rows stack flush, so that half lands in the // first pixel row of the next one. + // A full-width line under every row reads as a ladder of hard + // cuts -- the tester's words were that the list felt too hard. + // The rule now starts past the icon column and fades to nothing + // before the right edge, the same shape the section headers use. var thickness = Metrics.Scale; var y = max.Y - thickness * 0.5f; - dl.AddLine( - new Vector2(origin.X, y), - new Vector2(max.X, y), - state.BorderAbgr, + var inset = 10f * Metrics.Scale; + dl.DrawFadeRule( + new Vector2(origin.X + inset, y), + size.X - inset, + ColourUtil.ApplyAlpha(state.BorderAbgr, 0.55f), thickness ); } diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index b496c3e..bd07ea3 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -367,6 +367,25 @@ internal sealed class MainWindow : Window, IFocusableChatWindow _sidebar.Draw(bodyWidth, tabs, ref _activeTab); } + // The gap between the tab list and the conversation used to be a bare + // strip of window background with a hard edge on both sides -- tester + // feedback called the transition too hard. A faint surface wash that + // fades toward the messages turns the cut into a seam. The active row + // bridges across it (RowStyle.ActiveBridgeWidth), so the selected tab + // stays attached to its content on top of the wash. + { + var seamMin = new Vector2(ImGui.GetItemRectMax().X, ImGui.GetItemRectMin().Y); + var seamMax = new Vector2( + seamMin.X + ImGui.GetStyle().ItemSpacing.X, + ImGui.GetItemRectMax().Y + ); + var wash = ColourUtil.ApplyAlpha( + ColourUtil.RgbaToAbgr(Plugin.Instance.ThemeRegistry.Active.Colors.Surface), + 0.35f + ); + ImGui.GetWindowDrawList().AddRectFilledMultiColor(seamMin, seamMax, wash, 0u, 0u, wash); + } + ImGui.SameLine(); using (ImRaii.Group()) From eeffb5bc6b488e07fa0d1a9ca41ac7b311ffbdc0 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:26:47 +0200 Subject: [PATCH 09/11] fix(style): the hover fade ran at light speed, and stopped dead Tester feedback via Flo, aimed at the sidebar tabs but true everywhere: 14/s in means a full fade in seventy milliseconds, which is five frames -- fast enough to read as a flicker rather than a fade. Halved to 6.5/s in and 4/s out, which puts the rise around 150ms and the settle around 250ms, where UI fades usually live. And the curve was linear: one speed the whole way, then a dead stop. Consumers now get a smoothstep over the linear state -- soft start, soft landing -- while the state itself stays linear, because the evict rule and the advance step are written against it. Every hover in the plugin inherits both changes through the one Query call, so the sidebar, the popups, the ghost buttons and the message rows all breathe at the same pace. The timing test now guards the other direction: it fails if anyone drifts the rate back toward light speed. --- HellionChat/Ui/StyleEngine/HoverState.cs | 5 ++++- HellionChat/Util/HoverMath.cs | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/HellionChat/Ui/StyleEngine/HoverState.cs b/HellionChat/Ui/StyleEngine/HoverState.cs index 15775ad..0f1246c 100644 --- a/HellionChat/Ui/StyleEngine/HoverState.cs +++ b/HellionChat/Ui/StyleEngine/HoverState.cs @@ -61,7 +61,10 @@ internal static class HoverState // OR, never assign: a second caller in the same frame that is not hovered // must not cancel the first one that is. entry.Hovered |= hovered; - return entry.Value; + + // Callers get the eased value; the entry keeps the linear one, which is + // what the evict rule and the advance step are written against. + return HoverMath.Ease(entry.Value); } // Never re-entrant: BeginFrame runs once from the draw thread, AdvanceForTest diff --git a/HellionChat/Util/HoverMath.cs b/HellionChat/Util/HoverMath.cs index 7a2d624..6fd8263 100644 --- a/HellionChat/Util/HoverMath.cs +++ b/HellionChat/Util/HoverMath.cs @@ -1,12 +1,17 @@ namespace HellionChat.Util; // State rules for the held hover value, split from HoverState so the build suite -// can pin them without an ImGui frame. Rates follow Lightless (Selune.cs:36-37): -// slower out than in is what makes a fade read as deliberate rather than laggy. +// can pin them without an ImGui frame. Slower out than in is what makes a fade +// read as deliberate rather than laggy (the asymmetry follows Lightless, +// Selune.cs:36-37). +// +// The rates started at 14/s in and 8/s out -- a full fade-in in seventy +// milliseconds, which tester feedback called light speed. Half that now: rise in +// about 150ms, settle in about 250ms, which is where UI fades usually live. internal static class HoverMath { - internal const float FadeInPerSecond = 14f; - internal const float FadeOutPerSecond = 8f; + internal const float FadeInPerSecond = 6.5f; + internal const float FadeOutPerSecond = 4f; // Below this an entry is indistinguishable from zero and can be dropped. internal const float EvictBelow = 0.001f; @@ -20,4 +25,9 @@ internal static class HoverMath ); internal static bool ShouldEvict(float value, bool hovered) => !hovered && value <= EvictBelow; + + // Smoothstep over the linear state. The state itself stays linear -- evict + // and threshold logic depend on it -- but what callers paint with starts + // gently and lands gently instead of moving at one speed and stopping dead. + internal static float Ease(float t) => t * t * (3f - 2f * t); } From 9e62ed762d217c629cd79e12c44d99e0dd0de357 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:29:01 +0200 Subject: [PATCH 10/11] fix(style): slower still -- the fade takes a third of a second now The halved rates were still judged much too fast, so: 3/s in, 2/s out. Rise in roughly 330ms, settle in 500ms. The textbook says hover fades live around 150ms; the textbook does not play this game, and the person who does gets the vote. Timing guard moved with it. --- HellionChat/Util/HoverMath.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/HellionChat/Util/HoverMath.cs b/HellionChat/Util/HoverMath.cs index 6fd8263..c006eef 100644 --- a/HellionChat/Util/HoverMath.cs +++ b/HellionChat/Util/HoverMath.cs @@ -6,12 +6,14 @@ namespace HellionChat.Util; // Selune.cs:36-37). // // The rates started at 14/s in and 8/s out -- a full fade-in in seventy -// milliseconds, which tester feedback called light speed. Half that now: rise in -// about 150ms, settle in about 250ms, which is where UI fades usually live. +// milliseconds, which tester feedback called light speed. The first correction +// halved them and was still "much too fast", so: rise in about a third of a +// second, settle in half of one. Textbook says 150ms; the textbook does not +// play this game. internal static class HoverMath { - internal const float FadeInPerSecond = 6.5f; - internal const float FadeOutPerSecond = 4f; + internal const float FadeInPerSecond = 3f; + internal const float FadeOutPerSecond = 2f; // Below this an entry is indistinguishable from zero and can be dropped. internal const float EvictBelow = 0.001f; From dcc8ff18673a7ab3af7c3808e54d6e7027865d09 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:35:35 +0200 Subject: [PATCH 11/11] chore(release): close the v1.14.0 cycle Version to 1.14.0 in the csproj, changelog and roadmap for the local state. Not published: repo.json stays on 1.5.6.0, links untouched. The changelog leads with what the row became and is honest about how it got there: every style decision in this cycle was made by looking at variants in the lab, in-game, against the live theme -- and the two recurring traps of the day (the RGBA/ABGR contrast swap, popups that do not size to their content) each left a guard behind rather than just a fix. Smoke-tested continuously by Flo through the evening rather than at a single gate: the contrast bug, the clipped menu, the hover speed and the seam hardness were all his catches, live, and each is named in the log where it was fixed. --- HellionChat/HellionChat.csproj | 2 +- docs/CHANGELOG.md | 77 ++++++++++++++++++++++++++++++++++ docs/ROADMAP.md | 10 +++-- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index aaf2c79..2c7b7d6 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -1,7 +1,7 @@ - 1.13.0 + 1.14.0 enable enable diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f352c18..f7e7651 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,83 @@ releases as an overview and links to the release pages for details. --- +## [1.14.0] — unreleased (local only) + +The input row. It was the one surface a user actually works in and it spoke three +shape languages at once — a rounded pill, a square field, five filled default +buttons — sitting directly above a status bar that is rounded throughout. This +release makes the row one thing, and it teaches the popups the window's own +shapes while it is in there. **Not published** — the public release stays at +v1.5.6. + +### Added + +- A style lab under `/hellion lab`: variants of UI elements side by side, drawn + in real ImGui against the live theme. Permanent, as a dev playground — its + radio buttons default to whatever shipped, so the window doubles as a record + of which variant won. Every decision in this release was made by looking at it + in-game rather than at a drawing. +- `PopupRow`, the row a popup is made of: soft hover fill, a two-pixel accent + bar on the active entry — the sidebar row's shape, which is also exactly what + Character Select+ draws for its own popup entries. The channel picker and the + new input-row menu are built from it. +- A colour-channel guard in preflight (Block G). `EnsureContrast` works in ABGR, + theme colours are RGBA, both are `uint`, and the mistake of feeding one into + the other happened six times in a single day — each found only by a human + staring at unreadable text. The guard flags any raw theme member reaching the + helper; on its first real run it caught three cases a hand-written search had + missed minutes earlier. + +### Changed + +- The five filled buttons in the input row are two ghost buttons and a menu. + Symbols stay outside because they are used mid-sentence; theme, settings, + screenshot mode and hide moved in. The ghosts are flat at rest and glow on + hover — the first caller `DrawGlowBorder` ever had. +- The screenshot toggle was allowed into the menu only because its state moved + to the status bar first, as an accent camera pill that shows while the mode is + on. Knowing what the screen shows before the screenshot key is pressed is the + entire point of the mode; a state behind a closed menu answers nothing. +- The input field paints its own rounded surface and hands ImGui a transparent + frame, so the widget draws only text and caret. Focus is a two-pixel accent + rail on the left edge — the same mark rows and popups use. The field is about + a hundred pixels wider, reclaimed from the button reserve. +- The channel pill is chamfered, the corner language of the segmented control, + with a faint white depth gradient. +- The channel header traded its surface plate for a tenth-opacity accent wash. + The plate was the exact shape the settings window abandoned in v1.11.0 — it + reads fine on blue themes and vanishes on violet ones. +- The sidebar softened, on tester feedback that the transition to the chat field + was too hard: the full-width border line under every row is a fading rule now, + the gap between list and messages carries a surface wash instead of two hard + edges, and the selected tab bridges across it so it touches the conversation + it selects. +- Hover fades run at a quarter of their old speed with a smoothstep curve — + soft start, soft landing. 14/s in was judged light speed; it is 3/s now, and + a test guards against drifting back. Every hover in the plugin inherits both + changes through the one shared code path. + +### Fixed + +- Popups size themselves from their widest visible entry. Both new popups + carried a guessed minimum width, and German outran it within a day — + `Schnellauswahl` and `Chat ausblenden` were clipped mid-word. +- Popup entry text was unreadable at rest on several themes and only became + legible on hover: the RGBA-into-ABGR mistake again, in `PopupRow` and three + spots in the lab. Fixed, and the new preflight guard exists so the seventh + occurrence dies in CI rather than in a screenshot. +- The pill label was raw theme text on an accent fill with no contrast check. + +### Known issues + +- The channel header still recomputes its widths every frame rather than on the + status bar's one-second tick. +- `Metrics.Scale` still calls a Dalamud property that is marked obsolete. +- The first-run wizard is now the last surface drawn in ImGui defaults. It is + the next cycle. + +--- + ## [1.13.0] — unreleased (local only) Typography. The message list was the last surface still drawn in ImGui defaults, diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c918a00..4e6b69f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -14,7 +14,7 @@ be a poor fit for the plugin's privacy-first scope during brainstorming. The published release is **v1.5.6**. Development since then runs as a UI rebuild towards v2.0.0 and is not published: the whole window layer is being rewritten from ImGui defaults to custom drawing. -Versions v1.6.0 through v1.13.0 are local development states, and `repo.json` deliberately keeps +Versions v1.6.0 through v1.14.0 are local development states, and `repo.json` deliberately keeps its download links on v1.5.6 so nobody updates into a partial state. Where it stands: @@ -35,8 +35,12 @@ Where it stands: - **v1.13.0** — typography. Named type roles instead of one size, a channel header above the conversation, timestamps in a column of their own, and rows with a surface. Screenshot mode reached one of four surfaces that draw a tab name and now reaches all of them. -- **v1.14.0 onwards** — the input bar, then the first-run wizard: the last two surfaces still - drawn in ImGui defaults. +- **v1.14.0** — the input row: two ghost buttons and a menu where five filled defaults sat, a + field with its own surface and focus rail, popups built from the sidebar's row shape, and a + softened seam between the tab list and the conversation. Plus a style lab under + `/hellion lab` and a preflight guard for the RGBA/ABGR contrast trap. +- **v1.15.0** — the first-run wizard, the last surface still drawn in ImGui defaults, and the + first thing a tester sees. The sidebar is no longer on this list. Earlier drafts had it moving from tab rows to channel rows, but both reasons for that are gone: it was flat and unstyled when the idea was written,