Skip to main content

frus_widgets/
cupertinotheme.rs

1//! The theme of the Apple-style widgets: their primary colour, their bars' and pages'
2//! backgrounds, and their type (milestone 628).
3//!
4//! It is **part of the main theme**, as in the reference: unless something says otherwise,
5//! the Apple-style widgets take their primary colour, the colour on it and the page
6//! background from the main [`Theme`], and only what the main theme has no word for — a
7//! bar's translucent background, the type — from Apple's own defaults. An application
8//! changes any of it with [`Theme::with_cupertino_override_theme`]; a subtree replaces all of
9//! it with [`CupertinoTheme::around`].
10
11use frus_core::{Color, FontWeight, TextDecoration, TextStyle};
12
13use crate::media::{Brightness, MediaQuery};
14use crate::{CupertinoColors, CupertinoDynamicColor, Theme};
15
16/// The type of the Apple-style widgets **as asked for**: each style an application sets, or
17/// `None` for the default.
18///
19/// The defaults are the reference's (`cupertino/text_theme.dart`): body text at 17 px, a
20/// navigation bar's title semibold at 17, its large title bold at 34, a tab's label at 10,
21/// and the letter spacing Apple's guidelines give each. Their colours adapt: the label colour
22/// for text, the primary colour for actions, the inactive grey for tab labels.
23///
24/// The reference names Apple's system faces, which exist only on Apple's platforms; the
25/// defaults here name no family, so they are set in the application's own.
26#[derive(Clone, Copy, Debug, Default, PartialEq)]
27pub struct CupertinoTextThemeData {
28    /// The colour of the actions' text. Unset, the system blue — or, for the defaults the
29    /// theme makes when no text theme is given, the theme's primary colour.
30    pub primary_color: Option<CupertinoDynamicColor>,
31    /// Body text.
32    pub text_style: Option<TextStyle>,
33    /// The text of a button.
34    pub action_text_style: Option<TextStyle>,
35    /// The text of a small button.
36    pub action_small_text_style: Option<TextStyle>,
37    /// A tab bar's labels.
38    pub tab_label_text_style: Option<TextStyle>,
39    /// A navigation bar's title.
40    pub nav_title_text_style: Option<TextStyle>,
41    /// A navigation bar's large title.
42    pub nav_large_title_text_style: Option<TextStyle>,
43    /// A navigation bar's buttons.
44    pub nav_action_text_style: Option<TextStyle>,
45    /// A picker's items.
46    pub picker_text_style: Option<TextStyle>,
47    /// A date and time picker's items.
48    pub date_time_picker_text_style: Option<TextStyle>,
49}
50
51/// The type of the Apple-style widgets **resolved**: every style, its colour picked for where
52/// it is used. [`CupertinoTheme::of`] makes one.
53#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct CupertinoTextTheme {
55    /// Body text.
56    pub text_style: TextStyle,
57    /// The text of a button.
58    pub action_text_style: TextStyle,
59    /// The text of a small button.
60    pub action_small_text_style: TextStyle,
61    /// A tab bar's labels.
62    pub tab_label_text_style: TextStyle,
63    /// A navigation bar's title.
64    pub nav_title_text_style: TextStyle,
65    /// A navigation bar's large title.
66    pub nav_large_title_text_style: TextStyle,
67    /// A navigation bar's buttons.
68    pub nav_action_text_style: TextStyle,
69    /// A picker's items.
70    pub picker_text_style: TextStyle,
71    /// A date and time picker's items.
72    pub date_time_picker_text_style: TextStyle,
73}
74
75/// Body text: 17 px, −0.41 apart, undecorated (`text_theme.dart:19`).
76const TEXT: TextStyle = TextStyle::new(17.0)
77    .letter_spacing(-0.41)
78    .decoration(TextDecoration::NONE);
79/// A button's text: as body text, in the primary colour (`text_theme.dart:34`).
80const ACTION: TextStyle = TEXT;
81/// A small button's text: 15 px, −0.23 apart (`text_theme.dart:49`).
82const ACTION_SMALL: TextStyle = TextStyle::new(15.0)
83    .letter_spacing(-0.23)
84    .decoration(TextDecoration::NONE);
85/// A tab's label: 10 px, medium, −0.24 apart (`text_theme.dart:63`).
86const TAB_LABEL: TextStyle = TextStyle::new(10.0)
87    .weight(FontWeight::Medium)
88    .letter_spacing(-0.24);
89/// A navigation bar's title: 17 px, semibold, −0.41 apart (`text_theme.dart:72`).
90const NAV_TITLE: TextStyle = TextStyle::new(17.0)
91    .weight(FontWeight::SemiBold)
92    .letter_spacing(-0.41);
93/// A navigation bar's large title: 34 px, bold, 0.38 apart (`text_theme.dart:81`).
94const NAV_LARGE_TITLE: TextStyle = TextStyle::new(34.0)
95    .weight(FontWeight::Bold)
96    .letter_spacing(0.38);
97/// A picker's items: 21 px, regular, −0.6 apart (`text_theme.dart:101`).
98const PICKER: TextStyle = TextStyle::new(21.0)
99    .weight(FontWeight::Regular)
100    .letter_spacing(-0.6);
101/// A date and time picker's items: 21 px, regular, 0.4 apart (`text_theme.dart:116`).
102const DATE_TIME_PICKER: TextStyle = TextStyle::new(21.0)
103    .weight(FontWeight::Regular)
104    .letter_spacing(0.4);
105
106impl CupertinoTextThemeData {
107    /// Nothing asked for: every style the default.
108    pub const NONE: Self = Self {
109        primary_color: None,
110        text_style: None,
111        action_text_style: None,
112        action_small_text_style: None,
113        tab_label_text_style: None,
114        nav_title_text_style: None,
115        nav_large_title_text_style: None,
116        nav_action_text_style: None,
117        picker_text_style: None,
118        date_time_picker_text_style: None,
119    };
120
121    /// The same, its actions in `color`.
122    #[must_use]
123    pub fn primary_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
124        self.primary_color = Some(color.into());
125        self
126    }
127
128    /// The same, its body text in `style`.
129    #[must_use]
130    pub const fn text_style(mut self, style: TextStyle) -> Self {
131        self.text_style = Some(style);
132        self
133    }
134
135    /// The same, its buttons' text in `style`.
136    #[must_use]
137    pub const fn action_text_style(mut self, style: TextStyle) -> Self {
138        self.action_text_style = Some(style);
139        self
140    }
141
142    /// The same, its navigation bar's title in `style`.
143    #[must_use]
144    pub const fn nav_title_text_style(mut self, style: TextStyle) -> Self {
145        self.nav_title_text_style = Some(style);
146        self
147    }
148
149    /// The same, its navigation bar's large title in `style`.
150    #[must_use]
151    pub const fn nav_large_title_text_style(mut self, style: TextStyle) -> Self {
152        self.nav_large_title_text_style = Some(style);
153        self
154    }
155
156    /// **Every style, for where it is used**: a style asked for as it was asked, a default in
157    /// the colour `theme` resolves its role to.
158    pub fn resolve(&self, theme: &Theme) -> CupertinoTextTheme {
159        let label = CupertinoColors::LABEL.resolve(theme);
160        let inactive = CupertinoColors::INACTIVE_GRAY.resolve(theme);
161        let primary = self
162            .primary_color
163            .unwrap_or(CupertinoColors::SYSTEM_BLUE)
164            .resolve(theme);
165        let action = self.action_text_style.unwrap_or(ACTION.color(primary));
166        CupertinoTextTheme {
167            text_style: self.text_style.unwrap_or(TEXT.color(label)),
168            action_text_style: action,
169            action_small_text_style: self
170                .action_small_text_style
171                .unwrap_or(ACTION_SMALL.color(primary)),
172            tab_label_text_style: self
173                .tab_label_text_style
174                .unwrap_or(TAB_LABEL.color(inactive)),
175            nav_title_text_style: self.nav_title_text_style.unwrap_or(NAV_TITLE.color(label)),
176            nav_large_title_text_style: self
177                .nav_large_title_text_style
178                .unwrap_or(NAV_LARGE_TITLE.color(label)),
179            // A navigation bar's buttons are buttons: unset, the default button text, in
180            // the primary colour — not the action style an application set.
181            nav_action_text_style: self.nav_action_text_style.unwrap_or(ACTION.color(primary)),
182            picker_text_style: self.picker_text_style.unwrap_or(PICKER.color(label)),
183            date_time_picker_text_style: self
184                .date_time_picker_text_style
185                .unwrap_or(DATE_TIME_PICKER.color(label)),
186        }
187    }
188}
189
190/// The theme of the Apple-style widgets **as asked for**: each value an application sets, or
191/// `None` for the default — the reference's `CupertinoThemeData`.
192///
193/// Where it applies decides what the defaults are. As the main theme's override
194/// ([`Theme::with_cupertino_override_theme`]) the primary colour, the colour on it, the page
195/// background and the brightness default to the main theme's; as a subtree's own theme
196/// ([`CupertinoTheme::around`]) they default to Apple's — the system blue, white, the
197/// system background, and the system's brightness.
198///
199/// A colour may be [adaptive](CupertinoDynamicColor) or a plain [`Color`]; it is resolved
200/// where it is used.
201#[derive(Clone, Debug, Default, PartialEq)]
202pub struct CupertinoThemeData {
203    /// Light or dark. It decides how every adaptive colour under it resolves.
204    pub brightness: Option<Brightness>,
205    /// The colour of the controls: a button's label, a switch that is on, a selected tab.
206    pub primary_color: Option<CupertinoDynamicColor>,
207    /// The colour of what sits on the primary colour.
208    pub primary_contrasting_color: Option<CupertinoDynamicColor>,
209    /// The type.
210    pub text_theme: Option<Box<CupertinoTextThemeData>>,
211    /// The background of the bars: navigation, tab, toolbar.
212    pub bar_background_color: Option<CupertinoDynamicColor>,
213    /// The background of a page.
214    pub scaffold_background_color: Option<CupertinoDynamicColor>,
215    /// The handles of a text selection.
216    pub selection_handle_color: Option<CupertinoDynamicColor>,
217    /// Whether the widgets that adapt to the platform take this theme's colours on every
218    /// platform, or only on Apple's.
219    pub apply_theme_to_all: Option<bool>,
220}
221
222impl CupertinoThemeData {
223    /// Nothing asked for: every value the default.
224    pub fn new() -> Self {
225        Self::default()
226    }
227
228    /// The same, light or dark.
229    #[must_use]
230    pub fn brightness(mut self, brightness: Brightness) -> Self {
231        self.brightness = Some(brightness);
232        self
233    }
234
235    /// The same, its controls in `color`.
236    #[must_use]
237    pub fn primary_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
238        self.primary_color = Some(color.into());
239        self
240    }
241
242    /// The same, what sits on the primary colour in `color`.
243    #[must_use]
244    pub fn primary_contrasting_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
245        self.primary_contrasting_color = Some(color.into());
246        self
247    }
248
249    /// The same, set in `text_theme`.
250    #[must_use]
251    pub fn text_theme(mut self, text_theme: CupertinoTextThemeData) -> Self {
252        self.text_theme = Some(Box::new(text_theme));
253        self
254    }
255
256    /// The same, its bars on `color`.
257    #[must_use]
258    pub fn bar_background_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
259        self.bar_background_color = Some(color.into());
260        self
261    }
262
263    /// The same, its pages on `color`.
264    #[must_use]
265    pub fn scaffold_background_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
266        self.scaffold_background_color = Some(color.into());
267        self
268    }
269
270    /// The same, its selection handles in `color`.
271    #[must_use]
272    pub fn selection_handle_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
273        self.selection_handle_color = Some(color.into());
274        self
275    }
276
277    /// The same, its colours taken on every platform or only on Apple's.
278    #[must_use]
279    pub fn apply_theme_to_all(mut self, all: bool) -> Self {
280        self.apply_theme_to_all = Some(all);
281        self
282    }
283}
284
285/// The theme of the Apple-style widgets **resolved** for where it is used: every value
286/// concrete, every adaptive colour picked. [`CupertinoTheme::of`] makes one.
287#[derive(Clone, Copy, Debug, PartialEq)]
288pub struct CupertinoTheme {
289    /// Light or dark.
290    pub brightness: Brightness,
291    /// The colour of the controls.
292    pub primary_color: Color,
293    /// The colour of what sits on the primary colour.
294    pub primary_contrasting_color: Color,
295    /// The background of the bars.
296    pub bar_background_color: Color,
297    /// The background of a page.
298    pub scaffold_background_color: Color,
299    /// The handles of a text selection.
300    pub selection_handle_color: Color,
301    /// Whether the adaptive widgets take these colours on every platform.
302    pub apply_theme_to_all: bool,
303    /// The type.
304    pub text_theme: CupertinoTextTheme,
305}
306
307impl CupertinoTheme {
308    /// The bars' default background: nearly opaque, nearly white in the light and nearly
309    /// black in the dark, so that what scrolls under a bar shows through a little
310    /// (`cupertino/theme.dart:26`).
311    pub const DEFAULT_BAR_BACKGROUND: CupertinoDynamicColor =
312        CupertinoDynamicColor::with_brightness(
313            Color::from_argb_u32(0xF0F9F9F9),
314            Color::from_argb_u32(0xF01D1D1D),
315        );
316
317    /// **The Apple-style theme in force under `theme`**, resolved.
318    ///
319    /// A subtree's own ([`around`](Self::around)) if it has one, with Apple's defaults
320    /// under it; otherwise the main theme's, with the main theme's colours as the defaults
321    /// its override does not replace — the reference's `MaterialBasedCupertinoThemeData`.
322    pub fn of(theme: &Theme) -> Self {
323        let own = theme.cupertino_theme.as_deref();
324        let data = own
325            .or(theme.cupertino_override_theme.as_deref())
326            .cloned()
327            .unwrap_or_default();
328        let main = own.is_none();
329        let resolve = |c: CupertinoDynamicColor| c.resolve(theme);
330        let primary = data.primary_color.unwrap_or(if main {
331            theme.scheme.primary.into()
332        } else {
333            CupertinoColors::SYSTEM_BLUE
334        });
335        let text_theme = match data.text_theme.as_deref() {
336            Some(asked) => asked.resolve(theme),
337            None => CupertinoTextThemeData::NONE
338                .primary_color(primary)
339                .resolve(theme),
340        };
341        // The main theme's selection handles, if it names them, before Apple's blue.
342        let main_handles = if main {
343            theme.widgets.text_selection.handle_color.map(Into::into)
344        } else {
345            None
346        };
347        Self {
348            brightness: Self::brightness_of(theme),
349            primary_color: resolve(primary),
350            primary_contrasting_color: resolve(data.primary_contrasting_color.unwrap_or(if main {
351                theme.scheme.on_primary.into()
352            } else {
353                CupertinoColors::WHITE.into()
354            })),
355            bar_background_color: resolve(
356                data.bar_background_color
357                    .unwrap_or(Self::DEFAULT_BAR_BACKGROUND),
358            ),
359            scaffold_background_color: resolve(data.scaffold_background_color.unwrap_or(if main {
360                theme.background.into()
361            } else {
362                CupertinoColors::SYSTEM_BACKGROUND
363            })),
364            selection_handle_color: resolve(
365                data.selection_handle_color
366                    .or(main_handles)
367                    .unwrap_or(CupertinoColors::SYSTEM_BLUE),
368            ),
369            apply_theme_to_all: data.apply_theme_to_all.unwrap_or(false),
370            text_theme,
371        }
372    }
373
374    /// **Light or dark, for the Apple-style widgets under `theme`** — what every adaptive
375    /// colour resolves by.
376    ///
377    /// A subtree's own theme's brightness, else the system's; under the main theme, its
378    /// override's, else the main theme's own (the reference's `maybeBrightnessOf`).
379    pub fn brightness_of(theme: &Theme) -> Brightness {
380        match theme.cupertino_theme.as_deref() {
381            Some(own) => own
382                .brightness
383                .unwrap_or_else(|| MediaQuery::of().platform_brightness),
384            None => theme
385                .cupertino_override_theme
386                .as_deref()
387                .and_then(|o| o.brightness)
388                .unwrap_or_else(|| theme.brightness()),
389        }
390    }
391
392    /// Wraps `child` in `data`: the Apple-style widgets inside it take this theme, with
393    /// Apple's defaults for what it does not set, and the icons inside it take its primary
394    /// colour, as under the reference's `CupertinoTheme`.
395    pub fn around<Msg: 'static>(
396        data: CupertinoThemeData,
397        child: impl crate::Widget<Msg> + 'static,
398    ) -> crate::Themed<Msg> {
399        crate::Themed::tweak(
400            move |t| {
401                t.cupertino_theme = Some(Box::new(data.clone()));
402                t.widgets.icon.color = Some(CupertinoTheme::of(t).primary_color);
403            },
404            child,
405        )
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::{CupertinoTextThemeData, CupertinoTheme, CupertinoThemeData};
412    use crate::media::{Brightness, MediaQuery};
413    use crate::{
414        build_ui, Container, CupertinoColors as C, Flex, Runtime, Theme, ThemeBuilder, Widget,
415    };
416    use frus_core::{Color, FontWeight, Primitive, Size, TextStyle};
417
418    #[derive(Clone, Debug, PartialEq)]
419    enum Msg {}
420
421    /// **Under the main theme, its colours are the defaults**: its primary, the colour on
422    /// it, its background and its brightness — and Apple's only for what it has no word for.
423    #[test]
424    fn under_the_main_theme_its_colours_are_the_defaults() {
425        let theme = Theme::dark();
426        let c = CupertinoTheme::of(&theme);
427        assert_eq!(c.brightness, Brightness::Dark);
428        assert_eq!(c.primary_color, theme.scheme.primary);
429        assert_eq!(c.primary_contrasting_color, theme.scheme.on_primary);
430        assert_eq!(c.scaffold_background_color, theme.background);
431        assert_eq!(
432            c.bar_background_color,
433            CupertinoTheme::DEFAULT_BAR_BACKGROUND.dark_color
434        );
435        assert_eq!(c.selection_handle_color, C::SYSTEM_BLUE.dark_color);
436        assert!(!c.apply_theme_to_all);
437        // The actions' type is in the primary colour; text in the label colour.
438        assert_eq!(
439            c.text_theme.action_text_style.color,
440            Some(theme.scheme.primary)
441        );
442        assert_eq!(c.text_theme.text_style.color, Some(C::LABEL.dark_color));
443        // The main theme's own selection handles come before Apple's blue.
444        let mut named = Theme::light();
445        named.widgets.text_selection.handle_color = Some(Color::rgb8(1, 2, 3));
446        assert_eq!(
447            CupertinoTheme::of(&named).selection_handle_color,
448            Color::rgb8(1, 2, 3)
449        );
450    }
451
452    /// **The override replaces what it sets, and its brightness rules every adaptive
453    /// colour** — the system background turns black under a dark override of a light theme.
454    #[test]
455    fn the_override_replaces_what_it_sets() {
456        let theme = Theme::light().with_cupertino_override_theme(
457            CupertinoThemeData::new()
458                .brightness(Brightness::Dark)
459                .primary_color(C::SYSTEM_GREEN)
460                .scaffold_background_color(C::SYSTEM_BACKGROUND),
461        );
462        let c = CupertinoTheme::of(&theme);
463        assert_eq!(c.brightness, Brightness::Dark);
464        assert_eq!(c.primary_color, C::SYSTEM_GREEN.dark_color);
465        assert_eq!(c.scaffold_background_color, C::SYSTEM_BACKGROUND.dark_color);
466        // Still the main theme's: the colour on the primary.
467        assert_eq!(c.primary_contrasting_color, theme.scheme.on_primary);
468        assert_eq!(C::LABEL.resolve(&theme), C::LABEL.dark_color);
469        assert_eq!(
470            c.text_theme.action_text_style.color,
471            Some(C::SYSTEM_GREEN.dark_color)
472        );
473    }
474
475    /// **A subtree's own theme has Apple's defaults** — the system blue, white, the system
476    /// background — and, unless it says, the system's brightness.
477    #[test]
478    fn a_subtree_s_own_theme_has_apple_s_defaults() {
479        let mut theme = Theme::light();
480        theme.cupertino_theme = Some(Box::new(CupertinoThemeData::new()));
481        let surface = MediaQuery {
482            platform_brightness: Brightness::Dark,
483            ..MediaQuery::new(Size::new(100.0, 100.0))
484        };
485        surface.scope(|| {
486            let c = CupertinoTheme::of(&theme);
487            assert_eq!(c.brightness, Brightness::Dark);
488            assert_eq!(c.primary_color, C::SYSTEM_BLUE.dark_color);
489            assert_eq!(c.primary_contrasting_color, C::WHITE);
490            assert_eq!(c.scaffold_background_color, C::SYSTEM_BACKGROUND.dark_color);
491        });
492        // Its own brightness wins over the system's.
493        theme.cupertino_theme = Some(Box::new(
494            CupertinoThemeData::new().brightness(Brightness::Light),
495        ));
496        surface.scope(|| {
497            assert_eq!(
498                CupertinoTheme::of(&theme).scaffold_background_color,
499                C::SYSTEM_BACKGROUND.color
500            )
501        });
502    }
503
504    /// **The default type is the reference's**, size, weight and spacing; a style asked for
505    /// is taken as it is, and a text theme given without a primary colour puts its actions in
506    /// the system blue.
507    #[test]
508    fn the_default_type_is_the_reference_s() {
509        let t = CupertinoTheme::of(&Theme::light()).text_theme;
510        let spec = |s: TextStyle| (s.size, s.weight, s.letter_spacing);
511        assert_eq!(spec(t.text_style), (Some(17.0), None, Some(-0.41)));
512        assert_eq!(spec(t.action_text_style), (Some(17.0), None, Some(-0.41)));
513        assert_eq!(
514            spec(t.action_small_text_style),
515            (Some(15.0), None, Some(-0.23))
516        );
517        assert_eq!(
518            spec(t.tab_label_text_style),
519            (Some(10.0), Some(FontWeight::Medium), Some(-0.24))
520        );
521        assert_eq!(
522            spec(t.nav_title_text_style),
523            (Some(17.0), Some(FontWeight::SemiBold), Some(-0.41))
524        );
525        assert_eq!(
526            spec(t.nav_large_title_text_style),
527            (Some(34.0), Some(FontWeight::Bold), Some(0.38))
528        );
529        assert_eq!(
530            spec(t.picker_text_style),
531            (Some(21.0), Some(FontWeight::Regular), Some(-0.6))
532        );
533        assert_eq!(
534            spec(t.date_time_picker_text_style),
535            (Some(21.0), Some(FontWeight::Regular), Some(0.4))
536        );
537        assert_eq!(t.tab_label_text_style.color, Some(C::INACTIVE_GRAY.color));
538        assert_eq!(t.nav_title_text_style.color, Some(C::LABEL.color));
539
540        let mine = TextStyle::new(20.0);
541        let theme = Theme::light().with_cupertino_override_theme(
542            CupertinoThemeData::new()
543                .text_theme(CupertinoTextThemeData::NONE.nav_title_text_style(mine)),
544        );
545        let t = CupertinoTheme::of(&theme).text_theme;
546        assert_eq!(t.nav_title_text_style, mine);
547        assert_eq!(t.action_text_style.color, Some(C::SYSTEM_BLUE.color));
548    }
549
550    /// **A theme keeps its Apple-style parts through a fade, switching halfway, and a subtree
551    /// wrapped in its own theme resolves by it — its icons in its primary colour.**
552    #[test]
553    fn a_subtree_takes_its_own_theme() {
554        let green = Theme::light().with_cupertino_override_theme(
555            CupertinoThemeData::new().primary_color(C::SYSTEM_GREEN),
556        );
557        let plain = Theme::light();
558        assert_eq!(
559            plain.lerp(&green, 0.7).cupertino_override_theme,
560            green.cupertino_override_theme
561        );
562        assert_eq!(plain.lerp(&green, 0.3).cupertino_override_theme, None);
563
564        let swatch = || {
565            ThemeBuilder::new(|t: &Theme| {
566                Container::new()
567                    .width(50.0)
568                    .height(50.0)
569                    .color(CupertinoTheme::of(t).primary_color)
570            })
571        };
572        let icon = || {
573            ThemeBuilder::new(|t: &Theme| {
574                Container::new()
575                    .width(50.0)
576                    .height(50.0)
577                    .color(t.widgets.icon.color.unwrap_or(Color::TRANSPARENT))
578            })
579        };
580        let tree = Flex::<Msg>::column()
581            .width(200.0)
582            .height(200.0)
583            .child(swatch())
584            .child(CupertinoTheme::around(CupertinoThemeData::new(), swatch()))
585            .child(CupertinoTheme::around(
586                CupertinoThemeData::new().primary_color(C::SYSTEM_PINK),
587                icon(),
588            ));
589        let painted: Vec<Color> = build_ui(
590            &tree as &dyn Widget<Msg>,
591            Size::new(200.0, 200.0),
592            &Runtime::default(),
593            &Theme::light(),
594        )
595        .scene()
596        .primitives()
597        .iter()
598        .filter_map(|p| match p {
599            Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
600            _ => None,
601        })
602        .collect();
603        assert_eq!(
604            painted,
605            vec![
606                Theme::light().scheme.primary,
607                C::SYSTEM_BLUE.color,
608                C::SYSTEM_PINK.color
609            ]
610        );
611    }
612}