diff --git a/Terminal.Gui/Drawing/Color/Color.cs b/Terminal.Gui/Drawing/Color/Color.cs index a1b6f7b083..be92606905 100644 --- a/Terminal.Gui/Drawing/Color/Color.cs +++ b/Terminal.Gui/Drawing/Color/Color.cs @@ -321,9 +321,9 @@ public Color GetBrighterColor (double brightenAmount = 0.2, bool? isDarkBackgrou /// /// /// - /// If the color is already at the extreme for the given direction, returns a context-appropriate - /// gray ( for dark backgrounds, - /// for light backgrounds). + /// If the color has no room left for the step in the given direction, it is returned unchanged. + /// Dimming never moves a color the other way, and never flattens it onto the end of the range, + /// where text drawn in it would vanish into the ground it is drawn against. /// /// /// @@ -353,24 +353,28 @@ public Color GetDimmerColor (double dimAmount = 0.2, bool? isDarkBackground = nu // Determine direction: on dark bg (or null/default), reduce L; on light bg, increase L bool shouldDecrease = isDarkBackground ?? true; - // If the color is already at the extreme for the given direction, return a context-appropriate gray. - // Note: ColorHelper's HSL uses L in range 0-100. - if (shouldDecrease && hsl.L <= 10) - { - return new Color (ColorName16.DarkGray); - } + double newL = shouldDecrease ? lNorm - dimAmount : lNorm + dimAmount; - if (!shouldDecrease && hsl.L >= 90) + // A color with no room left for the step keeps what it has. Two other answers were tried here + // and both cost the caller something: a named gray (DarkGray when reducing, Gray when + // increasing) moves the color the wrong way - dimming #101014 returned the brighter #767676 - + // and clamping to the end of the range sinks the color into the ground it is drawn against, + // which is where text drawn in it stops being readable. Returning it unchanged does neither. + if (newL <= 0.0 || newL >= 1.0) { - return new Color (ColorName16.Gray); + return this; } - double newL = shouldDecrease ? Math.Max (0.0, lNorm - dimAmount) : Math.Min (1.0, lNorm + dimAmount); - - // If the new lightness is too close to the original, force a bigger change + // If the new lightness is too close to the original, force a bigger change - unless that step + // is the one that has no room. if (Math.Abs (newL - lNorm) < 0.1) { - newL = shouldDecrease ? Math.Max (0.0, lNorm - 2 * dimAmount) : Math.Min (1.0, lNorm + 2 * dimAmount); + double furtherL = shouldDecrease ? lNorm - 2 * dimAmount : lNorm + 2 * dimAmount; + + if (furtherL > 0.0 && furtherL < 1.0) + { + newL = furtherL; + } } HSL newHsl = new (hsl.H, hsl.S, (byte)(newL * 100)); diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index 03ffac2a02..e7ff8c751c 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -232,7 +232,14 @@ public static Scheme DeriveAccent (Scheme baseScheme, Attribute? defaultTerminal Color resolvedBg = ResolveNone (baseScheme.Normal.Background, defaultTerminalColors); Color resolvedFg = ResolveNone (baseScheme.Normal.Foreground, defaultTerminalColors, true); bool isDark = resolvedBg.IsDarkColor (); - Color accentBg = isDark ? resolvedBg.GetBrighterColor (0.1, isDark) : resolvedBg.GetDimmerColor (0.1, isDark); + // Both branches move the accent away from the base: brighter on a dark background, darker on a + // light one. The flag on these two selects the direction the color is moved, not the theme it + // is moved for, so the light-background branch asks for the dark direction on purpose. It used + // to pass isDark here and be saved by GetDimmerColor's named-gray fallback, which moved a + // near-white background to Gray - the accent it produced was an accident of that fallback. + Color accentBg = isDark + ? resolvedBg.GetBrighterColor (0.1, isDark) + : resolvedBg.GetDimmerColor (0.1, isDarkBackground: true); // Force opaque accentBg = new Color (accentBg.R, accentBg.G, accentBg.B, 255); diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/ColorClassTests.DarkLightAwareness.cs b/Tests/UnitTestsParallelizable/Drawing/Color/ColorClassTests.DarkLightAwareness.cs index 614e1a79c7..c6989c4070 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Color/ColorClassTests.DarkLightAwareness.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Color/ColorClassTests.DarkLightAwareness.cs @@ -120,25 +120,70 @@ public void GetDimmerColor_WithLightBackground_IncreasesLightness () } [Fact] - public void GetDimmerColor_VeryDarkInput_DarkBackground_ReturnsDarkGray () + public void GetDimmerColor_VeryDarkInput_DarkBackground_ReturnsTheColorUnchanged () { Color veryDark = new (10, 10, 10); Color dimmed = veryDark.GetDimmerColor (0.2, true); - // Very dark color on dark bg should fall back to DarkGray - Color darkGray = new (ColorName16.DarkGray); - Assert.Equal (darkGray, dimmed); + // A color with no room left to dim keeps what it has: a named gray would be brighter than the + // color it replaced, and black would take the little contrast the color still carries + Assert.Equal (veryDark, dimmed); } [Fact] - public void GetDimmerColor_VeryLightInput_LightBackground_ReturnsGray () + public void GetDimmerColor_VeryLightInput_LightBackground_ReturnsTheColorUnchanged () { Color veryLight = new (240, 240, 240); Color dimmed = veryLight.GetDimmerColor (0.2, false); - // Very light color on light bg should fall back to Gray - Color gray = new (ColorName16.Gray); - Assert.Equal (gray, dimmed); + Assert.Equal (veryLight, dimmed); + } + + // Text drawn in a color that was dimmed against a ground dimmed the same way has to stay legible: + // neither may be flattened onto the end of the range, where both become the same color. + [Fact] + public void GetDimmerColor_DarkBackground_KeepsDarkTextOffItsGround () + { + Color ground = new (16, 16, 20); + Color text = new (47, 47, 54); + + Assert.NotEqual (ground.GetDimmerColor (0.2, true), text.GetDimmerColor (0.2, true)); + } + + // A dimmer that brightens is what this guards: on a dark background no input may come back + // lighter than it went in, and a near-black one used to come back as DarkGray (#767676). + [Theory] + [InlineData (0, 0, 0)] + [InlineData (16, 16, 20)] + [InlineData (26, 26, 26)] + [InlineData (47, 47, 54)] + [InlineData (150, 150, 150)] + [InlineData (255, 255, 255)] + public void GetDimmerColor_DarkBackground_NeverBrightens (byte r, byte g, byte b) + { + Color color = new (r, g, b); + Color dimmed = color.GetDimmerColor (0.2, true); + + Assert.True ( + dimmed.R <= color.R && dimmed.G <= color.G && dimmed.B <= color.B, + $"Dimming #{color.R:x2}{color.G:x2}{color.B:x2} returned the brighter #{dimmed.R:x2}{dimmed.G:x2}{dimmed.B:x2}"); + } + + // The same contract in the other direction: dimming toward a light background washes a color out, + // and a near-white one must not come back darker. + [Theory] + [InlineData (255, 255, 255)] + [InlineData (240, 240, 240)] + [InlineData (100, 100, 100)] + [InlineData (0, 0, 0)] + public void GetDimmerColor_LightBackground_NeverDarkens (byte r, byte g, byte b) + { + Color color = new (r, g, b); + Color dimmed = color.GetDimmerColor (0.2, false); + + Assert.True ( + dimmed.R >= color.R && dimmed.G >= color.G && dimmed.B >= color.B, + $"Dimming #{color.R:x2}{color.G:x2}{color.B:x2} returned the darker #{dimmed.R:x2}{dimmed.G:x2}{dimmed.B:x2}"); } [Fact] diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs index 353258d848..f09d4b72d0 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs @@ -526,9 +526,10 @@ public void TransparentShadow_Draws_Transparent_At_Driver_Output () string output1 = app.Driver.GetOutput ().GetLastOutput (); output.WriteLine (output1); - // Printed with bright black (dark gray) text on bright black (dark gray) background making it invisible + // The cell under the shadow keeps its glyph on a darkened ground: the shadow dims what it covers + // rather than hiding it DriverAssert.AssertDriverOutputIs (""" - \x1b[30m\x1b[107m*\x1b[93m\x1b[100mB + \x1b[30m\x1b[107m*\x1b[93m\x1b[40mB """, output, app.Driver); @@ -572,7 +573,7 @@ public void TransparentShadow_OverWide_Draws_Transparent_At_Driver_Output () output.WriteLine (output1); DriverAssert.AssertDriverOutputIs (""" - \x1b[30m\x1b[107m*\x1b[90m\x1b[40m \x1b[97m\x1b[40m \x1b[93m\x1b[100m \x1b[97m\x1b[40m🍎 + \x1b[30m\x1b[107m*\x1b[30m\x1b[40m \x1b[97m\x1b[40m \x1b[93m\x1b[40m \x1b[97m\x1b[40m🍎 """, output, app.Driver);