use retroglyph_core::color::{Color, Style};
use crate::interact::Response;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Theme {
pub bg: Color,
pub panel_bg: Color,
pub border: Color,
pub title_bg: Color,
pub fg: Color,
pub accent: Color,
pub hover_bg: Color,
pub press_bg: Color,
pub dim: Color,
pub disabled: Color,
pub danger: Color,
pub success: Color,
}
impl Theme {
pub const DARK: Self = Self {
bg: Color::rgb(16, 16, 24),
panel_bg: Color::rgb(22, 22, 32),
border: Color::rgb(70, 74, 96),
title_bg: Color::rgb(30, 32, 48),
fg: Color::rgb(190, 192, 208),
accent: Color::rgb(90, 170, 250),
hover_bg: Color::rgb(40, 44, 64),
press_bg: Color::rgb(60, 110, 170),
dim: Color::rgb(110, 112, 130),
disabled: Color::rgb(80, 82, 96),
danger: Color::rgb(220, 90, 90),
success: Color::rgb(80, 200, 120),
};
pub const LIGHT: Self = Self {
bg: Color::rgb(240, 240, 246),
panel_bg: Color::rgb(255, 255, 255),
border: Color::rgb(160, 164, 180),
title_bg: Color::rgb(224, 226, 240),
fg: Color::rgb(20, 22, 32),
accent: Color::rgb(20, 100, 210),
hover_bg: Color::rgb(230, 236, 248),
press_bg: Color::rgb(160, 194, 240),
dim: Color::rgb(130, 132, 150),
disabled: Color::rgb(190, 192, 202),
danger: Color::rgb(200, 60, 60),
success: Color::rgb(30, 140, 75),
};
#[must_use]
pub const fn bg_for<Id>(&self, response: &Response<Id>, base: Color) -> Color {
match WidgetState::of(response) {
WidgetState::Pressed => self.press_bg,
WidgetState::Hovered => self.hover_bg,
WidgetState::Disabled | WidgetState::Focused | WidgetState::Idle => base,
}
}
#[must_use]
pub const fn fg_for<Id>(&self, response: &Response<Id>) -> Color {
match WidgetState::of(response) {
WidgetState::Disabled => self.disabled,
WidgetState::Pressed | WidgetState::Focused => self.accent,
WidgetState::Hovered => self.fg,
WidgetState::Idle => self.dim,
}
}
#[must_use]
pub fn style_for<Id>(&self, response: &Response<Id>, base: Color) -> Style {
match WidgetState::of(response) {
WidgetState::Disabled => Style::new().fg(self.disabled).bg(base),
WidgetState::Pressed => Style::new().fg(self.accent).bg(self.press_bg),
WidgetState::Focused => Style::new().fg(self.accent).bg(base),
WidgetState::Hovered => Style::new().fg(self.fg).bg(self.hover_bg),
WidgetState::Idle => Style::new().fg(self.dim).bg(base),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WidgetState {
Disabled,
Pressed,
Focused,
Hovered,
Idle,
}
impl WidgetState {
#[must_use]
pub const fn of<Id>(response: &Response<Id>) -> Self {
if response.disabled() {
Self::Disabled
} else if response.pressed() {
Self::Pressed
} else if response.focused() {
Self::Focused
} else if response.hovered() {
Self::Hovered
} else {
Self::Idle
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_and_light_are_distinct() {
assert_ne!(Theme::DARK, Theme::LIGHT);
}
#[cfg(feature = "serde")]
#[test]
fn serializes_and_deserializes() {
let json = serde_json::to_string(&Theme::DARK).expect("serialize");
let round_tripped: Theme = serde_json::from_str(&json).expect("deserialize");
assert_eq!(round_tripped, Theme::DARK);
}
#[test]
fn bg_for_is_base_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.panel_bg);
}
#[test]
fn bg_for_is_hover_bg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.hover_bg);
}
#[test]
fn bg_for_prefers_press_bg_over_hover_bg() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.press_bg);
}
#[test]
fn bg_for_is_base_when_disabled_even_if_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
disabled: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.panel_bg);
}
#[test]
fn fg_for_is_dim_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
assert_eq!(theme.fg_for(&response), theme.dim);
}
#[test]
fn fg_for_is_fg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.fg);
}
#[test]
fn fg_for_prefers_accent_over_hover_when_focused() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
focused: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.accent);
}
#[test]
fn fg_for_prefers_accent_over_hover_when_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.accent);
}
#[test]
fn fg_for_is_disabled_even_if_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
disabled: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.disabled);
}
#[test]
fn widget_state_of_precedence_disabled_over_everything() {
let response: Response<()> = Response {
disabled: true,
pressed: true,
focused: true,
hovered: true,
..Response::default()
};
assert_eq!(WidgetState::of(&response), WidgetState::Disabled);
}
#[test]
fn widget_state_of_is_idle_by_default() {
let response: Response<()> = Response::default();
assert_eq!(WidgetState::of(&response), WidgetState::Idle);
}
#[test]
fn style_for_is_dim_on_base_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.dim);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_is_disabled_on_base_when_disabled() {
let theme = Theme::DARK;
let response: Response<()> = Response {
disabled: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.disabled);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_is_fg_on_hover_bg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.fg);
assert_eq!(style.background(), theme.hover_bg);
}
#[test]
fn style_for_is_accent_on_base_when_focused_and_not_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
focused: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_prefers_focused_over_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
focused: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_is_accent_on_press_bg_when_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
pressed: true,
focused: true,
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.press_bg);
}
#[test]
fn style_for_prefers_disabled_over_everything_else() {
let theme = Theme::DARK;
let response: Response<()> = Response {
disabled: true,
pressed: true,
focused: true,
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.disabled);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn disabled_is_distinct_from_dim() {
assert_ne!(Theme::DARK.disabled, Theme::DARK.dim);
assert_ne!(Theme::LIGHT.disabled, Theme::LIGHT.dim);
}
#[test]
fn danger_and_success_are_distinct_per_theme() {
assert_ne!(Theme::DARK.danger, Theme::DARK.success);
assert_ne!(Theme::LIGHT.danger, Theme::LIGHT.success);
}
#[test]
fn dark_background_is_darker_than_light_background() {
let Color::Rgb {
r: dr,
g: dg,
b: db,
} = Theme::DARK.bg
else {
unreachable!()
};
let Color::Rgb {
r: lr,
g: lg,
b: lb,
} = Theme::LIGHT.bg
else {
unreachable!()
};
let dark_luma = u32::from(dr) + u32::from(dg) + u32::from(db);
let light_luma = u32::from(lr) + u32::from(lg) + u32::from(lb);
assert!(dark_luma < light_luma);
}
}