use ratatui::style::Color;
use typ_core::ThemeColors;
fn rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Rgb(r, g, b) => (r, g, b),
other => panic!("{other:?} is not truecolor"),
}
}
fn luminance(color: Color) -> f64 {
fn channel(value: u8) -> f64 {
let s = value as f64 / 255.0;
if s <= 0.039_28 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
}
}
let (r, g, b) = rgb(color);
0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
}
fn contrast(a: Color, b: Color) -> f64 {
let (x, y) = (luminance(a), luminance(b));
let (hi, lo) = if x > y { (x, y) } else { (y, x) };
(hi + 0.05) / (lo + 0.05)
}
fn assert_contrast(name: &str, fg: Color, bg: Color, floor: f64) {
let ratio = contrast(fg, bg);
assert!(
ratio >= floor,
"{name}: contrast {ratio:.2} is below the {floor:.1} floor"
);
}
#[test]
fn every_colour_in_the_theme_is_truecolor() {
let ThemeColors {
fg,
bg,
cursor_line_bg,
gutter_fg,
gutter_bg,
line_number_fg,
line_number_current_fg,
selection_bg,
selection_fg,
selection_primary_bg,
bracket_match_fg,
bracket_match_bg,
border,
border_focused,
status_bar_bg,
status_bar_fg,
status_bar_inactive_fg,
status_bar_accent,
tree_directory_fg,
tree_file_fg,
diagnostic_error,
diagnostic_warning,
diagnostic_info,
diagnostic_hint,
} = ThemeColors::default();
for (name, colour) in [
("fg", fg),
("bg", bg),
("cursor_line_bg", cursor_line_bg),
("gutter_fg", gutter_fg),
("gutter_bg", gutter_bg),
("line_number_fg", line_number_fg),
("line_number_current_fg", line_number_current_fg),
("selection_bg", selection_bg),
("selection_fg", selection_fg),
("selection_primary_bg", selection_primary_bg),
("bracket_match_fg", bracket_match_fg),
("bracket_match_bg", bracket_match_bg),
("border", border),
("border_focused", border_focused),
("status_bar_bg", status_bar_bg),
("status_bar_fg", status_bar_fg),
("status_bar_inactive_fg", status_bar_inactive_fg),
("status_bar_accent", status_bar_accent),
("tree_directory_fg", tree_directory_fg),
("tree_file_fg", tree_file_fg),
("diagnostic_error", diagnostic_error),
("diagnostic_warning", diagnostic_warning),
("diagnostic_info", diagnostic_info),
("diagnostic_hint", diagnostic_hint),
] {
let _ = rgb(colour);
assert!(
luminance(colour) >= 0.0,
"{name} produced a nonsensical luminance"
);
}
}
#[test]
fn body_text_is_comfortably_readable() {
let theme = ThemeColors::default();
assert_contrast("fg on bg", theme.fg, theme.bg, 7.0);
}
#[test]
fn text_stays_readable_on_both_kinds_of_selection() {
let theme = ThemeColors::default();
assert_contrast(
"selection_fg on selection_bg",
theme.selection_fg,
theme.selection_bg,
4.5,
);
assert_contrast(
"selection_fg on selection_primary_bg",
theme.selection_fg,
theme.selection_primary_bg,
4.5,
);
}
#[test]
fn the_primary_selection_is_distinguishable_from_the_others() {
let theme = ThemeColors::default();
assert_ne!(theme.selection_primary_bg, theme.selection_bg);
let ratio = contrast(theme.selection_primary_bg, theme.selection_bg);
assert!(
ratio >= 1.3,
"the two selection backgrounds differ by only {ratio:.2}, which reads as \
one colour under any gamma the user's terminal picks"
);
}
#[test]
fn the_current_lines_number_stands_out_from_the_rest() {
let theme = ThemeColors::default();
assert_ne!(theme.line_number_current_fg, theme.line_number_fg);
assert!(
luminance(theme.line_number_current_fg) > luminance(theme.line_number_fg),
"the current line's number must be the brighter of the two — a dimmer \
'here' reads as disabled"
);
}
#[test]
fn line_numbers_recede_without_becoming_unreadable() {
let theme = ThemeColors::default();
assert_contrast("line_number_fg on bg", theme.line_number_fg, theme.bg, 3.0);
assert!(
luminance(theme.line_number_fg) < luminance(theme.fg),
"line numbers must be quieter than the code they label"
);
}
#[test]
fn the_current_line_highlight_is_felt_rather_than_seen() {
let theme = ThemeColors::default();
assert_ne!(theme.cursor_line_bg, theme.bg, "it has to do something");
let ratio = contrast(theme.cursor_line_bg, theme.bg);
assert!(
ratio < 1.5,
"cursor_line_bg is {ratio:.2} against the background, which is a stripe \
across the screen rather than a hint at where the cursor is"
);
assert_contrast("fg on cursor_line_bg", theme.fg, theme.cursor_line_bg, 7.0);
}
#[test]
fn a_matched_bracket_is_visible_against_its_own_background() {
let theme = ThemeColors::default();
assert_contrast(
"bracket_match_fg on bracket_match_bg",
theme.bracket_match_fg,
theme.bracket_match_bg,
4.5,
);
}
#[test]
fn the_focused_border_is_brighter_than_an_unfocused_one() {
let theme = ThemeColors::default();
assert!(
luminance(theme.border_focused) > luminance(theme.border),
"focus is indicated by gaining attention, not losing it"
);
}
#[test]
fn the_status_bar_reads_against_its_own_background() {
let theme = ThemeColors::default();
assert_contrast(
"status_bar_fg",
theme.status_bar_fg,
theme.status_bar_bg,
4.5,
);
assert_contrast(
"status_bar_inactive_fg",
theme.status_bar_inactive_fg,
theme.status_bar_bg,
3.0,
);
assert!(
luminance(theme.status_bar_inactive_fg) < luminance(theme.status_bar_fg),
"the inactive colour must be the quieter one"
);
}
#[test]
fn the_tree_distinguishes_directories_from_files() {
let theme = ThemeColors::default();
assert_ne!(theme.tree_directory_fg, theme.tree_file_fg);
assert_contrast("tree_directory_fg", theme.tree_directory_fg, theme.bg, 4.5);
assert_contrast("tree_file_fg", theme.tree_file_fg, theme.bg, 4.5);
}
#[test]
fn every_diagnostic_severity_is_readable() {
let theme = ThemeColors::default();
for (name, colour) in [
("error", theme.diagnostic_error),
("warning", theme.diagnostic_warning),
("info", theme.diagnostic_info),
("hint", theme.diagnostic_hint),
] {
assert_contrast(name, colour, theme.bg, 4.5);
}
}
#[test]
fn error_and_warning_differ_by_more_than_hue() {
let theme = ThemeColors::default();
let ratio = contrast(theme.diagnostic_error, theme.diagnostic_warning);
assert!(
ratio >= 1.8,
"error and warning differ by only {ratio:.2} in lightness, so they are \
one colour to a red-green colour-blind reader"
);
}