Skip to main content

blockworx_paint/
scheme.rs

1//! What the user picks about appearance: the colour scheme family, the
2//! light/dark mode, and the bundled typeface. Pure choices β€” how they are
3//! persisted, and how the typeface is installed with the host, belong to the
4//! shell.
5
6use serde::{Deserialize, Serialize};
7
8use crate::palette::{Luminance, Palette};
9
10/// A color scheme family. Every scheme has both a dark and a light variant, so
11/// the [`Mode`] chooses which one is shown. Values are the canonical
12/// [tinted-theming](https://github.com/tinted-theming/schemes) base16 palettes.
13#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
14pub enum Scheme {
15    #[default]
16    Catppuccin,
17    Gruvbox,
18    RosePine,
19    Everforest,
20    Ayu,
21    TokyoNight,
22}
23
24impl Scheme {
25    pub const ALL: [Scheme; 6] = [
26        Scheme::Catppuccin,
27        Scheme::Gruvbox,
28        Scheme::RosePine,
29        Scheme::Everforest,
30        Scheme::Ayu,
31        Scheme::TokyoNight,
32    ];
33
34    pub fn display_name(self) -> &'static str {
35        match self {
36            Scheme::Catppuccin => "Catppuccin",
37            Scheme::Gruvbox => "Gruvbox",
38            Scheme::RosePine => "RosΓ© Pine",
39            Scheme::Everforest => "Everforest",
40            Scheme::Ayu => "Ayu",
41            Scheme::TokyoNight => "Tokyo Night",
42        }
43    }
44
45    /// The palette for this scheme at the requested end of the light/dark axis.
46    pub fn palette(self, luminance: Luminance) -> Palette {
47        match (self, luminance.is_dark()) {
48            (Scheme::Catppuccin, true) => Palette::catppuccin_mocha(),
49            (Scheme::Catppuccin, false) => Palette::catppuccin_latte(),
50            (Scheme::Gruvbox, true) => Palette::gruvbox_dark(),
51            (Scheme::Gruvbox, false) => Palette::gruvbox_light(),
52            (Scheme::RosePine, true) => Palette::rose_pine(),
53            (Scheme::RosePine, false) => Palette::rose_pine_dawn(),
54            (Scheme::Everforest, true) => Palette::everforest_dark(),
55            (Scheme::Everforest, false) => Palette::everforest_light(),
56            (Scheme::Ayu, true) => Palette::ayu_dark(),
57            (Scheme::Ayu, false) => Palette::ayu_light(),
58            (Scheme::TokyoNight, true) => Palette::tokyo_night_dark(),
59            (Scheme::TokyoNight, false) => Palette::tokyo_night_light(),
60        }
61    }
62}
63
64/// Whether to use a scheme's light or dark variant. `System` follows the OS
65/// light/dark preference. The glyphs are the ones a theme switcher conventionally
66/// shows (β˜€ / πŸŒ™ / πŸ’»).
67#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
68pub enum Mode {
69    Light,
70    #[default]
71    Dark,
72    System,
73}
74
75impl Mode {
76    pub const ALL: [Mode; 3] = [Mode::Light, Mode::Dark, Mode::System];
77
78    pub fn display_name(self) -> &'static str {
79        match self {
80            Mode::Light => "Light",
81            Mode::Dark => "Dark",
82            Mode::System => "System",
83        }
84    }
85
86    pub fn glyph(self) -> &'static str {
87        match self {
88            Mode::Light => "β˜€",
89            Mode::Dark => "πŸŒ™",
90            Mode::System => "πŸ’»",
91        }
92    }
93
94    /// Whether this mode resolves to a dark variant. `System` defers to the OS
95    /// preference `system_dark` (falling back to dark when it is unknown).
96    pub fn is_dark(self, system_dark: Option<bool>) -> bool {
97        match self {
98            Mode::Light => false,
99            Mode::Dark => true,
100            Mode::System => system_dark.unwrap_or(true),
101        }
102    }
103}
104
105/// The four bundled UI fonts. Each maps to an embedded face (see
106/// [`FontChoice::bytes`]) and a family name used both to install it with the
107/// host and to preview the menu entry in its own typeface.
108#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
109pub enum FontChoice {
110    #[default]
111    Basic,
112    Formal,
113    Monospace,
114    Sketchy,
115}
116
117impl FontChoice {
118    pub const ALL: [FontChoice; 4] = [
119        FontChoice::Basic,
120        FontChoice::Formal,
121        FontChoice::Monospace,
122        FontChoice::Sketchy,
123    ];
124
125    pub fn display_name(self) -> &'static str {
126        match self {
127            FontChoice::Basic => "Basic",
128            FontChoice::Formal => "Formal",
129            FontChoice::Monospace => "Monospace",
130            FontChoice::Sketchy => "Sketchy",
131        }
132    }
133
134    /// The font-family name this face is registered under with the host. Also the
135    /// key its bytes are stored under.
136    pub fn family_name(self) -> &'static str {
137        match self {
138            FontChoice::Basic => "Roboto",
139            FontChoice::Formal => "Forum",
140            FontChoice::Monospace => "Iosevka",
141            FontChoice::Sketchy => "Excalifont",
142        }
143    }
144
145    /// The embedded font bytes, for the host's font install and the SVG glyph
146    /// tracer.
147    pub fn bytes(self) -> &'static [u8] {
148        match self {
149            FontChoice::Basic => crate::font::ROBOTO,
150            FontChoice::Formal => crate::font::FORUM,
151            FontChoice::Monospace => crate::font::IOSEVKA,
152            FontChoice::Sketchy => crate::font::EXCALIFONT,
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn every_scheme_has_a_dark_and_a_light_variant() {
163        for scheme in Scheme::ALL {
164            assert!(scheme.palette(Luminance::Dark).is_dark(), "{scheme:?} dark");
165            assert!(
166                !scheme.palette(Luminance::Light).is_dark(),
167                "{scheme:?} light"
168            );
169        }
170    }
171
172    #[test]
173    fn mode_resolves_dark_following_the_os_for_system() {
174        assert!(!Mode::Light.is_dark(Some(true)));
175        assert!(Mode::Dark.is_dark(Some(false)));
176        // System follows the OS, defaulting to dark when unknown.
177        assert!(Mode::System.is_dark(Some(true)));
178        assert!(!Mode::System.is_dark(Some(false)));
179        assert!(Mode::System.is_dark(None));
180    }
181}