use ratatui::style::Color;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "theme-serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColorPalette {
pub primary: Color,
pub secondary: Color,
pub text: Color,
pub text_dim: Color,
pub text_disabled: Color,
pub text_placeholder: Color,
pub text_muted: Color,
pub bg: Color,
pub surface: Color,
pub surface_raised: Color,
pub border_focused: Color,
pub border: Color,
pub border_disabled: Color,
pub border_accent: Color,
pub separator: Color,
pub highlight_fg: Color,
pub highlight_bg: Color,
pub menu_highlight_fg: Color,
pub menu_highlight_bg: Color,
pub pressed_fg: Color,
pub pressed_bg: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub diff_add_fg: Color,
pub diff_add_bg: Color,
pub diff_del_fg: Color,
pub diff_del_bg: Color,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "theme-serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Theme {
pub name: String,
pub palette: ColorPalette,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn dark() -> Self {
Self {
name: "Dark".to_string(),
palette: ColorPalette {
primary: Color::Yellow,
secondary: Color::Cyan,
text: Color::White,
text_dim: Color::Gray,
text_disabled: Color::DarkGray,
text_placeholder: Color::DarkGray,
text_muted: Color::Rgb(140, 140, 140),
bg: Color::Reset,
surface: Color::Rgb(40, 40, 40),
surface_raised: Color::Rgb(50, 50, 50),
border_focused: Color::Yellow,
border: Color::Gray,
border_disabled: Color::DarkGray,
border_accent: Color::Cyan,
separator: Color::Rgb(80, 80, 80),
highlight_fg: Color::Black,
highlight_bg: Color::Yellow,
menu_highlight_fg: Color::White,
menu_highlight_bg: Color::Rgb(60, 100, 180),
pressed_fg: Color::Black,
pressed_bg: Color::White,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Cyan,
diff_add_fg: Color::Green,
diff_add_bg: Color::Rgb(0, 40, 0),
diff_del_fg: Color::Red,
diff_del_bg: Color::Rgb(40, 0, 0),
},
}
}
pub fn light() -> Self {
Self {
name: "Light".to_string(),
palette: ColorPalette {
primary: Color::Blue,
secondary: Color::Rgb(0, 128, 128),
text: Color::Rgb(30, 30, 30),
text_dim: Color::Rgb(100, 100, 100),
text_disabled: Color::Rgb(160, 160, 160),
text_placeholder: Color::Rgb(160, 160, 160),
text_muted: Color::Rgb(100, 100, 100),
bg: Color::Reset,
surface: Color::Rgb(250, 250, 250),
surface_raised: Color::Rgb(240, 240, 240),
border_focused: Color::Blue,
border: Color::Rgb(180, 180, 180),
border_disabled: Color::Rgb(200, 200, 200),
border_accent: Color::Rgb(0, 128, 128),
separator: Color::Rgb(200, 200, 200),
highlight_fg: Color::White,
highlight_bg: Color::Blue,
menu_highlight_fg: Color::White,
menu_highlight_bg: Color::Rgb(0, 120, 215),
pressed_fg: Color::White,
pressed_bg: Color::Rgb(30, 30, 30),
success: Color::Rgb(0, 128, 0),
warning: Color::Rgb(200, 150, 0),
error: Color::Rgb(200, 0, 0),
info: Color::Rgb(0, 128, 128),
diff_add_fg: Color::Rgb(0, 128, 0),
diff_add_bg: Color::Rgb(220, 255, 220),
diff_del_fg: Color::Rgb(200, 0, 0),
diff_del_bg: Color::Rgb(255, 220, 220),
},
}
}
pub fn style<S: for<'a> From<&'a Theme>>(&self) -> S {
S::from(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{ButtonStyle, CheckBoxStyle, InputStyle};
#[test]
fn test_dark_theme_matches_button_default() {
let theme = Theme::dark();
let themed: ButtonStyle = theme.style();
let default = ButtonStyle::default();
assert_eq!(themed.focused_fg, default.focused_fg);
assert_eq!(themed.focused_bg, default.focused_bg);
assert_eq!(themed.unfocused_fg, default.unfocused_fg);
assert_eq!(themed.unfocused_bg, default.unfocused_bg);
assert_eq!(themed.disabled_fg, default.disabled_fg);
assert_eq!(themed.pressed_fg, default.pressed_fg);
assert_eq!(themed.pressed_bg, default.pressed_bg);
assert_eq!(themed.toggled_fg, default.toggled_fg);
assert_eq!(themed.toggled_bg, default.toggled_bg);
}
#[test]
fn test_dark_theme_matches_input_default() {
let theme = Theme::dark();
let themed: InputStyle = theme.style();
let default = InputStyle::default();
assert_eq!(themed.focused_border, default.focused_border);
assert_eq!(themed.unfocused_border, default.unfocused_border);
assert_eq!(themed.disabled_border, default.disabled_border);
assert_eq!(themed.text_fg, default.text_fg);
assert_eq!(themed.cursor_fg, default.cursor_fg);
assert_eq!(themed.placeholder_fg, default.placeholder_fg);
}
#[test]
fn test_dark_theme_matches_checkbox_default() {
let theme = Theme::dark();
let themed: CheckBoxStyle = theme.style();
let default = CheckBoxStyle::default();
assert_eq!(themed.focused_fg, default.focused_fg);
assert_eq!(themed.unfocused_fg, default.unfocused_fg);
assert_eq!(themed.disabled_fg, default.disabled_fg);
assert_eq!(themed.checked_fg, default.checked_fg);
}
#[test]
fn test_light_theme_differs_from_dark() {
let dark = Theme::dark();
let light = Theme::light();
assert_ne!(dark.palette.text, light.palette.text);
assert_ne!(dark.palette.primary, light.palette.primary);
assert_ne!(dark.palette.surface, light.palette.surface);
}
#[test]
fn test_theme_default_is_dark() {
let default = Theme::default();
let dark = Theme::dark();
assert_eq!(default.palette, dark.palette);
}
#[test]
fn test_theme_clone_and_eq() {
let theme = Theme::dark();
let cloned = theme.clone();
assert_eq!(theme, cloned);
}
#[test]
fn test_color_palette_clone_and_eq() {
let palette = Theme::dark().palette;
let cloned = palette.clone();
assert_eq!(palette, cloned);
}
#[test]
fn test_style_generic_method() {
let theme = Theme::dark();
let _: ButtonStyle = theme.style();
let _: InputStyle = theme.style();
let _: CheckBoxStyle = theme.style();
}
#[test]
fn test_light_theme_produces_valid_styles() {
let theme = Theme::light();
let btn: ButtonStyle = theme.style();
let input: InputStyle = theme.style();
let cb: CheckBoxStyle = theme.style();
let default_btn = ButtonStyle::default();
assert_ne!(btn.focused_bg, default_btn.focused_bg);
assert_ne!(input.text_fg, Color::Reset);
assert_ne!(cb.focused_fg, Color::Reset);
}
}