theme.rs
| 1 | //! Tri-state color theme: auto (follow the OS), light, dark. |
| 2 | //! |
| 3 | //! The choice is persisted in localStorage and applied as a class on |
| 4 | //! `<html>`; `app.css` resolves the palette variables accordingly. |
| 5 | //! "Auto" means no class at all, so the stylesheet falls back to |
| 6 | //! `prefers-color-scheme`. |
| 7 | |
| 8 | use crate::icons::IconName; |
| 9 | |
| 10 | const STORAGE_KEY: &str = "fb-theme"; |
| 11 | |
| 12 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] |
| 13 | pub enum Theme { |
| 14 | /// Follow the OS `prefers-color-scheme` (the default). |
| 15 | Auto, |
| 16 | Light, |
| 17 | Dark, |
| 18 | } |
| 19 | |
| 20 | impl Theme { |
| 21 | /// The i18n key of this state's display name. |
| 22 | pub const fn name_key(self) -> &'static str { |
| 23 | match self { |
| 24 | Theme::Auto => crate::i18n::k::THEME_AUTO, |
| 25 | Theme::Light => crate::i18n::k::THEME_LIGHT, |
| 26 | Theme::Dark => crate::i18n::k::THEME_DARK, |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | /// The next state in the toggle cycle: auto → light → dark → auto. |
| 31 | pub const fn next(self) -> Theme { |
| 32 | match self { |
| 33 | Theme::Auto => Theme::Light, |
| 34 | Theme::Light => Theme::Dark, |
| 35 | Theme::Dark => Theme::Auto, |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /// Lowercase name, used in tooltips and as the localStorage value. |
| 40 | pub const fn label(self) -> &'static str { |
| 41 | match self { |
| 42 | Theme::Auto => "auto", |
| 43 | Theme::Light => "light", |
| 44 | Theme::Dark => "dark", |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /// The glyph the toggle button shows for this state. |
| 49 | pub const fn icon(self) -> IconName { |
| 50 | match self { |
| 51 | Theme::Auto => IconName::ThemeAuto, |
| 52 | Theme::Light => IconName::ThemeLight, |
| 53 | Theme::Dark => IconName::ThemeDark, |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | /// Persist the choice. |
| 58 | pub fn store(self) { |
| 59 | crate::util::storage_set(STORAGE_KEY, self.label()); |
| 60 | } |
| 61 | |
| 62 | /// The persisted choice; anything missing or invalid is |
| 63 | /// [`Theme::Auto`]. |
| 64 | pub fn load() -> Theme { |
| 65 | match crate::util::storage_get(STORAGE_KEY).as_deref() { |
| 66 | Some("light") => Theme::Light, |
| 67 | Some("dark") => Theme::Dark, |
| 68 | _ => Theme::Auto, |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /// Apply to `<html>`: auto clears the class (the stylesheet falls back |
| 73 | /// to `prefers-color-scheme`), light/dark force a palette. |
| 74 | pub fn apply(self) { |
| 75 | let Some(html) = web_sys::window() |
| 76 | .and_then(|w| w.document()) |
| 77 | .and_then(|d| d.document_element()) |
| 78 | else { |
| 79 | return; |
| 80 | }; |
| 81 | html.set_class_name(match self { |
| 82 | Theme::Auto => "", |
| 83 | t => t.label(), |
| 84 | }); |
| 85 | } |
| 86 | } |
| 87 |