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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user