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