Skip to main content

oxiui_theme/
overlay.rs

1//! Theme composition via [`PartialTheme`] overlays.
2//!
3//! [`overlay`] merges a [`PartialTheme`] (all fields optional) into a base
4//! [`CooljapanTheme`], returning a new theme where every `Some` field in
5//! `overrides` supersedes the corresponding field in `base`.
6//!
7//! ```rust
8//! use oxiui_core::{Color, Theme};
9//! use oxiui_theme::{CooljapanTheme, dark};
10//! use oxiui_theme::overlay::{overlay, PartialTheme};
11//!
12//! let base = oxiui_theme::dark();
13//! let red_bg = PartialTheme { background: Some(Color(200, 0, 0, 255)), ..PartialTheme::default() };
14//! let new_theme = overlay(base.as_ref(), &red_bg);
15//! assert_eq!(new_theme.palette().background, Color(200, 0, 0, 255));
16//! ```
17
18use crate::CooljapanTheme;
19use oxiui_core::{Color, FontSpec, Palette, Theme};
20
21/// A partial palette/font override — every field is optional.
22///
23/// Fields set to `Some(…)` replace the corresponding field in the base theme.
24/// `None` fields keep the base value unchanged.
25#[derive(Clone, Debug, Default)]
26pub struct PartialTheme {
27    /// Override for the background colour.
28    pub background: Option<Color>,
29    /// Override for the surface colour.
30    pub surface: Option<Color>,
31    /// Override for the primary brand colour.
32    pub primary: Option<Color>,
33    /// Override for the on-primary colour.
34    pub on_primary: Option<Color>,
35    /// Override for the primary text colour.
36    pub text_primary: Option<Color>,
37    /// Override for the muted / secondary text colour.
38    pub text_secondary: Option<Color>,
39    /// Override for the font family name.
40    pub font_family: Option<String>,
41    /// Override for the font size in logical pixels.
42    pub font_size: Option<f32>,
43    /// Override for the font weight.
44    pub font_weight: Option<u16>,
45}
46
47/// Merge `overrides` into `base`, returning a new [`CooljapanTheme`].
48///
49/// Any `Some` field in `overrides` replaces the corresponding field in `base`.
50/// All `None` fields keep the value from `base`.
51pub fn overlay(base: &dyn Theme, overrides: &PartialTheme) -> CooljapanTheme {
52    let bp = base.palette();
53    let bf = base.font();
54
55    let palette = Palette {
56        background: overrides.background.unwrap_or(bp.background),
57        surface: overrides.surface.unwrap_or(bp.surface),
58        primary: overrides.primary.unwrap_or(bp.primary),
59        on_primary: overrides.on_primary.unwrap_or(bp.on_primary),
60        text: overrides.text_primary.unwrap_or(bp.text),
61        muted: overrides.text_secondary.unwrap_or(bp.muted),
62    };
63    let font = FontSpec::new(
64        overrides.font_family.as_deref().unwrap_or(&bf.family),
65        overrides.font_size.unwrap_or(bf.size),
66        overrides.font_weight.unwrap_or(bf.weight),
67    );
68    CooljapanTheme::new(palette, font)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use oxiui_core::Theme;
75
76    fn base_theme() -> Box<dyn Theme> {
77        crate::dark()
78    }
79
80    #[test]
81    fn overlay_override_precedence() {
82        let base = base_theme();
83        let red = Color(200, 0, 0, 255);
84        let overrides = PartialTheme {
85            background: Some(red),
86            ..PartialTheme::default()
87        };
88        let result = overlay(base.as_ref(), &overrides);
89        assert_eq!(
90            result.palette().background,
91            red,
92            "overlay must apply the override background"
93        );
94    }
95
96    #[test]
97    fn overlay_none_fields_keep_base() {
98        let base = base_theme();
99        let original_bg = base.palette().background;
100        let overrides = PartialTheme::default(); // all None
101        let result = overlay(base.as_ref(), &overrides);
102        assert_eq!(
103            result.palette().background,
104            original_bg,
105            "empty overlay must preserve base"
106        );
107        assert_eq!(result.palette().surface, base.palette().surface);
108        assert_eq!(result.palette().text, base.palette().text);
109    }
110
111    #[test]
112    fn overlay_multiple_fields() {
113        let base = base_theme();
114        let new_text = Color(0, 255, 0, 255);
115        let new_primary = Color(0, 0, 255, 255);
116        let overrides = PartialTheme {
117            text_primary: Some(new_text),
118            primary: Some(new_primary),
119            ..PartialTheme::default()
120        };
121        let result = overlay(base.as_ref(), &overrides);
122        assert_eq!(result.palette().text, new_text);
123        assert_eq!(result.palette().primary, new_primary);
124        // Unset fields must still come from base.
125        assert_eq!(result.palette().background, base.palette().background);
126    }
127}