Skip to main content

native_theme/resolve/
mod.rs

1// Resolution engine: resolve() fills inheritance rules, validate() produces ResolvedThemeVariant.
2//
3// Split into submodules:
4// - inheritance: Phase 1-5 resolution rules (fill None fields from defaults/other widgets)
5// - validate: Field extraction, range checks, ResolvedThemeVariant construction
6
7mod inheritance;
8pub(crate) mod validate;
9pub(crate) mod validate_helpers;
10
11use crate::model::ThemeVariant;
12use crate::model::resolved::ResolvedThemeVariant;
13
14impl ThemeVariant {
15    /// Apply all ~91 inheritance rules in 5-phase order (pure data transform).
16    ///
17    /// After calling resolve(), most Option fields that were None will be filled
18    /// from defaults or related widget fields. Calling resolve() twice produces
19    /// the same result (idempotent).
20    ///
21    /// This method is a pure data transform: it does not perform any OS detection
22    /// or I/O. For full resolution including platform defaults (icon theme from
23    /// the system), use [`resolve_all()`](Self::resolve_all).
24    ///
25    /// # Phases
26    ///
27    /// 1. **Defaults internal chains** -- accent derives selection, focus_ring_color;
28    ///    selection derives selection_inactive.
29    /// 2. **Safety nets** -- platform-divergent fields get a reasonable fallback.
30    /// 3. **Widget-from-defaults** -- colors, geometry, fonts, text scale entries
31    ///    all inherit from defaults.
32    /// 4. **Widget-to-widget** -- inactive title bar fields fall back to active.
33    /// 5. **Icon set** -- fills icon_set from the compile-time system default.
34    pub fn resolve(&mut self) {
35        self.resolve_defaults_internal();
36        self.resolve_safety_nets();
37        self.resolve_widgets_from_defaults();
38        self.resolve_widget_to_widget();
39
40        // Phase 5: icon_set fallback — fill from system default if not set
41        if self.icon_set.is_none() {
42            self.icon_set = Some(crate::model::icons::system_icon_set());
43        }
44    }
45
46    /// Fill platform-detected defaults that require OS interaction.
47    ///
48    /// Currently fills `icon_theme` from the system icon theme if not already set.
49    /// This is separated from [`resolve()`](Self::resolve) because it performs
50    /// runtime OS detection (reading desktop environment settings), unlike the
51    /// pure inheritance rules in resolve().
52    pub fn resolve_platform_defaults(&mut self) {
53        if self.icon_theme.is_none() {
54            self.icon_theme = Some(crate::model::icons::system_icon_theme().to_string());
55        }
56    }
57
58    /// Apply all inheritance rules and platform defaults.
59    ///
60    /// Convenience method that calls [`resolve()`](Self::resolve) followed by
61    /// [`resolve_platform_defaults()`](Self::resolve_platform_defaults).
62    ///
63    /// **Note:** this does *not* auto-detect `font_dpi`. If `font_dpi` is
64    /// `None`, validation will use `DEFAULT_FONT_DPI` (96.0) for pt-to-px
65    /// conversion. To get automatic DPI detection, use
66    /// [`into_resolved()`](Self::into_resolved) or set `font_dpi` before
67    /// calling this method.
68    pub fn resolve_all(&mut self) {
69        self.resolve();
70        self.resolve_platform_defaults();
71    }
72
73    /// Resolve all inheritance rules and validate in one step.
74    ///
75    /// This is the recommended way to convert a `ThemeVariant` into a
76    /// [`ResolvedThemeVariant`]. It calls [`resolve_all()`](Self::resolve_all)
77    /// followed by [`validate()`](Self::validate), ensuring no fields are left
78    /// unresolved.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`crate::Error::Resolution`] if any fields remain `None` after
83    /// resolution (e.g., when accent color is missing and cannot be derived).
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use native_theme::ThemeSpec;
89    ///
90    /// let theme = ThemeSpec::preset("dracula").unwrap();
91    /// let variant = theme.dark.unwrap();
92    /// let resolved = variant.into_resolved().unwrap();
93    /// // All fields are now guaranteed populated
94    /// let accent = resolved.defaults.accent_color;
95    /// ```
96    #[must_use = "this returns the resolved theme and consumes self"]
97    pub fn into_resolved(mut self) -> crate::Result<ResolvedThemeVariant> {
98        // Auto-detect font_dpi from the OS when not already set (e.g. by an
99        // OS reader or TOML overlay). This ensures standalone preset loading
100        // applies the correct pt-to-px conversion for the current display.
101        // Done here (not in resolve_all) to preserve resolve_all idempotency.
102        if self.defaults.font_dpi.is_none() {
103            self.defaults.font_dpi = Some(crate::detect::system_font_dpi());
104        }
105        self.resolve_all();
106        self.validate()
107    }
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used, clippy::expect_used)]
112mod tests;