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/// Two hand-maintained copies of a forty-two-name list is how one of them loses
98/// a slot, which for [`ComponentStyleSlots::installed`] and
99/// [`ComponentStyleSlots::unchanged_against`] means an override that is silently
100/// never reported. The `Self { .. }`-less destructure inside each probe still
101/// makes a slot added to the struct a compile error; this makes a slot added
102/// *here* reach both askers at once.
103macro_rules! for_every_slot {
104    ($probe:ident) => {
105        $probe!(
106            button,
107            split_button,
108            splitter,
109            icon_button,
110            toggle,
111            checkbox,
112            radio,
113            radio_tile,
114            slider,
115            text_input,
116            combo_box,
117            menu_item,
118            panel,
119            card,
120            chart,
121            popover,
122            tooltip,
123            scroll_bar,
124            standard_item,
125            tab,
126            dialog,
127            snackbar,
128            toast,
129            banner,
130            badge,
131            progress_bar,
132            link,
133            segmented_control,
134            avatar,
135            calendar,
136            color_picker,
137            spin_box,
138            date_edit,
139            search_field,
140            rich_text_editor,
141            table,
142            list_container,
143            drop_zone,
144            drop_target,
145            grid_view,
146            web_view,
147            text_selection
148        )
149    };
150}
151
152impl ComponentStyleSlots {
153    /// The names of the slots that carry an override, in declaration order.
154    ///
155    /// The direct "what is installed here" question, kept for diagnostics and
156    /// tests. Its original consumer — the target-conformance audit's
157    /// [`unprojected_style_slots`](crate::accessibility::target_audit::unprojected_style_slots)
158    /// — now asks [`unchanged_against`](Self::unchanged_against) instead,
159    /// comparing the theme derived at two densities.
160    ///
161    /// The body destructures `Self` **without** a `..` rest pattern, so adding a
162    /// slot to the struct and forgetting it in `for_every_slot!` is a compile
163    /// error rather than a silently unreported override.
164    pub fn installed(&self) -> Vec<&'static str> {
165        macro_rules! probe {
166            ($($name:ident),* $(,)?) => {{
167                let Self { $($name),* } = self;
168                let mut out = Vec::new();
169                $(if $name.is_some() {
170                    out.push(stringify!($name));
171                })*
172                out
173            }};
174        }
175        for_every_slot!(probe)
176    }
177
178    /// The slots installed here that are **the same object** in `other` — the
179    /// ones a re-derivation left alone.
180    ///
181    /// One consumer, and it is the same one [`installed`](Self::installed)
182    /// has: the target-conformance audit's
183    /// [`unprojected_style_slots`](crate::accessibility::target_audit::unprojected_style_slots),
184    /// which asks a theme for the styles no
185    /// [`DensityProjection`](crate::styles::DensityProjection) rebuilds by
186    /// deriving it at two densities and comparing.
187    ///
188    /// `Rc::ptr_eq`, not any comparison of contents: a Tier-3 style is a trait
189    /// object with no equality of its own, and identity is the right question.
190    /// A projection that re-derives a slot builds a fresh `Rc` for it, so a slot
191    /// still pointing at the same allocation at two densities is one the ladder
192    /// did not reach — whoever installed it.
193    ///
194    /// The body destructures `self` without a `..` rest pattern, for the reason
195    /// [`installed`](Self::installed) does: adding a slot to the struct and
196    /// forgetting it in `for_every_slot!` is a compile error rather than a
197    /// silent omission. Both probes expand through that one roster, so neither
198    /// can be the copy that fell behind.
199    pub fn unchanged_against(&self, other: &Self) -> Vec<&'static str> {
200        fn same<T: ?Sized>(a: &Option<std::rc::Rc<T>>, b: &Option<std::rc::Rc<T>>) -> bool {
201            matches!((a, b), (Some(x), Some(y)) if std::rc::Rc::ptr_eq(x, y))
202        }
203        macro_rules! probe {
204            ($($name:ident),* $(,)?) => {{
205                // The destructure is what makes this exhaustive: a slot added
206                // to the struct and not listed below fails to compile here.
207                // `other` is then read field by field under the same names.
208                let Self { $($name),* } = self;
209                let mut out = Vec::new();
210                $(if same($name, &other.$name) {
211                    out.push(stringify!($name));
212                })*
213                out
214            }};
215        }
216        for_every_slot!(probe)
217    }
218
219    /// Whether no slot carries an override — the state every shipped preset
220    /// that ships raw tokens alone is in.
221    pub fn is_empty(&self) -> bool {
222        self.installed().is_empty()
223    }
224}
225
226impl std::fmt::Debug for ComponentStyleSlots {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        // Hand-rolled because `Rc<dyn FooStyle>` doesn't impl Debug.
229        // Show which slots are populated (Some/None) — the actual
230        // chrome behaviour isn't introspectable.
231        f.debug_struct("ComponentStyleSlots")
232            .field("button", &self.button.is_some())
233            .field("split_button", &self.split_button.is_some())
234            .field("splitter", &self.splitter.is_some())
235            .field("icon_button", &self.icon_button.is_some())
236            .field("toggle", &self.toggle.is_some())
237            .field("checkbox", &self.checkbox.is_some())
238            .field("radio", &self.radio.is_some())
239            .field("radio_tile", &self.radio_tile.is_some())
240            .field("slider", &self.slider.is_some())
241            .field("text_input", &self.text_input.is_some())
242            .field("combo_box", &self.combo_box.is_some())
243            .field("menu_item", &self.menu_item.is_some())
244            .field("panel", &self.panel.is_some())
245            .field("card", &self.card.is_some())
246            .field("chart", &self.chart.is_some())
247            .field("popover", &self.popover.is_some())
248            .field("tooltip", &self.tooltip.is_some())
249            .field("scroll_bar", &self.scroll_bar.is_some())
250            .field("standard_item", &self.standard_item.is_some())
251            .field("tab", &self.tab.is_some())
252            .field("dialog", &self.dialog.is_some())
253            .field("snackbar", &self.snackbar.is_some())
254            .field("toast", &self.toast.is_some())
255            .field("banner", &self.banner.is_some())
256            .field("badge", &self.badge.is_some())
257            .field("progress_bar", &self.progress_bar.is_some())
258            .field("link", &self.link.is_some())
259            .field("segmented_control", &self.segmented_control.is_some())
260            .field("avatar", &self.avatar.is_some())
261            .field("calendar", &self.calendar.is_some())
262            .field("color_picker", &self.color_picker.is_some())
263            .field("spin_box", &self.spin_box.is_some())
264            .field("date_edit", &self.date_edit.is_some())
265            .field("search_field", &self.search_field.is_some())
266            .field("rich_text_editor", &self.rich_text_editor.is_some())
267            .field("table", &self.table.is_some())
268            .field("list_container", &self.list_container.is_some())
269            .field("drop_zone", &self.drop_zone.is_some())
270            .field("drop_target", &self.drop_target.is_some())
271            .field("grid_view", &self.grid_view.is_some())
272            .field("web_view", &self.web_view.is_some())
273            .field("text_selection", &self.text_selection.is_some())
274            .finish()
275    }
276}
277
278impl PartialEq for ComponentStyleSlots {
279    fn eq(&self, other: &Self) -> bool {
280        // Rc trait-object pointer-equality is the only meaningful "are
281        // these the same style" check. Used for theme-equality (mostly
282        // tests + cache keys).
283        fn rc_eq<T: ?Sized>(a: &Option<std::rc::Rc<T>>, b: &Option<std::rc::Rc<T>>) -> bool {
284            match (a, b) {
285                (None, None) => true,
286                (Some(x), Some(y)) => std::rc::Rc::ptr_eq(x, y),
287                _ => false,
288            }
289        }
290        rc_eq(&self.button, &other.button)
291            && rc_eq(&self.radio_tile, &other.radio_tile)
292            && rc_eq(&self.split_button, &other.split_button)
293            && rc_eq(&self.splitter, &other.splitter)
294            && rc_eq(&self.icon_button, &other.icon_button)
295            && rc_eq(&self.toggle, &other.toggle)
296            && rc_eq(&self.checkbox, &other.checkbox)
297            && rc_eq(&self.radio, &other.radio)
298            && rc_eq(&self.slider, &other.slider)
299            && rc_eq(&self.text_input, &other.text_input)
300            && rc_eq(&self.combo_box, &other.combo_box)
301            && rc_eq(&self.menu_item, &other.menu_item)
302            && rc_eq(&self.panel, &other.panel)
303            && rc_eq(&self.card, &other.card)
304            && rc_eq(&self.chart, &other.chart)
305            && rc_eq(&self.popover, &other.popover)
306            && rc_eq(&self.tooltip, &other.tooltip)
307            && rc_eq(&self.scroll_bar, &other.scroll_bar)
308            && rc_eq(&self.standard_item, &other.standard_item)
309            && rc_eq(&self.tab, &other.tab)
310            && rc_eq(&self.dialog, &other.dialog)
311            && rc_eq(&self.snackbar, &other.snackbar)
312            && rc_eq(&self.toast, &other.toast)
313            && rc_eq(&self.banner, &other.banner)
314            && rc_eq(&self.badge, &other.badge)
315            && rc_eq(&self.progress_bar, &other.progress_bar)
316            && rc_eq(&self.link, &other.link)
317            && rc_eq(&self.segmented_control, &other.segmented_control)
318            && rc_eq(&self.avatar, &other.avatar)
319            && rc_eq(&self.calendar, &other.calendar)
320            && rc_eq(&self.color_picker, &other.color_picker)
321            && rc_eq(&self.spin_box, &other.spin_box)
322            && rc_eq(&self.date_edit, &other.date_edit)
323            && rc_eq(&self.search_field, &other.search_field)
324            && rc_eq(&self.rich_text_editor, &other.rich_text_editor)
325            && rc_eq(&self.table, &other.table)
326            && rc_eq(&self.list_container, &other.list_container)
327            && rc_eq(&self.drop_zone, &other.drop_zone)
328            && rc_eq(&self.drop_target, &other.drop_target)
329            && rc_eq(&self.grid_view, &other.grid_view)
330            && rc_eq(&self.web_view, &other.web_view)
331            && rc_eq(&self.text_selection, &other.text_selection)
332    }
333}