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.
This commit is contained in:
2026-08-19 19:04:05 +02:00
parent 6bdfecb1df
commit d7a308b522
4 changed files with 78 additions and 62 deletions
+43 -15
View File
@@ -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)