native_theme/resolve/context.rs
1//! Resolution-time inputs (font DPI, button order, icon theme) captured
2//! once per theme-build and passed by reference through the pipeline.
3//!
4//! Per `docs/archive/v0.5.7_gaps.md` §G7:
5//! intentionally NO `impl Default` — runtime-detected types must signal
6//! intent at the call site. Use [`ResolutionContext::from_system`] for
7//! production, or [`ResolutionContext::for_tests`] for deterministic
8//! test values.
9
10use std::borrow::Cow;
11
12use crate::model::DialogButtonOrder;
13
14/// Resolution-time inputs captured once per theme-build.
15///
16/// Replaces the `font_dpi: Option<f32>` parameter on
17/// [`ThemeMode::into_resolved`](crate::theme::ThemeMode::into_resolved) and
18/// the corresponding internal `OverlaySource` field.
19///
20/// Accessibility preferences live on
21/// [`SystemTheme`](crate::SystemTheme), NOT here — accessibility is a
22/// render-time concern, not a resolve-time concern
23/// (`docs/archive/v0.5.7_gaps.md` §G7).
24///
25/// # Examples
26///
27/// ```
28/// use native_theme::resolve::ResolutionContext;
29///
30/// // Production: auto-detect from OS.
31/// let ctx = ResolutionContext::from_system();
32/// assert!(ctx.font_dpi > 0.0);
33///
34/// // Tests: deterministic values.
35/// let ctx = ResolutionContext::for_tests();
36/// assert_eq!(ctx.font_dpi, 96.0);
37/// assert!(ctx.icon_theme.is_none());
38/// ```
39#[derive(Clone, Debug)]
40pub struct ResolutionContext {
41 /// Font DPI for pt-to-px conversion.
42 ///
43 /// [`from_system`](Self::from_system) detects it: 72.0 on macOS, 96.0 on
44 /// Windows, and on Linux KDE's `forceFontDPI` (with the `kde` feature),
45 /// then `Xft.dpi`, then xrandr's physical DPI (with `kde` or `portal`),
46 /// then 96.0; 96.0 elsewhere. When the OS theme is read
47 /// ([`SystemTheme::from_system`](crate::SystemTheme::from_system)), the
48 /// reader's own DPI replaces it: KDE's `forceFontDPI` → `Xft.dpi` →
49 /// xrandr → 96.0, GNOME's `Xft.dpi` → xrandr → 96.0, macOS's 72.0 and
50 /// Windows' 96.0.
51 pub font_dpi: f32,
52 /// Dialog button ordering (`PrimaryLeft` on KDE and Windows,
53 /// `PrimaryRight` elsewhere).
54 pub button_order: DialogButtonOrder,
55 /// Runtime-detected icon theme name, used when the preset and
56 /// per-variant `icon_theme` fields are both `None`. Three-tier
57 /// precedence in the pipeline: per-variant → `Theme`-level shared →
58 /// this runtime detection.
59 ///
60 /// [`from_system`](Self::from_system) puts the detected system icon
61 /// theme here, or `None` where detection failed;
62 /// [`system_icon_theme()`](crate::theme::system_icon_theme) gives the
63 /// reason.
64 pub icon_theme: Option<Cow<'static, str>>,
65}
66
67impl ResolutionContext {
68 /// Build the context by auto-detecting from the current OS.
69 ///
70 /// Calls:
71 /// - `crate::detect::system_font_dpi` for `font_dpi` (private)
72 /// - `crate::resolve::inheritance::platform_button_order` for
73 /// `button_order`
74 /// - [`crate::model::icons::system_icon_theme`] for `icon_theme`, which
75 /// is `None` where detection fails
76 #[must_use]
77 pub fn from_system() -> Self {
78 Self::with_detected_icon_theme(crate::model::icons::system_icon_theme())
79 }
80
81 /// [`from_system`](Self::from_system), with `detected` as the outcome
82 /// of the icon theme's detection.
83 pub(crate) fn with_detected_icon_theme(detected: crate::Result<String>) -> Self {
84 Self {
85 font_dpi: crate::detect::system_font_dpi(),
86 button_order: crate::resolve::inheritance::platform_button_order(),
87 icon_theme: detected.ok().map(Cow::Owned),
88 }
89 }
90
91 /// Deterministic values for tests: 96 DPI, `PrimaryRight` button
92 /// order, no detected `icon_theme`.
93 ///
94 /// Tests that need a specific DPI (e.g. 72.0 for Apple point→pixel)
95 /// can construct the struct via a literal:
96 /// `ResolutionContext { font_dpi: 72.0, ..ResolutionContext::for_tests() }`.
97 #[must_use]
98 pub fn for_tests() -> Self {
99 Self {
100 font_dpi: 96.0,
101 button_order: DialogButtonOrder::PrimaryRight,
102 icon_theme: None,
103 }
104 }
105}
106
107// Intentionally NO `impl Default for ResolutionContext`.
108// See module-level doc comment for the signal-intent rationale.