From 2f4518a6b184c6292080b88114e66c6611cb7297 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 18 Aug 2026 17:13:43 +0200 Subject: [PATCH] fix(style): give the settings pane the contrast it was missing The converted tab had the right structure and no legibility. Five separate causes, all of them contrast rather than colour choice. The content pane never set ChildBg, so it inherited whatever showed through the window, and the window is translucent by default. Settings text was sitting on moving scenery. It gets the surface tone now; the window's own opacity still applies on top, so the glass look survives. Section headings had no ground of their own and floated between the rows at roughly their weight, which meant the tab had lost its grouping entirely. They get the raised surface, and their accent bar now runs the full height instead of stopping at the title line. An off toggle was filled with the same surface tone as the row behind it and read as empty space. It gets an outline that fades out as it turns on, where the filled track carries the shape by itself. Two colours could not be picked statically at all, because themes here range from near-black to pastel: the label on a selected segment, and the knob on the track. Both now derive from the luminance of what they sit on, so neither can end up light-on-light. That is what ColourUtil.OnColour is for. Descriptions drop from TextMuted to TextFaint. Level with the label they made each row read as two settings rather than one with an explanation. --- HellionChat/PluginHostFactory.cs | 5 +++- .../Ui/Components/Settings/ContentArea.cs | 27 +++++++++++++++++++ .../Ui/Components/Settings/SettingsPalette.cs | 6 ++++- .../Ui/StyleEngine/Widgets/SectionHeader.cs | 12 +++++++-- .../StyleEngine/Widgets/SegmentedControl.cs | 10 ++++++- .../Ui/StyleEngine/Widgets/ToggleSwitch.cs | 22 ++++++++++++++- HellionChat/Ui/Windows/WidgetGalleryWindow.cs | 1 + HellionChat/Util/ColourUtil.cs | 17 ++++++++++++ 8 files changed, 94 insertions(+), 6 deletions(-) diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index f2ed0fd..83fba5f 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -162,7 +162,10 @@ internal static class PluginHostFactory services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar( sp.GetRequiredService() )); - services.AddSingleton(sp => new Ui.Components.Settings.ContentArea()); + services.AddSingleton(sp => new Ui.Components.Settings.ContentArea( + sp.GetRequiredService(), + sp.GetRequiredService() + )); services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker( sp.GetRequiredService(), sp.GetRequiredService() diff --git a/HellionChat/Ui/Components/Settings/ContentArea.cs b/HellionChat/Ui/Components/Settings/ContentArea.cs index 1e7da68..4021c0d 100644 --- a/HellionChat/Ui/Components/Settings/ContentArea.cs +++ b/HellionChat/Ui/Components/Settings/ContentArea.cs @@ -1,12 +1,39 @@ using System.Numerics; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; +using HellionChat.Themes; +using HellionChat.Ui.StyleEngine; +using HellionChat.Util; namespace HellionChat.Ui.Components.Settings; internal sealed class ContentArea { + private readonly ThemeRegistry _themes; + private readonly TokenResolver _resolver; + + public ContentArea(ThemeRegistry themes, TokenResolver resolver) + { + _themes = themes; + _resolver = resolver; + } + public void Draw(string activeTab, Action renderTab) { + // ChildBg was never set, so the pane inherited whatever showed through + // the window -- and the window is translucent by default. Settings text + // ended up sitting on moving scenery, which is legible in a screenshot + // and not while the game is running. + // + // Pushed as a colour rather than painted into the draw list so ImGui + // fills it before the scrollbar and border, and the window's own opacity + // still applies on top. + var colors = _themes.Active.Colors; + using var bg = ImRaii.PushColor( + ImGuiCol.ChildBg, + ColourUtil.RgbaToVector4(_resolver.Resolve(Token.SurfaceBase, colors)) + ); + using var child = ImRaii.Child("##settings-content", new Vector2(0, 0), true); if (!child.Success) { diff --git a/HellionChat/Ui/Components/Settings/SettingsPalette.cs b/HellionChat/Ui/Components/Settings/SettingsPalette.cs index d4db594..b2eee56 100644 --- a/HellionChat/Ui/Components/Settings/SettingsPalette.cs +++ b/HellionChat/Ui/Components/Settings/SettingsPalette.cs @@ -23,7 +23,9 @@ internal sealed class SettingsPalette new() { LabelAbgr = _palette.Abgr(Token.Text, c), - DescriptionAbgr = _palette.Abgr(Token.TextMuted, c), + // A step below the label, not level with it: at TextMuted the two + // lines carried the same weight and the row read as two settings. + DescriptionAbgr = _palette.Abgr(Token.TextFaint, c), SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c), BorderAbgr = _palette.Abgr(Token.Border, c), }; @@ -34,6 +36,7 @@ internal sealed class SettingsPalette TrackOffAbgr = _palette.Abgr(Token.SurfaceBase, c), TrackOnAbgr = _palette.Abgr(Token.AccentPrimary, c), KnobAbgr = _palette.Abgr(Token.Text, c), + BorderAbgr = _palette.Abgr(Token.Border, c), }; internal SectionHeaderColors Section(ThemeColors c) => @@ -44,6 +47,7 @@ internal sealed class SettingsPalette AccentAbgr = _palette.Abgr(Token.AccentPrimary, c), BorderAbgr = _palette.Abgr(Token.Border, c), HoverAbgr = _palette.Abgr(Token.SurfaceHover, c), + SurfaceAbgr = _palette.Abgr(Token.SurfaceRaised, c), }; internal SegmentedControlColors Segmented(ThemeColors c) => diff --git a/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs index 78c1ffd..1189b8b 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs @@ -13,6 +13,10 @@ internal readonly record struct SectionHeaderColors public uint AccentAbgr { get; init; } public uint BorderAbgr { get; init; } public uint HoverAbgr { get; init; } + + // Own ground. Without it the heading floats between the rows at roughly + // their own weight and stops separating anything. + public uint SurfaceAbgr { get; init; } } internal readonly record struct SectionHeaderStyle @@ -20,7 +24,7 @@ internal readonly record struct SectionHeaderStyle public SectionHeaderStyle() { } public float PadY { get; init; } = 6f; - public float AccentBarWidth { get; init; } = 2f; + public float AccentBarWidth { get; init; } = 3f; public float ChevronInset { get; init; } = 6f; } @@ -88,6 +92,8 @@ internal static class SectionHeader var dl = ImGui.GetWindowDrawList(); var max = origin + size; + dl.AddRectFilled(origin, max, ColourUtil.ApplyAlpha(colors.SurfaceAbgr, alpha)); + if (hoverAmount > 0f) dl.AddRectFilled( origin, @@ -95,10 +101,12 @@ internal static class SectionHeader ColourUtil.ApplyAlpha(colors.HoverAbgr, hoverAmount * alpha) ); + // Full height, not just the title line. Stopping at the title left the + // bar looking like a stray tick next to a two-line heading. var barWidth = style.AccentBarWidth * scale; dl.AddRectFilled( origin, - new Vector2(origin.X + barWidth, origin.Y + titleHeight + padY * 2f), + new Vector2(origin.X + barWidth, max.Y), ColourUtil.ApplyAlpha(colors.AccentAbgr, alpha) ); diff --git a/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs b/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs index 4fc5b63..acbd32f 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs @@ -117,7 +117,15 @@ internal static class SegmentedControl MetricsMath.Center(height, textSize.Y) ), ColourUtil.ApplyAlpha( - isSelected ? colors.SelectedLabelAbgr : colors.LabelAbgr, + isSelected + // On the accent fill, so the readable colour depends on + // how light that accent is in the active theme. + ? ColourUtil.OnColour( + colors.SelectedAbgr, + colors.SelectedLabelAbgr, + colors.TrackAbgr + ) + : colors.LabelAbgr, alpha ), label diff --git a/HellionChat/Ui/StyleEngine/Widgets/ToggleSwitch.cs b/HellionChat/Ui/StyleEngine/Widgets/ToggleSwitch.cs index 5575771..3d00ff2 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/ToggleSwitch.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/ToggleSwitch.cs @@ -11,6 +11,11 @@ internal readonly record struct ToggleSwitchColors public uint TrackOffAbgr { get; init; } public uint TrackOnAbgr { get; init; } public uint KnobAbgr { get; init; } + + // The off track needs an outline of its own. Filled with a surface tone it + // is the same colour as the row behind it, so an off switch read as empty + // space rather than as a control. + public uint BorderAbgr { get; init; } } internal readonly record struct ToggleSwitchStyle @@ -66,10 +71,25 @@ internal static class ToggleSwitch var track = ColourUtil.Lerp(colors.TrackOffAbgr, colors.TrackOnAbgr, amount); dl.AddRectFilled(origin, max, track, size.Y * 0.5f); + + // Fades out as the switch turns on, where the filled track carries the + // shape by itself. + if (amount < 1f) + dl.AddRect( + origin, + max, + ColourUtil.ApplyAlpha(colors.BorderAbgr, 1f - amount), + size.Y * 0.5f, + ImDrawFlags.None, + Metrics.Scale + ); + + // The knob picks its contrast from the track it sits on, so it stays + // visible on a pale accent and on a near-black surface alike. dl.AddCircleFilled( new Vector2(origin.X + knobX, origin.Y + size.Y * 0.5f), knobR, - colors.KnobAbgr, + ColourUtil.OnColour(track, colors.KnobAbgr, colors.TrackOffAbgr), 16 ); } diff --git a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs index 35ad0f0..3d51571 100644 --- a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs +++ b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs @@ -160,6 +160,7 @@ internal sealed class WidgetGalleryWindow : Window TrackOffAbgr = _palette.Abgr(Token.SurfaceRaised, c), TrackOnAbgr = _palette.Abgr(Token.AccentPrimary, c), KnobAbgr = _palette.Abgr(Token.Text, c), + BorderAbgr = _palette.Abgr(Token.Border, c), }; var size = ToggleSwitch.CalcSize(); diff --git a/HellionChat/Util/ColourUtil.cs b/HellionChat/Util/ColourUtil.cs index 9dee87d..462ac5e 100755 --- a/HellionChat/Util/ColourUtil.cs +++ b/HellionChat/Util/ColourUtil.cs @@ -108,6 +108,23 @@ internal static class ColourUtil // going fully saturated (effect level stays "subtle"). RGB-only on // purpose -- DrawHoverSheen owns the alpha falloff. // TEST-MIRROR: ../../../Hellion Build test/Util/ColourUtilTintTests.cs + // Relative luminance of an ABGR colour, 0..1, using the sRGB coefficients. + // Alpha is ignored: this answers "is this surface light or dark", and a + // translucent light surface still reads light against the window behind it. + internal static float Luminance(uint abgr) + { + var r = (abgr & 0xFFu) / 255f; + var g = ((abgr >> 8) & 0xFFu) / 255f; + var b = ((abgr >> 16) & 0xFFu) / 255f; + return 0.2126f * r + 0.7152f * g + 0.0722f * b; + } + + // Picks whichever of two candidates stands further from the background. + // Themes range from near-black to pastel, so a fixed text colour on an accent + // fill is legible in some and invisible in others. + internal static uint OnColour(uint background, uint light, uint dark) => + Luminance(background) > 0.5f ? dark : light; + internal static uint LerpTowardWhite(uint abgr, float t) { t = Math.Clamp(t, 0f, 1f);