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.
This commit is contained in:
2026-08-18 17:13:43 +02:00
parent 5a4c3b6707
commit 2f4518a6b1
8 changed files with 94 additions and 6 deletions
+4 -1
View File
@@ -162,7 +162,10 @@ internal static class PluginHostFactory
services.AddSingleton(sp => new Ui.Components.Settings.TabSidebar(
sp.GetRequiredService<FontManager>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea());
services.AddSingleton(sp => new Ui.Components.Settings.ContentArea(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Ui.Components.Settings.ThemePicker(
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Plugin>()
@@ -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<string> 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)
{
@@ -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) =>
@@ -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)
);
@@ -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
@@ -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
);
}
@@ -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();
+17
View File
@@ -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);