use super::{
color::Color, color::parse_color, theme_set::BORDER_FOCUS_LIGHTEN,
theme_set::ThemeColors,
};
const ACCENT_DIM: f32 = 0.15;
const ACCENT_VIVID: f32 = 0.25;
const FOREGROUND_DIM: f32 = 0.30;
const FOREGROUND_MUTED: f32 = 0.38;
const SELECTION_MIX: f32 = 0.35;
const INPUT_BG: f32 = 0.10;
const INPUT_BG_ACTIVE: f32 = 0.22;
macro_rules! define_palette {
($( $(#[$meta:meta])* $field:ident ),+ $(,)?) => {
#[derive(Debug, Clone, Copy)]
pub struct Palette {
$( $(#[$meta])* pub $field: Color, )+
}
#[derive(Debug, Default, Clone)]
pub struct ColorOverrides<'a> {
$(
#[doc = concat!(
"Override for the `", stringify!($field),
"` color (empty string = keep the theme value)."
)]
pub $field: &'a str,
)+
}
impl<'a> ColorOverrides<'a> {
pub fn from_lookup(lookup: impl Fn(&str) -> &'a str) -> Self {
Self { $( $field: lookup(stringify!($field)), )+ }
}
}
impl Palette {
pub fn entries(&self) -> Vec<(&'static str, Color)> {
vec![ $( (stringify!($field), self.$field), )+ ]
}
pub const KEYS: &'static [&'static str] =
&[ $( stringify!($field), )+ ];
}
};
}
define_palette! {
accent,
accent_dim,
accent_vivid,
foreground,
foreground_dim,
foreground_muted,
background,
header,
footer,
panel,
surface,
selection,
cursor,
input_bg,
input_bg_active,
border,
border_focus,
success,
warning,
error,
info,
}
impl Palette {
pub fn resolve(base: ThemeColors, overrides: &ColorOverrides<'_>) -> Self {
let get = |value: &str, fallback: Color| {
if let Some(color) = parse_color(value) {
return color;
}
if !value.is_empty() {
log::warn!(
"invalid color override {value:?}, keeping the theme color"
);
}
fallback
};
let accent = get(overrides.accent, base.accent);
let foreground = get(overrides.foreground, base.foreground);
let background = get(overrides.background, base.background);
let surface = get(overrides.surface, base.surface);
let border = get(overrides.border, base.border);
let base_border_focus = if overrides.border.is_empty() {
base.border_focus
} else {
border.lighten(BORDER_FOCUS_LIGHTEN)
};
Self {
accent,
accent_dim: get(overrides.accent_dim, accent.darken(ACCENT_DIM)),
accent_vivid: get(
overrides.accent_vivid,
accent.vivid(ACCENT_VIVID),
),
foreground,
foreground_dim: get(
overrides.foreground_dim,
foreground.darken(FOREGROUND_DIM),
),
foreground_muted: get(
overrides.foreground_muted,
foreground.darken(FOREGROUND_MUTED),
),
background,
header: get(overrides.header, base.header),
footer: get(overrides.footer, base.footer),
panel: get(overrides.panel, base.panel),
surface,
selection: get(
overrides.selection,
surface.mix(accent, SELECTION_MIX),
),
cursor: get(overrides.cursor, accent),
input_bg: get(overrides.input_bg, surface.lighten(INPUT_BG)),
input_bg_active: get(
overrides.input_bg_active,
surface.lighten(INPUT_BG_ACTIVE),
),
border,
border_focus: get(overrides.border_focus, base_border_focus),
success: get(overrides.success, base.success),
warning: get(overrides.warning, base.warning),
error: get(overrides.error, base.error),
info: get(overrides.info, base.info),
}
}
}
#[cfg(test)]
mod tests {
use super::{super::theme_set::ThemeRegistry, *};
fn base() -> ThemeColors {
ThemeRegistry::builtin().resolve("default")
}
#[test]
fn resolve_without_overrides_uses_theme_colors() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert_eq!(palette.accent, base().accent);
assert_eq!(palette.background, base().background);
assert_eq!(palette.header, base().header);
assert_eq!(palette.surface, base().surface);
}
#[test]
fn non_empty_override_wins_over_theme() {
let overrides = ColorOverrides {
accent: "#010203",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.accent, Color::Rgb(1, 2, 3));
assert_eq!(palette.foreground, base().foreground);
}
#[test]
fn empty_or_invalid_override_keeps_theme_color() {
let overrides = ColorOverrides {
accent: "",
header: "nope",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.accent, base().accent);
assert_eq!(palette.header, base().header);
}
#[test]
fn semantic_colors_carry_through_from_the_theme() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert_eq!(palette.error, base().error);
assert_eq!(palette.success, base().success);
assert_eq!(palette.info, base().info);
}
#[test]
fn variant_colors_are_derived_from_the_resolved_base() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert_eq!(palette.accent_dim, base().accent.darken(ACCENT_DIM));
assert_eq!(palette.accent_vivid, base().accent.vivid(ACCENT_VIVID));
assert_eq!(
palette.foreground_dim,
base().foreground.darken(FOREGROUND_DIM),
);
assert_eq!(
palette.foreground_muted,
base().foreground.darken(FOREGROUND_MUTED),
);
assert_eq!(palette.cursor, base().accent);
assert_ne!(palette.accent_dim, palette.accent);
}
#[test]
fn muted_text_is_dimmer_than_secondary_text() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert!(
palette.foreground_muted.luminance()
< palette.foreground_dim.luminance()
);
}
#[test]
fn selection_is_between_background_and_accent() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert_ne!(palette.selection, palette.background);
assert_ne!(palette.selection, palette.accent);
}
#[test]
fn derived_colors_honor_overrides() {
let overrides = ColorOverrides {
selection: "#010203",
cursor: "#040506",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.selection, Color::Rgb(1, 2, 3));
assert_eq!(palette.cursor, Color::Rgb(4, 5, 6));
}
#[test]
fn the_focus_border_comes_from_the_theme_by_default() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
assert_eq!(palette.border_focus, base().border_focus);
assert!(palette.border_focus.luminance() > palette.border.luminance());
}
#[test]
fn overriding_border_alone_drags_the_focus_border_with_it() {
let overrides = ColorOverrides {
border: "#4a4a4a",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.border, Color::hex("#4a4a4a"));
assert_eq!(
palette.border_focus,
Color::hex("#4a4a4a").lighten(BORDER_FOCUS_LIGHTEN),
);
}
#[test]
fn an_explicit_focus_border_override_wins() {
let overrides = ColorOverrides {
border: "#4a4a4a",
border_focus: "#010203",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.border_focus, Color::Rgb(1, 2, 3));
}
#[test]
fn a_focus_border_override_alone_leaves_the_border_untouched() {
let overrides = ColorOverrides {
border_focus: "#010203",
..ColorOverrides::default()
};
let palette = Palette::resolve(base(), &overrides);
assert_eq!(palette.border, base().border);
assert_eq!(palette.border_focus, Color::Rgb(1, 2, 3));
}
#[test]
fn keys_include_the_focus_border() {
assert!(Palette::KEYS.contains(&"border_focus"));
}
#[test]
fn entries_cover_every_field_once() {
let palette = Palette::resolve(base(), &ColorOverrides::default());
let entries = palette.entries();
assert_eq!(entries.len(), Palette::KEYS.len());
let mut names: Vec<&str> =
entries.iter().map(|(name, _)| *name).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), Palette::KEYS.len());
}
}