use super::color::{Color, lighten};
pub const DEFAULT_THEME: &str = "default";
const DEFAULT_ERROR: Color = Color::Rgb(0xf3, 0x8b, 0x8b);
const DEFAULT_WARNING: Color = Color::Rgb(0xff, 0xb9, 0x54);
const DEFAULT_SUCCESS: Color = Color::Rgb(0x8c, 0xc8, 0x8c);
const DEFAULT_INFO: Color = Color::Rgb(0x6d, 0xa8, 0xff);
const SURFACE_FACTOR: f32 = 0.06;
const SURFACE_ALT_FACTOR: f32 = 0.12;
const SURFACE_BAR_FACTOR: f32 = 0.18;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Surfaces {
pub surface: Color,
pub surface_alt: Color,
pub surface_bar: Color,
}
pub fn derive_surfaces(background: Color) -> Surfaces {
Surfaces {
surface: lighten(background, SURFACE_FACTOR),
surface_alt: lighten(background, SURFACE_ALT_FACTOR),
surface_bar: lighten(background, SURFACE_BAR_FACTOR),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThemeColors {
pub accent: Color,
pub selection_bg: Color,
pub cursor: Color,
pub background: Color,
pub error: Color,
pub warning: Color,
pub success: Color,
pub info: Color,
pub surface: Color,
pub surface_alt: Color,
pub surface_bar: Color,
}
impl ThemeColors {
pub fn new(
accent: Color,
selection_bg: Color,
cursor: Color,
background: Color,
) -> Self {
let surfaces = derive_surfaces(background);
Self {
accent,
selection_bg,
cursor,
background,
error: DEFAULT_ERROR,
warning: DEFAULT_WARNING,
success: DEFAULT_SUCCESS,
info: DEFAULT_INFO,
surface: surfaces.surface,
surface_alt: surfaces.surface_alt,
surface_bar: surfaces.surface_bar,
}
}
#[must_use]
pub fn with_semantics(
mut self,
error: Color,
warning: Color,
success: Color,
info: Color,
) -> Self {
self.error = error;
self.warning = warning;
self.success = success;
self.info = info;
self
}
#[must_use]
pub fn with_surfaces(mut self, surfaces: Surfaces) -> Self {
self.surface = surfaces.surface;
self.surface_alt = surfaces.surface_alt;
self.surface_bar = surfaces.surface_bar;
self
}
}
fn builtin_themes() -> Vec<(String, ThemeColors)> {
vec![
(
DEFAULT_THEME.to_string(),
ThemeColors::new(
Color::Rgb(0xe6, 0x8f, 0xff),
Color::Rgb(0x33, 0x38, 0x4a),
Color::Rgb(0xf3, 0x8b, 0x8b),
Color::Rgb(0x0d, 0x0d, 0x1e),
),
),
(
"nord".to_string(),
ThemeColors::new(
Color::Rgb(0x88, 0xc0, 0xd0),
Color::Rgb(0x3b, 0x42, 0x52),
Color::Rgb(0xbf, 0x61, 0x6a),
Color::Rgb(0x2e, 0x34, 0x40),
)
.with_semantics(
Color::Rgb(0xbf, 0x61, 0x6a),
Color::Rgb(0xeb, 0xcb, 0x8b),
Color::Rgb(0xa3, 0xbe, 0x8c),
Color::Rgb(0x88, 0xc0, 0xd0),
),
),
(
"monochrome".to_string(),
ThemeColors::new(
Color::Rgb(0xc0, 0xc0, 0xc0),
Color::Rgb(0x2a, 0x2a, 0x2a),
Color::Rgb(0xff, 0xff, 0xff),
Color::Rgb(0x10, 0x10, 0x10),
),
),
]
}
#[derive(Debug, Clone)]
pub struct ThemeRegistry {
themes: Vec<(String, ThemeColors)>,
}
impl ThemeRegistry {
pub fn builtin() -> Self {
Self {
themes: builtin_themes(),
}
}
#[must_use]
pub fn with_custom<I>(mut self, custom: I) -> Self
where
I: IntoIterator<Item = (String, ThemeColors)>,
{
for (name, colors) in custom {
match self.themes.iter_mut().find(|(n, _)| n == &name) {
Some(entry) => entry.1 = colors,
None => self.themes.push((name, colors)),
}
}
self
}
pub fn get(&self, name: &str) -> Option<ThemeColors> {
self.themes
.iter()
.find(|(n, _)| n == name)
.map(|(_, colors)| *colors)
}
pub fn resolve(&self, name: &str) -> ThemeColors {
self.get(name)
.or_else(|| self.get(DEFAULT_THEME))
.or_else(|| self.themes.first().map(|(_, colors)| *colors))
.unwrap_or_else(|| {
ThemeColors::new(
Color::Default,
Color::Default,
Color::Default,
Color::Default,
)
})
}
pub fn contains(&self, name: &str) -> bool {
self.themes.iter().any(|(n, _)| n == name)
}
pub fn names(&self) -> Vec<&str> {
self.themes.iter().map(|(n, _)| n.as_str()).collect()
}
pub fn next(&self, current: &str) -> String {
if self.themes.is_empty() {
return current.to_string();
}
let index = self
.themes
.iter()
.position(|(n, _)| n == current)
.unwrap_or(0);
let next = (index + 1) % self.themes.len();
self.themes[next].0.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_themes_have_distinct_accents() {
let registry = ThemeRegistry::builtin();
let default = registry.resolve("default").accent;
let nord = registry.resolve("nord").accent;
let mono = registry.resolve("monochrome").accent;
assert_ne!(default, nord);
assert_ne!(nord, mono);
assert_ne!(default, mono);
}
#[test]
fn next_cycles_through_all_and_wraps() {
let registry = ThemeRegistry::builtin();
assert_eq!(registry.next("default"), "nord");
assert_eq!(registry.next("nord"), "monochrome");
assert_eq!(registry.next("monochrome"), "default");
}
#[test]
fn resolve_falls_back_to_default_for_unknown_name() {
let registry = ThemeRegistry::builtin();
assert_eq!(registry.resolve("nope"), registry.resolve("default"));
}
#[test]
fn custom_theme_overrides_builtin_in_place() {
let custom = ThemeColors::new(
Color::Rgb(1, 2, 3),
Color::Rgb(4, 5, 6),
Color::Rgb(7, 8, 9),
Color::Rgb(10, 11, 12),
);
let registry = ThemeRegistry::builtin()
.with_custom([("nord".to_string(), custom)]);
assert_eq!(registry.resolve("nord").accent, Color::Rgb(1, 2, 3));
assert_eq!(registry.names(), vec!["default", "nord", "monochrome"]);
}
#[test]
fn custom_theme_is_appended_and_cycles() {
let custom = ThemeColors::new(
Color::Rgb(1, 2, 3),
Color::Rgb(4, 5, 6),
Color::Rgb(7, 8, 9),
Color::Rgb(10, 11, 12),
);
let registry = ThemeRegistry::builtin()
.with_custom([("dracula".to_string(), custom)]);
assert!(registry.contains("dracula"));
assert_eq!(registry.next("monochrome"), "dracula");
assert_eq!(registry.next("dracula"), "default");
}
#[test]
fn omitted_semantics_default_to_universal_values() {
let colors = ThemeColors::new(
Color::Rgb(1, 1, 1),
Color::Rgb(2, 2, 2),
Color::Rgb(3, 3, 3),
Color::Rgb(4, 4, 4),
);
assert_eq!(colors.error, DEFAULT_ERROR);
assert_eq!(colors.success, DEFAULT_SUCCESS);
}
}