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:
@@ -379,10 +379,9 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
#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<Plugin>()
|
||||
));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,10 +161,6 @@ internal static class ChannelHeader
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
var bottomRight = origin + new Vector2(width, 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
|
||||
@@ -173,6 +170,13 @@ internal static class ChannelHeader
|
||||
surfaceAbgr,
|
||||
4.5f
|
||||
);
|
||||
|
||||
dl.DrawEdgeTint(origin, bottomRight, ColourUtil.ApplyAlpha(accent, 0.10f), height);
|
||||
|
||||
var textY = origin.Y + MathF.Round(PadYRaw * scale);
|
||||
|
||||
if (plan.ShowName)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user