use ratatui::style::Color;
use super::adapt::blend;
use super::themes::UiTheme;
pub const AA_BODY_CONTRAST: f32 = 4.5;
#[allow(dead_code)]
pub const SECONDARY_CHROME_CONTRAST: f32 = 3.0;
#[must_use]
pub fn relative_luminance(color: Color) -> Option<f32> {
let (r, g, b) = resolvable_rgb(color)?;
Some(luminance_rgb(r, g, b))
}
#[must_use]
pub fn resolvable_rgb(color: Color) -> Option<(u8, u8, u8)> {
match color {
Color::Rgb(r, g, b) => Some((r, g, b)),
Color::Indexed(index) if index >= 16 => Some(indexed_rgb(index)),
_ => None,
}
}
fn luminance_rgb(r: u8, g: u8, b: u8) -> f32 {
fn channel(value: u8) -> f32 {
let c = f32::from(value) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
}
#[must_use]
pub fn contrast_from_luminance(a: f32, b: f32) -> f32 {
let (hi, lo) = if a >= b { (a, b) } else { (b, a) };
(hi + 0.05) / (lo + 0.05)
}
#[must_use]
pub fn contrast_ratio(fg: Color, bg: Color) -> Option<f32> {
Some(contrast_from_luminance(
relative_luminance(fg)?,
relative_luminance(bg)?,
))
}
#[must_use]
pub fn meets_contrast(fg: Color, bg: Color, min_ratio: f32) -> bool {
contrast_ratio(fg, bg).is_some_and(|ratio| ratio >= min_ratio)
}
#[must_use]
pub fn enforce_contrast(fg: Color, surface: Color, min_ratio: f32) -> Color {
if !matches!(fg, Color::Rgb(..)) {
return fg;
}
let (Some(fg_luma), Some(bg_luma)) = (relative_luminance(fg), relative_luminance(surface))
else {
return fg;
};
if contrast_from_luminance(fg_luma, bg_luma) >= min_ratio {
return fg;
}
const BLACK: Color = Color::Rgb(0, 0, 0);
const WHITE: Color = Color::Rgb(255, 255, 255);
let black_ratio = contrast_from_luminance(0.0, bg_luma);
let white_ratio = contrast_from_luminance(1.0, bg_luma);
let (pole, pole_ratio) = if white_ratio >= black_ratio {
(WHITE, white_ratio)
} else {
(BLACK, black_ratio)
};
if pole_ratio < min_ratio {
return pole;
}
let mut lo = 0.0_f32;
let mut hi = 1.0_f32;
let mut best = pole;
for _ in 0..20 {
let mid = f32::midpoint(lo, hi);
let candidate = blend(pole, fg, mid);
if meets_contrast(candidate, surface, min_ratio) {
best = candidate;
hi = mid;
} else {
lo = mid;
}
}
best
}
#[must_use]
pub fn effective_surface(cell_bg: Color, detected_background: Option<Color>) -> Option<Color> {
if resolvable_rgb(cell_bg).is_some() {
return Some(cell_bg);
}
detected_background.filter(|color| resolvable_rgb(*color).is_some())
}
#[must_use]
pub fn symbol_needs_text_contrast(symbol: &str) -> bool {
symbol.chars().any(|ch| {
!ch.is_whitespace()
&& !matches!(
ch,
'\u{2500}'..='\u{259F}'
| '\u{25A0}'..='\u{25FF}'
| '\u{2800}'..='\u{28FF}'
)
})
}
fn indexed_rgb(index: u8) -> (u8, u8, u8) {
const CUBE_LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
if index < 232 {
let i = u16::from(index) - 16;
let r = CUBE_LEVELS[(i / 36) as usize];
let g = CUBE_LEVELS[((i / 6) % 6) as usize];
let b = CUBE_LEVELS[(i % 6) as usize];
(r, g, b)
} else {
let level = 8 + 10 * (index - 232);
(level, level, level)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThemeContrastViolation {
pub pair: &'static str,
pub fg: Color,
pub bg: Color,
pub ratio: f32,
pub floor: f32,
}
#[allow(dead_code)]
#[must_use]
pub fn theme_uses_terminal_owned_surfaces(theme: &UiTheme) -> bool {
theme.surface_bg == Color::Reset
}
#[allow(dead_code)]
#[must_use]
pub fn theme_contrast_violations(theme: &UiTheme) -> Vec<ThemeContrastViolation> {
let mut violations = Vec::new();
let mut check = |pair: &'static str, fg: Color, bg: Color, floor: f32| {
if let Some(ratio) = contrast_ratio(fg, bg)
&& ratio < floor
{
violations.push(ThemeContrastViolation {
pair,
fg,
bg,
ratio,
floor,
});
}
};
macro_rules! audit {
($floor:expr; $(($fg:ident, $bg:ident)),+ $(,)?) => {
$(check(
concat!(stringify!($fg), " on ", stringify!($bg)),
theme.$fg,
theme.$bg,
$floor,
);)+
};
}
audit!(AA_BODY_CONTRAST;
(text_body, surface_bg),
(text_body, panel_bg),
(text_body, composer_bg),
(text_body, elevated_bg),
(text_soft, surface_bg),
(text_soft, panel_bg),
(text_soft, composer_bg),
(text_soft, elevated_bg),
(text_muted, surface_bg),
(text_muted, panel_bg),
(text_muted, composer_bg),
(text_muted, elevated_bg),
(text_body, selection_bg),
(error_text, error_surface),
);
audit!(SECONDARY_CHROME_CONTRAST;
(text_hint, surface_bg),
(text_hint, panel_bg),
(text_hint, composer_bg),
(text_hint, elevated_bg),
(text_dim, surface_bg),
(status_ready, surface_bg),
(status_working, surface_bg),
(status_warning, surface_bg),
(warning, surface_bg),
(success, surface_bg),
(info, surface_bg),
(diff_added_fg, diff_added_bg),
(diff_deleted_fg, diff_deleted_bg),
);
violations
}