From 3e46600fc33e1da70c593c93fcd413e9288f95c9 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 19 Aug 2026 19:09:22 +0200 Subject: [PATCH] fix(style): the popup text fed RGBA into the contrast helper -- and a guard so this stops recurring Fifth and sixth occurrences of the same defect in one day, this time in PopupRow and three spots in the lab. The menu entries and the channel picker were unreadable at rest on the green theme and only became legible on hover, because the hover lerp pulls toward a correctly converted accent -- which is exactly the symptom the tester reported. The shape of the mistake is always identical: a raw ThemeColors member (RGBA) handed to EnsureContrast (ABGR). The compiler cannot see it, both layouts are uint, and a stray extra RgbaToAbgr around the call makes the result look plausible while measuring a contrast between two colours that are never on screen. So this commit is mostly the guard. preflight Block G runs scripts/verify-colour-channels.sh, which flags any raw theme member in either argument of EnsureContrast across the UI tree. Falsified before trusting: broken deliberately, it goes red; and on its very first real run it caught three offenders in the lab that a hand-rolled grep had missed minutes earlier. --- .../Ui/StyleEngine/Widgets/PopupRow.cs | 6 +- HellionChat/Ui/Windows/InputBarLabWindow.cs | 24 ++++++-- scripts/preflight.sh | 3 + scripts/verify-colour-channels.sh | 60 +++++++++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100755 scripts/verify-colour-channels.sh diff --git a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs index c502462..ba8a6ef 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/PopupRow.cs @@ -64,8 +64,10 @@ internal static class PopupRow accentAbgr ); - var textAbgr = ColourUtil.RgbaToAbgr( - ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + var textAbgr = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextPrimary), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f ); if (active || amount > 0f) textAbgr = ColourUtil.Lerp(textAbgr, accentAbgr, active ? 0.6f : amount * 0.4f); diff --git a/HellionChat/Ui/Windows/InputBarLabWindow.cs b/HellionChat/Ui/Windows/InputBarLabWindow.cs index b9a8f4c..17f8f52 100644 --- a/HellionChat/Ui/Windows/InputBarLabWindow.cs +++ b/HellionChat/Ui/Windows/InputBarLabWindow.cs @@ -178,8 +178,10 @@ internal sealed class InputBarLabWindow : Window { var glyph = icon.ToIconString(); var size = ImGui.CalcTextSize(glyph); - var tint = ColourUtil.RgbaToAbgr( - ColourUtil.EnsureContrast(c.TextPrimary, c.ChildBg, 4.5f) + var tint = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextPrimary), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f ); if (amount > 0f) tint = ColourUtil.Lerp(tint, accent, amount); @@ -286,7 +288,11 @@ internal sealed class InputBarLabWindow : Window break; } - var ink = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.WindowBg, c.Accent, 4.5f)); + var ink = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.WindowBg), + ColourUtil.RgbaToAbgr(c.Accent), + 4.5f + ); using (fonts.FontAwesome.Push()) { var arrow = FontAwesomeIcon.ArrowRight.ToIconString(); @@ -333,7 +339,11 @@ internal sealed class InputBarLabWindow : Window var max = origin + new Vector2(width, height); var dl = ImGui.GetWindowDrawList(); - var accent = ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.Accent, c.ChildBg, 4.5f)); + var accent = ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.Accent), + ColourUtil.RgbaToAbgr(c.ChildBg), + 4.5f + ); // The band variant is the one the mockup drew and v1.11.0 abandoned. // Here at a fraction of the strength, as a tint rather than a plate -- @@ -388,7 +398,11 @@ internal sealed class InputBarLabWindow : Window var width = dl.DrawTrackedText( pos, text, - ColourUtil.RgbaToAbgr(ColourUtil.EnsureContrast(c.TextMuted, c.WindowBg, 4.5f)), + ColourUtil.EnsureContrast( + ColourUtil.RgbaToAbgr(c.TextMuted), + ColourUtil.RgbaToAbgr(c.WindowBg), + 4.5f + ), 1.6f * Metrics.Scale ); diff --git a/scripts/preflight.sh b/scripts/preflight.sh index 1eb618f..af60f19 100755 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -18,6 +18,9 @@ echo "==> preflight: Block B — manifest shape" echo "==> preflight: Block C — changelog sync" ./scripts/verify-changelog-sync.sh +echo "==> preflight: Block G — colour channel audit" +./scripts/verify-colour-channels.sh + echo "==> preflight: Block D — plugin compile health" dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet diff --git a/scripts/verify-colour-channels.sh b/scripts/verify-colour-channels.sh new file mode 100755 index 0000000..9bf6b92 --- /dev/null +++ b/scripts/verify-colour-channels.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# verify-colour-channels.sh — Block G of preflight. +# +# EnsureContrast works in ABGR. ThemeColors fields are RGBA. Feeding one into +# the other swaps red and blue on both arguments, measures a contrast between +# two colours that are never on screen, and returns a value in the wrong order +# on top — and because RgbaToAbgr is an involution, a stray extra conversion +# around the call makes it LOOK plausible while being wrong. +# +# This happened four times on 2026-08-19 alone (channel header, ghost buttons, +# pill text, popup rows), each found only by a human staring at an unreadable +# window. The compiler cannot catch it — both layouts are uint. This script +# catches the one shape every occurrence shared: a raw ThemeColors member as a +# direct argument to EnsureContrast. +# +# Legitimate calls convert first (RgbaToAbgr(...)) or pass values that are +# already ABGR (fields/locals named *Abgr, palette.Abgr(...)). + +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +fail() { echo "verify-colour-channels: FAIL — $1" >&2; exit 1; } +ok() { echo "verify-colour-channels: OK — $1"; } + +HITS="$(python3 - "$ROOT/HellionChat" <<'PY' +import re +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +# A ThemeColors member (via any local alias) fed straight into EnsureContrast, +# in either argument position, spanning line breaks. Members already in ABGR +# end in "Abgr" and are exempt. +pattern = re.compile( + r"EnsureContrast\(\s*" + r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*," + r"|EnsureContrast\([^;]{0,200}?,\s*" + r"(?:[A-Za-z_][\w.]*\.)?(?:Colors|c|colors)\.(?!\w*Abgr\b)\w+\s*[,)]", + re.S, +) + +hits = [] +for f in root.rglob("*.cs"): + if "obj" in f.parts or "bin" in f.parts: + continue + text = f.read_text(encoding="utf-8") + for m in pattern.finditer(text): + line = text.count("\n", 0, m.start()) + 1 + hits.append(f"{f.relative_to(root.parent)}:{line}") + +print("\n".join(hits)) +PY +)" + +if [ -n "$HITS" ]; then + fail "raw ThemeColors member fed into EnsureContrast (expects ABGR — wrap in RgbaToAbgr): +$HITS" +fi + +ok "no raw RGBA theme members reach EnsureContrast"