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