use crate::geometry::Insets;
use crate::style::{Color, Font, Rgba, Weight};
#[derive(Clone, Debug, Default)]
pub enum ThemeSource {
#[default]
FollowSystem,
Light,
Dark,
Custom(Theme),
}
impl ThemeSource {
pub fn wants_dark(&self, system_dark: impl Fn() -> bool) -> bool {
match self {
ThemeSource::FollowSystem => system_dark(),
ThemeSource::Light => false,
ThemeSource::Dark => true,
ThemeSource::Custom(_) => false,
}
}
pub fn resolve_theme(&self, system_is_dark: bool) -> Theme {
match self {
ThemeSource::FollowSystem => {
if system_is_dark {
Theme::native_dark()
} else {
Theme::native()
}
}
ThemeSource::Light => Theme::light(),
ThemeSource::Dark => Theme::dark(),
ThemeSource::Custom(theme) => theme.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct Theme {
pub background: Color,
pub label: Color,
pub secondary_label: Color,
pub accent: Color,
pub separator: Color,
pub row_highlight: Color,
pub row_font: Font,
pub header_font: Font,
pub row_height: f32,
pub corner_radius: f32,
pub padding: Insets,
pub column_gap: f32,
}
impl Default for Theme {
fn default() -> Self {
Theme::light()
}
}
const ACCENT_FALLBACK: Rgba = Rgba::opaque(0, 122, 255);
impl Theme {
pub fn light() -> Self {
Theme {
background: Color::rgb(246, 246, 246),
label: Color::rgb(0, 0, 0),
secondary_label: Color::rgb(140, 140, 140),
accent: Color::Accent,
separator: Color::rgb(210, 210, 210),
row_highlight: Color::Accent,
row_font: Font::system(13.0, Weight::Regular),
header_font: Font::system(13.0, Weight::Bold),
row_height: 22.0,
corner_radius: 8.0,
padding: Insets::symmetric(6.0, 5.0),
column_gap: 10.0,
}
}
pub fn dark() -> Self {
Theme {
background: Color::rgb(40, 40, 40),
label: Color::rgb(255, 255, 255),
secondary_label: Color::rgb(150, 150, 150),
separator: Color::rgb(70, 70, 70),
..Theme::light()
}
}
pub fn native() -> Self {
Theme {
background: Color::Rgba(246, 246, 246, 209),
..Theme::light()
}
}
pub fn native_dark() -> Self {
Theme {
background: Color::Rgba(40, 40, 40, 204),
..Theme::dark()
}
}
pub fn resolve(&self, color: Color) -> Rgba {
match color {
Color::Rgba(r, g, b, a) => Rgba::new(r, g, b, a),
Color::Label => literal(self.label, Rgba::BLACK),
Color::SecondaryLabel => literal(self.secondary_label, Rgba::opaque(140, 140, 140)),
Color::Accent => literal(self.accent, ACCENT_FALLBACK),
Color::Separator => literal(self.separator, Rgba::opaque(210, 210, 210)),
Color::SystemRed => Rgba::opaque(255, 59, 48),
Color::SystemOrange => Rgba::opaque(255, 149, 0),
Color::SystemGreen => Rgba::opaque(52, 199, 89),
Color::SystemYellow => Rgba::opaque(255, 204, 0),
}
}
}
fn literal(color: Color, fallback: Rgba) -> Rgba {
match color {
Color::Rgba(r, g, b, a) => Rgba::new(r, g, b, a),
_ => fallback,
}
}
#[derive(Clone, Debug, Default)]
pub struct MenuOptions {
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub theme: ThemeSource,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal_color_passes_through() {
let t = Theme::light();
assert_eq!(t.resolve(Color::Rgba(1, 2, 3, 4)), Rgba::new(1, 2, 3, 4));
assert_eq!(t.resolve(Color::rgb(10, 20, 30)), Rgba::opaque(10, 20, 30));
}
#[test]
fn label_resolves_per_theme() {
assert_eq!(Theme::light().resolve(Color::Label), Rgba::BLACK);
assert_eq!(Theme::dark().resolve(Color::Label), Rgba::WHITE);
}
#[test]
fn separator_differs_between_light_and_dark() {
let light = Theme::light().resolve(Color::Separator);
let dark = Theme::dark().resolve(Color::Separator);
assert_ne!(light, dark);
}
#[test]
fn system_colors_are_fixed_regardless_of_theme() {
assert_eq!(
Theme::light().resolve(Color::SystemRed),
Theme::dark().resolve(Color::SystemRed)
);
assert_eq!(
Theme::light().resolve(Color::SystemRed),
Rgba::opaque(255, 59, 48)
);
}
#[test]
fn accent_does_not_recurse_and_has_a_fallback() {
assert_eq!(Theme::light().resolve(Color::Accent), ACCENT_FALLBACK);
}
#[test]
fn custom_accent_is_honored() {
let mut theme = Theme::light();
theme.accent = Color::rgb(200, 0, 100);
assert_eq!(theme.resolve(Color::Accent), Rgba::opaque(200, 0, 100));
}
fn background_alpha(theme: &Theme) -> u8 {
match theme.background {
Color::Rgba(_, _, _, a) => a,
other => panic!("expected a literal Rgba background, got {other:?}"),
}
}
#[test]
fn light_and_dark_backgrounds_are_opaque() {
assert_eq!(background_alpha(&Theme::light()), 255);
assert_eq!(background_alpha(&Theme::dark()), 255);
}
#[test]
fn native_backgrounds_are_translucent() {
assert!(background_alpha(&Theme::native()) < 255);
assert!(background_alpha(&Theme::native_dark()) < 255);
}
#[test]
fn native_mirrors_light_and_dark_for_non_background_fields() {
let light = Theme::light();
let native = Theme::native();
assert_eq!(native.label, light.label);
assert_eq!(native.accent, light.accent);
assert_eq!(native.separator, light.separator);
assert_eq!(native.row_highlight, light.row_highlight);
assert_eq!(native.row_font, light.row_font);
assert_eq!(native.corner_radius, light.corner_radius);
let dark = Theme::dark();
let native_dark = Theme::native_dark();
assert_eq!(native_dark.label, dark.label);
assert_eq!(native_dark.separator, dark.separator);
}
#[test]
fn follow_system_resolves_to_native_variant() {
let resolved_dark = ThemeSource::FollowSystem.resolve_theme(true);
assert_eq!(
background_alpha(&resolved_dark),
background_alpha(&Theme::native_dark())
);
assert_eq!(resolved_dark.resolve(Color::Label), Rgba::WHITE);
let resolved_light = ThemeSource::FollowSystem.resolve_theme(false);
assert_eq!(
background_alpha(&resolved_light),
background_alpha(&Theme::native())
);
assert_eq!(resolved_light.resolve(Color::Label), Rgba::BLACK);
}
#[test]
fn theme_source_resolves() {
assert_eq!(
ThemeSource::FollowSystem
.resolve_theme(true)
.resolve(Color::Label),
Rgba::WHITE
);
assert_eq!(
ThemeSource::FollowSystem
.resolve_theme(false)
.resolve(Color::Label),
Rgba::BLACK
);
assert_eq!(
ThemeSource::Dark.resolve_theme(false).resolve(Color::Label),
Rgba::WHITE
);
}
}