Automatically adjust ANSI color contrast (#34033)

Closes #33253 in a way that doesn't regress #32175 - namely,
automatically adjusts the contrast between the foreground and background
text in the terminal such that it's above a certain threshold. The
threshold is configurable in settings, and can be set to 0 to turn off
this feature and use exactly the colors the theme specifies even if they
are illegible.

## One Light Theme Before
<img width="220" alt="Screenshot 2025-07-07 at 6 00 47 PM"
src="https://github.com/user-attachments/assets/096754a6-f79f-4fea-a86e-cb7b8ff45d60"
/>

(Last row is highlighted because otherwise the text is unreadable; the
foreground and background are the same color.)

## One Light Theme After

(This is with the new default contrast adjustment setting.)

<img width="215" alt="Screenshot 2025-07-07 at 6 22 02 PM"
src="https://github.com/user-attachments/assets/b082fefe-76f5-4231-b704-ff387983a3cb"
/>

This approach was inspired by @mitchellh's use of automatic contrast
adjustment in [Ghostty](https://ghostty.org/) - thanks, Mitchell! The
main difference is that we're using APCA's formula instead of WCAG for
[these
reasons](https://khan-tw.medium.com/wcag2-are-you-still-using-it-ui-contrast-visibility-standard-readability-contrast-f34eb73e89ee).

Release Notes:

- Added automatic dynamic contrast adjustment for terminal foreground
and background colors
This commit is contained in:
Richard Feldman 2025-07-07 18:39:11 -04:00 committed by GitHub
parent 877ef5e1b1
commit 9b7632d5f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 669 additions and 8 deletions

View file

@ -1352,7 +1352,7 @@
// 5. Never show the scrollbar:
// "never"
"show": null
}
},
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": 15,
@ -1369,6 +1369,21 @@
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// Most terminal themes have APCA values of 40-70.
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
},
"code_actions_on_format": {},
// Settings related to running tasks.

View file

@ -25,6 +25,7 @@ use alacritty_terminal::{
use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size};
use language::Buffer;
use settings::Settings as _;
use terminal::terminal_settings::TerminalSettings;
use terminal_view::terminal_element::TerminalElement;
use theme::ThemeSettings;
use ui::{IntoElement, prelude::*};
@ -257,8 +258,17 @@ impl Render for TerminalOutput {
point: ic.point,
cell: ic.cell.clone(),
});
let (cells, rects) =
TerminalElement::layout_grid(grid, 0, &text_style, text_system, None, window, cx);
let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast;
let (cells, rects) = TerminalElement::layout_grid(
grid,
0,
&text_style,
text_system,
None,
minimum_contrast,
window,
cx,
);
// lines are 0-indexed, so we must add 1 to get the number of lines
let text_line_height = text_style.line_height_in_pixels(window.rem_size());

View file

@ -49,6 +49,7 @@ pub struct TerminalSettings {
pub max_scroll_history_lines: Option<usize>,
pub toolbar: Toolbar,
pub scrollbar: ScrollbarSettings,
pub minimum_contrast: f32,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -229,6 +230,21 @@ pub struct TerminalSettingsContent {
pub toolbar: Option<ToolbarContent>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
/// The minimum APCA perceptual contrast between foreground and background colors.
///
/// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
/// especially for dark mode. Values range from 0 to 106.
///
/// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
/// https://readtech.org/ARC/tests/bronze-simple-mode/
/// - 0: No contrast adjustment
/// - 45: Minimum for large fluent text (36px+)
/// - 60: Minimum for other content text
/// - 75: Minimum for body text
/// - 90: Preferred for body text
///
/// Default: 0 (no adjustment)
pub minimum_contrast: Option<f32>,
}
impl settings::Settings for TerminalSettings {
@ -237,7 +253,18 @@ impl settings::Settings for TerminalSettings {
type FileContent = TerminalSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
sources.json_merge()
let settings: Self = sources.json_merge()?;
// Validate minimum_contrast for APCA
if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
anyhow::bail!(
"terminal.minimum_contrast must be between 0 and 106, but got {}. \
APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
settings.minimum_contrast
);
}
Ok(settings)
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {

View file

@ -0,0 +1,474 @@
use gpui::Hsla;
/// APCA (Accessible Perceptual Contrast Algorithm) constants
/// Based on APCA 0.0.98G-4g W3 compatible constants
/// https://github.com/Myndex/apca-w3
struct APCAConstants {
// Main TRC exponent for monitor perception
main_trc: f32,
// sRGB coefficients
s_rco: f32,
s_gco: f32,
s_bco: f32,
// G-4g constants for use with 2.4 exponent
norm_bg: f32,
norm_txt: f32,
rev_txt: f32,
rev_bg: f32,
// G-4g Clamps and Scalers
blk_thrs: f32,
blk_clmp: f32,
scale_bow: f32,
scale_wob: f32,
lo_bow_offset: f32,
lo_wob_offset: f32,
delta_y_min: f32,
lo_clip: f32,
}
impl Default for APCAConstants {
fn default() -> Self {
Self {
main_trc: 2.4,
s_rco: 0.2126729,
s_gco: 0.7151522,
s_bco: 0.0721750,
norm_bg: 0.56,
norm_txt: 0.57,
rev_txt: 0.62,
rev_bg: 0.65,
blk_thrs: 0.022,
blk_clmp: 1.414,
scale_bow: 1.14,
scale_wob: 1.14,
lo_bow_offset: 0.027,
lo_wob_offset: 0.027,
delta_y_min: 0.0005,
lo_clip: 0.1,
}
}
}
/// Calculates the perceptual lightness contrast using APCA.
/// Returns a value between approximately -108 and 106.
/// Negative values indicate light text on dark background.
/// Positive values indicate dark text on light background.
///
/// The APCA algorithm is more perceptually accurate than WCAG 2.x,
/// especially for dark mode interfaces. Key improvements include:
/// - Better accuracy for dark backgrounds
/// - Polarity-aware (direction matters)
/// - Perceptually uniform across the range
///
/// Common APCA Lc thresholds per ARC Bronze Simple Mode:
/// https://readtech.org/ARC/tests/bronze-simple-mode/
/// - Lc 45: Minimum for large fluent text (36px+)
/// - Lc 60: Minimum for other content text
/// - Lc 75: Minimum for body text
/// - Lc 90: Preferred for body text
///
/// Most terminal themes use colors with APCA values of 40-70.
///
/// https://github.com/Myndex/apca-w3
pub fn apca_contrast(text_color: Hsla, background_color: Hsla) -> f32 {
let constants = APCAConstants::default();
let text_y = srgb_to_y(text_color, &constants);
let bg_y = srgb_to_y(background_color, &constants);
// Apply soft clamp to near-black colors
let text_y_clamped = if text_y > constants.blk_thrs {
text_y
} else {
text_y + (constants.blk_thrs - text_y).powf(constants.blk_clmp)
};
let bg_y_clamped = if bg_y > constants.blk_thrs {
bg_y
} else {
bg_y + (constants.blk_thrs - bg_y).powf(constants.blk_clmp)
};
// Return 0 for extremely low delta Y
if (bg_y_clamped - text_y_clamped).abs() < constants.delta_y_min {
return 0.0;
}
let sapc;
let output_contrast;
if bg_y_clamped > text_y_clamped {
// Normal polarity: dark text on light background
sapc = (bg_y_clamped.powf(constants.norm_bg) - text_y_clamped.powf(constants.norm_txt))
* constants.scale_bow;
// Low contrast smooth rollout to prevent polarity reversal
output_contrast = if sapc < constants.lo_clip {
0.0
} else {
sapc - constants.lo_bow_offset
};
} else {
// Reverse polarity: light text on dark background
sapc = (bg_y_clamped.powf(constants.rev_bg) - text_y_clamped.powf(constants.rev_txt))
* constants.scale_wob;
output_contrast = if sapc > -constants.lo_clip {
0.0
} else {
sapc + constants.lo_wob_offset
};
}
// Return Lc (lightness contrast) scaled to percentage
output_contrast * 100.0
}
/// Converts sRGB color to Y (luminance) for APCA calculation
fn srgb_to_y(color: Hsla, constants: &APCAConstants) -> f32 {
let rgba = color.to_rgb();
// Linearize and apply coefficients
let r_linear = (rgba.r).powf(constants.main_trc);
let g_linear = (rgba.g).powf(constants.main_trc);
let b_linear = (rgba.b).powf(constants.main_trc);
constants.s_rco * r_linear + constants.s_gco * g_linear + constants.s_bco * b_linear
}
/// Adjusts the foreground color to meet the minimum APCA contrast against the background.
/// The minimum_apca_contrast should be an absolute value (e.g., 75 for Lc 75).
///
/// This implementation gradually adjusts the lightness while preserving the hue and
/// saturation as much as possible, only falling back to black/white when necessary.
pub fn ensure_minimum_contrast(
foreground: Hsla,
background: Hsla,
minimum_apca_contrast: f32,
) -> Hsla {
if minimum_apca_contrast <= 0.0 {
return foreground;
}
let current_contrast = apca_contrast(foreground, background).abs();
if current_contrast >= minimum_apca_contrast {
return foreground;
}
// First, try to adjust lightness while preserving hue and saturation
let adjusted = adjust_lightness_for_contrast(foreground, background, minimum_apca_contrast);
let adjusted_contrast = apca_contrast(adjusted, background).abs();
if adjusted_contrast >= minimum_apca_contrast {
return adjusted;
}
// If that's not enough, gradually reduce saturation while adjusting lightness
let desaturated =
adjust_lightness_and_saturation_for_contrast(foreground, background, minimum_apca_contrast);
let desaturated_contrast = apca_contrast(desaturated, background).abs();
if desaturated_contrast >= minimum_apca_contrast {
return desaturated;
}
// Last resort: use black or white
let black = Hsla {
h: 0.0,
s: 0.0,
l: 0.0,
a: foreground.a,
};
let white = Hsla {
h: 0.0,
s: 0.0,
l: 1.0,
a: foreground.a,
};
let black_contrast = apca_contrast(black, background).abs();
let white_contrast = apca_contrast(white, background).abs();
if white_contrast > black_contrast {
white
} else {
black
}
}
/// Adjusts only the lightness to meet the minimum contrast, preserving hue and saturation
fn adjust_lightness_for_contrast(
foreground: Hsla,
background: Hsla,
minimum_apca_contrast: f32,
) -> Hsla {
// Determine if we need to go lighter or darker
let bg_luminance = srgb_to_y(background, &APCAConstants::default());
let should_go_darker = bg_luminance > 0.5;
// Binary search for the optimal lightness
let mut low = if should_go_darker { 0.0 } else { foreground.l };
let mut high = if should_go_darker { foreground.l } else { 1.0 };
let mut best_l = foreground.l;
for _ in 0..20 {
let mid = (low + high) / 2.0;
let test_color = Hsla {
h: foreground.h,
s: foreground.s,
l: mid,
a: foreground.a,
};
let contrast = apca_contrast(test_color, background).abs();
if contrast >= minimum_apca_contrast {
best_l = mid;
// Try to get closer to the minimum
if should_go_darker {
low = mid;
} else {
high = mid;
}
} else {
if should_go_darker {
high = mid;
} else {
low = mid;
}
}
// If we're close enough to the target, stop
if (contrast - minimum_apca_contrast).abs() < 1.0 {
best_l = mid;
break;
}
}
Hsla {
h: foreground.h,
s: foreground.s,
l: best_l,
a: foreground.a,
}
}
/// Adjusts both lightness and saturation to meet the minimum contrast
fn adjust_lightness_and_saturation_for_contrast(
foreground: Hsla,
background: Hsla,
minimum_apca_contrast: f32,
) -> Hsla {
// Try different saturation levels
let saturation_steps = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0];
for &sat_multiplier in &saturation_steps {
let test_color = Hsla {
h: foreground.h,
s: foreground.s * sat_multiplier,
l: foreground.l,
a: foreground.a,
};
let adjusted = adjust_lightness_for_contrast(test_color, background, minimum_apca_contrast);
let contrast = apca_contrast(adjusted, background).abs();
if contrast >= minimum_apca_contrast {
return adjusted;
}
}
// If we get here, even grayscale didn't work, so return the grayscale attempt
Hsla {
h: foreground.h,
s: 0.0,
l: foreground.l,
a: foreground.a,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla { h, s, l, a }
}
fn hsla_from_hex(hex: u32) -> Hsla {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
let b = (hex & 0xFF) as f32 / 255.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) / 2.0;
if max == min {
// Achromatic
Hsla {
h: 0.0,
s: 0.0,
l,
a: 1.0,
}
} else {
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if max == r {
(g - b) / d + if g < b { 6.0 } else { 0.0 }
} else if max == g {
(b - r) / d + 2.0
} else {
(r - g) / d + 4.0
} / 6.0;
Hsla { h, s, l, a: 1.0 }
}
}
#[test]
fn test_apca_contrast() {
// Test black text on white background (should be positive)
let black = hsla(0.0, 0.0, 0.0, 1.0);
let white = hsla(0.0, 0.0, 1.0, 1.0);
let contrast = apca_contrast(black, white);
assert!(
contrast > 100.0,
"Black on white should have high positive contrast, got {}",
contrast
);
// Test white text on black background (should be negative)
let contrast_reversed = apca_contrast(white, black);
assert!(
contrast_reversed < -100.0,
"White on black should have high negative contrast, got {}",
contrast_reversed
);
// Same color should have zero contrast
let gray = hsla(0.0, 0.0, 0.5, 1.0);
let contrast_same = apca_contrast(gray, gray);
assert!(
contrast_same.abs() < 1.0,
"Same color should have near-zero contrast, got {}",
contrast_same
);
// APCA is NOT commutative - polarity matters
assert!(
(contrast + contrast_reversed).abs() > 1.0,
"APCA should not be commutative"
);
}
#[test]
fn test_srgb_to_y() {
let constants = APCAConstants::default();
// Test known Y values
let black = hsla(0.0, 0.0, 0.0, 1.0);
let y_black = srgb_to_y(black, &constants);
assert!(
y_black.abs() < 0.001,
"Black should have Y near 0, got {}",
y_black
);
let white = hsla(0.0, 0.0, 1.0, 1.0);
let y_white = srgb_to_y(white, &constants);
assert!(
(y_white - 1.0).abs() < 0.001,
"White should have Y near 1, got {}",
y_white
);
}
#[test]
fn test_ensure_minimum_contrast() {
let white_bg = hsla(0.0, 0.0, 1.0, 1.0);
let light_gray = hsla(0.0, 0.0, 0.9, 1.0);
// Light gray on white has poor contrast
let initial_contrast = apca_contrast(light_gray, white_bg).abs();
assert!(
initial_contrast < 15.0,
"Initial contrast should be low, got {}",
initial_contrast
);
// Should be adjusted to black for better contrast (using APCA Lc 45 as minimum)
let adjusted = ensure_minimum_contrast(light_gray, white_bg, 45.0);
assert_eq!(adjusted.l, 0.0); // Should be black
assert_eq!(adjusted.a, light_gray.a); // Alpha preserved
// Test with dark background
let black_bg = hsla(0.0, 0.0, 0.0, 1.0);
let dark_gray = hsla(0.0, 0.0, 0.1, 1.0);
// Dark gray on black has poor contrast
let initial_contrast = apca_contrast(dark_gray, black_bg).abs();
assert!(
initial_contrast < 15.0,
"Initial contrast should be low, got {}",
initial_contrast
);
// Should be adjusted to white for better contrast
let adjusted = ensure_minimum_contrast(dark_gray, black_bg, 45.0);
assert_eq!(adjusted.l, 1.0); // Should be white
// Test when contrast is already sufficient
let black = hsla(0.0, 0.0, 0.0, 1.0);
let adjusted = ensure_minimum_contrast(black, white_bg, 45.0);
assert_eq!(adjusted, black); // Should remain unchanged
}
#[test]
fn test_one_light_theme_exact_colors() {
// Test with exact colors from One Light theme
// terminal.background and terminal.ansi.white are both #fafafaff
let fafafa = hsla_from_hex(0xfafafa);
// They should be identical
let bg = fafafa;
let fg = fafafa;
// Contrast should be 0 (no contrast)
let contrast = apca_contrast(fg, bg);
assert!(
contrast.abs() < 1.0,
"Same color should have near-zero APCA contrast, got {}",
contrast
);
// With minimum APCA contrast of 15 (very low, but detectable), it should adjust
let adjusted = ensure_minimum_contrast(fg, bg, 15.0);
// The new algorithm preserves colors, so we just need to check contrast
let new_contrast = apca_contrast(adjusted, bg).abs();
assert!(
new_contrast >= 15.0,
"Adjusted contrast {} should be >= 15.0",
new_contrast
);
// The adjusted color should have sufficient contrast
let new_contrast = apca_contrast(adjusted, bg).abs();
assert!(
new_contrast >= 15.0,
"Adjusted APCA contrast {} should be >= 15.0",
new_contrast
);
}
}

View file

@ -1,3 +1,4 @@
use crate::color_contrast;
use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
use gpui::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
@ -204,9 +205,9 @@ impl TerminalElement {
grid: impl Iterator<Item = IndexedCell>,
start_line_offset: i32,
text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
text_system: &WindowTextSystem,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
minimum_contrast: f32,
window: &Window,
cx: &App,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
@ -285,8 +286,15 @@ impl TerminalElement {
{
if !is_blank(&cell) {
let cell_text = cell.c.to_string();
let cell_style =
TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
let cell_style = TerminalElement::cell_style(
&cell,
fg,
bg,
theme,
text_style,
hyperlink,
minimum_contrast,
);
let layout_cell = text_system.shape_line(
cell_text.into(),
@ -341,13 +349,17 @@ impl TerminalElement {
fn cell_style(
indexed: &IndexedCell,
fg: terminal::alacritty_terminal::vte::ansi::Color,
// bg: terminal::alacritty_terminal::ansi::Color,
bg: terminal::alacritty_terminal::vte::ansi::Color,
colors: &Theme,
text_style: &TextStyle,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
minimum_contrast: f32,
) -> TextRun {
let flags = indexed.cell.flags;
let mut fg = convert_color(&fg, colors);
let bg = convert_color(&bg, colors);
fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast);
// Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty
// uses 0.75. We're using 0.7 because it's pretty well in the middle of that.
@ -680,6 +692,7 @@ impl Element for TerminalElement {
let buffer_font_size = settings.buffer_font_size(cx);
let terminal_settings = TerminalSettings::get_global(cx);
let minimum_contrast = terminal_settings.minimum_contrast;
let font_family = terminal_settings.font_family.as_ref().map_or_else(
|| settings.buffer_font.family.clone(),
@ -853,6 +866,7 @@ impl Element for TerminalElement {
last_hovered_word
.as_ref()
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
minimum_contrast,
window,
cx,
),
@ -874,6 +888,7 @@ impl Element for TerminalElement {
last_hovered_word.as_ref().map(|last_hovered_word| {
(link_style, &last_hovered_word.word_match)
}),
minimum_contrast,
window,
cx,
)
@ -1390,3 +1405,122 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme:
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contrast_adjustment_logic() {
// Test the core contrast adjustment logic without needing full app context
// Test case 1: Light colors (poor contrast)
let white_fg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 1.0,
a: 1.0,
};
let light_gray_bg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.95,
a: 1.0,
};
// Should have poor contrast
let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs();
assert!(
actual_contrast < 30.0,
"White on light gray should have poor APCA contrast: {}",
actual_contrast
);
// After adjustment with minimum APCA contrast of 45, should be darker
let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0);
assert!(
adjusted.l < white_fg.l,
"Adjusted color should be darker than original"
);
let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs();
assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast");
// Test case 2: Dark colors (poor contrast)
let black_fg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.0,
a: 1.0,
};
let dark_gray_bg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.05,
a: 1.0,
};
// Should have poor contrast
let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs();
assert!(
actual_contrast < 30.0,
"Black on dark gray should have poor APCA contrast: {}",
actual_contrast
);
// After adjustment with minimum APCA contrast of 45, should be lighter
let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0);
assert!(
adjusted.l > black_fg.l,
"Adjusted color should be lighter than original"
);
let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs();
assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast");
// Test case 3: Already good contrast
let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0);
assert_eq!(
good_contrast, black_fg,
"Good contrast should not be adjusted"
);
}
#[test]
fn test_white_on_white_contrast_issue() {
// This test reproduces the exact issue from the bug report
// where white ANSI text on white background should be adjusted
// Simulate One Light theme colors
let white_fg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.98, // #fafafaff is approximately 98% lightness
a: 1.0,
};
let white_bg = gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.98, // Same as foreground - this is the problem!
a: 1.0,
};
// With minimum contrast of 0.0, no adjustment should happen
let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0);
assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0");
// With minimum APCA contrast of 15, it should adjust to a darker color
let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0);
assert!(
adjusted.l < white_fg.l,
"White on white should become darker, got l={}",
adjusted.l
);
// Verify the contrast is now acceptable
let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs();
assert!(
new_contrast >= 15.0,
"Adjusted APCA contrast {} should be >= 15.0",
new_contrast
);
}
}

View file

@ -1,3 +1,4 @@
mod color_contrast;
mod persistence;
pub mod terminal_element;
pub mod terminal_panel;