Skip to main content

frus_widgets/
widgettheme.rs

1//! **Per-widget defaults**: the middle term of `what the caller said ?? what the theme
2//! says ?? what the framework ships`.
3//!
4//! Three milestones in a row ended on the same missing piece. An application could
5//! override one card's elevation, one divider's height, one splash's colour — and had
6//! no way to say *every card in this application*, short of writing its own wrapper
7//! around each widget. The reference resolves nearly every property through exactly
8//! this chain, and only the middle term was absent here.
9//!
10//! Every field is an `Option`, and `None` means "the framework's own default" — so a
11//! theme that sets nothing behaves exactly as no theme at all, and a theme that sets one
12//! field changes one thing.
13//!
14//! ```ignore
15//! let mut theme = Theme::dark();
16//! theme.widgets.card.elevation = Some(0.0);       // a flat application
17//! theme.widgets.divider.height = Some(1.0);       // hairlines, flush
18//! theme.widgets.ink.color = Some(accent.fade(0.2));
19//! ```
20//!
21//! **Layout properties are included** — a divider's height, a card's margin, a drawer's
22//! width — which is why [`Widget::style_themed`](crate::Widget::style_themed) exists
23//! beside `style`. A theme that could only reach paint would be able to recolour a
24//! divider but not make one thin, which is the setting an application actually wants.
25
26use frus_core::{BorderRadius, Color, Insets, ShapeBorder, TextAlign, TextOverflow, TextStyle};
27
28use crate::card::CardVariant;
29
30/// **The shape a box takes**, on the rungs everything here is resolved on
31/// (`card.dart`, `button_style.dart`, and every other `shape` in the reference).
32///
33/// The caller's word first, then the theme's shape, then the theme's plain **radius** read
34/// as a rounded rectangle, then the widget's own default.
35///
36/// The third rung is the one worth explaining. A radius was the only thing a theme could
37/// say until milestone 450, and applications have written them; a theme that names a
38/// `shape` outranks one that names only a `radius`, and naming both is naming the shape.
39pub fn resolve_shape(
40    own: Option<ShapeBorder>,
41    themed: Option<ShapeBorder>,
42    radius: Option<BorderRadius>,
43    fallback: ShapeBorder,
44) -> ShapeBorder {
45    own.or(themed)
46        .or_else(|| radius.map(ShapeBorder::rounded))
47        .unwrap_or(fallback)
48}
49
50/// The per-widget defaults carried by a [`Theme`](crate::Theme).
51///
52/// Adding a widget here is adding a field: the pattern is one `Option` per builder the
53/// widget already has, resolved in the same order everywhere.
54#[derive(Clone, Debug, Default, PartialEq)]
55pub struct WidgetThemes {
56    /// Defaults for [`Alert`](crate::Alert).
57    pub alert: AlertTheme,
58    /// Defaults for [`Dialog`](crate::Dialog) and [`AlertDialog`](crate::AlertDialog).
59    pub dialog: DialogTheme,
60    /// Which glyphs the four named buttons carry.
61    pub action_icons: ActionIconTheme,
62    /// Defaults for [`AppBar`](crate::AppBar).
63    pub app_bar: AppBarTheme,
64    /// Defaults for [`Autocomplete`](crate::Autocomplete).
65    pub autocomplete: AutocompleteTheme,
66    /// Defaults for [`Badge`](crate::Badge).
67    pub badge: BadgeTheme,
68    /// Defaults for [`MaterialBanner`](crate::MaterialBanner).
69    pub banner: BannerTheme,
70    /// Defaults for [`BottomAppBar`](crate::BottomAppBar).
71    pub bottom_app_bar: BottomAppBarTheme,
72    /// Defaults for [`BottomSheet`](crate::BottomSheet).
73    pub bottom_sheet: BottomSheetTheme,
74    /// Defaults for [`Breadcrumb`](crate::Breadcrumb).
75    pub breadcrumb: BreadcrumbTheme,
76    /// Defaults for [`Button`](crate::Button).
77    pub button: ButtonTheme,
78    /// Defaults for [`Card`](crate::Card).
79    pub card: CardTheme,
80    /// Defaults for [`Checkbox`](crate::Checkbox).
81    pub checkbox: CheckboxTheme,
82    /// Defaults for [`Chip`](crate::Chip).
83    pub chip: ChipTheme,
84    /// Defaults for [`DatePicker`](crate::DatePicker).
85    pub date_picker: DatePickerTheme,
86    /// Defaults for [`Divider`](crate::Divider).
87    pub divider: DividerTheme,
88    /// Defaults for [`Drawer`](crate::Drawer).
89    pub drawer: DrawerTheme,
90    /// Defaults for [`ListTile`](crate::ListTile).
91    pub list_tile: ListTileTheme,
92    /// Defaults for [`DropdownButton`](crate::DropdownButton) and its options.
93    pub dropdown: DropdownTheme,
94    /// Defaults for [`ExpansionTile`](crate::ExpansionTile).
95    pub expansion_tile: ExpansionTileTheme,
96    /// Defaults for [`ExpansionPanelList`](crate::ExpansionPanelList).
97    pub expansion_panel_list: ExpansionPanelListTheme,
98    /// Defaults for [`FloatingActionButton`](crate::FloatingActionButton).
99    pub fab: FabTheme,
100    /// Defaults for a form's error summary, [`ErrorSummary`](crate::ErrorSummary).
101    pub form: FormTheme,
102    /// Defaults for [`GridTileBar`](crate::GridTileBar).
103    pub grid_tile_bar: GridTileBarTheme,
104    /// Defaults for [`Icon`](crate::Icon).
105    pub icon: IconTheme,
106    /// Defaults for [`IconButton`](crate::IconButton).
107    pub icon_button: IconButtonTheme,
108    /// Defaults for the **ink ripple** — every surface that splashes, including [`InkWell`](crate::InkWell) and [`Button`](crate::Button).
109    pub ink: InkTheme,
110    /// Defaults for [`Kanban`](crate::Kanban).
111    pub kanban: KanbanTheme,
112    /// Defaults for [`ReorderableList`](crate::ReorderableList).
113    pub reorderable: ReorderableTheme,
114    /// Defaults for [`Kbd`](crate::Kbd).
115    pub kbd: KbdTheme,
116    /// Defaults for [`PopupMenuButton`](crate::PopupMenuButton) and its items.
117    pub menu: MenuTheme,
118    /// Defaults for [`NavigationBar`](crate::NavigationBar).
119    pub nav_bar: NavBarTheme,
120    /// Defaults for [`NavigationRail`](crate::NavigationRail) and [`BottomBar`](crate::BottomBar).
121    pub nav_rail: NavRailTheme,
122    /// Defaults for [`NavigationDrawer`](crate::NavigationDrawer).
123    pub nav_drawer: NavDrawerTheme,
124    /// Defaults for the **scrollbars** the framework draws over a scrollable.
125    pub scrollbar: ScrollbarTheme,
126    /// Defaults for [`SearchBar`](crate::SearchBar).
127    pub search_bar: SearchBarTheme,
128    /// Defaults for the view a [`SearchAnchor`](crate::SearchAnchor) opens.
129    pub search_view: SearchViewTheme,
130    /// Defaults for the two progress indicators.
131    pub progress: ProgressTheme,
132    /// Defaults for [`RadioGroup`](crate::RadioGroup).
133    pub radio: RadioTheme,
134    /// Defaults for [`SegmentedButton`](crate::SegmentedButton).
135    pub segmented: SegmentedTheme,
136    /// Defaults for [`Slider`](crate::Slider) and [`RangeSlider`](crate::RangeSlider).
137    pub slider: SliderTheme,
138    /// Defaults for [`SnackBar`](crate::SnackBar).
139    pub snack_bar: SnackBarTheme,
140    /// Defaults for [`Steps`](crate::Steps).
141    pub steps: StepsTheme,
142    /// Defaults for [`Switch`](crate::Switch).
143    pub switch: SwitchTheme,
144    /// Defaults for [`TabPageSelector`](crate::TabPageSelector).
145    pub page_selector: PageSelectorTheme,
146    /// Defaults for [`TabBar`](crate::TabBar).
147    pub tab_bar: TabBarTheme,
148    /// Defaults for [`Table`](crate::Table).
149    pub table: TableTheme,
150    /// The text style that text takes when it names none of its own.
151    pub text: DefaultTextStyle,
152    /// Defaults for [`MenuBar`](crate::MenuBar) and its menus.
153    pub menu_bar: MenuBarTheme,
154    /// The window's three buttons, when the application paints them on the title bar's line
155    /// (milestone 640).
156    pub caption_buttons: CaptionButtonsTheme,
157    /// The colours of a [`WindowMenuBar`](crate::WindowMenuBar)'s line (milestone 642).
158    pub title_bar: TitleBarTheme,
159    /// How a selection in text that is not a field looks: a
160    /// [`selectable`](crate::Text::selectable) text's, and a
161    /// [`SelectionArea`](crate::SelectionArea)'s.
162    pub text_selection: TextSelectionTheme,
163    /// Defaults for [`TextField`](crate::TextField).
164    pub text_field: TextFieldTheme,
165    /// Defaults for [`TimePicker`](crate::TimePicker) and [`TimeRange`](crate::TimeRange).
166    pub time_picker: TimePickerTheme,
167    /// Defaults for [`Timeline`](crate::Timeline).
168    pub timeline: TimelineTheme,
169    /// Defaults for [`ToggleButtons`](crate::ToggleButtons).
170    pub toggle_buttons: ToggleButtonsTheme,
171    /// Defaults for [`Tooltip`](crate::Tooltip).
172    pub tooltip: TooltipTheme,
173    /// Defaults for [`Tree`](crate::Tree).
174    pub tree: TreeTheme,
175}
176
177/// How a selection in text that is not a field looks — a
178/// [`selectable`](crate::Text::selectable) text's and a [`SelectionArea`](crate::SelectionArea)'s.
179///
180/// A field has its own ([`TextFieldTheme::handle_color`]); this is for the words around it.
181/// [`SelectionArea::selection_color`](crate::SelectionArea::selection_color) and
182/// [`SelectionArea::handle_color`](crate::SelectionArea::handle_color) set it for one area.
183#[derive(Clone, Copy, Debug, Default, PartialEq)]
184pub struct TextSelectionTheme {
185    /// The highlight under the selected words. Unset, the theme's `selection`.
186    pub selection_color: Option<Color>,
187    /// The handles under a touch selection. Unset, the scheme's `primary`.
188    pub handle_color: Option<Color>,
189    /// A text field's caret (milestone 659) — the reference keeps it here too. Unset, the
190    /// scheme's `primary`.
191    pub cursor_color: Option<Color>,
192}
193
194impl TextSelectionTheme {
195    /// The highlight, resolved against `theme`.
196    pub fn selection_color_in(&self, theme: &crate::Theme) -> Color {
197        self.selection_color.unwrap_or(theme.selection)
198    }
199
200    /// The handles' colour, resolved against `theme`.
201    pub fn handle_color_in(&self, theme: &crate::Theme) -> Color {
202        self.handle_color.unwrap_or(theme.scheme.primary)
203    }
204}
205
206/// Defaults for [`Badge`](crate::Badge).
207///
208/// A badge is an **alert** rather than an accent, which is why its untold fill is the
209/// scheme's `error` and not its `primary`. The two sizes are the reference's: `small_size`
210/// is the diameter of a badge with no label, `large_size` the height of one that carries
211/// a count.
212#[derive(Clone, Copy, Debug, Default, PartialEq)]
213pub struct BadgeTheme {
214    /// The pill's fill.
215    pub background_color: Option<Color>,
216    /// The label's colour on that fill.
217    pub text_color: Option<Color>,
218    /// The label's type.
219    pub text_style: Option<TextStyle>,
220    /// The diameter of a badge with no label.
221    pub small_size: Option<f32>,
222    /// The height of a badge that carries one.
223    pub large_size: Option<f32>,
224    /// The label's room either side.
225    pub padding: Option<f32>,
226    /// Where on its child a pinned badge goes: the top-end corner otherwise.
227    pub alignment: Option<frus_core::AlignmentGeometry>,
228    /// How far a pinned badge with a label is moved, `(across, down)`: `(4, -4)` otherwise,
229    /// mirrored for a right-to-left reading.
230    pub offset: Option<(f32, f32)>,
231}
232
233/// Defaults for [`Checkbox`](crate::Checkbox).
234///
235/// A checkbox is two controls wearing one name: ticked it is a **filled box** with a
236/// mark punched through it, unticked it is an **outline** and nothing else. The colours
237/// do not carry over between the two, which is why they are named apart rather than as
238/// one "active"/"inactive" pair.
239#[derive(Clone, Debug, Default, PartialEq)]
240pub struct CheckboxTheme {
241    /// The box's fill, ticked.
242    pub fill_color: Option<Color>,
243    /// The tick drawn on that fill.
244    pub check_color: Option<Color>,
245    /// The outline, unticked and at rest.
246    pub border_color: Option<Color>,
247    /// The outline under a pointer, a finger or focus. The reference resolves this side
248    /// per state, and an outline that did not answer would look inert.
249    pub active_border_color: Option<Color>,
250    /// The box's corner radius.
251    pub radius: Option<f32>,
252    /// The label beside it.
253    pub label_color: Option<Color>,
254    /// **How much room it reserves for a finger** — the reference's per-widget say over
255    /// the theme's (`checkbox.dart:512`). Unset, the theme's own answer.
256    pub tap_target: Option<crate::theme::TapTarget>,
257    /// The halo under a pointer, the keyboard or a finger, state by state. Unset, the
258    /// reference's (milestone 634).
259    pub overlay_color: Option<crate::WidgetStateProperty<Color>>,
260    /// That halo's radius. Unset, 20.
261    pub splash_radius: Option<f32>,
262    /// How compact a checkbox is. Unset, the standard density, whatever the theme's.
263    pub visual_density: Option<crate::VisualDensity>,
264}
265
266/// Defaults for [`RadioGroup`](crate::RadioGroup).
267#[derive(Clone, Debug, Default, PartialEq)]
268pub struct RadioTheme {
269    /// The ring and the dot of the chosen option.
270    pub selected_color: Option<Color>,
271    /// The ring of an option that is not chosen, at rest.
272    pub border_color: Option<Color>,
273    /// That ring under a pointer, a finger or focus.
274    pub active_border_color: Option<Color>,
275    /// The labels.
276    pub label_color: Option<Color>,
277    /// **How much room it reserves for a finger** — the reference's per-widget say over
278    /// the theme's (`radio.dart:734`). Unset, the theme's own answer.
279    pub tap_target: Option<crate::theme::TapTarget>,
280    /// The halo under a pointer, the keyboard or a finger, state by state. Unset, the
281    /// reference's (milestone 635).
282    pub overlay_color: Option<crate::WidgetStateProperty<Color>>,
283    /// That halo's radius. Unset, 20.
284    pub splash_radius: Option<f32>,
285    /// How compact a radio is. Unset, the theme's density.
286    pub visual_density: Option<crate::VisualDensity>,
287}
288
289/// Defaults for [`Slider`](crate::Slider) and [`RangeSlider`](crate::RangeSlider).
290#[derive(Clone, Debug, Default, PartialEq)]
291pub struct SliderTheme {
292    /// The value bubble's type. The reference calls it the *value indicator* and sets it in
293    /// `labelLarge`.
294    pub value_indicator_text_style: Option<TextStyle>,
295    /// The travelled part of the track.
296    pub active_track_color: Option<Color>,
297    /// The part still to travel.
298    pub inactive_track_color: Option<Color>,
299    /// The thumb's fill.
300    pub thumb_color: Option<Color>,
301    /// The ring around the thumb.
302    pub thumb_border_color: Option<Color>,
303    /// How far a thumb sits off the track at rest — a slider's, and each of a range
304    /// slider's. Unset, the reference's 1.
305    pub thumb_elevation: Option<f32>,
306    /// And while it is held. Unset, the reference's 6.
307    pub pressed_thumb_elevation: Option<f32>,
308    /// The track's height (milestone 648, as the rest below). Unset, the reference's 4; the
309    /// travelled part is 2 more.
310    pub track_height: Option<f32>,
311    /// The part between a slider's value and its secondary value.
312    pub secondary_active_track_color: Option<Color>,
313    /// The ticks on the travelled part. Unset, `on_primary` at 38 %.
314    pub active_tick_mark_color: Option<Color>,
315    /// The ticks on the rest. Unset, `on_surface_variant` at 38 %.
316    pub inactive_tick_mark_color: Option<Color>,
317    /// A disabled slider's travelled part.
318    pub disabled_active_track_color: Option<Color>,
319    /// A disabled slider's rest of the track.
320    pub disabled_inactive_track_color: Option<Color>,
321    /// A disabled slider's thumb.
322    pub disabled_thumb_color: Option<Color>,
323    /// The halo around the thumb, per state.
324    pub overlay_color: Option<crate::WidgetStateProperty<Color>>,
325    /// The value indicator's fill. Unset, `primary`.
326    pub value_indicator_color: Option<Color>,
327    /// When the value indicator shows. Unset, only on a slider with divisions.
328    pub show_value_indicator: Option<crate::ShowValueIndicator>,
329    /// What a press and a slide may do (milestone 650). Unset, tap and slide.
330    pub allowed_interaction: Option<crate::SliderInteraction>,
331    /// A slider's track (milestone 654). Unset, the reference's rounded rectangle.
332    pub track_shape:
333        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderTrackShape>>,
334    /// A range slider's track. Unset, the reference's rounded rectangle, square between the thumbs.
335    pub range_track_shape:
336        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderTrackShape>>,
337    /// A slider's tick marks. Unset, the reference's dots.
338    pub tick_mark_shape:
339        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderTickMarkShape>>,
340    /// A range slider's tick marks. Unset, the reference's dots.
341    pub range_tick_mark_shape:
342        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderTickMarkShape>>,
343    /// A slider's thumb. Unset, the reference's disc.
344    pub thumb_shape:
345        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderThumbShape>>,
346    /// A range slider's thumbs. Unset, the reference's disc.
347    pub range_thumb_shape:
348        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderThumbShape>>,
349    /// The halo around a thumb, on both sliders. Unset, the reference's disc.
350    pub overlay_shape:
351        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderOverlayShape>>,
352    /// A slider's value indicator. Unset, the reference's drop.
353    pub value_indicator_shape:
354        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderValueIndicatorShape>>,
355    /// A range slider's value indicators. Unset, the reference's rectangle.
356    pub range_value_indicator_shape:
357        Option<crate::slidershape::SliderShape<dyn crate::slidershape::SliderValueIndicatorShape>>,
358}
359
360/// Defaults for [`Switch`](crate::Switch).
361#[derive(Clone, Debug, Default, PartialEq)]
362pub struct SwitchTheme {
363    /// The track, on.
364    pub track_color: Option<Color>,
365    /// The track, off.
366    pub inactive_track_color: Option<Color>,
367    /// The thumb, on.
368    pub thumb_color: Option<Color>,
369    /// The thumb, off.
370    pub inactive_thumb_color: Option<Color>,
371    /// The thumb in any state, on or off — before `thumb_color` and `inactive_thumb_color`
372    /// (milestone 637).
373    pub thumb_colors: Option<crate::WidgetStateProperty<Color>>,
374    /// The track in any state, on or off — before `track_color` and
375    /// `inactive_track_color`.
376    pub track_colors: Option<crate::WidgetStateProperty<Color>>,
377    /// The rule round the track in any state. Unset, `outline` round an off track and none
378    /// round an on one.
379    pub track_outline_color: Option<crate::WidgetStateProperty<Color>>,
380    /// How wide that rule is. Unset, 2.
381    pub track_outline_width: Option<f32>,
382    /// The halo round the thumb, state by state. Unset, `primary` round an on switch and
383    /// `on_surface` round an off one, 8 % under a pointer and 10 % focused or pressed.
384    pub overlay_color: Option<crate::WidgetStateProperty<Color>>,
385    /// The halo's radius. Unset, 20.
386    pub splash_radius: Option<f32>,
387    /// The room either side of the track. Unset, 4 left and right.
388    pub padding: Option<Insets>,
389    /// The glyph inside the thumb, on. Unset, the scheme's `on_primary_container`
390    /// (`switch.dart:2338`).
391    pub icon_color: Option<Color>,
392    /// The glyph inside the thumb, off. Unset, the scheme's `surface_container_highest`
393    /// (`switch.dart:2349`) — the track's own colour, so the glyph reads as a hole in the
394    /// thumb rather than as a mark on it.
395    pub inactive_icon_color: Option<Color>,
396    /// **How much room it reserves for a finger** — the reference's per-widget say over
397    /// the theme's (`switch.dart:603`). Unset, the theme's own answer.
398    pub tap_target: Option<crate::theme::TapTarget>,
399}
400
401/// The text style a **subtree** hands down — the reference's `DefaultTextStyle`, and an
402/// inherited widget there for the reason it is a theme entry here: an app bar, a list
403/// tile or a dialog wants every run of words inside it to read the same way without
404/// reaching into each one, including the ones it never sees because a caller passed them
405/// in already assembled.
406///
407/// It is the reference's shape exactly: a [`TextStyle`] plus the four questions that are
408/// about the **box** rather than the type. It carried its own copy of every typographic
409/// field until `TextStyle` learned to say *unset*; now the style is just a style.
410///
411/// A [`Text`](crate::Text) resolves `what the caller said ?? what this says ?? what the
412/// framework ships`, field by field, and **a default the caller did not choose does not
413/// count as having said something** — which is what an `Option` per field means and what
414/// `TextStyle::new(16.0)` could not express.
415///
416/// [`DefaultTextStyle::around`] wraps a subtree with one, and that is how
417/// [`AppBar::toolbar_text_style`](crate::AppBar::toolbar_text_style) delivers its own.
418#[derive(Clone, Copy, Debug, Default, PartialEq)]
419pub struct DefaultTextStyle {
420    /// The type: size, weight, slant, colour, decoration — each answered or left open.
421    pub style: TextStyle,
422    /// Where the lines sit inside the box.
423    pub align: Option<TextAlign>,
424    /// Whether the text wraps at the width it is given.
425    pub soft_wrap: Option<bool>,
426    /// What becomes of text that does not fit.
427    pub overflow: Option<TextOverflow>,
428    /// At most this many lines.
429    pub max_lines: Option<usize>,
430    /// Whether the texts in this subtree **take part in a selection that spans them** — set by
431    /// [`SelectionArea`](crate::SelectionArea) and by nothing else. Unlike the fields above it is
432    /// not a matter of how a text looks, and it never turns off once on: a nested area is still
433    /// inside the outer one.
434    pub selectable: bool,
435}
436
437impl DefaultTextStyle {
438    /// Nothing said at all — what a theme carries until something sets a field.
439    pub const NONE: Self = Self {
440        style: TextStyle::NONE,
441        align: None,
442        soft_wrap: None,
443        overflow: None,
444        max_lines: None,
445        selectable: false,
446    };
447
448    /// A handover of type and nothing else.
449    pub const fn from_text_style(style: TextStyle) -> Self {
450        Self {
451            style,
452            ..Self::NONE
453        }
454    }
455
456    /// This style with `over`'s answers laid on top, **field by field**: where `over` said
457    /// nothing, this one's answer survives.
458    ///
459    /// The operation an inherited style needs at every level it passes through — two
460    /// nested subtrees each setting one field must leave a text wearing both.
461    #[must_use]
462    pub fn merge(self, over: Self) -> Self {
463        Self {
464            style: self.style.merge(over.style),
465            align: over.align.or(self.align),
466            soft_wrap: over.soft_wrap.or(self.soft_wrap),
467            overflow: over.overflow.or(self.overflow),
468            max_lines: over.max_lines.or(self.max_lines),
469            selectable: self.selectable || over.selectable,
470        }
471    }
472
473    /// Wraps `child` so that every [`Text`](crate::Text) in its subtree wears this style
474    /// where it has not answered for itself — the reference's `DefaultTextStyle` widget.
475    ///
476    /// **Merged onto** whatever an enclosing subtree already handed down rather than
477    /// replacing it, so two nested wrappers each setting one field leave a text wearing
478    /// both:
479    ///
480    /// ```ignore
481    /// DefaultTextStyle::from_text_style(TextStyle::NONE.color(muted))
482    ///     .around(a_column_of_labels)
483    /// ```
484    pub fn around<Msg: 'static>(
485        self,
486        child: impl crate::Widget<Msg> + 'static,
487    ) -> crate::Themed<Msg> {
488        crate::Themed::tweak(move |t| t.widgets.text = t.widgets.text.merge(self), child)
489    }
490}
491
492/// Defaults for [`Icon`](crate::Icon) — the reference's `IconTheme`, which is an
493/// **inherited** widget there for the same reason this is scoped here: an app bar, a
494/// list tile or a button bar wants every glyph inside it one colour without recolouring
495/// the words beside them.
496///
497/// `Themed::tweak` is how a subtree sets it, and that is exactly how
498/// [`AppBar::icon_theme`](crate::AppBar::icon_theme) delivers its own.
499#[derive(Clone, Copy, Debug, Default, PartialEq)]
500pub struct IconTheme {
501    /// The glyph's colour. Unset, the scheme's `on_surface` — an icon is text's peer,
502    /// not a decoration.
503    pub color: Option<Color>,
504    /// The square's side, in logical pixels. Unset, 24: the grid the icons are drawn on.
505    pub size: Option<f32>,
506}
507
508/// Defaults for [`AppBar`](crate::AppBar).
509#[derive(Clone, Copy, Debug, Default, PartialEq)]
510pub struct AppBarTheme {
511    /// Whether the title is centred. Unset, the **platform**'s convention decides, which
512    /// is the one place the framework's last word is a system convention rather than a
513    /// taste of its own.
514    pub center_title: Option<bool>,
515    /// The title's type.
516    pub title_style: Option<TextStyle>,
517    /// The bar's surface.
518    pub background: Option<Color>,
519    /// What is drawn on it — the title's colour.
520    pub foreground: Option<Color>,
521    /// How far off the page the bar sits. Unset, the bar is flat, as the reference's is.
522    pub elevation: Option<f32>,
523    /// The toolbar's height.
524    pub height: Option<f32>,
525    /// The colour of the shadow an elevated bar casts. Unset, **transparent**, as the
526    /// reference's app bar defaults are (line 2542): a shadow is cast only where a colour is
527    /// named.
528    pub shadow_color: Option<Color>,
529    /// The colour laid over the surface in proportion to the elevation — Material 3's
530    /// way of showing height, and the one that still reads on a dark background.
531    pub surface_tint: Option<Color>,
532    /// **What shape the bar is** — the reference's `AppBarTheme.shape`. Unset, square.
533    ///
534    /// It was an `Option<BorderRadius>` under this name until milestone 455: the last
535    /// theme field left carrying the reference's *word* with a corner radius behind it.
536    pub shape: Option<ShapeBorder>,
537}
538
539/// Defaults for [`TextField`](crate::TextField).
540#[derive(Clone, Copy, Debug, Default, PartialEq)]
541pub struct TextFieldTheme {
542    /// The container's fill. A filled field takes the theme's high surface container by
543    /// default; an outlined one has none.
544    pub fill: Option<Color>,
545    /// The border, or the underline, at rest.
546    pub border_color: Option<Color>,
547    /// The border once focused.
548    pub focused_border_color: Option<Color>,
549    /// Border, label and helper colour while an error is showing.
550    pub error_color: Option<Color>,
551    /// The same under the pointer: an errored field **deepens** on hover
552    /// (`input_decorator.dart:5981`). Unset, the scheme's `on_error_container`.
553    pub error_hover_color: Option<Color>,
554    /// The value's colour.
555    pub text_color: Option<Color>,
556    /// The label and the hint, at rest.
557    pub label_color: Option<Color>,
558    /// The label once focused.
559    pub focused_label_color: Option<Color>,
560    /// The helper line.
561    pub helper_color: Option<Color>,
562    /// The prefix and suffix icons.
563    pub icon_color: Option<Color>,
564    /// The handles under a touch selection. Unset, the scheme's `primary`.
565    pub handle_color: Option<Color>,
566    /// Corner radius.
567    pub radius: Option<f32>,
568    /// Border weight at rest.
569    pub border_width: Option<f32>,
570    /// Border weight once focused.
571    pub focused_border_width: Option<f32>,
572    /// When a field's label floats (milestone 656). Unset, auto.
573    pub floating_label_behavior: Option<crate::FloatingLabelBehavior>,
574    /// Where a floated label sits. Unset, at the start.
575    pub floating_label_alignment: Option<crate::FloatingLabelAlignment>,
576    /// A field's caret (milestone 659). Unset, the text selection's, then `primary`.
577    pub cursor_color: Option<Color>,
578    /// The caret while an error shows. Unset, the error colour.
579    pub cursor_error_color: Option<Color>,
580}
581
582/// Defaults for [`Button`](crate::Button): a style for each kind of button — the
583/// reference's `ElevatedButtonTheme`, `FilledButtonTheme`, `OutlinedButtonTheme` and
584/// `TextButtonTheme` — and, under them, plain values for every kind (milestone 632).
585#[derive(Clone, Debug, Default, PartialEq)]
586pub struct ButtonTheme {
587    /// How an elevated button looks, state by state.
588    pub elevated_style: Option<crate::ButtonStyle>,
589    /// How a filled button looks — tonal and danger ones included, as the reference's
590    /// tonal button shares the filled button's theme.
591    pub filled_style: Option<crate::ButtonStyle>,
592    /// How an outlined button looks.
593    pub outlined_style: Option<crate::ButtonStyle>,
594    /// How a text button looks.
595    pub text_button_style: Option<crate::ButtonStyle>,
596    /// **What shape it is** (`shape_border.dart`), over the plain `radius` below:
597    /// a button's, whose default is a **pill** (`button_style.dart`). Unset, the widget decides.
598    pub shape: Option<ShapeBorder>,
599    /// The surface under the label, whatever the variant would have used.
600    pub color: Option<Color>,
601    /// The label's colour.
602    pub label_color: Option<Color>,
603    /// The label's type.
604    pub label_style: Option<TextStyle>,
605    /// The outline's colour.
606    pub border_color: Option<Color>,
607    /// Its thickness; `0.0` removes it.
608    pub border_width: Option<f32>,
609    /// The corner radii. Unset, a button is a stadium.
610    pub radius: Option<BorderRadius>,
611    /// The room either side of the label.
612    pub padding: Option<f32>,
613    /// The button's height.
614    pub height: Option<f32>,
615    /// The narrowest it will be, however short its label.
616    pub min_width: Option<f32>,
617    /// How far it sits off the surface, **in every state**. Unset, the variant's own
618    /// heights, which move with the state — see [`Button::elevation`](crate::Button::elevation).
619    pub elevation: Option<f32>,
620}
621
622/// Defaults for [`Card`](crate::Card).
623#[derive(Clone, Copy, Debug, Default, PartialEq)]
624pub struct CardTheme {
625    /// **What shape it is** (`shape_border.dart`), over the plain `radius` below:
626    /// a card's. Unset, the widget decides.
627    pub shape: Option<ShapeBorder>,
628    /// Which of the three cards an untold `Card::new()` is.
629    pub variant: Option<CardVariant>,
630    /// How far off the surface it sits.
631    pub elevation: Option<f32>,
632    /// Its background, overriding the variant's tone.
633    pub color: Option<Color>,
634    /// Its corner radii.
635    pub radius: Option<BorderRadius>,
636    /// The room it leaves around itself.
637    pub margin: Option<f32>,
638    /// The room it leaves inside itself.
639    pub padding: Option<f32>,
640}
641
642/// Defaults for [`Chip`](crate::Chip).
643#[derive(Clone, Copy, Debug, Default, PartialEq)]
644pub struct ChipTheme {
645    /// **What shape it is** (`shape_border.dart`), over the plain `radius` below:
646    /// a chip's. Unset, the widget decides.
647    pub shape: Option<ShapeBorder>,
648    /// The surface under an unselected chip.
649    pub color: Option<Color>,
650    /// The surface under a selected one.
651    pub selected_color: Option<Color>,
652    /// The label's colour when unselected.
653    pub label_color: Option<Color>,
654    /// The label's colour when selected.
655    pub selected_label_color: Option<Color>,
656    /// The label's type.
657    pub label_style: Option<TextStyle>,
658    /// The outline's colour.
659    pub border_color: Option<Color>,
660    /// Its thickness; `0.0` removes it.
661    pub border_width: Option<f32>,
662    /// The corner radii.
663    pub radius: Option<BorderRadius>,
664    /// The room inside the outline.
665    pub padding: Option<f32>,
666    /// The room on either side of the label.
667    pub label_padding: Option<f32>,
668    /// The chip's height.
669    pub height: Option<f32>,
670    /// The size of a leading icon, a checkmark or a delete cross.
671    pub icon_size: Option<f32>,
672    /// Whether a selected chip shows a checkmark.
673    pub show_checkmark: Option<bool>,
674}
675
676/// Defaults for [`ListTile`](crate::ListTile) — the reference's `ListTileThemeData`.
677///
678/// Every field is what the tile would otherwise decide for itself, on the usual rungs: the
679/// tile's own word, then this, then the framework's. An application that wants all its
680/// tiles rounded, or all its selected rows tinted, says it **once** here rather than on
681/// every tile it ever builds.
682#[derive(Clone, Copy, Debug, Default, PartialEq)]
683pub struct ListTileTheme {
684    /// The tile's surface. Unset, transparent — a tile takes the colour of what it sits on.
685    pub tile_color: Option<Color>,
686    /// The surface while the tile is the chosen one.
687    pub selected_tile_color: Option<Color>,
688    /// The colour its words and slots take while it is the chosen one.
689    pub selected_color: Option<Color>,
690    /// The two slots' icon colour.
691    pub icon_color: Option<Color>,
692    /// The title's and subtitle's colour.
693    pub text_color: Option<Color>,
694    /// What shape the tile is — taken by its surface and by its ink.
695    pub shape: Option<ShapeBorder>,
696    /// The room kept inside the tile, round its content.
697    pub content_padding: Option<Insets>,
698    /// The title's type.
699    pub title_style: Option<TextStyle>,
700    /// The subtitle's.
701    pub subtitle_style: Option<TextStyle>,
702    /// The gap between the slots and the text column.
703    pub title_gap: Option<f32>,
704    /// How much room the leading slot is guaranteed, so the text lines up down a list.
705    pub min_leading_width: Option<f32>,
706    /// The tile's minimum height, over the one its line count asks for.
707    pub min_height: Option<f32>,
708    /// Whether tiles are the tighter kind.
709    pub dense: Option<bool>,
710}
711
712/// Defaults for [`LinearProgressIndicator`](crate::LinearProgressIndicator) and
713/// [`CircularProgressIndicator`](crate::CircularProgressIndicator) — the reference's
714/// `ProgressIndicatorThemeData`.
715///
716/// Neither of them read a theme at all before this, and between them they had **two**
717/// builders: a width and a size. Everything else — the height of the bar, the colour of
718/// its track, the colour of the fill — was decided inside `paint` where nothing could
719/// reach it.
720#[derive(Clone, Copy, Debug, Default, PartialEq)]
721pub struct ProgressTheme {
722    /// The colour of the moving part: the bar's fill and the ring's dots. Unset,
723    /// `primary` (`progress_indicator.dart:1618`).
724    pub color: Option<Color>,
725    /// The colour of the bar's track — the part not yet filled. Unset,
726    /// `secondary_container` (`progress_indicator.dart:1621`).
727    pub linear_track_color: Option<Color>,
728    /// How tall the bar is. Unset, four (`progress_indicator.dart:1624`).
729    pub linear_min_height: Option<f32>,
730    /// The circle **behind the arc** of a circular indicator. Unset, none in the 2023 look;
731    /// in the newer one `secondary_container` for a determinate indicator and none for an
732    /// indeterminate one (`progress_indicator.dart:1590`).
733    pub circular_track_color: Option<Color>,
734    /// The corners of the bar and of its track. Unset, square in the 2023 look and 2 px in
735    /// the newer one (`progress_indicator.dart:1627`).
736    pub border_radius: Option<BorderRadius>,
737    /// The dot at the far end of the track, which says where the bar is going, in the
738    /// newer look. Unset, `primary` (`progress_indicator.dart:1630`).
739    pub stop_indicator_color: Option<Color>,
740    /// That dot's radius, in the newer look. Unset, two (`progress_indicator.dart:1633`).
741    /// Zero draws none.
742    pub stop_indicator_radius: Option<f32>,
743    /// The gap left between a line and the track, in the newer look. Unset, four
744    /// (`progress_indicator.dart:1636`).
745    pub track_gap: Option<f32>,
746    /// **Which look** a bar takes when it does not say: the 2023 one (`true`), or the newer
747    /// one with a gap, a stop dot and rounded ends (`false`). Unset, the 2023 one, as the
748    /// reference's default (`progress_indicator.dart:595`, milestone 629).
749    pub year2023: Option<bool>,
750    /// How thick a circular indicator's arc is. Unset, four
751    /// (`progress_indicator.dart:1593`).
752    pub stroke_width: Option<f32>,
753    /// Where that arc's line sits on the circle: `-1` inside it, `0` centred on it, `1`
754    /// outside. Unset, centred in the 2023 look and inside in the newer one
755    /// (`progress_indicator.dart:1596`, milestone 630).
756    pub stroke_align: Option<f32>,
757    /// How the arc's ends are drawn. Unset, flat — square while spinning — in the 2023 look
758    /// and round in the newer one (`progress_indicator.dart:754`).
759    pub stroke_cap: Option<frus_core::StrokeCap>,
760    /// The side of a circular indicator's circle. Unset, 36 in the 2023 look and 40 in the
761    /// newer one (`progress_indicator.dart:1552`, `:1599`).
762    pub circular_size: Option<f32>,
763    /// The room around a circular indicator's circle. Unset, none in the 2023 look and four
764    /// on every side in the newer one (`progress_indicator.dart:1608`).
765    pub circular_track_padding: Option<f32>,
766}
767
768/// Defaults for [`Tooltip`](crate::Tooltip) — the reference's `TooltipThemeData`.
769///
770/// A tooltip is the one widget an application is likely to use on *every* icon it draws,
771/// so saying what one looks like on each of them is saying it a hundred times.
772#[derive(Clone, Copy, Debug, Default, PartialEq)]
773pub struct TooltipTheme {
774    /// The bubble's surface. Unset, `inverse_surface` — a tooltip is a label **over**
775    /// the interface, not another panel of it.
776    pub background: Option<Color>,
777    /// The label's type. Unset, `body_small`.
778    pub text_style: Option<TextStyle>,
779    /// The label's colour. Unset, `on_inverse_surface`.
780    pub text_color: Option<Color>,
781    /// Where the lines sit once the label has wrapped. Unset, the start edge, as the
782    /// reference's `_defaultTextAlign` is (`tooltip.dart:398`).
783    pub text_align: Option<TextAlign>,
784    /// The room kept round the label.
785    pub padding: Option<Insets>,
786    /// What shape the bubble is.
787    pub shape: Option<ShapeBorder>,
788    /// The radius of its corners, for a theme that would rather give the number than the
789    /// shape. Outranked by [`shape`](Self::shape).
790    pub radius: Option<f32>,
791    /// How wide the bubble may get before the label wraps.
792    pub max_width: Option<f32>,
793}
794
795/// Defaults for [`ExpansionPanelList`](crate::ExpansionPanelList).
796///
797/// A list of panels is one **surface** that splits, so what it needs said once is the
798/// surface: its tone, its corners, the room an open panel is lifted by, and the hairline
799/// between two shut ones.
800#[derive(Clone, Copy, Debug, Default, PartialEq)]
801pub struct ExpansionPanelListTheme {
802    /// The surface the panels are drawn on. Unset, the scheme's low container — the tone
803    /// a card takes, because that is what a run of shut panels is.
804    pub background: Option<Color>,
805    /// The corner radius of a card: the outer corners of a run, and all four of an open
806    /// panel's.
807    pub radius: Option<f32>,
808    /// The room an open panel is given either side of it, which is what makes it read as
809    /// lifted out of the card rather than merely coloured differently.
810    pub gap: Option<f32>,
811    /// The hairline between two shut panels of one card.
812    pub divider_color: Option<Color>,
813    /// How far the cards sit off the page. Unset, the reference's 2; `0.0` is flat.
814    pub elevation: Option<f32>,
815}
816
817/// Defaults for [`ExpansionTile`](crate::ExpansionTile) — the reference's
818/// `ExpansionTileThemeData`.
819///
820/// The tile already had every one of these as a **builder**; what it had nowhere was a
821/// place to say them **once**. A settings screen is a column of these, and an
822/// application that wants its open sections tinted said so on each of them.
823///
824/// Every colour comes in a pair, because an expansion tile is two things: the row that
825/// is always there, and the row while what it hides is showing.
826#[derive(Clone, Copy, Debug, Default, PartialEq)]
827pub struct ExpansionTileTheme {
828    /// The row's surface while the tile is **open**.
829    pub background: Option<Color>,
830    /// And while it is shut.
831    pub collapsed_background: Option<Color>,
832    /// The title's colour while it is open.
833    pub text_color: Option<Color>,
834    /// And while it is shut.
835    pub collapsed_text_color: Option<Color>,
836    /// The chevron's colour while it is open.
837    pub icon_color: Option<Color>,
838    /// And while it is shut.
839    pub collapsed_icon_color: Option<Color>,
840    /// The room kept inside the row, round its content.
841    pub tile_padding: Option<Insets>,
842    /// The room kept round what the tile hides, while it is showing.
843    pub children_padding: Option<Insets>,
844    /// What shape the row is while the tile is open.
845    pub shape: Option<ShapeBorder>,
846    /// And while it is shut. Deliberately **not** falling back to
847    /// [`shape`](Self::shape): the reference keeps the two apart
848    /// (`expansion_tile_theme.dart:55`), and a tile that is square when shut and rounded
849    /// when open is a design, not a mistake.
850    pub collapsed_shape: Option<ShapeBorder>,
851}
852
853/// Defaults for [`FloatingActionButton`](crate::FloatingActionButton) — the
854/// reference's `FloatingActionButtonThemeData`.
855#[derive(Clone, Copy, Debug, Default, PartialEq)]
856pub struct FabTheme {
857    /// The surface. Unset, `primary_container`
858    /// (`floating_action_button.dart:810`) — **not** `primary`, which is what a filled
859    /// button takes and what this framework's `fab_button` helper was giving it.
860    pub background: Option<Color>,
861    /// What is drawn on it. Unset, `on_primary_container`.
862    pub foreground: Option<Color>,
863    /// How far off the page it sits at rest. Unset, six.
864    pub elevation: Option<f32>,
865    /// And under a pointer. Unset, eight — whatever the resting height is, as the reference's
866    /// build resolves it (line 517).
867    pub hover_elevation: Option<f32>,
868    /// What shape it is. Unset, the corner its size asks for — sixteen, twelve or
869    /// twenty-eight (`floating_action_button.dart:816`).
870    pub shape: Option<ShapeBorder>,
871    /// The radius of its corners, for a theme that would rather give the number than the
872    /// shape. Outranked by [`shape`](Self::shape).
873    pub radius: Option<f32>,
874    /// How big the glyph inside is. Unset, the size's own — twenty-four, or
875    /// thirty-six on a large one.
876    pub icon_size: Option<f32>,
877    /// An extended button's type. Unset, `label_large`.
878    pub label_style: Option<TextStyle>,
879    /// The room either side of an extended button's content.
880    pub extended_padding: Option<f32>,
881    /// The gap between an extended button's glyph and its words. Unset, eight.
882    pub extended_gap: Option<f32>,
883}
884
885/// Which glyphs the framework's four named buttons carry — the reference's
886/// `ActionIconTheme` (`action_icons_theme.dart`).
887///
888/// Unset, each takes the framework's own, and the back arrow takes the platform's: a
889/// chevron where the platform's own back control is a chevron, an arrow everywhere else.
890/// An application with its own icon set should not have to leave four of the framework's
891/// showing through.
892#[derive(Clone, Copy, Debug, Default, PartialEq)]
893pub struct ActionIconTheme {
894    /// The glyph on a [`BackButton`](crate::BackButton).
895    pub back: Option<crate::IconData>,
896    /// On a [`CloseButton`](crate::CloseButton).
897    pub close: Option<crate::IconData>,
898    /// On a [`DrawerButton`](crate::DrawerButton).
899    pub drawer: Option<crate::IconData>,
900    /// On an [`EndDrawerButton`](crate::EndDrawerButton).
901    pub end_drawer: Option<crate::IconData>,
902}
903
904/// Defaults for [`Divider`](crate::Divider).
905#[derive(Clone, Copy, Debug, Default, PartialEq)]
906pub struct DividerTheme {
907    /// The room the separator takes in the layout.
908    pub height: Option<f32>,
909    /// The thickness of the line drawn inside that room.
910    pub thickness: Option<f32>,
911    /// Its colour.
912    pub color: Option<Color>,
913    /// Its inset from the leading edge.
914    pub indent: Option<f32>,
915    /// Its inset from the trailing edge.
916    pub end_indent: Option<f32>,
917}
918
919/// Defaults for [`Drawer`](crate::Drawer).
920///
921/// The rounding is on the **inner** edge only — the one that meets the content — and
922/// the outer edge stays square against the window it is docked to, which is what the
923/// reference's own shape resolves to. Which edge that is follows the direction: the
924/// panel does not know which side of the screen it landed on until it paints.
925#[derive(Clone, Copy, Debug, Default, PartialEq)]
926pub struct DrawerTheme {
927    /// **What shape a leading panel is** — the reference's `DrawerThemeData.shape`
928    /// (`drawer.dart:268`). Unset, the framework rounds the panel's *inner* edge by
929    /// [`DRAWER_RADIUS`](crate::DRAWER_RADIUS).
930    pub shape: Option<ShapeBorder>,
931    /// **And a trailing one** — the reference's `endShape` (`drawer.dart:269`), which is a
932    /// separate field rather than a mirror of the first because a panel's rounded edge is
933    /// the one facing the page, and that is the opposite side.
934    ///
935    /// A trailing panel does **not** fall back to [`shape`](Self::shape): a theme that
936    /// named only the leading panel's shape has said nothing about the other one, and the
937    /// framework's own default is a better answer than a shape rounded on the wrong edge.
938    pub end_shape: Option<ShapeBorder>,
939    /// The panel's width.
940    pub width: Option<f32>,
941    /// The panel's fill.
942    pub background_color: Option<Color>,
943    /// The hairline on the panel's inner edge.
944    pub border_color: Option<Color>,
945    /// That hairline's thickness; `0.0` removes it.
946    pub border_width: Option<f32>,
947    /// The rounding of the **inner** edge's two corners — the edge that meets the
948    /// content. The outer one stays square against the window.
949    pub radius: Option<f32>,
950    /// How far off the surface the panel sits. `0.0` casts no shadow, and neither does a
951    /// height with no [`shadow_color`](Self::shadow_color).
952    pub elevation: Option<f32>,
953    /// The shadow's colour. Unset, **transparent**, as the reference's Material 3 drawer is
954    /// (its defaults, line 795): a panel is lifted by its tone, and shadowed only on request.
955    pub shadow_color: Option<Color>,
956    /// The scrim behind a modal panel, **alpha included**.
957    pub scrim_color: Option<Color>,
958}
959
960/// Defaults for [`SegmentedButton`](crate::SegmentedButton).
961#[derive(Clone, Copy, Debug, Default, PartialEq)]
962pub struct SegmentedTheme {
963    /// The fill under the chosen segment.
964    pub selected_color: Option<Color>,
965    /// The labels' colour.
966    pub label_color: Option<Color>,
967    /// The chosen segment's label colour, and its checkmark's.
968    pub selected_label_color: Option<Color>,
969    /// The outline's colour, and the hairlines' between segments.
970    pub border_color: Option<Color>,
971    /// Their thickness; `0.0` removes both.
972    pub border_width: Option<f32>,
973    /// The group's outer radius. Unset, the ends are stadium-rounded.
974    pub radius: Option<f32>,
975    /// The control's height.
976    pub height: Option<f32>,
977    /// The room either side of a label.
978    pub padding: Option<f32>,
979    /// The labels' type.
980    pub label_style: Option<TextStyle>,
981    /// The checkmark's size.
982    pub icon_size: Option<f32>,
983    /// Whether the chosen segment carries a checkmark.
984    pub show_selected_icon: Option<bool>,
985}
986
987/// Defaults for [`TabBar`](crate::TabBar) — the bar, its labels and its indicator.
988#[derive(Clone, Copy, Debug, Default, PartialEq)]
989pub struct TabBarTheme {
990    /// Which of the two bars an untold `TabBar::new()` is.
991    pub variant: Option<crate::tabs::TabBarVariant>,
992    /// The indicator's colour.
993    pub indicator_color: Option<Color>,
994    /// Its thickness — and, on a primary bar, the radius of its top corners.
995    pub indicator_weight: Option<f32>,
996    /// The **selected** label's colour.
997    pub label_color: Option<Color>,
998    /// Every other label's colour.
999    pub unselected_label_color: Option<Color>,
1000    /// The labels' type.
1001    pub label_style: Option<TextStyle>,
1002    /// The colour of the hairline between the bar and what it labels.
1003    pub divider_color: Option<Color>,
1004    /// Its thickness; `0.0` removes it.
1005    pub divider_height: Option<f32>,
1006    /// The room on either side of a label.
1007    pub label_padding: Option<f32>,
1008    /// The tabs' height, the indicator excluded.
1009    pub tab_height: Option<f32>,
1010    /// Where the tabs sit when they do not fill the bar.
1011    pub alignment: Option<crate::tabs::TabAlignment>,
1012}
1013
1014/// Defaults for [`TabPageSelector`](crate::TabPageSelector) — the row of dots that says
1015/// which page of several you are on.
1016#[derive(Clone, Copy, Debug, Default, PartialEq)]
1017pub struct PageSelectorTheme {
1018    /// A dot's diameter.
1019    pub size: Option<f32>,
1020    /// The gap between two dots.
1021    pub gap: Option<f32>,
1022    /// What the dot you are on is filled with. Unset, the accent.
1023    pub color: Option<Color>,
1024    /// The ring every dot is drawn with.
1025    pub border_color: Option<Color>,
1026}
1027
1028/// Defaults for [`IconButton`](crate::IconButton).
1029#[derive(Clone, Copy, Debug, Default, PartialEq)]
1030pub struct IconButtonTheme {
1031    /// The box's side.
1032    pub size: Option<f32>,
1033    /// The glyph's size inside it.
1034    pub icon_size: Option<f32>,
1035    /// The surface under the glyph.
1036    pub color: Option<Color>,
1037    /// The glyph's colour.
1038    pub icon_color: Option<Color>,
1039    /// The outline's colour.
1040    pub border_color: Option<Color>,
1041    /// Its thickness; `0.0` removes it.
1042    pub border_width: Option<f32>,
1043    /// The corner radii. Unset, an icon button is a circle.
1044    pub radius: Option<BorderRadius>,
1045    /// **How much room it reserves for a finger** — the reference's per-widget say over
1046    /// the theme's (`icon_button.dart:708`). Unset, the theme's own answer.
1047    pub tap_target: Option<crate::theme::TapTarget>,
1048}
1049
1050/// Defaults for the **ink ripple** — every surface that splashes, including
1051/// [`InkWell`](crate::InkWell) and [`Button`](crate::Button).
1052#[derive(Clone, Copy, Debug, Default, PartialEq)]
1053pub struct InkTheme {
1054    /// The splash colour, **alpha included**. A widget that computes its own from its
1055    /// surface (a button splashing in its `on` colour) still yields to this when it is
1056    /// set: an application that has chosen an ink colour has chosen it everywhere.
1057    pub color: Option<Color>,
1058}
1059
1060/// Defaults for [`SnackBar`](crate::SnackBar).
1061///
1062/// The reference's Material 3 snackbar sets its content in `bodyMedium` and its action in
1063/// `labelLarge`, and both are read from the type scale rather than written down here.
1064#[derive(Clone, Copy, Debug, Default, PartialEq)]
1065pub struct SnackBarTheme {
1066    /// **What shape it is** (`shape_border.dart`), over the plain `radius` below:
1067    /// a snack bar's, which its behaviour already decides — see [`crate::SnackBarBehavior`]. Unset, the widget decides.
1068    pub shape: Option<ShapeBorder>,
1069    /// **Where its bars sit**, and therefore what they look like. Unset, the reference's
1070    /// `Fixed` (`snack_bar.dart:986`). See [`SnackBarBehavior`](crate::SnackBarBehavior).
1071    pub behavior: Option<crate::toast::SnackBarBehavior>,
1072    /// What a **floating** bar keeps clear of the page. Unset, the reference's
1073    /// `insetPadding` (`snack_bar.dart:989`). Silent under `Fixed`.
1074    pub inset_padding: Option<Insets>,
1075    /// A **floating** bar's width, instead of the room it is given. Unset, the room it is
1076    /// given. Silent under `Fixed`, where a width would contradict the behaviour.
1077    pub width: Option<f32>,
1078    /// The message's type.
1079    pub content_text_style: Option<TextStyle>,
1080    /// The action's type.
1081    pub action_text_style: Option<TextStyle>,
1082    /// The bar's surface. Unset, the scheme's `inverse_surface`.
1083    pub background_color: Option<Color>,
1084    /// The message's colour. Unset, the scheme's `on_inverse_surface`.
1085    pub text_color: Option<Color>,
1086    /// The action's colour. Unset, the scheme's `inverse_primary` — the one role in the
1087    /// scheme whose whole reason for existing is being legible on an inverted surface.
1088    pub action_text_color: Option<Color>,
1089    /// The close cross's colour. Unset, the scheme's `on_inverse_surface`
1090    /// (`snack_bar.dart:995`).
1091    pub close_icon_color: Option<Color>,
1092    /// The stripe down a notification's leading edge, whichever kind it is. Unset, the
1093    /// kind decides.
1094    pub accent_color: Option<Color>,
1095    /// The stripe for a **success**, the one kind Material 3 has no role for. Unset, a
1096    /// green of this crate's own choosing.
1097    pub success_color: Option<Color>,
1098    /// The corner. Unset, the reference's 4 for a **floating** bar and **nothing at all**
1099    /// for a fixed one, which is flush against the edges and has nothing to round.
1100    pub radius: Option<f32>,
1101    /// How far off the page it sits. Unset, the reference's 6.
1102    pub elevation: Option<f32>,
1103}
1104
1105/// Defaults for [`PopupMenuButton`](crate::PopupMenuButton) and its items.
1106#[derive(Clone, Copy, Debug, Default, PartialEq)]
1107pub struct MenuTheme {
1108    /// The items' type.
1109    pub text_style: Option<TextStyle>,
1110    /// **The panel's surface.** Unset, `surface_container` — a menu is a distinct area
1111    /// within the surface, not a thing that floats above it in a different colour
1112    /// (`popup_menu.dart:1858`).
1113    pub background: Option<Color>,
1114    /// What shape the panel is.
1115    pub shape: Option<ShapeBorder>,
1116    /// The radius of its corners, for a theme that would rather give the number than the
1117    /// shape. Outranked by [`shape`](Self::shape).
1118    pub radius: Option<f32>,
1119    /// How far off the page the panel sits, in pixels. Unset, three
1120    /// (`popup_menu.dart:1839`).
1121    pub elevation: Option<f32>,
1122    /// The colour of the panel's shadow. Unset, the scheme's shadow at 30 %, the
1123    /// framework's colour for a height; [`Color::TRANSPARENT`] casts none.
1124    pub shadow_color: Option<Color>,
1125    /// The room kept **above and below** the rows, inside the panel
1126    /// (`popup_menu.dart:1872`).
1127    pub padding: Option<Insets>,
1128    /// The room kept either side of a row's label (`popup_menu.dart:1876`).
1129    pub item_padding: Option<Insets>,
1130    /// How tall one row is. Unset, the smallest box a finger can be asked to hit.
1131    pub item_height: Option<f32>,
1132    /// **An outline round the panel.** Unset, none: the panel's shadow says it is off the
1133    /// page. A desktop's menus draw a hairline as well (milestone 636).
1134    pub border: Option<frus_core::BorderSide>,
1135    /// **How much room a separator takes**, its line in the middle. Unset, the divider's own.
1136    pub divider_height: Option<f32>,
1137}
1138
1139impl MenuTheme {
1140    /// This theme, with `other`'s answer wherever this one says nothing.
1141    pub fn or(self, other: MenuTheme) -> MenuTheme {
1142        MenuTheme {
1143            text_style: self.text_style.or(other.text_style),
1144            background: self.background.or(other.background),
1145            shape: self.shape.or(other.shape),
1146            radius: self.radius.or(other.radius),
1147            elevation: self.elevation.or(other.elevation),
1148            shadow_color: self.shadow_color.or(other.shadow_color),
1149            padding: self.padding.or(other.padding),
1150            item_padding: self.item_padding.or(other.item_padding),
1151            item_height: self.item_height.or(other.item_height),
1152            border: self.border.or(other.border),
1153            divider_height: self.divider_height.or(other.divider_height),
1154        }
1155    }
1156
1157    /// **A desktop's menus** (milestone 636): 13 px words on 26 px rows, 12 px either side,
1158    /// 4 px above and below the rows, a 6 px corner, a hairline in `outline_variant`, a low
1159    /// shadow, and 9 px separators — what an application's menus on Linux, macOS and Windows
1160    /// look like, rather than a phone's 48 px rows.
1161    pub fn desktop(theme: &crate::Theme) -> MenuTheme {
1162        MenuTheme {
1163            text_style: Some(TextStyle::new(13.0)),
1164            background: None,
1165            shape: None,
1166            radius: Some(6.0),
1167            elevation: Some(2.0),
1168            shadow_color: None,
1169            padding: Some(Insets::new(4.0, 0.0, 4.0, 0.0)),
1170            item_padding: Some(Insets::new(0.0, 12.0, 0.0, 12.0)),
1171            item_height: Some(26.0),
1172            border: Some(frus_core::BorderSide::new(
1173                theme.scheme.outline_variant,
1174                1.0,
1175            )),
1176            divider_height: Some(9.0),
1177        }
1178    }
1179}
1180
1181/// **The window's three buttons**, as the application paints them on the title bar's line
1182/// (milestone 640). Every colour optional: unset, the desktop's.
1183#[derive(Clone, Copy, Debug, Default, PartialEq)]
1184pub struct CaptionButtonsTheme {
1185    /// The glyphs. Unset, the line's foreground ([`TitleBarTheme`], the system's), then
1186    /// `on_surface`.
1187    pub glyph_color: Option<Color>,
1188    /// The glyphs of a window that is not the active one. Unset, the line's inactive
1189    /// foreground, then the glyph half way to the line.
1190    pub inactive_glyph_color: Option<Color>,
1191    /// Under the pointer. Unset, the glyph at 8 % over the line.
1192    pub hover_color: Option<Color>,
1193    /// Pressed. Unset, the glyph at 4 % over the line.
1194    pub pressed_color: Option<Color>,
1195    /// The close button under the pointer. Unset, the desktop's red.
1196    pub close_hover_color: Option<Color>,
1197    /// The close button pressed. Unset, a darker red.
1198    pub close_pressed_color: Option<Color>,
1199    /// The close glyph on red. Unset, white.
1200    pub close_glyph_hover_color: Option<Color>,
1201}
1202
1203/// **The colours of a [`WindowMenuBar`](crate::WindowMenuBar)'s line** (milestone 642), on the
1204/// title bar's line or under the system's.
1205///
1206/// Every colour optional. Unset, **the system's**: on the title bar's line, the window's
1207/// caption as the desktop draws it — light or dark as the system's apps are, the accent
1208/// colour where the person asked for it on title bars, quieter on a window that is not the
1209/// active one. Where the system says nothing — off the line — the
1210/// [`MenuBarTheme`]'s, then `surface_container` and `on_surface`.
1211///
1212/// What it says reaches everything on the line: the bar's words and their highlight, and the
1213/// window's three buttons' glyphs and washes, unless their own themes say otherwise.
1214#[derive(Clone, Copy, Debug, Default, PartialEq)]
1215pub struct TitleBarTheme {
1216    /// The line's surface.
1217    pub background: Option<Color>,
1218    /// The words and glyphs on it.
1219    pub foreground: Option<Color>,
1220    /// The line's surface on a window that is not the active one. Unset, `background`'s
1221    /// answer for an inactive window.
1222    pub inactive_background: Option<Color>,
1223    /// The words and glyphs on a window that is not the active one. Unset, `foreground`'s
1224    /// answer for an inactive window.
1225    pub inactive_foreground: Option<Color>,
1226}
1227
1228/// Defaults for [`MenuBar`](crate::MenuBar): the bar, the words on it, and the menus it opens
1229/// (milestone 636).
1230///
1231/// Unset, a bar on a desktop theme (Linux, macOS, Windows) is a desktop's — 30 px tall,
1232/// 13 px words 8 px either side, a rounded highlight on the word under the pointer and on
1233/// the word whose menu is open — and on a phone's theme it keeps the 48 px a finger needs.
1234#[derive(Clone, Copy, Debug, Default, PartialEq)]
1235pub struct MenuBarTheme {
1236    /// How tall the bar is.
1237    pub height: Option<f32>,
1238    /// The bar's surface. Unset, `surface_container`.
1239    pub background: Option<Color>,
1240    /// The room at the bar's ends, around the row of words.
1241    pub padding: Option<Insets>,
1242    /// The room either side of a word.
1243    pub item_padding: Option<Insets>,
1244    /// The words' type.
1245    pub text_style: Option<TextStyle>,
1246    /// The words' colour. Unset, `on_surface`.
1247    pub foreground: Option<Color>,
1248    /// The highlight behind the word under the pointer, and behind the word whose menu is
1249    /// open. Unset, `on_surface` over the bar, at 8 % and 12 %.
1250    pub highlight: Option<Color>,
1251    /// The highlight's corners.
1252    pub item_radius: Option<f32>,
1253    /// The room the highlight keeps above and below it, inside the bar.
1254    pub item_inset: Option<f32>,
1255    /// **The menus the bar opens.** What this says outranks the desktop's menus and then the
1256    /// application's [`MenuTheme`].
1257    pub menus: MenuTheme,
1258}
1259
1260/// Defaults for [`DropdownButton`](crate::DropdownButton) and its options.
1261///
1262/// The `menu_*` fields are the **panel** its options float on. They are this theme's and
1263/// not [`MenuTheme`]'s, because the reference's dropdown button does not read the menu
1264/// theme either: its list has its own height, eight, where a menu has three.
1265#[derive(Clone, Copy, Debug, Default, PartialEq)]
1266pub struct DropdownTheme {
1267    /// The value's and the options' type.
1268    pub text_style: Option<TextStyle>,
1269    /// The panel's surface. Unset, `surface_container`, the popup menu's.
1270    pub menu_background: Option<Color>,
1271    /// What shape the panel is.
1272    pub menu_shape: Option<ShapeBorder>,
1273    /// The radius of the panel's corners, for a theme that would rather give the number
1274    /// than the shape. Outranked by [`menu_shape`](Self::menu_shape).
1275    pub menu_radius: Option<f32>,
1276    /// How far off the page the panel sits, in pixels. Unset, eight, the reference's.
1277    pub menu_elevation: Option<f32>,
1278    /// The colour of the panel's shadow. Unset, the scheme's shadow at 30 %;
1279    /// [`Color::TRANSPARENT`] casts none.
1280    pub menu_shadow_color: Option<Color>,
1281    /// The room kept **above and below** the options, inside the panel. Unset, eight.
1282    pub menu_padding: Option<Insets>,
1283}
1284
1285/// Defaults for [`Autocomplete`](crate::Autocomplete) and its suggestions.
1286#[derive(Clone, Copy, Debug, Default, PartialEq)]
1287pub struct AutocompleteTheme {
1288    /// The suggestions' type.
1289    pub text_style: Option<TextStyle>,
1290}
1291
1292/// Defaults for [`Table`](crate::Table).
1293///
1294/// The reference names the two apart — a heading is `titleSmall`, a cell is `bodyMedium` —
1295/// which is why one `text_style` would be the wrong shape here.
1296#[derive(Clone, Copy, Debug, Default, PartialEq)]
1297pub struct TableTheme {
1298    /// The column headings' type.
1299    pub heading_text_style: Option<TextStyle>,
1300    /// The cells' type.
1301    pub data_text_style: Option<TextStyle>,
1302}
1303
1304/// Defaults for [`DatePicker`](crate::DatePicker).
1305#[derive(Clone, Copy, Debug, Default, PartialEq)]
1306pub struct DatePickerTheme {
1307    /// The days' type.
1308    pub day_text_style: Option<TextStyle>,
1309    /// The weekday initials above them.
1310    pub weekday_text_style: Option<TextStyle>,
1311}
1312
1313/// Defaults for the **scrollbars** the framework draws over a scrollable.
1314///
1315/// The thumb is a **fade of one colour**, not three colours: at rest, warmed towards its
1316/// hovered value under a pointer, and at its held value the moment it is grabbed. A theme
1317/// substitutes the colour being faded and may move the three levels; it does not hand
1318/// over three finished colours, because interpolating between those means interpolating
1319/// colours and this framework has been bitten by doing that in the wrong space more than
1320/// once. Fading one colour is the arithmetic that was already right.
1321///
1322/// There is no field for a **track**. The framework paints none — the reference's is
1323/// transparent unless a caller asks for one — and a field that painted nothing whatever
1324/// it was set to would be worse than no field.
1325#[derive(Clone, Copy, Debug, Default, PartialEq)]
1326pub struct ScrollbarTheme {
1327    /// The thumb's thickness. Unset, 8, or 4 under a theme whose platform is Android —
1328    /// and it is the **thumb's**, not a slot's: the margin holds it clear of the edge
1329    /// rather than shrinking it to fit.
1330    pub thickness: Option<f32>,
1331    /// How far the thumb is held clear of the edge it runs along. Unset, 2, or none under
1332    /// an Android theme.
1333    pub margin: Option<f32>,
1334    /// How short the thumb is allowed to get on a very long page. Unset, 48 — below
1335    /// which it stops reading as a handle and stops being one to grab.
1336    pub min_thumb_length: Option<f32>,
1337    /// The thumb's corner radius. Unset, half its thickness, which is a pill — or square
1338    /// ends under an Android theme.
1339    pub radius: Option<f32>,
1340    /// The colour the thumb is a fade of. Unset, the scheme's `on_surface`.
1341    pub thumb_color: Option<Color>,
1342    /// Its opacity at rest. Unset, 0.30 on a dark surface and 0.10 on a light one; under an
1343    /// Android theme, with no `thumb_color` either, the thumb rests an opaque grey instead.
1344    pub opacity: Option<f32>,
1345    /// Its opacity with a pointer near it, which it warms towards. Unset, 0.65 and 0.50.
1346    pub hover_opacity: Option<f32>,
1347    /// Its opacity while it is held. Unset, 0.75 and 0.60. This one does not fade in —
1348    /// the hand is already on it, and a fade would only lag behind the grab.
1349    pub drag_opacity: Option<f32>,
1350    /// Whether a pointer can wake, hover and drag the thumb. Unset, it can — except under
1351    /// a theme whose platform is Android, where the bar only shows where the page is
1352    /// (`scrollbar.dart:218`).
1353    pub interactive: Option<bool>,
1354}
1355
1356/// Defaults for [`NavigationBar`](crate::NavigationBar) — the bar at the **head** of a
1357/// screen, not the one along the bottom, which is `NavRailTheme`.
1358///
1359/// There is no field for the back button's size. The bar names one explicitly when it
1360/// builds the button, and an explicit argument outranks a theme — a field here would be
1361/// one that the widget never reads, which is worse than an absent one. A theme that wants
1362/// a different back button sets [`IconButtonTheme`] and drops the bar's own override.
1363#[derive(Clone, Copy, Debug, Default, PartialEq)]
1364pub struct NavBarTheme {
1365    /// The room the bar takes. Unset, 56 — the reference's toolbar height.
1366    pub height: Option<f32>,
1367    /// The bar's own padding. Unset, nothing above or below and a leading inset that
1368    /// clears the back-gesture zone, so the button stays clickable without starting a
1369    /// swipe.
1370    pub padding: Option<Insets>,
1371    /// What it is painted on. Unset, the theme's background.
1372    pub background: Option<Color>,
1373    /// The title's type. Unset, the theme's `title_large` — a bar title is a *title*, not
1374    /// body text. Its `color`, when set, is the title's colour.
1375    pub title_style: Option<TextStyle>,
1376    /// The hairline along the bottom edge. Unset, the scheme's `outline_variant`.
1377    pub divider_color: Option<Color>,
1378    /// How thick that hairline is. Unset, one pixel.
1379    pub divider_thickness: Option<f32>,
1380}
1381
1382/// Defaults for [`NavigationRail`](crate::NavigationRail) and [`BottomBar`](crate::BottomBar).
1383#[derive(Clone, Debug, Default, PartialEq)]
1384pub struct NavRailTheme {
1385    /// **The shape of a selected destination's indicator** (`navigation_rail.dart:1148`).
1386    /// Unset, the reference's pill.
1387    pub indicator_shape: Option<frus_core::ShapeBorder>,
1388    /// **The highlight over a destination, per state** — over the framework's own state
1389    /// layer, and under the destination's own word (`navigation_bar.dart:232`).
1390    ///
1391    /// The first field here that is not a plain value but a
1392    /// [`WidgetStateProperty`](crate::WidgetStateProperty). It owns a `Vec`, which is why
1393    /// this struct and [`WidgetThemes`] stopped being `Copy` in milestone 448 — a theme
1394    /// that weighed eight kilobytes had no business being copied implicitly anyway.
1395    pub overlay_color: Option<crate::widgetstate::WidgetStateProperty<frus_core::Color>>,
1396    /// The destinations' labels.
1397    pub label_text_style: Option<TextStyle>,
1398    /// The count carried by a destination's badge.
1399    pub badge_text_style: Option<TextStyle>,
1400    /// The **rail's** surface. Unset, the scheme's `surface`.
1401    pub background_color: Option<Color>,
1402    /// The pill behind the selected destination. Unset, the scheme's
1403    /// `secondary_container` — **opaque**, as the reference's is.
1404    pub indicator_color: Option<Color>,
1405    /// The selected destination's glyph. Unset, `on_secondary_container`: it is drawn on
1406    /// the indicator, so it takes the indicator's content colour.
1407    pub selected_icon_color: Option<Color>,
1408    /// An unselected destination's glyph. Unset, `on_surface_variant`.
1409    pub unselected_icon_color: Option<Color>,
1410    /// The selected destination's label. Unset, `on_surface` — the label sits *below* the
1411    /// indicator rather than on it, which is why it is not the indicator's content colour.
1412    pub selected_label_color: Option<Color>,
1413    /// An unselected destination's label. Unset, `on_surface` on a rail and
1414    /// `on_surface_variant` on a bar, which is the one place the reference gives the two
1415    /// different answers.
1416    pub unselected_label_color: Option<Color>,
1417    /// The destinations' glyphs. Unset, the reference's 24.
1418    pub icon_size: Option<f32>,
1419    /// Whether a **rail's** selected destination gets an indicator behind its glyph.
1420    /// Unset, `true` (`navigation_rail.dart:1233`). `false` is the arrangement that
1421    /// predates it, where the selected destination says so in the accent instead.
1422    pub use_indicator: Option<bool>,
1423    /// How far off the page a **rail** sits. Unset, `0` — flat, as the reference's is
1424    /// (`navigation_rail.dart:1236`), the rail being separated by a rule and not by a
1425    /// shadow.
1426    pub elevation: Option<f32>,
1427    /// The **bottom bar's** surface, a rung higher than the rail's. Unset, the scheme's
1428    /// `surface_container`.
1429    ///
1430    /// The reference keeps a theme object per navigation widget and gives them different
1431    /// defaults; this crate keeps one for the two, so the two surfaces are two fields
1432    /// rather than two structs.
1433    pub bar_background_color: Option<Color>,
1434}
1435
1436/// Defaults for [`SearchBar`](crate::SearchBar) — the reference's `SearchBarThemeData`.
1437///
1438/// Every field there is a `WidgetStateProperty`, resolved against the bar's own state; here
1439/// they are plain values, with one exception that matters and several that do not. The
1440/// exception is the highlight, and this framework already answers it: a state layer is one
1441/// rule applied from the ground toward the ink, so a bar does not need a colour per state
1442/// to light under a pointer. The rest — a fill, a shape or a padding that changes when the
1443/// bar is hovered — are things the reference *can* say and its own defaults never do.
1444#[derive(Clone, Copy, Debug, Default, PartialEq)]
1445pub struct SearchBarTheme {
1446    /// The bar's fill. Unset, the scheme's `surface_container_high`
1447    /// (`search_anchor.dart:1859`).
1448    pub background_color: Option<Color>,
1449    /// The colour its shadow is cast in, **alpha included**. Unset, the scheme's `shadow`
1450    /// at 30 %.
1451    pub shadow_color: Option<Color>,
1452    /// How far off the page it sits. Unset, `6` (`search_anchor.dart:1863`).
1453    pub elevation: Option<f32>,
1454    /// Its shape. Unset, a stadium (`search_anchor.dart:1892`).
1455    pub shape: Option<frus_core::ShapeBorder>,
1456    /// Its corners, read as a rounded rectangle when no shape was named.
1457    pub radius: Option<BorderRadius>,
1458    /// An outline around it. Unset, none — the reference has no default side
1459    /// (`search_anchor.dart:1888`), a raised surface being told apart by its shadow.
1460    pub side: Option<frus_core::BorderSide>,
1461    /// The room inside it. Unset, `8` either side, applied twice (once around the row and
1462    /// once around the field), which is where the sixteen between an icon and the first
1463    /// letter comes from.
1464    pub padding: Option<frus_core::Insets>,
1465    /// The narrowest it will go. Unset, `360`.
1466    pub min_width: Option<f32>,
1467    /// The widest. Unset, `800`.
1468    pub max_width: Option<f32>,
1469    /// Its height. Unset, `56`.
1470    pub height: Option<f32>,
1471    /// The value's type. Unset, `bodyLarge` in `on_surface`.
1472    pub text_style: Option<TextStyle>,
1473    /// The hint's. Unset, the value's, then `bodyLarge` in `on_surface_variant` — the
1474    /// reference falls back to the value's style **before** its own default, so a bar told
1475    /// to use one type does not say its hint in another.
1476    pub hint_style: Option<TextStyle>,
1477}
1478
1479/// Defaults for the view a [`SearchAnchor`](crate::SearchAnchor) opens — the reference's
1480/// `SearchViewThemeData`.
1481///
1482/// Kept apart from [`SearchBarTheme`] because the reference keeps them apart, and because
1483/// the two disagree where it matters: the view's shape depends on whether it is the screen,
1484/// its header is taller than a bar when it is, and it has a rule under that header that a
1485/// bar has nothing to do with.
1486#[derive(Clone, Copy, Debug, Default, PartialEq)]
1487pub struct SearchViewTheme {
1488    /// The view's fill. Unset, the scheme's `surface_container_high`
1489    /// (`search_anchor.dart:1936`) — the same rung the bar takes, so one grows out of the
1490    /// other rather than changing colour on the way.
1491    pub background_color: Option<Color>,
1492    /// How far off the page it sits. Unset, `6`.
1493    pub elevation: Option<f32>,
1494    /// Its shape. Unset, corners of `28` floating and square full-screen.
1495    pub shape: Option<frus_core::ShapeBorder>,
1496    /// Its corners, read as a rounded rectangle when no shape was named.
1497    pub radius: Option<BorderRadius>,
1498    /// An outline around it. Unset, none (`search_anchor.dart:1944`).
1499    pub side: Option<frus_core::BorderSide>,
1500    /// The rule under the header. Unset, the scheme's `outline`
1501    /// (`search_anchor.dart:1967`).
1502    pub divider_color: Option<Color>,
1503    /// The header's height. Unset, `72` full-screen and a bar's `56` floating.
1504    pub header_height: Option<f32>,
1505    /// The header's type. Unset, `bodyLarge` in `on_surface`.
1506    pub header_text_style: Option<TextStyle>,
1507    /// The header hint's. Unset, `bodyLarge` in `on_surface_variant`.
1508    pub header_hint_style: Option<TextStyle>,
1509    /// The narrowest the view goes. Unset, `360`.
1510    pub min_width: Option<f32>,
1511    /// The shortest. Unset, `240`.
1512    pub min_height: Option<f32>,
1513    /// The room either side of the header's contents. Unset, `8`.
1514    pub bar_padding: Option<frus_core::Insets>,
1515}
1516
1517/// Defaults for [`NavigationDrawer`](crate::NavigationDrawer).
1518///
1519/// The reference keeps `NavigationDrawerThemeData` apart from the rail's and the bar's
1520/// (`navigation_drawer_theme.dart`) and this follows it, where [`NavRailTheme`] holds two
1521/// widgets at once. The reason is the defaults: a drawer's surface, type step, indicator
1522/// size and tile height all differ from a rail's, and a shared struct would have had to
1523/// carry a second copy of every one of them.
1524#[derive(Clone, Debug, Default, PartialEq)]
1525pub struct NavDrawerTheme {
1526    /// The panel's surface. Unset, the scheme's `surface_container_low`
1527    /// (`navigation_drawer.dart:740`) — a rung above a rail's `surface`, the drawer being
1528    /// a sheet over the page rather than a strip beside it.
1529    pub background_color: Option<Color>,
1530    /// How far off the page the panel sits. Unset, `1` (`navigation_drawer.dart:729`).
1531    pub elevation: Option<f32>,
1532    /// The shadow's colour. Unset, **transparent**, as the reference's navigation drawer
1533    /// defaults are (line 746): the panel's height is its tone, and a shadow is cast only
1534    /// where a colour is named.
1535    pub shadow_color: Option<Color>,
1536    /// The pill behind the selected destination. Unset, the scheme's
1537    /// `secondary_container`.
1538    pub indicator_color: Option<Color>,
1539    /// That pill's shape. Unset, a stadium (`navigation_drawer.dart:731`).
1540    pub indicator_shape: Option<frus_core::ShapeBorder>,
1541    /// That pill's box, `(width, height)`. Unset, `336 x 56`
1542    /// (`navigation_drawer.dart:732`). The width is a **ceiling**: the pill never grows
1543    /// past the room its tile has, which is what keeps 336 inside a 304-wide panel.
1544    pub indicator_size: Option<(f32, f32)>,
1545    /// The highlight over a destination, per state — over the framework's state layer, and
1546    /// under the destination's own word.
1547    pub overlay_color: Option<crate::widgetstate::WidgetStateProperty<frus_core::Color>>,
1548    /// A tile's height. Unset, `56` (`navigation_drawer.dart:730`).
1549    pub tile_height: Option<f32>,
1550    /// The room either side of a tile. Unset, `12` left and right
1551    /// (`navigation_drawer.dart:69`).
1552    pub tile_padding: Option<frus_core::Insets>,
1553    /// The destinations' glyphs. Unset, `24`.
1554    pub icon_size: Option<f32>,
1555    /// The destinations' labels. Unset, `labelLarge` (`navigation_drawer.dart:768`) — a
1556    /// step above a rail's, a drawer's label having a line to itself.
1557    pub label_text_style: Option<TextStyle>,
1558    /// The count a destination carries. Unset, `labelSmall`.
1559    pub badge_text_style: Option<TextStyle>,
1560    /// A selected destination's glyph **and** label. Unset, `on_secondary_container`:
1561    /// here the indicator is the whole row, so both stand on it and both take its content
1562    /// colour — the one place this differs from a rail, where the label sits outside the
1563    /// pill.
1564    pub selected_color: Option<Color>,
1565    /// An unselected destination's glyph and label. Unset, `on_surface_variant`.
1566    pub unselected_color: Option<Color>,
1567}
1568
1569/// Defaults for [`Alert`](crate::Alert) — the message box, not the dialog. See
1570/// [`DialogTheme`] for [`AlertDialog`](crate::AlertDialog).
1571#[derive(Clone, Copy, Debug, Default, PartialEq)]
1572pub struct AlertTheme {
1573    /// The heading's type.
1574    pub title_text_style: Option<TextStyle>,
1575    /// The message's type.
1576    pub content_text_style: Option<TextStyle>,
1577}
1578
1579/// Defaults for [`BottomAppBar`](crate::BottomAppBar).
1580#[derive(Clone, Copy, Debug, Default, PartialEq)]
1581pub struct BottomAppBarTheme {
1582    /// The bar's surface. Unset, the scheme's `surface_container`.
1583    pub color: Option<Color>,
1584}
1585
1586/// Defaults for [`BottomSheet`](crate::BottomSheet).
1587#[derive(Clone, Copy, Debug, Default, PartialEq)]
1588pub struct BottomSheetTheme {
1589    /// **What shape the sheet is** — the reference's `BottomSheetThemeData.shape`. Unset,
1590    /// the framework rounds the **top** corners only: the bottom edge is flush against the
1591    /// window, and rounding it would cut two notches out of the screen.
1592    pub shape: Option<ShapeBorder>,
1593    /// The radius of those top two corners, for a theme that would rather give the number
1594    /// than the shape. Outranked by [`shape`](Self::shape).
1595    pub radius: Option<f32>,
1596    /// The sheet's surface. Unset, the scheme's `surface_container_low`.
1597    pub background_color: Option<Color>,
1598}
1599
1600/// Defaults for [`MaterialBanner`](crate::MaterialBanner).
1601#[derive(Clone, Copy, Debug, Default, PartialEq)]
1602pub struct BannerTheme {
1603    /// The banner's colour. Unset, a low container tone.
1604    pub color: Option<Color>,
1605    /// What the surface is tinted towards for its elevation. Unset, nothing is.
1606    pub surface_tint: Option<Color>,
1607    /// The shadow's colour. Unset, there is none.
1608    pub shadow_color: Option<Color>,
1609    /// The rule along the bottom, drawn only where the banner is flat. Unset, the
1610    /// scheme's `outlineVariant`.
1611    pub divider_color: Option<Color>,
1612    /// How far off the page it sits. Unset, `0`: flat, with a rule under it — see
1613    /// [`BANNER_ELEVATION`](crate::BANNER_ELEVATION).
1614    pub elevation: Option<f32>,
1615    /// The padding around the message row. Unset, it depends on where the actions went.
1616    pub padding: Option<Insets>,
1617    /// The margin around the banner. Unset, it depends on the elevation.
1618    pub margin: Option<Insets>,
1619    /// What the leading slot is held off the message by. Unset, 16 at its trailing edge.
1620    pub leading_padding: Option<Insets>,
1621    /// What the message is set in. Unset, `bodyMedium`.
1622    pub content_text_style: Option<TextStyle>,
1623}
1624
1625/// Defaults for [`Dialog`](crate::Dialog) and [`AlertDialog`](crate::AlertDialog).
1626#[derive(Clone, Copy, Debug, Default, PartialEq)]
1627pub struct DialogTheme {
1628    /// The surface's colour. Unset, the scheme's `surfaceContainerHigh`.
1629    pub color: Option<Color>,
1630    /// How far off the page it sits. Unset, 6.
1631    pub elevation: Option<f32>,
1632    /// The shadow's colour. Unset, **transparent**: the reference's Material 3 dialog
1633    /// shows its height by its container tone, not by a drop shadow.
1634    pub shadow_color: Option<Color>,
1635    /// What the surface is tinted towards for its elevation. Unset, nothing is — see
1636    /// [`Self::shadow_color`] for why.
1637    pub surface_tint: Option<Color>,
1638    /// **What shape it is** (`dialog.dart`). Unset, a 28-radius rounded rectangle.
1639    ///
1640    /// It carried the reference's *name* and a `BorderRadius` until milestone 451, which
1641    /// is the deviation this field is the clearest case of: `shape` in the reference is a
1642    /// `ShapeBorder`, and a corner radius is one of the things one can be.
1643    pub shape: Option<ShapeBorder>,
1644    /// How far the dialog is held off the window's edges. Unset, 40 across and 24 down.
1645    pub inset_padding: Option<Insets>,
1646    /// The glyph above the title. Unset, the scheme's `secondary`.
1647    pub icon_color: Option<Color>,
1648    /// The heading's type. Unset, `headlineSmall`.
1649    pub title_text_style: Option<TextStyle>,
1650    /// What the dialog says. Unset, `bodyMedium`.
1651    pub content_text_style: Option<TextStyle>,
1652}
1653
1654/// Defaults for [`Steps`](crate::Steps).
1655#[derive(Clone, Copy, Debug, Default, PartialEq)]
1656pub struct StepsTheme {
1657    /// The caption under each marker.
1658    pub label_text_style: Option<TextStyle>,
1659    /// The number inside the marker. It is a **glyph on a circle** rather than something
1660    /// read at length, so the framework's own default does not pass through the reader's
1661    /// font setting — a theme that sets this one takes that decision back.
1662    pub index_text_style: Option<TextStyle>,
1663}
1664
1665/// Defaults for [`Tree`](crate::Tree).
1666#[derive(Clone, Copy, Debug, Default, PartialEq)]
1667pub struct TreeTheme {
1668    /// The rows' type.
1669    pub text_style: Option<TextStyle>,
1670}
1671
1672/// Defaults for [`Timeline`](crate::Timeline).
1673#[derive(Clone, Copy, Debug, Default, PartialEq)]
1674pub struct TimelineTheme {
1675    /// An entry's heading.
1676    pub title_text_style: Option<TextStyle>,
1677    /// The line under it.
1678    pub detail_text_style: Option<TextStyle>,
1679}
1680
1681/// Defaults for [`Breadcrumb`](crate::Breadcrumb).
1682///
1683/// The reference has no breadcrumb, so its type is argued rather than read: a trail is a
1684/// **secondary** line of navigation above the page's own content, which is `bodyMedium`.
1685#[derive(Clone, Copy, Debug, Default, PartialEq)]
1686pub struct BreadcrumbTheme {
1687    /// The segments' type, the separators included.
1688    pub text_style: Option<TextStyle>,
1689}
1690
1691/// Defaults for [`ReorderableList`](crate::ReorderableList)'s grip.
1692///
1693/// The grip is the one part of a reorderable list the framework draws itself, so it is the
1694/// one part a theme has anything to say about — the rows are the application's.
1695#[derive(Clone, Copy, Debug, Default, PartialEq)]
1696pub struct ReorderableTheme {
1697    /// The glyph the grip carries.
1698    pub handle_icon: Option<crate::IconData>,
1699    /// Its colour, over the muted foreground.
1700    pub handle_color: Option<Color>,
1701    /// Its size.
1702    pub handle_icon_size: Option<f32>,
1703}
1704
1705/// Defaults for [`Kanban`](crate::Kanban).
1706///
1707/// The reference has no board, so its type is argued: a card is a small piece of content
1708/// read on its own, which is `bodyLarge` — the step the reference gives a list tile's title
1709/// and this framework already gives a `Tree` row.
1710#[derive(Clone, Copy, Debug, Default, PartialEq)]
1711pub struct KanbanTheme {
1712    /// A text card's label.
1713    pub card_text_style: Option<TextStyle>,
1714    /// A column's heading over its cards.
1715    pub column_title_text_style: Option<TextStyle>,
1716}
1717
1718/// Defaults for [`Form`](crate::Form)'s error summary.
1719///
1720/// The reference has no summary list, so its type is argued: a bullet restates an error
1721/// already shown under its field, which is `bodySmall` — the step the reference gives the
1722/// helper line those errors come from.
1723#[derive(Clone, Copy, Debug, Default, PartialEq)]
1724pub struct FormTheme {
1725    /// A summary bullet's type.
1726    pub bullet_text_style: Option<TextStyle>,
1727    /// The summary's heading over them.
1728    pub summary_title_text_style: Option<TextStyle>,
1729}
1730
1731/// Defaults for [`TimePicker`](crate::TimePicker) and [`TimeRange`](crate::TimeRange).
1732///
1733/// The reference names three of these four. Its dial is `bodyLarge`, its help line
1734/// `labelMedium`, and its day period `titleMedium` — which is the same step as the dial and
1735/// is why the AM/PM cells here are simply cells. The **preview** is the fourth: the
1736/// reference puts an editable pair of fields at `displayMedium` where this widget shows one
1737/// read-only line above two grids, so it takes the heading step that line is.
1738#[derive(Clone, Copy, Debug, Default, PartialEq)]
1739pub struct TimePickerTheme {
1740    /// The hour, minute and AM/PM cells.
1741    pub dial_text_style: Option<TextStyle>,
1742    /// The `HH:MM` line above them.
1743    pub preview_text_style: Option<TextStyle>,
1744    /// The "Hour" and "Minute" lines over each grid.
1745    pub help_text_style: Option<TextStyle>,
1746    /// The "Start" and "End" headings of a [`TimeRange`](crate::TimeRange).
1747    pub range_label_text_style: Option<TextStyle>,
1748}
1749
1750/// Defaults for [`Kbd`](crate::Kbd).
1751///
1752/// The reference has no key cap, so its type is argued rather than read: a shortcut hint is
1753/// a **label**, and a key cap is the one place in this framework where the glyphs stand for
1754/// what is printed on a keyboard, hence the monospaced default.
1755#[derive(Clone, Copy, Debug, Default, PartialEq)]
1756pub struct KbdTheme {
1757    /// The cap's label.
1758    pub text_style: Option<TextStyle>,
1759}
1760
1761/// Defaults for [`ToggleButtons`](crate::ToggleButtons).
1762///
1763/// The three border colours are named apart although the reference defaults all three to
1764/// the same value (`toggle_buttons.dart:601`, `:635`, `:651`): a bank is one object and
1765/// its outline does not break up because one of its buttons is on, but an application
1766/// that wants the chosen pair marked out needs somewhere to say so.
1767#[derive(Clone, Debug, Default, PartialEq)]
1768pub struct ToggleButtonsTheme {
1769    /// What is drawn in a button that is off — its text, its glyph, anything under it
1770    /// that reads the theme for a colour.
1771    pub color: Option<Color>,
1772    /// The same, for a button that is on.
1773    pub selected_color: Option<Color>,
1774    /// The same, when the bank cannot be pressed.
1775    pub disabled_color: Option<Color>,
1776    /// A button's ground, per state. A plain colour — `WidgetStateProperty::all` —
1777    /// fills every button rather than only the ones that are on, as the reference's does.
1778    pub fill_color: Option<crate::WidgetStateProperty<Color>>,
1779    /// The hairline around and beside a button that is off.
1780    pub border_color: Option<Color>,
1781    /// The hairline around and beside a button that is on. A seam between an on button and
1782    /// an off one takes this, from either side.
1783    pub selected_border_color: Option<Color>,
1784    /// The hairline when the bank cannot be pressed.
1785    pub disabled_border_color: Option<Color>,
1786    /// The hairline's width, around the bank and between its buttons.
1787    pub border_width: Option<f32>,
1788    /// The **bank's** corners: the buttons at its two ends take a pair each, and the ones
1789    /// between them are square.
1790    pub border_radius: Option<BorderRadius>,
1791    /// The type any text in the bank takes. Its colour is ignored — that is the three
1792    /// content colours' to answer, and it depends on the state.
1793    pub text_style: Option<TextStyle>,
1794    /// The smallest a button may be across.
1795    pub min_width: Option<f32>,
1796    /// And down. Untold, both are the tap target.
1797    pub min_height: Option<f32>,
1798    /// The largest. Untold, unbounded.
1799    pub max_width: Option<f32>,
1800    /// Likewise.
1801    pub max_height: Option<f32>,
1802}
1803
1804/// Defaults for [`GridTileBar`](crate::GridTileBar).
1805///
1806/// The two heights are two fields and not one, because it is the **content** that picks
1807/// between them: a bar with a title and a subtitle is 68 and any other is 48. An
1808/// application setting one has not thereby said anything about the other.
1809#[derive(Clone, Copy, Debug, Default, PartialEq)]
1810pub struct GridTileBarTheme {
1811    /// What is drawn behind the words. Untold, nothing at all: the picture is the
1812    /// background, which is the arrangement the widget is for.
1813    pub background_color: Option<Color>,
1814    /// The colour of the words and glyphs on it. Untold, white — the bar stands on a
1815    /// photograph, and a photograph has no brightness the theme knows about.
1816    pub foreground_color: Option<Color>,
1817    /// The title's type. Untold, the scale's `title_medium`.
1818    pub title_style: Option<TextStyle>,
1819    /// The subtitle's. Untold, `body_small`.
1820    pub subtitle_style: Option<TextStyle>,
1821    /// How tall a bar carrying one line is.
1822    pub height: Option<f32>,
1823    /// How tall one carrying a title **and** a subtitle is.
1824    pub two_line_height: Option<f32>,
1825}
1826
1827#[cfg(test)]
1828mod tests {
1829    /// **The rungs a shape is resolved on**, and the third one in particular: a radius
1830    /// was all a theme could say until milestone 450, and applications have written them.
1831    /// A theme naming a `shape` outranks one naming only a `radius`; naming both is
1832    /// naming the shape.
1833    #[test]
1834    fn a_shape_outranks_a_radius_and_a_caller_outranks_both() {
1835        use super::{resolve_shape, BorderRadius, ShapeBorder};
1836        let fallback = ShapeBorder::stadium();
1837        let told = ShapeBorder::circle();
1838        let themed = ShapeBorder::beveled(3.0);
1839
1840        assert_eq!(resolve_shape(None, None, None, fallback), fallback);
1841        assert_eq!(
1842            resolve_shape(None, None, Some(BorderRadius::uniform(4.0)), fallback),
1843            ShapeBorder::rounded(4.0),
1844            "a plain radius still speaks"
1845        );
1846        assert_eq!(
1847            resolve_shape(
1848                None,
1849                Some(themed),
1850                Some(BorderRadius::uniform(4.0)),
1851                fallback
1852            ),
1853            themed,
1854            "and a shape beside it outranks it"
1855        );
1856        assert_eq!(
1857            resolve_shape(
1858                Some(told),
1859                Some(themed),
1860                Some(BorderRadius::uniform(4.0)),
1861                fallback
1862            ),
1863            told,
1864            "and the caller outranks the theme"
1865        );
1866    }
1867
1868    use super::*;
1869    use crate::card::{Card, CardVariant, CARD_MARGIN};
1870    use crate::divider::{Divider, DIVIDER_SPACE};
1871    use crate::flex::Flex;
1872    use crate::runtime::Runtime;
1873    use crate::theme::{TextTheme, Theme};
1874    use crate::ui::build_ui;
1875    use crate::widget::Widget;
1876    use frus_core::{Color, Primitive, Rect, Size};
1877
1878    #[derive(Clone, Debug, PartialEq)]
1879    enum Msg {}
1880
1881    /// Builds the widget **inside a sized parent**, which is where a margin and an
1882    /// `Auto` width mean anything, and returns the frame's primitives.
1883    fn framed(widget: impl Widget<Msg> + 'static, theme: &Theme) -> Vec<Primitive> {
1884        let tree = Flex::column().width(200.0).height(100.0).child(widget);
1885        build_ui(&tree, Size::new(200.0, 100.0), &Runtime::default(), theme)
1886            .scene()
1887            .primitives()
1888            .to_vec()
1889    }
1890
1891    /// The first crisp box the tree paints — the widget's own surface, past any shadow.
1892    fn painted_rect(widget: impl Widget<Msg> + 'static, theme: &Theme) -> Rect {
1893        framed(widget, theme)
1894            .iter()
1895            .find_map(|p| match p {
1896                Primitive::Rect { rect, blur, .. } if *blur == 0.0 => Some(*rect),
1897                _ => None,
1898            })
1899            .expect("the widget paints a box")
1900    }
1901
1902    #[test]
1903    fn the_theme_reaches_layout_and_not_only_paint() {
1904        // The point of `style_themed`. A divider's *height* is a layout property, and a
1905        // theme that could not set it would be able to recolour a separator but not make
1906        // one thin — which is the setting an application actually asks for.
1907        let plain = Theme::default();
1908        let mut thin = Theme::default();
1909        thin.widgets.divider.height = Some(1.0);
1910
1911        // The line is centred in its box, so a taller box puts it further down; the box
1912        // itself is what the theme moved.
1913        // The line is centred in its box: a 16 px box puts it at 7.5, a 1 px box at 0.
1914        let tall = painted_rect(Divider::new(), &plain);
1915        let thin_line = painted_rect(Divider::new(), &thin);
1916        assert!(
1917            tall.y > thin_line.y,
1918            "the themed divider's box is shorter: {tall:?} against {thin_line:?}"
1919        );
1920        assert_eq!(tall.height, 1.0, "the line itself is unchanged");
1921        assert_eq!(thin_line.height, 1.0);
1922    }
1923
1924    #[test]
1925    fn the_caller_outranks_the_theme_and_the_theme_outranks_the_framework() {
1926        // The whole chain in one assertion per link, on a layout property so that both
1927        // halves of the resolution are exercised.
1928        let plain = Theme::default();
1929        let mut themed = Theme::default();
1930        themed.widgets.card.margin = Some(20.0);
1931
1932        let x = |card: Card<Msg>, theme: &Theme| painted_rect(card, theme).x;
1933
1934        assert_eq!(x(Card::new(), &plain), CARD_MARGIN, "the framework's");
1935        assert_eq!(x(Card::new(), &themed), 20.0, "the theme's");
1936        assert_eq!(
1937            x(Card::new().margin(2.0), &themed),
1938            2.0,
1939            "the caller's, over the theme's"
1940        );
1941    }
1942
1943    #[test]
1944    fn a_theme_can_change_what_an_untold_widget_is() {
1945        // Not just a number: the *variant* is themeable, so an application can be flat
1946        // throughout without writing `.filled()` at every call site.
1947        let mut flat = Theme::default();
1948        flat.widgets.card.variant = Some(CardVariant::Filled);
1949        let shadows = |theme: &Theme| crate::shadowprobe::casts(&framed(Card::<Msg>::new(), theme));
1950        assert_eq!(shadows(&Theme::default()), 1, "elevated by default");
1951        assert_eq!(shadows(&flat), 0, "flat because the theme says so");
1952    }
1953
1954    #[test]
1955    fn the_ink_colour_is_themeable_even_where_a_widget_computes_its_own() {
1956        // A `Button` derives its splash from its own `on` colour, which is the right
1957        // default and the wrong thing to insist on: an application that has chosen an
1958        // ink colour has chosen it for every surface.
1959        let mine = Color::rgb8(255, 0, 128);
1960        let mut theme = Theme::default();
1961        theme.widgets.ink.color = Some(mine);
1962        // An enabled one: a button with nothing to do is disabled, and does not splash.
1963        let button = crate::button::Button::<()>::new("Go").on_press(());
1964        assert_eq!(
1965            Widget::<()>::ink(&button, &theme).map(|i| i.color),
1966            Some(mine)
1967        );
1968        // And a plain surface takes it too.
1969        assert_eq!(crate::ink::default_splash(&theme), mine);
1970    }
1971
1972    #[test]
1973    fn changing_a_theme_invalidates_the_layout_cache() {
1974        // The cache keys on a fingerprint of the *effective* style, which now includes
1975        // the theme's say. If it did not, a theme swap would keep the old geometry and
1976        // the change would appear only after something else moved.
1977        let runtime = Runtime::default();
1978        let tree = Flex::<Msg>::column()
1979            .width(200.0)
1980            .height(100.0)
1981            .child(Divider::new());
1982        let plain = Theme::default();
1983        let mut thin = Theme::default();
1984        thin.widgets.divider.height = Some(1.0);
1985
1986        let height = |theme: &Theme| {
1987            build_ui(&tree, Size::new(200.0, 100.0), &runtime, theme)
1988                .scene()
1989                .primitives()
1990                .iter()
1991                .find_map(|p| match p {
1992                    Primitive::Rect { rect, .. } => Some(rect.y),
1993                    _ => None,
1994                })
1995                .expect("a line")
1996        };
1997        // The same runtime, and therefore the same cache, across both builds.
1998        let first = height(&plain);
1999        let second = height(&thin);
2000        assert_ne!(
2001            first, second,
2002            "a themed size must not be served from the cache of an unthemed one"
2003        );
2004        assert_eq!(height(&plain), first, "and back again");
2005    }
2006
2007    #[test]
2008    fn the_defaults_are_still_reachable_through_an_empty_theme() {
2009        let plain = Theme::default();
2010        assert_eq!(
2011            Widget::<Msg>::style_themed(&Divider::new(), &plain).height,
2012            frus_layout::Dimension::Length(DIVIDER_SPACE)
2013        );
2014    }
2015
2016    /// The size every `Primitive::Text` in a frame was drawn at, in the order they were
2017    /// painted. A widget's type is not reachable any other way — these widgets paint their
2018    /// labels instead of laying them out.
2019    fn text_sizes(widget: impl Widget<Msg> + 'static, theme: &Theme) -> Vec<f32> {
2020        framed(widget, theme)
2021            .iter()
2022            .filter_map(|p| match p {
2023                Primitive::Text { size, .. } => Some(*size),
2024                _ => None,
2025            })
2026            .collect()
2027    }
2028
2029    /// The weight of the first text in a frame.
2030    fn first_weight(widget: impl Widget<Msg> + 'static, theme: &Theme) -> Option<u16> {
2031        framed(widget, theme).iter().find_map(|p| match p {
2032            Primitive::Text { weight, .. } => Some(weight.to_u16()),
2033            _ => None,
2034        })
2035    }
2036
2037    #[test]
2038    fn a_widget_no_longer_decides_its_own_type() {
2039        // Milestone 413. Twelve widgets set their text in a private constant that no theme
2040        // and no caller could reach, and one of them had **drifted two pixels from the
2041        // reference** without anybody being able to see it. Each is checked here at the
2042        // step the reference names, and then moved by a theme — which is the part that was
2043        // impossible before, and the reason the drift went unseen.
2044        let plain = Theme::default();
2045
2046        // A snackbar's content is `bodyMedium` — 14 px, not the 16 the constant said.
2047        assert_eq!(
2048            text_sizes(crate::toast::SnackBar::<Msg>::new("Saved"), &plain),
2049            vec![plain.text.body_medium.size.unwrap()]
2050        );
2051        let mut loud = Theme::default();
2052        loud.widgets.snack_bar.content_text_style = Some(TextStyle::new(30.0));
2053        assert_eq!(
2054            text_sizes(crate::toast::SnackBar::<Msg>::new("Saved"), &loud),
2055            vec![30.0],
2056            "the theme has a say"
2057        );
2058        assert_eq!(
2059            text_sizes(
2060                crate::toast::SnackBar::<Msg>::new("Saved").content_text_style(TextStyle::new(9.0)),
2061                &loud
2062            ),
2063            vec![9.0],
2064            "and the caller has the last word over it"
2065        );
2066
2067        // A key cap is a label, and monospaced.
2068        let kbd = || crate::kbd::Kbd::new("Ctrl");
2069        assert_eq!(
2070            text_sizes(kbd(), &plain),
2071            vec![plain.text.label_medium.size.unwrap()]
2072        );
2073        assert_eq!(
2074            framed(kbd(), &plain).iter().find_map(|p| match p {
2075                Primitive::Text { family, .. } => Some(*family),
2076                _ => None,
2077            }),
2078            Some(Some(frus_core::FontFamily::Monospace)),
2079            "a cap stands for what is printed on a keyboard"
2080        );
2081
2082        // A tree row is a list tile's title.
2083        let tree = || {
2084            crate::tree::Tree::<Msg>::new(|_| -> Msg { unreachable!() })
2085                .node(1, 0, "root", false, false)
2086        };
2087        assert_eq!(
2088            text_sizes(tree(), &plain),
2089            vec![plain.text.body_large.size.unwrap()]
2090        );
2091        assert_eq!(
2092            text_sizes(tree().text_style(TextStyle::new(7.0)), &plain),
2093            vec![7.0],
2094            "said on the widget, whatever order the builders came in"
2095        );
2096    }
2097
2098    #[test]
2099    fn the_reference_names_a_table_s_two_rows_apart() {
2100        // A data table's heading is `titleSmall` and its cells `bodyMedium` — **two
2101        // different steps**, which one `SIZE` constant for the whole widget could not say.
2102        let theme = Theme::default();
2103        let table = crate::table::Table::<Msg>::new(1)
2104            .header(&["Name"])
2105            .row(&["Ada"]);
2106        assert_eq!(
2107            text_sizes(table, &theme),
2108            vec![
2109                theme.text.title_small.size.unwrap(),
2110                theme.text.body_medium.size.unwrap(),
2111            ]
2112        );
2113        // And the heading carries the medium weight the step does.
2114        let heading = crate::table::Table::<Msg>::new(1).header(&["Name"]);
2115        assert_eq!(
2116            first_weight(heading, &theme),
2117            Some(frus_core::FontWeight::Medium.to_u16())
2118        );
2119    }
2120
2121    #[test]
2122    fn a_table_carries_its_own_word_down_to_cells_it_did_not_build_directly() {
2123        // `Table` and `DatePicker` hand their override down the **theme**
2124        // (`Widget::theme_override`) rather than through the cells' fields: a table builds
2125        // cells in half a dozen places and a calendar has five constructors, and a value
2126        // carried down the theme reaches all of them without any of them being taught to
2127        // pass it on.
2128        let theme = Theme::default();
2129        let table = crate::table::Table::<Msg>::new(1)
2130            .header(&["Name"])
2131            .row(&["Ada"])
2132            .heading_text_style(TextStyle::new(21.0))
2133            .data_text_style(TextStyle::new(8.0));
2134        assert_eq!(text_sizes(table, &theme), vec![21.0, 8.0]);
2135        // Saying nothing costs nothing: no override, no theme to allocate.
2136        assert!(Widget::<Msg>::theme_override(
2137            &crate::table::Table::<Msg>::new(1).header(&["Name"]),
2138            &theme
2139        )
2140        .is_none());
2141    }
2142
2143    #[test]
2144    fn the_un_themed_path_reads_the_same_scale_as_the_themed_one() {
2145        // `Widget::style` has no theme — it is what the transparent wrappers ask a child
2146        // when they want its size. It answers from `TextTheme::M3`, the same const
2147        // `Theme::default()` carries, rather than from a fallback constant beside it: two
2148        // numbers that have to agree is the shape the last five milestones were about.
2149        assert_eq!(TextTheme::M3, Theme::default().text);
2150        let kbd = crate::kbd::Kbd::new("Ctrl");
2151        assert_eq!(
2152            Widget::<Msg>::style(&kbd),
2153            Widget::<Msg>::style_themed(&kbd, &Theme::default()),
2154            "the same widget, sized the same way, with and without a theme in hand"
2155        );
2156    }
2157
2158    #[test]
2159    fn the_last_seven_widgets_stop_deciding_their_own_type() {
2160        // Milestone 414, the tail of 413. Five of these seven turned out to be **read**
2161        // from the reference after all, and the note that sent them here said only two
2162        // were — which is the same mistake as the constants themselves, an estimate
2163        // standing in for a look.
2164        let plain = Theme::default();
2165
2166        // Argued: a trail is a *secondary* line of navigation, `bodyMedium`.
2167        let trail = || {
2168            crate::breadcrumb::Breadcrumb::<Msg>::new(|_| -> Msg { unreachable!() }).crumb("Home")
2169        };
2170        assert_eq!(
2171            text_sizes(trail(), &plain),
2172            vec![plain.text.body_medium.size.unwrap()]
2173        );
2174        assert_eq!(
2175            text_sizes(trail().text_style(TextStyle::new(9.0)), &plain),
2176            vec![9.0],
2177            "and a caller can still say otherwise"
2178        );
2179        let mut themed = Theme::default();
2180        themed.widgets.breadcrumb.text_style = Some(TextStyle::new(31.0));
2181        assert_eq!(text_sizes(trail(), &themed), vec![31.0]);
2182    }
2183
2184    #[test]
2185    fn a_bar_titles_at_the_reference_s_weight() {
2186        // The app bar's title was a private `TextStyle::new(22.0).weight(Medium)`: the size
2187        // right and **the weight wrong**, because `titleLarge` is regular. Nothing tested it
2188        // — the one test that looked compared it against the constant it came from, which is
2189        // a tautology, not a check.
2190        let plain = Theme::default();
2191        assert_eq!(
2192            first_weight(crate::navbar::NavigationBar::<Msg>::new("Inbox"), &plain),
2193            Some(frus_core::FontWeight::Regular.to_u16())
2194        );
2195        assert_eq!(
2196            text_sizes(crate::navbar::NavigationBar::<Msg>::new("Inbox"), &plain),
2197            vec![plain.text.title_large.size.unwrap()],
2198            "and a navigation bar titles at 22, not at a private 20"
2199        );
2200    }
2201
2202    #[test]
2203    fn a_theme_that_says_nothing_is_the_absence_of_a_theme() {
2204        // The whole contract of `None`: a fresh theme must carry no opinions at all, or
2205        // every widget's built-in default silently becomes unreachable.
2206        assert_eq!(Theme::default().widgets, WidgetThemes::default());
2207        assert_eq!(WidgetThemes::default().card.elevation, None);
2208        assert_eq!(WidgetThemes::default().divider.height, None);
2209        assert_eq!(WidgetThemes::default().drawer.width, None);
2210        assert_eq!(WidgetThemes::default().ink.color, None);
2211        assert_eq!(WidgetThemes::default().tab_bar.indicator_color, None);
2212        assert_eq!(WidgetThemes::default().chip.height, None);
2213        assert_eq!(WidgetThemes::default().button.height, None);
2214        assert_eq!(WidgetThemes::default().segmented.height, None);
2215        assert_eq!(WidgetThemes::default().icon_button.size, None);
2216        assert_eq!(WidgetThemes::default().snack_bar.content_text_style, None);
2217        assert_eq!(WidgetThemes::default().table.heading_text_style, None);
2218        assert_eq!(WidgetThemes::default().date_picker.day_text_style, None);
2219        assert_eq!(WidgetThemes::default().kbd.text_style, None);
2220        assert_eq!(WidgetThemes::default().breadcrumb.text_style, None);
2221        assert_eq!(WidgetThemes::default().kanban.card_text_style, None);
2222        assert_eq!(WidgetThemes::default().form.bullet_text_style, None);
2223        assert_eq!(WidgetThemes::default().time_picker.dial_text_style, None);
2224        assert_eq!(
2225            WidgetThemes::default().slider.value_indicator_text_style,
2226            None
2227        );
2228    }
2229}