use ratatui::style::{Color, Modifier, Style};
use crate::theme::Palette;
const FAVOURITE_LIGHTEN: f32 = 0.08;
#[derive(Debug, Clone, Copy)]
pub struct Colors {
pub accent: Color,
pub tab_active: Color,
pub dim: Color,
pub muted: Color,
pub foreground: Color,
pub selection_bg: Color,
pub multi_select_bg: Color,
pub danger: Color,
pub positive: Color,
pub changes: Color,
pub favourite: Color,
pub cursor: Color,
pub header_bg: Color,
pub footer_bg: Color,
pub surface_bg: Color,
pub border: Color,
}
impl Colors {
pub fn from_palette(palette: &Palette) -> Self {
let rat = ratada::style::to_ratatui;
Colors {
accent: rat(palette.accent),
tab_active: rat(complement(palette.accent)),
dim: rat(palette.foreground_dim),
muted: rat(palette.foreground.darken(0.15)),
foreground: rat(palette.foreground),
selection_bg: rat(palette.selection),
multi_select_bg: rat(palette.surface.mix(palette.accent, 0.18)),
danger: rat(palette.error),
positive: rat(palette.success),
changes: rat(palette.warning),
favourite: rat(palette.warning.lighten(FAVOURITE_LIGHTEN)),
cursor: rat(palette.cursor),
header_bg: rat(palette.header),
footer_bg: rat(palette.footer),
surface_bg: rat(palette.surface),
border: rat(palette.border),
}
}
pub fn header_style(&self) -> Style {
Style::default()
.fg(self.accent)
.add_modifier(Modifier::BOLD)
}
pub fn selection_style(&self) -> Style {
Style::default()
.bg(self.selection_bg)
.add_modifier(Modifier::BOLD)
}
}
fn complement(color: crate::theme::Color) -> crate::theme::Color {
let Some((hue, saturation, lightness)) = color.to_hsl() else {
return color;
};
crate::theme::Color::from_hsl(
(hue + 180.0).rem_euclid(360.0),
saturation,
lightness,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
#[test]
fn complement_rotates_the_hue_and_round_trips() {
let accent = Config::default().palette().accent;
let once = complement(accent);
let twice = complement(once);
let (Some((hue, _, _)), Some((back, _, _))) =
(accent.to_hsl(), twice.to_hsl())
else {
panic!("the default accent is an rgb colour");
};
assert!((hue - back).abs() < 1.0, "two rotations return the hue");
let Some((rotated, _, _)) = once.to_hsl() else {
panic!("rgb");
};
assert!((rotated - (hue + 180.0).rem_euclid(360.0)).abs() < 1.0);
}
#[test]
fn a_monochrome_theme_leaves_no_colour_role_coloured() {
let mut config = Config::default();
config.appearance.theme = "monochrome".to_string();
let colors = Colors::from_palette(&config.palette());
for (role, color) in [
("accent", colors.accent),
("favourite", colors.favourite),
("changes", colors.changes),
("positive", colors.positive),
("danger", colors.danger),
("cursor", colors.cursor),
] {
let Color::Rgb(red, green, blue) = color else {
panic!("{role} is not an rgb colour");
};
assert!(
red == green && green == blue,
"{role} is not grey under monochrome: {red},{green},{blue}"
);
}
}
#[test]
fn the_favourite_star_stays_brighter_than_the_change_count() {
let colors = Colors::from_palette(&Config::default().palette());
assert_ne!(colors.favourite, colors.changes);
}
}