Skip to main content

oxiui_theme/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3//! COOLJAPAN dark/light themes — Tokyo Night palette.
4//!
5//! # Quick start
6//! ```
7//! let theme = oxiui_theme::cooljapan_default();
8//! assert_eq!(theme.palette().background.0, 26); // Tokyo Night dark #1A1B26
9//! ```
10
11use oxiui_core::{Color, FontSpec, Palette, Theme};
12
13pub mod high_contrast;
14pub use high_contrast::{cooljapan_high_contrast, cooljapan_high_contrast_light};
15
16pub mod anim_tokens;
17pub mod breakpoint;
18pub mod builder;
19pub mod color;
20pub mod compile;
21pub mod gallery;
22pub mod icons;
23pub mod inheritance;
24pub mod lazy_palette;
25pub mod manager;
26pub mod overlay;
27pub mod palette_ext;
28pub mod serial;
29pub mod spec;
30pub mod style_cache;
31pub mod stylesheet;
32pub mod tokens;
33pub mod typography;
34
35pub use anim_tokens::{
36    fade_in, scale_up, slide_in, AnimationKeyframe, AnimationSpec, EasingKind, FillMode,
37    IterationCount, TransitionSpec,
38};
39pub use breakpoint::Breakpoint;
40pub use builder::{ContrastWarning, PaletteBuilder, ValidationResult, WcagLevel};
41pub use compile::CompiledStyleSheet;
42pub use gallery::{
43    make_catppuccin_latte, make_catppuccin_mocha, make_dracula, make_material_dark,
44    make_material_light, make_nord_dark, make_nord_light, make_solarized_dark,
45    make_solarized_light,
46};
47pub use icons::{BuiltinIcons, IconName, IconSet, IconVariant};
48pub use inheritance::resolve as resolve_inheritance;
49pub use lazy_palette::LazyPaletteVariants;
50pub use manager::{ThemeListener, ThemeManager};
51pub use overlay::{overlay, PartialTheme};
52pub use palette_ext::ExtendedPalette;
53pub use serial::{deserialize_theme, serialize_theme, ThemeSnapshot};
54pub use spec::{
55    elevation_shadow, elevation_shadows, elevation_to_shadow, BorderSpec, BorderSpecs, BorderStyle,
56    ShadowSpec,
57};
58pub use style_cache::StyleCache;
59pub use stylesheet::{
60    ComputedStyle, CssValue, ParseDiagnostic, ParseResult, Rule, Selector, SelectorPart,
61    Specificity, StyleSheet,
62};
63pub use tokens::{DesignTokens, RadiusStep, SpacingStep};
64pub use typography::{TextStyleToken, TypographyScale};
65
66/// COOLJAPAN theme implementing [`Theme`]: a colour [`Palette`] plus a [`FontSpec`].
67///
68/// Construct with [`cooljapan_default`], [`dark`], or [`light`], or build a
69/// custom one directly via [`CooljapanTheme::new`].
70#[derive(Clone, Debug)]
71pub struct CooljapanTheme {
72    palette: Palette,
73    font: FontSpec,
74}
75
76impl CooljapanTheme {
77    /// Construct a theme from an explicit palette and font.
78    pub fn new(palette: Palette, font: FontSpec) -> Self {
79        Self { palette, font }
80    }
81}
82
83impl Theme for CooljapanTheme {
84    fn palette(&self) -> &Palette {
85        &self.palette
86    }
87    fn font(&self) -> &FontSpec {
88        &self.font
89    }
90}
91
92/// Extension trait adding design tokens, typography, and the extended palette
93/// to any [`Theme`].
94///
95/// Implemented for every `Theme` via a blanket impl, returning the COOLJAPAN
96/// defaults. Concrete themes may override these methods for custom token sets.
97pub trait ThemeExt: Theme {
98    /// The theme's design-token scale (spacing / radius / elevation / opacity).
99    ///
100    /// Returns by value for flexibility; use [`ThemeExt::design_tokens`] for a
101    /// reference-returning variant backed by a process-lifetime static.
102    fn tokens(&self) -> DesignTokens {
103        DesignTokens::default()
104    }
105
106    /// Returns a reference to the [`DesignTokens`] for this theme.
107    ///
108    /// The default implementation stores the default tokens in a
109    /// `OnceLock`-backed static so that the reference lifetime is `'static`.
110    /// Override this method in concrete theme structs that own a `DesignTokens`
111    /// field to return a reference to that field instead.
112    fn design_tokens(&self) -> &DesignTokens {
113        static DEFAULT: std::sync::OnceLock<DesignTokens> = std::sync::OnceLock::new();
114        DEFAULT.get_or_init(DesignTokens::default)
115    }
116
117    /// The theme's typographic scale (returned by value).
118    ///
119    /// Use [`ThemeExt::typography_ref`] for a reference-returning variant.
120    fn typography(&self) -> TypographyScale {
121        TypographyScale::default()
122    }
123
124    /// Returns a reference to the [`TypographyScale`] for this theme.
125    ///
126    /// Backed by a `OnceLock`-bound static in the blanket impl. Override in
127    /// concrete structs that carry a `TypographyScale` field.
128    fn typography_ref(&self) -> &TypographyScale {
129        static DEFAULT: std::sync::OnceLock<TypographyScale> = std::sync::OnceLock::new();
130        DEFAULT.get_or_init(TypographyScale::default)
131    }
132
133    /// Returns `true` if this theme is designed for high-contrast display.
134    ///
135    /// The default returns `false`. Override in high-contrast theme variants.
136    fn is_high_contrast(&self) -> bool {
137        false
138    }
139
140    /// Returns the effective [`Palette`], with contrast boosted if the OS or
141    /// user has requested high-contrast mode via the `OXIUI_HIGH_CONTRAST`
142    /// environment variable.
143    ///
144    /// When the env var is set to `"1"` or `"true"` (case-insensitive) and the
145    /// theme is not already a high-contrast theme, a simple contrast-boost is
146    /// applied: the background and surface colours are blended slightly toward
147    /// black to deepen dark tones.
148    fn effective_palette(&self) -> Palette {
149        let prefs_high_contrast = os_prefers_high_contrast();
150        if prefs_high_contrast && !self.is_high_contrast() {
151            let mut p = self.palette().clone();
152            p.background = blend_to_black(p.background, 0.1);
153            p.surface = blend_to_black(p.surface, 0.05);
154            p
155        } else {
156            self.palette().clone()
157        }
158    }
159
160    /// The extended semantic palette, derived from [`Theme::palette`].
161    ///
162    /// `dark` is inferred from the background luminance: backgrounds darker than
163    /// mid-grey are treated as dark themes for status-colour selection.
164    fn extended_palette(&self) -> ExtendedPalette {
165        let p = self.palette();
166        let bg = p.background;
167        let luma = color::wcag_luminance(bg.0, bg.1, bg.2);
168        ExtendedPalette::derive(p.clone(), luma < 0.5)
169    }
170}
171
172impl<T: Theme + ?Sized> ThemeExt for T {}
173
174// ── OS accessibility helpers ────────────────────────────────────────────────
175
176/// Returns `true` if the OS (or user preference env var) requests high-contrast
177/// mode.
178///
179/// Reads the `OXIUI_HIGH_CONTRAST` environment variable. Accepted truthy values
180/// are `"1"` and `"true"` (case-insensitive). All other values — including
181/// absent variable — return `false`.
182pub fn os_prefers_high_contrast() -> bool {
183    std::env::var("OXIUI_HIGH_CONTRAST")
184        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
185        .unwrap_or(false)
186}
187
188/// Returns `true` if the OS (or user preference env var) requests reduced
189/// motion (fewer animations / transitions).
190///
191/// Reads the `OXIUI_REDUCED_MOTION` environment variable. Accepted truthy
192/// values are `"1"` and `"true"` (case-insensitive).
193pub fn os_prefers_reduced_motion() -> bool {
194    std::env::var("OXIUI_REDUCED_MOTION")
195        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
196        .unwrap_or(false)
197}
198
199// ── Private palette-blend helpers ───────────────────────────────────────────
200
201/// Blends `color` toward pure black by `factor` (0.0 = no change, 1.0 = black).
202///
203/// Each channel is reduced proportionally: `channel * (1.0 - factor)`.
204/// The alpha channel is preserved unchanged.
205fn blend_to_black(color: oxiui_core::Color, factor: f32) -> oxiui_core::Color {
206    let factor = factor.clamp(0.0, 1.0);
207    let scale = 1.0 - factor;
208    oxiui_core::Color(
209        (color.0 as f32 * scale).round() as u8,
210        (color.1 as f32 * scale).round() as u8,
211        (color.2 as f32 * scale).round() as u8,
212        color.3,
213    )
214}
215
216fn make_dark() -> Box<dyn Theme> {
217    Box::new(CooljapanTheme::new(
218        Palette {
219            background: Color(26, 27, 38, 255), // #1A1B26
220            surface: Color(36, 40, 59, 255),    // #24283B
221            primary: Color(122, 162, 247, 255), // #7AA2F7
222            on_primary: Color(26, 27, 38, 255), // #1A1B26
223            text: Color(192, 202, 245, 255),    // #C0CAF5
224            muted: Color(86, 95, 137, 255),     // #565F89
225        },
226        FontSpec::new("Inter", 14.0, 400),
227    ))
228}
229
230fn make_light() -> Box<dyn Theme> {
231    Box::new(CooljapanTheme::new(
232        Palette {
233            background: Color(216, 218, 228, 255), // #D8DAE4
234            surface: Color(255, 255, 255, 255),    // #FFFFFF
235            primary: Color(68, 100, 200, 255),     // #4464C8
236            on_primary: Color(255, 255, 255, 255),
237            text: Color(30, 35, 60, 255), // #1E233C
238            muted: Color(120, 130, 155, 255),
239        },
240        FontSpec::new("Inter", 14.0, 400),
241    ))
242}
243
244/// Returns the COOLJAPAN default theme (dark / Tokyo Night).
245pub fn cooljapan_default() -> Box<dyn Theme> {
246    make_dark()
247}
248
249/// Returns the COOLJAPAN dark theme (Tokyo Night).
250pub fn dark() -> Box<dyn Theme> {
251    make_dark()
252}
253
254/// Returns the COOLJAPAN light theme.
255pub fn light() -> Box<dyn Theme> {
256    make_light()
257}