From 1464b9fd8e433f1fc2af41f7bb5befd54f666290 Mon Sep 17 00:00:00 2001 From: LynxTWO Date: Sun, 23 Aug 2026 20:16:18 -0400 Subject: [PATCH] Light the key housing instead of outlining it on hover The hover outline popped up and then sat there through the press. An outline is a UI convention wearing a console's clothes: a real key sits in a lit housing, and what changes as it moves is how much of that light gets past it. So there is a lamp behind the cap now, seen through the seam around it. Approaching a key brings it up to a low glow; pressing takes it up with the press, continuously, rather than flipping between two states. The asymmetry came free. The homography already shrinks and tilts a pressed face, so the cap pulls away from the housing further on one side and more of the lamp shows exactly there. The physics had it; the lamp only needed putting behind it. Warm amber-gold, deeper on the light bench where a glow barely registers. Warm because the console is cool green-grey with a teal accent, so a warm light reads as illumination from inside the box rather than as one more status colour. The first attempt sat the lamp a hair proud of the face so a lit ring always existed, and it rendered as an amber outline - exactly the thing being removed. Light at a seam is diffuse; a hard-edged stroke of any colour is a border. The ring is gone and the bloom carries it. The restraint is in the opacity ramp rather than the hue, and the ramp constants are pinned equal to the style setters by test: if they drift the lamp jumps the instant a key is touched, and nothing else in the suite would see it. Also turns the standby legend down to the floor of its contract, which is as dim as it goes while still meeting the large-text threshold. --- docs/DECISION-LOG.md | 42 +++++++++ .../Controls/SoftBodyKey.cs | 20 +++++ .../Theme/AnalogControls.axaml | 54 ++++++++++-- src/CUETools.Linux.App/Theme/Palette.axaml | 29 +++++-- tests/CUETools.Linux.Tests/SeamLightTests.cs | 87 +++++++++++++++++++ 5 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 tests/CUETools.Linux.Tests/SeamLightTests.cs diff --git a/docs/DECISION-LOG.md b/docs/DECISION-LOG.md index 287d4fe..d38f73a 100644 --- a/docs/DECISION-LOG.md +++ b/docs/DECISION-LOG.md @@ -1446,3 +1446,45 @@ hairlines. The exemption says an inactive control need not meet AA. It does not say it may be unreadable. REVISIT WHEN: A theme is added. Both bounds are relative to that theme's own live key, so the numbers do not transfer. + +DECISION: D-088 Hover and press are told by light, not by an outline +STATUS: Confirmed +CHOICE: The key template gains a housing lamp behind the cap, seen +through the seam around it. Approaching a key brings that lamp up to a +low glow; pressing it takes the lamp up with the press, continuously, +rather than switching between two states. The hover outline that used +to appear on the face, and then persist through the press, is gone. +Owner report 2026-08-23: an outline popping up on hover and staying +there is a UI convention wearing a console's clothes, where a real key +in a lit housing shows more of that light as it moves. +The asymmetry needed no code. The soft-body homography already shrinks +and tilts a pressed face, so the cap pulls away from the housing +further on one side, and more of the lamp shows exactly there. The +physics had it; the lamp only had to be put behind it. +COLOUR: warm amber-gold, `KeySeamColor`, #F0A24A on the dark bench and +#C9762A on the light one. Warm on purpose: the console is cool +green-grey with a teal accent, so a warm light reads as illumination +from inside the box rather than as another status colour. A test pins +both properties - that it is warm, and that it is far enough from the +accent hue to read as a different fixture. +RENDERED FIRST, TWICE. The first attempt sat the lamp 1.5 DIP proud of +the face so a lit ring always existed. It rendered as an amber +OUTLINE, which is the exact thing being removed. The fix was to drop +the proud ring entirely and let the bloom carry it: light at a seam is +diffuse, and a hard-edged stroke of any colour reads as a border. +RESTRAINT: the elegance is in the opacity ramp, not the hue. 0 at +rest, 0.34 on hover, 0.85 held. The C# ramp constants and the XAML +style setters are pinned equal by test, because if they drift the lamp +jumps the instant a key is touched and nothing else would catch it. +BECAUSE: the outline was the last piece of the key that was not +modelled on anything physical. +REVISIT WHEN: the levels are by eye. If the glow reads as brash, the +opacities are the knob, not the colour. + +AMENDMENT TO D-087 (2026-08-23, same session): the standby legend was +turned down on owner report that it could be "quite a bit dimmer". +KeyStandby is now #42786F dark and #577F79 light, sitting at the floor +of its own contract - 3.2:1 to 3.8:1 across the resting and pressed +faces - and the glow effect went from 0.30 to 0.20 opacity. It cannot +go lower without moving that floor, and 3:1 is already the large-text +threshold rather than the normal-text one. diff --git a/src/CUETools.Linux.App/Controls/SoftBodyKey.cs b/src/CUETools.Linux.App/Controls/SoftBodyKey.cs index 2ecce19..c35fb31 100644 --- a/src/CUETools.Linux.App/Controls/SoftBodyKey.cs +++ b/src/CUETools.Linux.App/Controls/SoftBodyKey.cs @@ -29,6 +29,12 @@ public static class SoftBodyKey /// The bench is lit from above, so sinking also means sliding into shadow. private const double Drop = 0.80; + /// Housing-lamp levels, matched to the style setters on keySeam so + /// the ramp starts exactly where hover left off and hands back to the same + /// value. If these drift apart the light jumps at the start of a press. + public const double SeamHover = 0.34; + public const double SeamPressed = 0.85; + public static void Attach(Button button) { if (!Enabled) return; @@ -49,6 +55,20 @@ void Apply() if (button.GetVisualDescendant("keyRecess") is Border recess) recess.Opacity = amount <= 0.001 ? 0 : 1; + // The housing lamp comes up with the press, the way a console key + // brightens as it goes down, rather than switching between two + // style states. Handing it back matters: a locally set value + // outranks a style setter, so leaving one here would kill the + // hover glow permanently the first time the key was pressed. Same + // trap as D-084 (2), one property along. + if (button.GetVisualDescendant("keySeam") is Border seam) + { + if (amount <= 0.001) + seam.ClearValue(Visual.OpacityProperty); + else + seam.Opacity = SeamHover + amount * (SeamPressed - SeamHover); + } + if (amount <= 0.001) { face.RenderTransform = null; diff --git a/src/CUETools.Linux.App/Theme/AnalogControls.axaml b/src/CUETools.Linux.App/Theme/AnalogControls.axaml index 2dc3a98..aeb6d90 100644 --- a/src/CUETools.Linux.App/Theme/AnalogControls.axaml +++ b/src/CUETools.Linux.App/Theme/AnalogControls.axaml @@ -30,6 +30,33 @@ + + + + + + + + + + + + - + + + @@ -374,7 +418,7 @@ @@ -493,7 +537,7 @@ diff --git a/src/CUETools.Linux.App/Theme/Palette.axaml b/src/CUETools.Linux.App/Theme/Palette.axaml index e1424ce..d9a8260 100644 --- a/src/CUETools.Linux.App/Theme/Palette.axaml +++ b/src/CUETools.Linux.App/Theme/Palette.axaml @@ -25,11 +25,19 @@ #34CFC0 - + translucent letters is not off, it is turned right down. Turned + down again on 08-23 owner note; this now sits at the floor of its + own contract, 3.2:1 to 3.8:1 across the resting and pressed faces, + against 14:1 for a live key and the 1.2:1 the old structural Line + gave, which could not be read at all. It cannot go lower without + moving that floor, and 3:1 is already the large-text threshold. --> + + + #F0A24A #34CFC0 #2E000000 #5A000000 @@ -89,12 +97,17 @@ - + mid-press and takes the dead release. Turned down on the 08-23 + owner note to the floor of its own contract. --> + + + #C9762A #087067 diff --git a/tests/CUETools.Linux.Tests/SeamLightTests.cs b/tests/CUETools.Linux.Tests/SeamLightTests.cs new file mode 100644 index 0000000..5caa9af --- /dev/null +++ b/tests/CUETools.Linux.Tests/SeamLightTests.cs @@ -0,0 +1,87 @@ +using System.Text.RegularExpressions; +using Avalonia.Headless.XUnit; +using Avalonia.Media; +using Avalonia.Styling; +using CUETools.Linux.App.Controls; +using Xunit; + +namespace CUETools.Linux.Tests; + +// D-088. Hover and press are told by light from the housing rather than by an +// outline. Three things about that can break quietly, so all three are pinned. +public class SeamLightTests +{ + private static string Theme() => File.ReadAllText( + Path.Combine(FindRepoRoot(), "src", "CUETools.Linux.App", "Theme", "AnalogControls.axaml")); + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir != null && !File.Exists(Path.Combine(dir, "eng", "build.sh"))) + dir = Path.GetDirectoryName(dir); + Assert.NotNull(dir); + return dir!; + } + + private static double SeamOpacityFor(string pseudoClass) + { + // the style block for keySeam under this pseudo-class, and the opacity it sets + var m = Regex.Match( + Theme(), + @"Selector=""Button:" + Regex.Escape(pseudoClass) + + @"\s*/template/\s*Border#keySeam""\s*>\s* SoftBodyKey.SeamHover, + "pressing a key has to brighten its lamp, not dim it"); + } + + [Fact] + public void HoverNoLongerDrawsAnOutlineOnTheKeyFace() + { + // the owner's report (2026-08-23): the hover outline popped up and then + // persisted through the press. An outline is a UI convention wearing a + // console's clothes; this guards the replacement from being undone. + Assert.DoesNotMatch( + new Regex(@"Selector=""Button:pointerover\s*/template/\s*Border#keyFace"""), + Theme()); + } + + [AvaloniaFact] + public void TheHousingLampIsWarmAndNotMistakableForTheAccent() + { + // the console is cool green-grey with a teal accent. The lamp has to read + // as light from inside the box, which means warm - and a lamp that drifted + // toward the accent hue would read as another status colour instead. + foreach (ThemeVariant variant in new[] { ThemeVariant.Dark, ThemeVariant.Light }) + { + Assert.True(Avalonia.Application.Current!.TryGetResource( + "KeySeamColor", variant, out object? value), "KeySeamColor missing"); + var seam = Assert.IsType(value); + + Assert.True(seam.R > seam.B + 60, + $"{variant} KeySeamColor #{seam} is not warm: red must lead blue clearly"); + Assert.True(seam.R >= seam.G && seam.G > seam.B, + $"{variant} KeySeamColor #{seam} is not on the amber side of warm"); + + Assert.True(Avalonia.Application.Current.TryGetResource( + "StatusAccentColor", variant, out object? accentValue), "StatusAccentColor missing"); + var accent = Assert.IsType(accentValue); + int distance = Math.Abs(seam.R - accent.R) + Math.Abs(seam.G - accent.G) + Math.Abs(seam.B - accent.B); + Assert.True(distance > 150, + $"{variant} the housing lamp #{seam} is too close to the accent #{accent} to read as a different fixture"); + } + } +}