Skip to main content

teksilo_core/styles/
component_style_slots.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Typed `Rc<dyn FooStyle>` slot bag — the theme-wide override channel
5//! for the four-tier styling system.
6//!
7//! Each themable widget reads its slot like:
8//!
9//! ```ignore
10//! let style = self.style_override
11//!     .clone()
12//!     .or_else(|| ctx.theme().style_slots.button.clone())
13//!     .unwrap_or_else(|| Rc::new(RecipeButtonStyle::default()));
14//! ```
15//!
16//! Per-call `.style(...)` overrides win first; theme-wide
17//! `style_slots.button = Some(...)` wins second; the widget's local
18//! `Recipe*Style` default is the fallback.
19//!
20//! `Option` per slot rather than a populated default because the
21//! `Recipe*Style` types live in `teksilo-widgets` and can't be imported by
22//! `teksilo-core` (cycle). Apps that want theme-wide custom styles
23//! install them explicitly:
24//!
25//! ```ignore
26//! let mut theme = intui::light();
27//! theme.style_slots.button = Some(Rc::new(MyGlassButton));
28//! ```
29
30use crate::styles::{
31    SharedAvatarStyle, SharedBadgeStyle, SharedBannerStyle, SharedButtonStyle, SharedCalendarStyle,
32    SharedCardStyle, SharedChartStyle, SharedCheckboxStyle, SharedColorPickerStyle,
33    SharedComboBoxStyle, SharedDateEditStyle, SharedDialogStyle, SharedDropTargetStyle,
34    SharedDropZoneStyle, SharedGridViewStyle, SharedIconButtonStyle, SharedLinkStyle,
35    SharedListContainerStyle, SharedMenuItemStyle, SharedPanelStyle, SharedPopoverStyle,
36    SharedProgressBarStyle, SharedRadioStyle, SharedRadioTileStyle, SharedRichTextEditorStyle,
37    SharedScrollBarStyle, SharedSearchFieldStyle, SharedSegmentedControlStyle, SharedSliderStyle,
38    SharedSnackbarStyle, SharedSpinBoxStyle, SharedSplitButtonStyle, SharedSplitterStyle,
39    SharedStandardItemStyle, SharedTabStyle, SharedTableStyle, SharedTextInputStyle,
40    SharedTextSelectionStyle, SharedToastStyle, SharedToggleStyle, SharedTooltipStyle,
41    SharedWebViewStyle,
42};
43
44/// Typed slot bag living on [`crate::styles::Theme`]. One slot per
45/// themable widget. `None` means "use the widget's local default
46/// `Recipe*Style`"; `Some(rc)` installs the override theme-wide.
47#[derive(Default, Clone)]
48pub struct ComponentStyleSlots {
49    pub button: Option<SharedButtonStyle>,
50    pub split_button: Option<SharedSplitButtonStyle>,
51    pub splitter: Option<SharedSplitterStyle>,
52    pub icon_button: Option<SharedIconButtonStyle>,
53    pub toggle: Option<SharedToggleStyle>,
54    pub checkbox: Option<SharedCheckboxStyle>,
55    pub radio: Option<SharedRadioStyle>,
56    pub radio_tile: Option<SharedRadioTileStyle>,
57    pub slider: Option<SharedSliderStyle>,
58    pub text_input: Option<SharedTextInputStyle>,
59    pub combo_box: Option<SharedComboBoxStyle>,
60    pub menu_item: Option<SharedMenuItemStyle>,
61    pub panel: Option<SharedPanelStyle>,
62    pub card: Option<SharedCardStyle>,
63    pub chart: Option<SharedChartStyle>,
64    pub popover: Option<SharedPopoverStyle>,
65    pub tooltip: Option<SharedTooltipStyle>,
66    pub scroll_bar: Option<SharedScrollBarStyle>,
67    pub standard_item: Option<SharedStandardItemStyle>,
68    pub tab: Option<SharedTabStyle>,
69    pub dialog: Option<SharedDialogStyle>,
70    pub snackbar: Option<SharedSnackbarStyle>,
71    pub toast: Option<SharedToastStyle>,
72    pub banner: Option<SharedBannerStyle>,
73    pub badge: Option<SharedBadgeStyle>,
74    pub progress_bar: Option<SharedProgressBarStyle>,
75    pub link: Option<SharedLinkStyle>,
76    pub segmented_control: Option<SharedSegmentedControlStyle>,
77    pub avatar: Option<SharedAvatarStyle>,
78    pub calendar: Option<SharedCalendarStyle>,
79    pub color_picker: Option<SharedColorPickerStyle>,
80    pub spin_box: Option<SharedSpinBoxStyle>,
81    pub date_edit: Option<SharedDateEditStyle>,
82    pub search_field: Option<SharedSearchFieldStyle>,
83    pub rich_text_editor: Option<SharedRichTextEditorStyle>,
84    pub table: Option<SharedTableStyle>,
85    pub list_container: Option<SharedListContainerStyle>,
86    pub drop_zone: Option<SharedDropZoneStyle>,
87    pub drop_target: Option<SharedDropTargetStyle>,
88    pub grid_view: Option<SharedGridViewStyle>,
89    pub web_view: Option<SharedWebViewStyle>,
90    /// Touch text-selection chrome — the selection handles and the magnifier.
91    /// See [`TextSelectionStyle`](crate::styles::TextSelectionStyle).
92    pub text_selection: Option<SharedTextSelectionStyle>,
93}
94
95/// Every slot's name, written **once**, expanded through by each probe below.
96///
97/// [`ComponentStyleSlots::NAMES`], [`ComponentStyleSlots::installed`],
98/// [`ComponentStyleSlots::unchanged_against`] and the `Debug` and `PartialEq`
99/// impls all expand this one roster, so none of them can be the copy that fell
100/// behind: a hand-written list per asker is how one of them loses a slot, which
101/// for `installed` means an override that is silently never reported. Every
102/// probe except `NAMES` also destructures `Self` without a `..` rest pattern, so
103/// a slot added to the struct and not to this roster is a compile error.
104macro_rules! for_every_slot {
105    ($probe:ident) => {
106        $probe!(
107            button,
108            split_button,
109            splitter,
110            icon_button,
111            toggle,
112            checkbox,
113            radio,
114            radio_tile,
115            slider,
116            text_input,
117            combo_box,
118            menu_item,
119            panel,
120            card,
121            chart,
122            popover,
123            tooltip,
124            scroll_bar,
125            standard_item,
126            tab,
127            dialog,
128            snackbar,
129            toast,
130            banner,
131            badge,
132            progress_bar,
133            link,
134            segmented_control,
135            avatar,
136            calendar,
137            color_picker,
138            spin_box,
139            date_edit,
140            search_field,
141            rich_text_editor,
142            table,
143            list_container,
144            drop_zone,
145            drop_target,
146            grid_view,
147            web_view,
148            text_selection
149        )
150    };
151}
152
153impl ComponentStyleSlots {
154    /// Every slot's name, in declaration order: the roster that
155    /// [`installed`](Self::installed) and
156    /// [`unchanged_against`](Self::unchanged_against) answer from.
157    ///
158    /// For a test that wants to say which slots a preset fills and which it
159    /// deliberately leaves to the widget's default, as set equality against
160    /// this list rather than against a hand-copied one.
161    pub const NAMES: &'static [&'static str] = {
162        macro_rules! probe {
163            ($($name:ident),* $(,)?) => {
164                &[$(stringify!($name)),*]
165            };
166        }
167        for_every_slot!(probe)
168    };
169
170    /// The names of the slots that carry an override, in declaration order.
171    ///
172    /// The direct "what is installed here" question, kept for diagnostics and
173    /// tests. Its original consumer — the target-conformance audit's
174    /// [`unprojected_style_slots`](crate::accessibility::target_audit::unprojected_style_slots)
175    /// — now asks [`unchanged_against`](Self::unchanged_against) instead,
176    /// comparing the theme derived at two densities.
177    ///
178    /// The body destructures `Self` **without** a `..` rest pattern, so adding a
179    /// slot to the struct and forgetting it in `for_every_slot!` is a compile
180    /// error rather than a silently unreported override.
181    pub fn installed(&self) -> Vec<&'static str> {
182        macro_rules! probe {
183            ($($name:ident),* $(,)?) => {{
184                let Self { $($name),* } = self;
185                let mut out = Vec::new();
186                $(if $name.is_some() {
187                    out.push(stringify!($name));
188                })*
189                out
190            }};
191        }
192        for_every_slot!(probe)
193    }
194
195    /// The slots installed here that are **the same object** in `other` — the
196    /// ones a re-derivation left alone.
197    ///
198    /// One consumer, and it is the same one [`installed`](Self::installed)
199    /// has: the target-conformance audit's
200    /// [`unprojected_style_slots`](crate::accessibility::target_audit::unprojected_style_slots),
201    /// which asks a theme for the styles no
202    /// [`DensityProjection`](crate::styles::DensityProjection) rebuilds by
203    /// deriving it at two densities and comparing.
204    ///
205    /// `Rc::ptr_eq`, not any comparison of contents: a Tier-3 style is a trait
206    /// object with no equality of its own, and identity is the right question.
207    /// A projection that re-derives a slot builds a fresh `Rc` for it, so a slot
208    /// still pointing at the same allocation at two densities is one the ladder
209    /// did not reach — whoever installed it.
210    ///
211    /// The body destructures `self` without a `..` rest pattern, for the reason
212    /// [`installed`](Self::installed) does: adding a slot to the struct and
213    /// forgetting it in `for_every_slot!` is a compile error rather than a
214    /// silent omission. Both probes expand through that one roster, so neither
215    /// can be the copy that fell behind.
216    pub fn unchanged_against(&self, other: &Self) -> Vec<&'static str> {
217        fn same<T: ?Sized>(a: &Option<std::rc::Rc<T>>, b: &Option<std::rc::Rc<T>>) -> bool {
218            matches!((a, b), (Some(x), Some(y)) if std::rc::Rc::ptr_eq(x, y))
219        }
220        macro_rules! probe {
221            ($($name:ident),* $(,)?) => {{
222                // The destructure is what makes this exhaustive: a slot added
223                // to the struct and not listed below fails to compile here.
224                // `other` is then read field by field under the same names.
225                let Self { $($name),* } = self;
226                let mut out = Vec::new();
227                $(if same($name, &other.$name) {
228                    out.push(stringify!($name));
229                })*
230                out
231            }};
232        }
233        for_every_slot!(probe)
234    }
235
236    /// Whether no slot carries an override — the state every shipped preset
237    /// that ships raw tokens alone is in.
238    pub fn is_empty(&self) -> bool {
239        self.installed().is_empty()
240    }
241}
242
243impl std::fmt::Debug for ComponentStyleSlots {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        // Hand-rolled because `Rc<dyn FooStyle>` doesn't impl Debug.
246        // Show which slots are populated (Some/None): the actual
247        // chrome behaviour isn't introspectable.
248        macro_rules! probe {
249            ($($name:ident),* $(,)?) => {{
250                // No `..`: a slot missing from the roster fails to compile.
251                let Self { $($name),* } = self;
252                f.debug_struct("ComponentStyleSlots")
253                    $(.field(stringify!($name), &$name.is_some()))*
254                    .finish()
255            }};
256        }
257        for_every_slot!(probe)
258    }
259}
260
261impl PartialEq for ComponentStyleSlots {
262    fn eq(&self, other: &Self) -> bool {
263        // Rc trait-object pointer-equality is the only meaningful "are
264        // these the same style" check. Used for theme-equality (mostly
265        // tests + cache keys).
266        fn rc_eq<T: ?Sized>(a: &Option<std::rc::Rc<T>>, b: &Option<std::rc::Rc<T>>) -> bool {
267            match (a, b) {
268                (None, None) => true,
269                (Some(x), Some(y)) => std::rc::Rc::ptr_eq(x, y),
270                _ => false,
271            }
272        }
273        macro_rules! probe {
274            ($($name:ident),* $(,)?) => {{
275                // No `..`: a slot missing from the roster fails to compile.
276                let Self { $($name),* } = self;
277                true $(&& rc_eq($name, &other.$name))*
278            }};
279        }
280        for_every_slot!(probe)
281    }
282}
283
284// `styles.rs` declares this module with `#[path]`, so a child module resolves
285// from `styles/`, not from a directory named after this file.
286#[cfg(test)]
287#[path = "component_style_slots/tests.rs"]
288mod tests;