Skip to content

fix: a dimmed color must stay readable and never brighten - #5687

Open
UtopleMan wants to merge 1 commit into
tui-cs:mainfrom
UtopleMan:fix/dimmer-never-brightens
Open

UtopleMan wants to merge 1 commit into
tui-cs:mainfrom
UtopleMan:fix/dimmer-never-brightens

Conversation

@UtopleMan

@UtopleMan UtopleMan commented Sep 23, 2026 •

Copy link
Copy Markdown

Problem

Color.GetDimmerColor substitutes a named gray when the color has no room left for the step:

if (shouldDecrease && hsl.L <= 10)
{
    return new Color (ColorName16.DarkGray);
}

On a dark theme DarkGray (#767676) is brighter than the color it replaces, so the method brightens what it was asked to dim. Measured against 2.5.0:

#101014 -> #767676   LIGHTER
#2f2f36 -> #000000   darker
#3a3a42 -> #070708   darker
#56565e -> #222225   darker
#c6c6c6 -> #949494   darker

ShadowView builds a transparent shadow by dimming the cells it covers, so on a dark ground the shadow comes out lighter than the screen it falls on, and dark grey text inside the shadow is lifted rather than sunk. The workaround already in ShadowView is evidence of the same defect:

// If the BG is DarkGray, GetDimmerColor gave up. Instead of using the attribute in the Driver under the shadow,
// use the Normal attribute from the View under the shadow.

Fix

A color with no room left for the step is returned unchanged.

Clamping to the end of the range was the other candidate, and it costs readability: a near-black foreground would land on black and a near-black ground with it, so text under a shadow disappears into what it is drawn against. The named gray was standing in for two things at once — "do not return the same color" and "keep it visible" — and returning the color unchanged is the one of those that never lies about the direction.

#0a0a0a (dark direction)  -> #0a0a0a   unchanged, was #767676
#101014 (dark direction)  -> #101014   unchanged, was #767676
#2f2f36 (dark direction)  -> #000000   unchanged behaviour
#f0f0f0 (light direction) -> #f0f0f0   unchanged, was #808080

Scheme.DeriveAccent

The accent derivation asked for the wrong direction on a light background:

Color accentBg = isDark ? resolvedBg.GetBrighterColor (0.1, isDark) : resolvedBg.GetDimmerColor (0.1, isDark);

Its own summary says the background is "shifted slightly brighter (on dark) or dimmer (on light)", but passing isDark: false asks GetDimmerColor to wash a near-white background further toward white. It only produced a darker accent because the Gray fallback caught it. It now asks for the dark direction on purpose, so it no longer depends on the fallback this PR removes.

Tests

  • GetDimmerColor_VeryDarkInput_DarkBackground_* and ..._VeryLightInput_LightBackground_* now assert the color is returned unchanged.
  • Two theories hold the contract itself: dimming on a dark background never returns a brighter color, and dimming toward a light background never returns a darker one.
  • One case holds readability directly: a foreground and the ground under it, dimmed the same way, do not come back as the same color.
  • ShadowTests.TransparentShadow_*_Draws_Transparent_At_Driver_Output record that a cell under a transparent shadow keeps its glyph on a darkened ground (\x1b[100m -> \x1b[40m, and a black foreground stays \x1b[30m instead of becoming \x1b[90m). If hiding what a shadow covers is the intended behaviour rather than dimming it, say so and I will withdraw that half.

Tests/UnitTestsParallelizable (17634) and Tests/UnitTests.NonParallelizable (32) pass. Tests/UnitTests.Legacy fails to start on my machine both with and without this change, so it is untouched by it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Update the XML documentation to describe clamping toward black or white.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Fixes Color.GetDimmerColor so extreme colors clamp directionally instead of falling back to gray values that may brighten the color.

Changes:

  • Removes incorrect gray fallback returns.
  • Updates tests for clamping and monotonic dimming.
  • Requires updating the method documentation to reflect the new behavior.
File Summary
Tests/​UnitTestsParallelizable/​Drawing/​Color/​ColorClassTests.DarkLightAwareness.cs Adds regression and directional monotonicity tests.
Terminal.Gui/​Drawing/​Color/​Color.cs Corrects dimming behavior; documentation still describes the removed gray fallbacks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Terminal.Gui/Drawing/Color/Color.cs Outdated
Comment on lines +356 to +359
// A color already at the extreme for the given direction is clamped by the math below: reducing
// the lightness of a near-black color lands on black, and increasing a near-white one lands on
// white. Substituting a named gray here would move the color the other way - dimming #101014
// returned DarkGray (#767676), which is brighter than what it was asked to dim.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Update the public XML documentation to describe black/white clamping instead of named-gray fallbacks.

Review effort: Lite
Findings: 1 Low severity

Open (1)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The public API remarks still document the removed named-gray fallback behavior.

Review effort: Balanced
Findings: 1 Low severity

Open (1)

GetDimmerColor substituted a named gray when the color had no room left for
the step: DarkGray when reducing lightness, Gray when increasing it. On a dark
theme DarkGray is brighter than what it replaced, so the method brightened what
it was asked to dim - #101014 came back as #767676.

ShadowView builds a transparent shadow by dimming the cells it covers, so on a
dark ground the shadow came out lighter than the screen it fell on and lifted
the dark grey text inside it. The workaround already in ShadowView - "if the BG
is DarkGray, GetDimmerColor gave up" - is evidence of the same defect.

A color with no room left is now returned unchanged. Clamping to the end of the
range instead would sink it into the ground it is drawn against, which is where
text drawn in it stops being readable, and that is the other half of what the
named gray was standing in for.

Scheme.DeriveAccent asked for the wrong direction on a light background - it
passed isDark, which washes a near-white background further toward white - and
only produced a darker accent because the Gray fallback caught it. It now asks
for the dark direction on purpose.

Tests: the two cases asserting the named grays assert the color is returned
unchanged; two theories hold the contract that dimming never moves a color the
wrong way; one holds that a dimmed foreground stays off its dimmed ground. The
two transparent-shadow driver-output cases record that a shadow now dims what
it covers rather than hiding it.
@UtopleMan
UtopleMan force-pushed the fix/dimmer-never-brightens branch from 5ad2dee to cb1a25b Compare September 24, 2026 07:32
@UtopleMan UtopleMan changed the title fix: GetDimmerColor must not brighten what it dims fix: a dimmed color must stay readable and never brighten Sep 24, 2026

@BDisp BDisp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@harder

harder commented Sep 30, 2026

Copy link
Copy Markdown
Member

Thanks for the fix. I think the PR merge block is incorrect and pointing to a comment that was addressed already.

However I found one case you may want to address before merging: ShadowView calls GetDimmerColor(0.9) for the background under a transparent shadow. With the new guard, a mid-gray background such as #808080 is returned unchanged because the full step would cross black. That means the shadow may no longer darken its background.

The updated shadow tests use white backgrounds, so they don’t cover this case. Could you add a test with a mid or dark background and adjust the behavior so the shadow still darkens it without making covered text disappear?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants