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"); + } + } +}