use super::Theme;
use ratatui::style::Color;
#[derive(Debug, Clone, Copy)]
pub struct Tokens {
pub surface: Surface,
pub text: Text,
pub state: State,
pub accent: Accent,
pub syntax: Syntax,
pub heading: Heading,
pub status: Status,
pub list: List,
pub table: Table,
pub git: Git,
}
#[derive(Debug, Clone, Copy)]
pub struct Surface {
pub base: Color,
pub raised: Color,
pub border: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Text {
pub primary: Color,
pub muted: Color,
pub on_accent: Color,
pub title: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct State {
pub selection_bg: Color,
pub selection_fg: Color,
pub focus: Color,
pub search_bg: Color,
pub current_match_bg: Color,
pub match_fg: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Accent {
pub primary: Color,
pub alt: Color,
pub link: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Syntax {
pub inline_code: Color,
pub code_fg: Color,
pub code_border: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Heading {
pub h1: Color,
pub h2: Color,
pub h3: Color,
pub other: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Status {
pub bg: Color,
pub fg: Color,
pub help_bg: Color,
pub gutter: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct List {
pub marker: Color,
pub task_marker: Color,
pub block_quote_fg: Color,
pub block_quote_border: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Table {
pub header: Color,
pub border: Color,
}
#[derive(Debug, Clone, Copy)]
pub struct Git {
pub new: Color,
pub modified: Color,
}
impl Tokens {
#[must_use]
pub fn from_theme(theme: Theme) -> Self {
match theme {
Theme::Default => super::themes::default_(),
Theme::Dracula => super::themes::dracula(),
Theme::SolarizedDark => super::themes::solarized_dark(),
Theme::SolarizedLight => super::themes::solarized_light(),
Theme::Nord => super::themes::nord(),
Theme::GruvboxDark => super::themes::gruvbox_dark(),
Theme::GruvboxLight => super::themes::gruvbox_light(),
Theme::GithubLight => super::themes::github_light(),
}
}
}
#[cfg(test)]
mod tests {
use super::super::contrast::{contrast_ratio, luminance_delta};
use super::*;
const MIN_SELECTION_DELTA: f64 = 3.0;
#[test]
fn selection_bg_is_distinct_from_surfaces() {
let mut failures: Vec<String> = Vec::new();
for &theme in Theme::ALL {
let t = Tokens::from_theme(theme);
for (label, surface) in [
("surface.base", t.surface.base),
("surface.raised", t.surface.raised),
] {
if let Some(delta) = luminance_delta(t.state.selection_bg, surface)
&& delta < MIN_SELECTION_DELTA
{
failures.push(format!(
" {theme:?}: selection_bg too close to {label} (Δ={delta:.2}, min={MIN_SELECTION_DELTA})",
));
}
}
}
assert!(
failures.is_empty(),
"derived selection backgrounds collide perceptually with surfaces:\n{}",
failures.join("\n"),
);
}
#[test]
fn focus_is_visible_against_surface() {
const MIN_FOCUS_RATIO: f64 = 3.0;
let mut failures: Vec<String> = Vec::new();
for &theme in Theme::ALL {
let t = Tokens::from_theme(theme);
if let Some(ratio) = contrast_ratio(t.state.focus, t.surface.base)
&& ratio < MIN_FOCUS_RATIO
{
failures.push(format!(
" {theme:?}: focus on surface.base = {ratio:.2}:1 < {MIN_FOCUS_RATIO}:1",
));
}
}
assert!(
failures.is_empty(),
"focus state too low contrast:\n{}",
failures.join("\n")
);
}
}