#[derive(Debug, Clone)]
pub struct FigureTheme {
pub background: String,
pub axis_color: String,
pub grid_color: String,
pub label_color: String,
pub label_font: String,
pub label_font_family: String,
pub palette: Vec<String>,
pub positive: String,
pub negative: String,
pub highlight: String,
}
const PALETTE: [&str; 10] = [
"#4d90fe", "#e0703c", "#5cb87a", "#c94f7c", "#d9b64e", "#7e6bd9", "#3fb6c9", "#e0555a", "#8fbf5f", "#c78bd9",
];
const POSITIVE_COLOR: &str = "#5cb87a";
const NEGATIVE_COLOR: &str = "#e0555a";
const HIGHLIGHT_DARK: &str = "#ffffff";
const HIGHLIGHT_LIGHT: &str = "#1a1a2e";
const HIGHLIGHT_HIGH_CONTRAST: &str = "#ffff00";
impl FigureTheme {
pub fn dark() -> Self {
Self {
background: "#0d0f14".to_owned(),
axis_color: "#3a3f4b".to_owned(),
grid_color: "#22262f".to_owned(),
label_color: "#9aa0ac".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_font_family: "sans-serif".to_owned(),
palette: PALETTE.iter().map(|&s| s.to_owned()).collect(),
positive: POSITIVE_COLOR.to_owned(),
negative: NEGATIVE_COLOR.to_owned(),
highlight: HIGHLIGHT_DARK.to_owned(),
}
}
pub fn light() -> Self {
Self {
background: "#ffffff".to_owned(),
axis_color: "#c7ccd6".to_owned(),
grid_color: "#eceff3".to_owned(),
label_color: "#4a5060".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_font_family: "sans-serif".to_owned(),
palette: PALETTE.iter().map(|&s| s.to_owned()).collect(),
positive: POSITIVE_COLOR.to_owned(),
negative: NEGATIVE_COLOR.to_owned(),
highlight: HIGHLIGHT_LIGHT.to_owned(),
}
}
pub fn high_contrast() -> Self {
Self {
background: "#000000".to_owned(),
axis_color: "#ffffff".to_owned(),
grid_color: "#4d4d4d".to_owned(),
label_color: "#ffffff".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_font_family: "sans-serif".to_owned(),
palette: PALETTE.iter().map(|&s| s.to_owned()).collect(),
positive: POSITIVE_COLOR.to_owned(),
negative: NEGATIVE_COLOR.to_owned(),
highlight: HIGHLIGHT_HIGH_CONTRAST.to_owned(),
}
}
}
impl Default for FigureTheme {
fn default() -> Self {
Self::dark()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_and_light_both_carry_the_full_palette() {
assert_eq!(FigureTheme::dark().palette.len(), PALETTE.len());
assert_eq!(FigureTheme::light().palette.len(), PALETTE.len());
assert_eq!(FigureTheme::high_contrast().palette.len(), PALETTE.len());
}
#[test]
fn default_is_dark() {
assert_eq!(FigureTheme::default().background, FigureTheme::dark().background);
}
#[test]
fn highlight_color_differs_from_the_background_in_every_built_in_theme() {
for theme in [FigureTheme::dark(), FigureTheme::light(), FigureTheme::high_contrast()] {
assert_ne!(
theme.highlight.to_ascii_lowercase(),
theme.background.to_ascii_lowercase(),
"highlight must contrast against the background, got highlight={} background={}",
theme.highlight,
theme.background
);
}
}
#[test]
fn high_contrast_uses_pure_black_background_and_pure_white_chrome() {
let theme = FigureTheme::high_contrast();
assert_eq!(theme.background, "#000000");
assert_eq!(theme.axis_color, "#ffffff");
assert_eq!(theme.label_color, "#ffffff");
assert_ne!(theme.grid_color, theme.background, "grid must remain visible against a pure-black background");
}
#[test]
fn high_contrast_shares_the_same_palette_and_semantic_colors_as_dark_and_light() {
let dark = FigureTheme::dark();
let hc = FigureTheme::high_contrast();
assert_eq!(hc.palette, dark.palette);
assert_eq!(hc.positive, dark.positive);
assert_eq!(hc.negative, dark.negative);
}
#[test]
fn dark_theme_highlight_is_unchanged_from_the_pre_fix_literal() {
assert_eq!(FigureTheme::dark().highlight, "#ffffff");
}
}