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"