Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions Terminal.Gui/Drawing/Color/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,9 @@ public Color GetBrighterColor (double brightenAmount = 0.2, bool? isDarkBackgrou
/// <list type="bullet">
/// <item>
/// <description>
/// If the color is already at the extreme for the given direction, returns a context-appropriate
/// gray (<see cref="ColorName16.DarkGray"/> for dark backgrounds,
/// <see cref="ColorName16.Gray"/> 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.
/// </description>
/// </item>
/// <item>
Expand Down Expand Up @@ -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));
Expand Down
9 changes: 8 additions & 1 deletion Terminal.Gui/Drawing/Scheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading