Skip to main content

compose_rust/
widgets.rs

1#![allow(non_snake_case)]
2
3use dioxus_core::{Callback, Element, EventHandler};
4use dioxus_core_macro::{Props, component, rsx};
5use dioxus_hooks::use_signal;
6use dioxus_signals::WritableExt as _;
7
8use crate as dioxus_elements;
9use crate::Key;
10use crate::drawing::DrawList;
11use crate::schema::{
12    Alignment, Arrangement, ButtonVariant, ColorRole, IconRole, MaterialRole, MotionRole, Paint,
13    ShapeRole, SlotRole, SpaceRole, TextAlign, TextOverflow, TypeRole,
14};
15use std::cell::Cell;
16use std::rc::Rc;
17
18/// A key-down event whose consumption state is shared with the Host boundary.
19#[derive(Clone, Debug)]
20pub struct KeyEvent {
21    key: Key,
22    shift_key: bool,
23    ctrl_key: bool,
24    alt_key: bool,
25    meta_key: bool,
26    consumed: Rc<Cell<bool>>,
27}
28
29impl KeyEvent {
30    pub(crate) fn new(
31        key: Key,
32        shift_key: bool,
33        ctrl_key: bool,
34        alt_key: bool,
35        meta_key: bool,
36    ) -> Self {
37        Self {
38            key,
39            shift_key,
40            ctrl_key,
41            alt_key,
42            meta_key,
43            consumed: Rc::new(Cell::new(false)),
44        }
45    }
46
47    pub fn key(&self) -> Key {
48        self.key
49    }
50
51    pub fn shift_key(&self) -> bool {
52        self.shift_key
53    }
54
55    pub fn ctrl_key(&self) -> bool {
56        self.ctrl_key
57    }
58
59    pub fn alt_key(&self) -> bool {
60        self.alt_key
61    }
62
63    pub fn meta_key(&self) -> bool {
64        self.meta_key
65    }
66
67    pub fn consume(&self) {
68        self.consumed.set(true);
69    }
70
71    pub fn consumed(&self) -> bool {
72        self.consumed.get()
73    }
74}
75
76/// A role that was not set is tag 0, which means "not sent". The Renderer
77/// never sees a zero role, so it never has to guess what an unset role meant.
78fn role(value: Option<impl Into<u16>>) -> i64 {
79    value.map_or(0, |value| i64::from(value.into()))
80}
81
82/// A Modifier value that was not set sends nothing at all, rather than a zero. No padding
83/// and a padding of zero look the same on screen, but only one of them should cost a
84/// mutation on every frame the widget appears in.
85fn opt_dp(value: Option<f32>) -> Option<f64> {
86    value.map(f64::from)
87}
88
89fn opt_role(value: Option<impl Into<u16>>) -> Option<i64> {
90    value.map(|value| i64::from(value.into()))
91}
92
93fn opt_paint(value: Option<Paint>) -> Option<i64> {
94    value.map(|paint| paint.to_bits() as i64)
95}
96
97fn dp(value: Option<f32>) -> f64 {
98    f64::from(value.unwrap_or(0.0))
99}
100
101#[component]
102pub fn Column(
103    /// Asks the Renderer to report this node's measured size, under the name a
104    /// screen gave it with `use_node_size`. Nothing is measured without it.
105    #[props(default)]
106    observe_size: Option<i64>,
107    /// How important this node's changes are. The curve and the length are the design
108    /// system's answer to that, and are never sent from here.
109    #[props(default)]
110    motion: Option<MotionRole>,
111    /// What this node's surface is made of. Blur on the systems that blur, a lifted tone
112    /// on the ones that lift, a flat fill on the rest.
113    #[props(default)]
114    material: Option<MaterialRole>,
115    #[props(default)] weight: Option<f32>,
116    #[props(default)] width: Option<f32>,
117    #[props(default)] height: Option<f32>,
118    #[props(default)] padding: Option<f32>,
119    #[props(default)] padding_role: Option<SpaceRole>,
120    #[props(default)] background: Option<Paint>,
121    #[props(default)] shape_role: Option<ShapeRole>,
122    #[props(default)] corner_radius: Option<f32>,
123    #[props(default)] border_width: Option<f32>,
124    #[props(default)] border_color: Option<Paint>,
125    #[props(default)] elevation: Option<f32>,
126    #[props(default)] fill_max_width: bool,
127    #[props(default)] fill_max_height: bool,
128    #[props(default)] arrangement: Option<Arrangement>,
129    #[props(default)] spacing: Option<f32>,
130    #[props(default)] space_role: Option<SpaceRole>,
131    #[props(default)] alignment: Option<Alignment>,
132    children: Element,
133) -> Element {
134    rsx! {
135        column {
136            observe_size,
137            motion: opt_role(motion),
138            material: opt_role(material),
139            weight: opt_dp(weight),
140            width: opt_dp(width),
141            height: opt_dp(height),
142            padding: opt_dp(padding),
143            padding_role: opt_role(padding_role),
144            background: opt_paint(background),
145            shape_role: opt_role(shape_role),
146            corner_radius: opt_dp(corner_radius),
147            border_width: opt_dp(border_width),
148            border_color: opt_paint(border_color),
149            elevation: opt_dp(elevation),
150            fill_max_width,
151            fill_max_height,
152            arrangement: role(arrangement),
153            spacing: dp(spacing),
154            space_role: role(space_role),
155            alignment: role(alignment),
156            {children}
157        }
158    }
159}
160
161#[component]
162pub fn Row(
163    /// Asks the Renderer to report this node's measured size, under the name a
164    /// screen gave it with `use_node_size`. Nothing is measured without it.
165    #[props(default)]
166    observe_size: Option<i64>,
167    /// How important this node's changes are. The curve and the length are the design
168    /// system's answer to that, and are never sent from here.
169    #[props(default)]
170    motion: Option<MotionRole>,
171    /// What this node's surface is made of. Blur on the systems that blur, a lifted tone
172    /// on the ones that lift, a flat fill on the rest.
173    #[props(default)]
174    material: Option<MaterialRole>,
175    #[props(default)] weight: Option<f32>,
176    #[props(default)] width: Option<f32>,
177    #[props(default)] height: Option<f32>,
178    #[props(default)] padding: Option<f32>,
179    #[props(default)] padding_role: Option<SpaceRole>,
180    #[props(default)] background: Option<Paint>,
181    #[props(default)] shape_role: Option<ShapeRole>,
182    #[props(default)] corner_radius: Option<f32>,
183    #[props(default)] border_width: Option<f32>,
184    #[props(default)] border_color: Option<Paint>,
185    #[props(default)] elevation: Option<f32>,
186    #[props(default)] fill_max_width: bool,
187    #[props(default)] fill_max_height: bool,
188    #[props(default)] arrangement: Option<Arrangement>,
189    #[props(default)] spacing: Option<f32>,
190    #[props(default)] space_role: Option<SpaceRole>,
191    #[props(default)] alignment: Option<Alignment>,
192    children: Element,
193) -> Element {
194    rsx! {
195        row {
196            observe_size,
197            motion: opt_role(motion),
198            material: opt_role(material),
199            weight: opt_dp(weight),
200            width: opt_dp(width),
201            height: opt_dp(height),
202            padding: opt_dp(padding),
203            padding_role: opt_role(padding_role),
204            background: opt_paint(background),
205            shape_role: opt_role(shape_role),
206            corner_radius: opt_dp(corner_radius),
207            border_width: opt_dp(border_width),
208            border_color: opt_paint(border_color),
209            elevation: opt_dp(elevation),
210            fill_max_width,
211            fill_max_height,
212            arrangement: role(arrangement),
213            spacing: dp(spacing),
214            space_role: role(space_role),
215            alignment: role(alignment),
216            {children}
217        }
218    }
219}
220
221#[component]
222pub fn ComposeBox(
223    /// Asks the Renderer to report this node's measured size, under the name a
224    /// screen gave it with `use_node_size`. Nothing is measured without it.
225    #[props(default)]
226    observe_size: Option<i64>,
227    /// How important this node's changes are. The curve and the length are the design
228    /// system's answer to that, and are never sent from here.
229    #[props(default)]
230    motion: Option<MotionRole>,
231    /// What this node's surface is made of. Blur on the systems that blur, a lifted tone
232    /// on the ones that lift, a flat fill on the rest.
233    #[props(default)]
234    material: Option<MaterialRole>,
235    #[props(default)] weight: Option<f32>,
236    #[props(default)] width: Option<f32>,
237    #[props(default)] height: Option<f32>,
238    #[props(default)] padding: Option<f32>,
239    #[props(default)] padding_role: Option<SpaceRole>,
240    #[props(default)] background: Option<Paint>,
241    #[props(default)] shape_role: Option<ShapeRole>,
242    #[props(default)] corner_radius: Option<f32>,
243    #[props(default)] border_width: Option<f32>,
244    #[props(default)] border_color: Option<Paint>,
245    #[props(default)] elevation: Option<f32>,
246    #[props(default)] fill_max_width: bool,
247    #[props(default)] fill_max_height: bool,
248    #[props(default)] alignment: Option<Alignment>,
249    children: Element,
250) -> Element {
251    rsx! {
252        composebox {
253            observe_size,
254            motion: opt_role(motion),
255            material: opt_role(material),
256            weight: opt_dp(weight),
257            width: opt_dp(width),
258            height: opt_dp(height),
259            padding: opt_dp(padding),
260            padding_role: opt_role(padding_role),
261            background: opt_paint(background),
262            shape_role: opt_role(shape_role),
263            corner_radius: opt_dp(corner_radius),
264            border_width: opt_dp(border_width),
265            border_color: opt_paint(border_color),
266            elevation: opt_dp(elevation),
267            fill_max_width,
268            fill_max_height,
269            alignment: role(alignment),
270            {children}
271        }
272    }
273}
274
275/// The whole content with a vertical scroll attached. The scroll position is the
276/// Renderer's, like focus and animation state, so scrolling never reaches the Host.
277#[component]
278pub fn ScrollColumn(
279    #[props(default)] weight: Option<f32>,
280    #[props(default)] width: Option<f32>,
281    #[props(default)] height: Option<f32>,
282    #[props(default)] padding: Option<f32>,
283    #[props(default)] padding_role: Option<SpaceRole>,
284    #[props(default)] background: Option<Paint>,
285    #[props(default)] shape_role: Option<ShapeRole>,
286    #[props(default)] corner_radius: Option<f32>,
287    #[props(default)] border_width: Option<f32>,
288    #[props(default)] border_color: Option<Paint>,
289    #[props(default)] elevation: Option<f32>,
290    #[props(default)] fill_max_width: bool,
291    #[props(default)] fill_max_height: bool,
292    children: Element,
293) -> Element {
294    rsx! {
295        scrollcolumn {
296            weight: opt_dp(weight),
297            width: opt_dp(width),
298            height: opt_dp(height),
299            padding: opt_dp(padding),
300            padding_role: opt_role(padding_role),
301            background: opt_paint(background),
302            shape_role: opt_role(shape_role),
303            corner_radius: opt_dp(corner_radius),
304            border_width: opt_dp(border_width),
305            border_color: opt_paint(border_color),
306            elevation: opt_dp(elevation),
307            fill_max_width,
308            fill_max_height,
309            {children}
310        }
311    }
312}
313
314/// `type_role` alone takes the design system's size, weight, line height and letter
315/// spacing. Each override replaces one axis and costs one `SetProp`, so changing the font
316/// size does not resend the rest of the text's styling.
317#[component]
318pub fn Text(
319    #[props(default)] weight: Option<f32>,
320    #[props(default)] width: Option<f32>,
321    #[props(default)] height: Option<f32>,
322    #[props(default)] padding: Option<f32>,
323    #[props(default)] padding_role: Option<SpaceRole>,
324    #[props(default)] background: Option<Paint>,
325    #[props(default)] shape_role: Option<ShapeRole>,
326    #[props(default)] corner_radius: Option<f32>,
327    #[props(default)] border_width: Option<f32>,
328    #[props(default)] border_color: Option<Paint>,
329    #[props(default)] elevation: Option<f32>,
330    #[props(default)] fill_max_width: bool,
331    #[props(default)] fill_max_height: bool,
332    /// Runs of different treatment inside [text]: a bold phrase, a link, a piece of
333    /// code. Left out, the widget travels exactly as it did before runs existed.
334    #[props(default)]
335    spans: crate::spans::TextSpans,
336    #[props(into)] text: String,
337    #[props(default)] type_role: Option<TypeRole>,
338    #[props(default)] font_size: Option<f32>,
339    #[props(default)] font_weight: Option<u16>,
340    #[props(default)] line_height: Option<f32>,
341    #[props(default)] letter_spacing: Option<f32>,
342    #[props(default)] color: Option<Paint>,
343    #[props(default)] text_align: Option<TextAlign>,
344    #[props(default)] max_lines: Option<u32>,
345    #[props(default)] overflow: Option<TextOverflow>,
346) -> Element {
347    rsx! {
348        text {
349            weight: opt_dp(weight),
350            width: opt_dp(width),
351            height: opt_dp(height),
352            padding: opt_dp(padding),
353            padding_role: opt_role(padding_role),
354            background: opt_paint(background),
355            shape_role: opt_role(shape_role),
356            corner_radius: opt_dp(corner_radius),
357            border_width: opt_dp(border_width),
358            border_color: opt_paint(border_color),
359            elevation: opt_dp(elevation),
360            fill_max_width,
361            fill_max_height,
362            // Left out entirely when there are none, so a Text that says nothing about
363            // runs travels exactly as it did before runs existed.
364            spans: (!spans.is_empty()).then_some(spans),
365            text,
366            type_role: role(type_role),
367            font_size: dp(font_size),
368            font_weight: i64::from(font_weight.unwrap_or(0)),
369            line_height: dp(line_height),
370            letter_spacing: dp(letter_spacing),
371            color: color.map_or(0, |paint| paint.to_bits() as i64),
372            text_align: role(text_align),
373            max_lines: i64::from(max_lines.unwrap_or(0)),
374            overflow: role(overflow),
375        }
376    }
377}
378
379#[component]
380pub fn TextField(
381    #[props(default)] weight: Option<f32>,
382    #[props(default)] width: Option<f32>,
383    #[props(default)] height: Option<f32>,
384    #[props(default)] padding: Option<f32>,
385    #[props(default)] padding_role: Option<SpaceRole>,
386    #[props(default)] background: Option<Paint>,
387    #[props(default)] shape_role: Option<ShapeRole>,
388    #[props(default)] corner_radius: Option<f32>,
389    #[props(default)] border_width: Option<f32>,
390    #[props(default)] border_color: Option<Paint>,
391    #[props(default)] elevation: Option<f32>,
392    #[props(default)] fill_max_width: bool,
393    #[props(default)] fill_max_height: bool,
394    #[props(into, default)] placeholder: String,
395    #[props(default = true)] enabled: bool,
396    #[props(default)] multiline: bool,
397    /// The rung of the type ladder the field's own text is set in. A field holding a file
398    /// path or a snippet of code wants the monospace rung; prose does not.
399    #[props(default)]
400    type_role: Option<TypeRole>,
401    #[props(default)] on_value_change: EventHandler<String>,
402    #[props(default)] on_submit: EventHandler<String>,
403    #[props(default)] on_focus_lost: EventHandler<()>,
404    #[props(default)] on_key_down: EventHandler<KeyEvent>,
405) -> Element {
406    rsx! {
407        textfield {
408            weight: opt_dp(weight),
409            width: opt_dp(width),
410            height: opt_dp(height),
411            padding: opt_dp(padding),
412            padding_role: opt_role(padding_role),
413            background: opt_paint(background),
414            shape_role: opt_role(shape_role),
415            corner_radius: opt_dp(corner_radius),
416            border_width: opt_dp(border_width),
417            border_color: opt_paint(border_color),
418            elevation: opt_dp(elevation),
419            fill_max_width,
420            fill_max_height,
421            placeholder,
422            enabled,
423            multiline,
424            type_role: role(type_role),
425            onvaluechange: move |event| on_value_change.call((*event.data()).clone()),
426            onsubmit: move |event| on_submit.call((*event.data()).clone()),
427            onfocuslost: move |_| on_focus_lost.call(()),
428            onkeydown: move |event| on_key_down.call((*event.data()).clone()),
429        }
430    }
431}
432
433/// The variant is the seam the design system's component rule attaches to.
434/// The same rsx draws differently per system, and that is correct behaviour.
435#[component]
436pub fn Button(
437    #[props(default)] weight: Option<f32>,
438    #[props(default)] width: Option<f32>,
439    #[props(default)] height: Option<f32>,
440    #[props(default)] padding: Option<f32>,
441    #[props(default)] padding_role: Option<SpaceRole>,
442    #[props(default)] background: Option<Paint>,
443    #[props(default)] shape_role: Option<ShapeRole>,
444    #[props(default)] corner_radius: Option<f32>,
445    #[props(default)] border_width: Option<f32>,
446    #[props(default)] border_color: Option<Paint>,
447    #[props(default)] elevation: Option<f32>,
448    #[props(default)] fill_max_width: bool,
449    #[props(default)] fill_max_height: bool,
450    #[props(into)] text: String,
451    /// The meaning of the glyph on it, never a picture: `IconRole::Search` comes out as
452    /// this design system's search icon.
453    ///
454    /// With a `text` beside it the button shows both. With `text` empty it is the glyph
455    /// alone, which is what every one of the reference toolbars is made of, and the role
456    /// is what names it for assistive technology.
457    #[props(default)]
458    icon: Option<IconRole>,
459    #[props(default = true)] enabled: bool,
460    #[props(default)] variant: Option<ButtonVariant>,
461    /// The label's colour, for the rare button whose meaning is not the variant's.
462    /// A destructive action is the case that needs it: it is a plain button in every
463    /// design system, and what marks it is that its label is the error colour.
464    #[props(default)]
465    color: Option<Paint>,
466    #[props(default)] on_click: EventHandler<()>,
467) -> Element {
468    rsx! {
469        button {
470            weight: opt_dp(weight),
471            width: opt_dp(width),
472            height: opt_dp(height),
473            padding: opt_dp(padding),
474            padding_role: opt_role(padding_role),
475            background: opt_paint(background),
476            shape_role: opt_role(shape_role),
477            corner_radius: opt_dp(corner_radius),
478            border_width: opt_dp(border_width),
479            border_color: opt_paint(border_color),
480            elevation: opt_dp(elevation),
481            fill_max_width,
482            fill_max_height,
483            text,
484            icon: opt_role(icon),
485            enabled,
486            variant: role(variant),
487            color: opt_paint(color),
488            onclick: move |_| on_click.call(()),
489        }
490    }
491}
492
493/// A hairline between two rows of a grouped list.
494///
495/// One [`Divider`] under the name application code already uses. Nothing about the weight,
496/// the colour or the inset is decided here: a Material rule, a Fluent layer stroke and an
497/// Apple grouped-list separator are three different lines, and which one gets drawn is the
498/// active design system's answer rather than a constant this library holds.
499///
500/// `color` overrides that answer for the rare case where a list rules itself in something
501/// other than the quiet edge. Leaving it unset is the usual thing to do.
502#[component]
503pub fn Separator(#[props(default)] color: Option<Paint>) -> Element {
504    rsx! {
505        Divider { background: color }
506    }
507}
508
509#[component]
510pub fn Spacer(
511    #[props(default)] weight: Option<f32>,
512    #[props(default)] width: Option<f32>,
513    #[props(default)] height: Option<f32>,
514    #[props(default)] padding: Option<f32>,
515    #[props(default)] padding_role: Option<SpaceRole>,
516    #[props(default)] background: Option<Paint>,
517    #[props(default)] shape_role: Option<ShapeRole>,
518    #[props(default)] corner_radius: Option<f32>,
519    #[props(default)] border_width: Option<f32>,
520    #[props(default)] border_color: Option<Paint>,
521    #[props(default)] elevation: Option<f32>,
522    #[props(default)] fill_max_width: bool,
523    #[props(default)] fill_max_height: bool,
524) -> Element {
525    rsx! {
526        spacer {
527            weight: opt_dp(weight),
528            width: opt_dp(width),
529            height: opt_dp(height),
530            padding: opt_dp(padding),
531            padding_role: opt_role(padding_role),
532            background: opt_paint(background),
533            shape_role: opt_role(shape_role),
534            corner_radius: opt_dp(corner_radius),
535            border_width: opt_dp(border_width),
536            border_color: opt_paint(border_color),
537            elevation: opt_dp(elevation),
538            fill_max_width,
539            fill_max_height,
540        }
541    }
542}
543
544/// The paths of the files a reader let go over a node.
545///
546/// Separated on the wire by a NUL, which is the one byte no path on any of the three
547/// desktops may contain. A newline would have been shorter to read and wrong: a file
548/// called "notes\nfor tuesday" is legal on two of them, and splitting on newlines would
549/// have turned one file into two.
550#[derive(Clone, Debug, PartialEq)]
551pub struct FileDrop {
552    paths: Vec<String>,
553}
554
555impl FileDrop {
556    /// Takes apart the one string the paths travelled in.
557    ///
558    /// Public because the separation is part of what this type promises, and a test that
559    /// could not build one could only check it through a window.
560    pub fn new(joined: &str) -> Self {
561        Self {
562            paths: joined
563                .split('\0')
564                .filter(|path| !path.is_empty())
565                .map(str::to_owned)
566                .collect(),
567        }
568    }
569
570    /// Every path that arrived, in the order the platform gave them.
571    pub fn paths(&self) -> &[String] {
572        &self.paths
573    }
574}
575
576/// The visible item range the Renderer asks the Host to materialise.
577#[derive(Clone, Copy, Debug, Eq, PartialEq)]
578pub struct RangeRequest {
579    start: u32,
580    count: u32,
581}
582
583impl RangeRequest {
584    pub(crate) fn new(start: u32, count: u32) -> Self {
585        Self { start, count }
586    }
587
588    pub fn start(&self) -> usize {
589        self.start as usize
590    }
591
592    pub fn count(&self) -> usize {
593        self.count as usize
594    }
595}
596
597/// A windowed list. The Host declares `item_count` and a stable key per item, and
598/// materialises **exactly** the range the Renderer last requested.
599///
600/// The read-ahead buffer belongs to the Renderer, which owns the scroll position and so
601/// knows how far ahead to ask. Widening the range here would break the Renderer's placement:
602/// `start` is the global index of the first child it receives, and that is what lets it draw
603/// a real Compose `LazyColumn` of `item_count` items. The data stays in the Host, so
604/// scrolling back re-materialises an identical subtree.
605#[component]
606pub fn LazyColumn(
607    #[props(default)] weight: Option<f32>,
608    #[props(default)] width: Option<f32>,
609    #[props(default)] height: Option<f32>,
610    #[props(default)] padding: Option<f32>,
611    #[props(default)] padding_role: Option<SpaceRole>,
612    #[props(default)] background: Option<Paint>,
613    #[props(default)] shape_role: Option<ShapeRole>,
614    #[props(default)] corner_radius: Option<f32>,
615    #[props(default)] border_width: Option<f32>,
616    #[props(default)] border_color: Option<Paint>,
617    #[props(default)] elevation: Option<f32>,
618    #[props(default)] fill_max_width: bool,
619    #[props(default)] fill_max_height: bool,
620    item_count: usize,
621    #[props(default)] key_of: Option<Callback<usize, String>>,
622    item: Callback<usize, Element>,
623) -> Element {
624    let mut range = use_signal(|| (0_usize, 0_usize));
625    let (start, count) = range();
626    let first = start.min(item_count);
627    let last = first.saturating_add(count).min(item_count);
628    rsx! {
629        lazycolumn {
630            weight: opt_dp(weight),
631            width: opt_dp(width),
632            height: opt_dp(height),
633            padding: opt_dp(padding),
634            padding_role: opt_role(padding_role),
635            background: opt_paint(background),
636            shape_role: opt_role(shape_role),
637            corner_radius: opt_dp(corner_radius),
638            border_width: opt_dp(border_width),
639            border_color: opt_paint(border_color),
640            elevation: opt_dp(elevation),
641            fill_max_width,
642            fill_max_height,
643            item_count: item_count as i64,
644            onrangerequest: move |event: dioxus_core::Event<RangeRequest>| {
645                let requested = event.data();
646                range.set((requested.start(), requested.count()));
647            },
648            for index in first..last {
649                {
650                    let item_key = key_of
651                        .map_or_else(|| index.to_string(), |key_of| key_of.call(index));
652                    rsx! {
653                        composebox { key: "{item_key}", item_key, {item.call(index)} }
654                    }
655                }
656            }
657        }
658    }
659}
660
661/// A grouped container. What a card looks like, its background, corner and resting
662/// elevation, is the design system's decision, so the widget carries no appearance of its
663/// own. `Modifier::Elevation` overrides the resting height when the Host has a reason to.
664#[component]
665pub fn Card(
666    /// Asks the Renderer to report this node's measured size, under the name a
667    /// screen gave it with `use_node_size`. Nothing is measured without it.
668    #[props(default)]
669    observe_size: Option<i64>,
670    /// How important this node's changes are. The curve and the length are the design
671    /// system's answer to that, and are never sent from here.
672    #[props(default)]
673    motion: Option<MotionRole>,
674    /// What this node's surface is made of. Blur on the systems that blur, a lifted tone
675    /// on the ones that lift, a flat fill on the rest.
676    #[props(default)]
677    material: Option<MaterialRole>,
678    #[props(default)] weight: Option<f32>,
679    #[props(default)] width: Option<f32>,
680    #[props(default)] height: Option<f32>,
681    #[props(default)] padding: Option<f32>,
682    #[props(default)] padding_role: Option<SpaceRole>,
683    #[props(default)] background: Option<Paint>,
684    #[props(default)] shape_role: Option<ShapeRole>,
685    #[props(default)] corner_radius: Option<f32>,
686    #[props(default)] border_width: Option<f32>,
687    #[props(default)] border_color: Option<Paint>,
688    #[props(default)] elevation: Option<f32>,
689    #[props(default)] fill_max_width: bool,
690    #[props(default)] fill_max_height: bool,
691    children: Element,
692) -> Element {
693    rsx! {
694        card {
695            observe_size,
696            motion: opt_role(motion),
697            material: opt_role(material),
698            weight: opt_dp(weight),
699            width: opt_dp(width),
700            height: opt_dp(height),
701            padding: opt_dp(padding),
702            padding_role: opt_role(padding_role),
703            background: opt_paint(background),
704            shape_role: opt_role(shape_role),
705            corner_radius: opt_dp(corner_radius),
706            border_width: opt_dp(border_width),
707            border_color: opt_paint(border_color),
708            elevation: opt_dp(elevation),
709            fill_max_width,
710            fill_max_height,
711            {children}
712        }
713    }
714}
715
716/// A plain background-and-elevation container. Use it where a `Card`'s grouping meaning
717/// would be wrong and only the surface is wanted.
718#[component]
719pub fn Surface(
720    /// Asks the Renderer to report this node's measured size, under the name a
721    /// screen gave it with `use_node_size`. Nothing is measured without it.
722    #[props(default)]
723    observe_size: Option<i64>,
724    /// How important this node's changes are. The curve and the length are the design
725    /// system's answer to that, and are never sent from here.
726    #[props(default)]
727    motion: Option<MotionRole>,
728    /// What this node's surface is made of. Blur on the systems that blur, a lifted tone
729    /// on the ones that lift, a flat fill on the rest.
730    #[props(default)]
731    material: Option<MaterialRole>,
732    #[props(default)] weight: Option<f32>,
733    #[props(default)] width: Option<f32>,
734    #[props(default)] height: Option<f32>,
735    #[props(default)] padding: Option<f32>,
736    #[props(default)] padding_role: Option<SpaceRole>,
737    #[props(default)] background: Option<Paint>,
738    #[props(default)] shape_role: Option<ShapeRole>,
739    #[props(default)] corner_radius: Option<f32>,
740    #[props(default)] border_width: Option<f32>,
741    #[props(default)] border_color: Option<Paint>,
742    #[props(default)] elevation: Option<f32>,
743    #[props(default)] fill_max_width: bool,
744    #[props(default)] fill_max_height: bool,
745    children: Element,
746) -> Element {
747    rsx! {
748        surface {
749            observe_size,
750            motion: opt_role(motion),
751            material: opt_role(material),
752            weight: opt_dp(weight),
753            width: opt_dp(width),
754            height: opt_dp(height),
755            padding: opt_dp(padding),
756            padding_role: opt_role(padding_role),
757            background: opt_paint(background),
758            shape_role: opt_role(shape_role),
759            corner_radius: opt_dp(corner_radius),
760            border_width: opt_dp(border_width),
761            border_color: opt_paint(border_color),
762            elevation: opt_dp(elevation),
763            fill_max_width,
764            fill_max_height,
765            {children}
766        }
767    }
768}
769
770/// A modal. `open` seeds the Renderer's own open state and carries changes that came from
771/// somewhere other than the Renderer; the Renderer runs the enter and exit itself so the
772/// animation never round trips through the Host. `on_dismiss` fires when the user asks to
773/// close it, and the Host decides whether to honour that by setting `open` to false.
774#[component]
775pub fn Dialog(
776    #[props(default)] weight: Option<f32>,
777    #[props(default)] width: Option<f32>,
778    #[props(default)] height: Option<f32>,
779    #[props(default)] padding: Option<f32>,
780    #[props(default)] padding_role: Option<SpaceRole>,
781    #[props(default)] background: Option<Paint>,
782    #[props(default)] shape_role: Option<ShapeRole>,
783    #[props(default)] corner_radius: Option<f32>,
784    #[props(default)] border_width: Option<f32>,
785    #[props(default)] border_color: Option<Paint>,
786    #[props(default)] elevation: Option<f32>,
787    #[props(default)] fill_max_width: bool,
788    #[props(default)] fill_max_height: bool,
789    #[props(default)] open: bool,
790    #[props(default)] on_dismiss: EventHandler<()>,
791    children: Element,
792) -> Element {
793    rsx! {
794        dialog {
795            weight: opt_dp(weight),
796            width: opt_dp(width),
797            height: opt_dp(height),
798            padding: opt_dp(padding),
799            padding_role: opt_role(padding_role),
800            background: opt_paint(background),
801            shape_role: opt_role(shape_role),
802            corner_radius: opt_dp(corner_radius),
803            border_width: opt_dp(border_width),
804            border_color: opt_paint(border_color),
805            elevation: opt_dp(elevation),
806            fill_max_width,
807            fill_max_height,
808            open,
809            ondismiss: move |_| on_dismiss.call(()),
810            {children}
811        }
812    }
813}
814
815/// A popup anchored to `anchor`, which is the widget the menu hangs off. The anchor is the
816/// first child on the wire and the entries follow it, so the Renderer can place the popup
817/// without the Host knowing any screen coordinates.
818///
819/// Each entry supplies its own `on_click`, which is what tells the Host which one was
820/// chosen. The Renderer closes the popup itself.
821#[component]
822pub fn Menu(
823    #[props(default)] weight: Option<f32>,
824    #[props(default)] width: Option<f32>,
825    #[props(default)] height: Option<f32>,
826    #[props(default)] padding: Option<f32>,
827    #[props(default)] padding_role: Option<SpaceRole>,
828    #[props(default)] background: Option<Paint>,
829    #[props(default)] shape_role: Option<ShapeRole>,
830    #[props(default)] corner_radius: Option<f32>,
831    #[props(default)] border_width: Option<f32>,
832    #[props(default)] border_color: Option<Paint>,
833    #[props(default)] elevation: Option<f32>,
834    #[props(default)] fill_max_width: bool,
835    #[props(default)] fill_max_height: bool,
836    #[props(default)] expanded: bool,
837    #[props(default)] on_dismiss: EventHandler<()>,
838    anchor: Element,
839    children: Element,
840) -> Element {
841    rsx! {
842        menu {
843            weight: opt_dp(weight),
844            width: opt_dp(width),
845            height: opt_dp(height),
846            padding: opt_dp(padding),
847            padding_role: opt_role(padding_role),
848            background: opt_paint(background),
849            shape_role: opt_role(shape_role),
850            corner_radius: opt_dp(corner_radius),
851            border_width: opt_dp(border_width),
852            border_color: opt_paint(border_color),
853            elevation: opt_dp(elevation),
854            fill_max_width,
855            fill_max_height,
856            open: expanded,
857            ondismiss: move |_| on_dismiss.call(()),
858            {anchor}
859            {children}
860        }
861    }
862}
863
864/// A row of tabs. Each child is one tab.
865///
866/// `selected_index` seeds the Renderer's selection and moves it when the Host changes it.
867/// Tapping a tab changes the selection in the Renderer and reports it by firing that tab's
868/// own `on_click`, so switching tabs costs one event and no re-render of the tab strip.
869#[component]
870pub fn Tabs(
871    #[props(default)] weight: Option<f32>,
872    #[props(default)] width: Option<f32>,
873    #[props(default)] height: Option<f32>,
874    #[props(default)] padding: Option<f32>,
875    #[props(default)] padding_role: Option<SpaceRole>,
876    #[props(default)] background: Option<Paint>,
877    /// The colour of the mark that shows which tab is selected.
878    ///
879    /// The design system decides where this is unset, which is what an adaptive
880    /// application wants. A unified one names it, because its reference may have no
881    /// accent at all and a system asked the question answers with its own.
882    #[props(default)]
883    color: Option<Paint>,
884    #[props(default)] shape_role: Option<ShapeRole>,
885    #[props(default)] corner_radius: Option<f32>,
886    #[props(default)] border_width: Option<f32>,
887    #[props(default)] border_color: Option<Paint>,
888    #[props(default)] elevation: Option<f32>,
889    #[props(default)] fill_max_width: bool,
890    #[props(default)] fill_max_height: bool,
891    #[props(default)] selected_index: usize,
892    children: Element,
893) -> Element {
894    rsx! {
895        tabs {
896            weight: opt_dp(weight),
897            width: opt_dp(width),
898            height: opt_dp(height),
899            padding: opt_dp(padding),
900            padding_role: opt_role(padding_role),
901            background: opt_paint(background),
902            color: opt_paint(color),
903            shape_role: opt_role(shape_role),
904            corner_radius: opt_dp(corner_radius),
905            border_width: opt_dp(border_width),
906            border_color: opt_paint(border_color),
907            elevation: opt_dp(elevation),
908            fill_max_width,
909            fill_max_height,
910            selected_index: selected_index as i64,
911            {children}
912        }
913    }
914}
915
916/// The bar across the top of a screen. Its children are its content, left to right. How the
917/// bar is sized, spaced and separated from what is below it is the design system's rule.
918#[component]
919pub fn TopAppBar(
920    #[props(default)] weight: Option<f32>,
921    #[props(default)] width: Option<f32>,
922    #[props(default)] height: Option<f32>,
923    #[props(default)] padding: Option<f32>,
924    #[props(default)] padding_role: Option<SpaceRole>,
925    #[props(default)] background: Option<Paint>,
926    #[props(default)] shape_role: Option<ShapeRole>,
927    #[props(default)] corner_radius: Option<f32>,
928    #[props(default)] border_width: Option<f32>,
929    #[props(default)] border_color: Option<Paint>,
930    #[props(default)] elevation: Option<f32>,
931    #[props(default)] fill_max_width: bool,
932    #[props(default)] fill_max_height: bool,
933    /// The window's title, where this bar is the window's own caption.
934    ///
935    /// A property rather than a child, because the Renderer has to be able to find it.
936    /// Three design systems centre the window title and put everything else at the
937    /// leading edge, and a bar whose children are an arbitrary tree gives the Renderer no
938    /// way to tell which of them is the title. Where the bar is not the caption, or the
939    /// design system puts its title at the start, this draws in the same place a first
940    /// child would.
941    #[props(default)]
942    title: String,
943    children: Element,
944) -> Element {
945    rsx! {
946        topappbar {
947            weight: opt_dp(weight),
948            width: opt_dp(width),
949            height: opt_dp(height),
950            padding: opt_dp(padding),
951            padding_role: opt_role(padding_role),
952            background: opt_paint(background),
953            shape_role: opt_role(shape_role),
954            corner_radius: opt_dp(corner_radius),
955            border_width: opt_dp(border_width),
956            border_color: opt_paint(border_color),
957            elevation: opt_dp(elevation),
958            fill_max_width,
959            fill_max_height,
960            text: title,
961            {children}
962        }
963    }
964}
965
966/// The horizontal axis of the same windowing protocol `LazyColumn` uses, with the same
967/// contract: the Host materialises exactly the range the Renderer last asked for, and the
968/// read-ahead buffer belongs to the Renderer because the scroll position does.
969#[component]
970pub fn LazyRow(
971    #[props(default)] weight: Option<f32>,
972    #[props(default)] width: Option<f32>,
973    #[props(default)] height: Option<f32>,
974    #[props(default)] padding: Option<f32>,
975    #[props(default)] padding_role: Option<SpaceRole>,
976    #[props(default)] background: Option<Paint>,
977    #[props(default)] shape_role: Option<ShapeRole>,
978    #[props(default)] corner_radius: Option<f32>,
979    #[props(default)] border_width: Option<f32>,
980    #[props(default)] border_color: Option<Paint>,
981    #[props(default)] elevation: Option<f32>,
982    #[props(default)] fill_max_width: bool,
983    #[props(default)] fill_max_height: bool,
984    item_count: usize,
985    #[props(default)] key_of: Option<Callback<usize, String>>,
986    item: Callback<usize, Element>,
987) -> Element {
988    let mut range = use_signal(|| (0_usize, 0_usize));
989    let (start, count) = range();
990    let first = start.min(item_count);
991    let last = first.saturating_add(count).min(item_count);
992    rsx! {
993        lazyrow {
994            weight: opt_dp(weight),
995            width: opt_dp(width),
996            height: opt_dp(height),
997            padding: opt_dp(padding),
998            padding_role: opt_role(padding_role),
999            background: opt_paint(background),
1000            shape_role: opt_role(shape_role),
1001            corner_radius: opt_dp(corner_radius),
1002            border_width: opt_dp(border_width),
1003            border_color: opt_paint(border_color),
1004            elevation: opt_dp(elevation),
1005            fill_max_width,
1006            fill_max_height,
1007            item_count: item_count as i64,
1008            onrangerequest: move |event: dioxus_core::Event<RangeRequest>| {
1009                let requested = event.data();
1010                range.set((requested.start(), requested.count()));
1011            },
1012            for index in first..last {
1013                {
1014                    let item_key = key_of
1015                        .map_or_else(|| index.to_string(), |key_of| key_of.call(index));
1016                    rsx! {
1017                        composebox { key: "{item_key}", item_key, {item.call(index)} }
1018                    }
1019                }
1020            }
1021        }
1022    }
1023}
1024
1025/// An explanation attached to its child. It is a description as much as a hover popup: the
1026/// Renderer also exposes the text to the accessibility tree, so a pointer is not the only
1027/// way to reach it.
1028#[component]
1029pub fn Tooltip(
1030    #[props(default)] weight: Option<f32>,
1031    #[props(default)] width: Option<f32>,
1032    #[props(default)] height: Option<f32>,
1033    #[props(default)] padding: Option<f32>,
1034    #[props(default)] padding_role: Option<SpaceRole>,
1035    #[props(default)] background: Option<Paint>,
1036    #[props(default)] shape_role: Option<ShapeRole>,
1037    #[props(default)] corner_radius: Option<f32>,
1038    #[props(default)] border_width: Option<f32>,
1039    #[props(default)] border_color: Option<Paint>,
1040    #[props(default)] elevation: Option<f32>,
1041    #[props(default)] fill_max_width: bool,
1042    #[props(default)] fill_max_height: bool,
1043    #[props(into)] text: String,
1044    children: Element,
1045) -> Element {
1046    rsx! {
1047        tooltip {
1048            weight: opt_dp(weight),
1049            width: opt_dp(width),
1050            height: opt_dp(height),
1051            padding: opt_dp(padding),
1052            padding_role: opt_role(padding_role),
1053            background: opt_paint(background),
1054            shape_role: opt_role(shape_role),
1055            corner_radius: opt_dp(corner_radius),
1056            border_width: opt_dp(border_width),
1057            border_color: opt_paint(border_color),
1058            elevation: opt_dp(elevation),
1059            fill_max_width,
1060            fill_max_height,
1061            text,
1062            {children}
1063        }
1064    }
1065}
1066
1067/// Pixels the widget vocabulary cannot produce: charts, sparklines, signature pads, rings.
1068///
1069/// The list is a value, not a callback, because drawing code cannot cross the boundary into
1070/// an ahead-of-time compiled Renderer. Two equal lists are one attribute comparison, so a
1071/// frame that redraws the same chart sends nothing. Size comes from the Modifier chain, and
1072/// coordinates are dp from the widget's top-left corner.
1073///
1074/// Colour goes through `Paint`, so `Paint::Role(ColorRole::Primary)` draws in whatever the
1075/// active design system calls primary.
1076#[component]
1077pub fn Canvas(
1078    #[props(default)] weight: Option<f32>,
1079    #[props(default)] width: Option<f32>,
1080    #[props(default)] height: Option<f32>,
1081    #[props(default)] padding: Option<f32>,
1082    #[props(default)] padding_role: Option<SpaceRole>,
1083    #[props(default)] background: Option<Paint>,
1084    #[props(default)] shape_role: Option<ShapeRole>,
1085    #[props(default)] corner_radius: Option<f32>,
1086    #[props(default)] border_width: Option<f32>,
1087    #[props(default)] border_color: Option<Paint>,
1088    #[props(default)] elevation: Option<f32>,
1089    #[props(default)] fill_max_width: bool,
1090    #[props(default)] fill_max_height: bool,
1091    commands: DrawList,
1092) -> Element {
1093    rsx! {
1094        canvas {
1095            weight: opt_dp(weight),
1096            width: opt_dp(width),
1097            height: opt_dp(height),
1098            padding: opt_dp(padding),
1099            padding_role: opt_role(padding_role),
1100            background: opt_paint(background),
1101            shape_role: opt_role(shape_role),
1102            corner_radius: opt_dp(corner_radius),
1103            border_width: opt_dp(border_width),
1104            border_color: opt_paint(border_color),
1105            elevation: opt_dp(elevation),
1106            fill_max_width,
1107            fill_max_height,
1108            commands,
1109        }
1110    }
1111}
1112
1113/// A registered asset drawn as a picture. The bytes reached the Renderer once, through
1114/// `Host::register_asset`, and what crosses per frame is the id.
1115#[component]
1116pub fn Image(
1117    #[props(default)] weight: Option<f32>,
1118    #[props(default)] width: Option<f32>,
1119    #[props(default)] height: Option<f32>,
1120    #[props(default)] padding: Option<f32>,
1121    #[props(default)] padding_role: Option<SpaceRole>,
1122    #[props(default)] background: Option<Paint>,
1123    #[props(default)] shape_role: Option<ShapeRole>,
1124    #[props(default)] corner_radius: Option<f32>,
1125    #[props(default)] border_width: Option<f32>,
1126    #[props(default)] border_color: Option<Paint>,
1127    #[props(default)] elevation: Option<f32>,
1128    #[props(default)] fill_max_width: bool,
1129    #[props(default)] fill_max_height: bool,
1130    asset_id: u32,
1131) -> Element {
1132    rsx! {
1133        image {
1134            weight: opt_dp(weight),
1135            width: opt_dp(width),
1136            height: opt_dp(height),
1137            padding: opt_dp(padding),
1138            padding_role: opt_role(padding_role),
1139            background: opt_paint(background),
1140            shape_role: opt_role(shape_role),
1141            corner_radius: opt_dp(corner_radius),
1142            border_width: opt_dp(border_width),
1143            border_color: opt_paint(border_color),
1144            elevation: opt_dp(elevation),
1145            fill_max_width,
1146            fill_max_height,
1147            asset: i64::from(asset_id),
1148        }
1149    }
1150}
1151
1152/// A registered icon, tinted by a role. `Host::register_icon` registers the meaning rather
1153/// than a picture, so the same declaration comes out as the icon each design system draws.
1154#[component]
1155pub fn Icon(
1156    #[props(default)] weight: Option<f32>,
1157    #[props(default)] width: Option<f32>,
1158    #[props(default)] height: Option<f32>,
1159    #[props(default)] padding: Option<f32>,
1160    #[props(default)] padding_role: Option<SpaceRole>,
1161    #[props(default)] background: Option<Paint>,
1162    #[props(default)] shape_role: Option<ShapeRole>,
1163    #[props(default)] corner_radius: Option<f32>,
1164    #[props(default)] border_width: Option<f32>,
1165    #[props(default)] border_color: Option<Paint>,
1166    #[props(default)] elevation: Option<f32>,
1167    #[props(default)] fill_max_width: bool,
1168    #[props(default)] fill_max_height: bool,
1169    asset_id: u32,
1170    #[props(default)] color: Option<Paint>,
1171) -> Element {
1172    rsx! {
1173        icon {
1174            weight: opt_dp(weight),
1175            width: opt_dp(width),
1176            height: opt_dp(height),
1177            padding: opt_dp(padding),
1178            padding_role: opt_role(padding_role),
1179            background: opt_paint(background),
1180            shape_role: opt_role(shape_role),
1181            corner_radius: opt_dp(corner_radius),
1182            border_width: opt_dp(border_width),
1183            border_color: opt_paint(border_color),
1184            elevation: opt_dp(elevation),
1185            fill_max_width,
1186            fill_max_height,
1187            asset: i64::from(asset_id),
1188            color: color.map_or(0, |paint| paint.to_bits() as i64),
1189        }
1190    }
1191}
1192
1193/// A date, as whole days since 1970-01-01.
1194///
1195/// The widget says what the value is and what range is allowed. How it is picked, a
1196/// calendar grid, a wheel or a flyout, belongs to the design system, and there is no
1197/// property that lets the Host ask for one of them.
1198///
1199/// Time zone, locale, the first day of the week and the display format stay in the
1200/// Renderer, which is the only side that can read the platform's settings. A format string
1201/// crossing here would turn "follows the platform" into a claim the Renderer cannot keep.
1202#[component]
1203pub fn DatePicker(
1204    #[props(default)] weight: Option<f32>,
1205    #[props(default)] width: Option<f32>,
1206    #[props(default)] height: Option<f32>,
1207    #[props(default)] padding: Option<f32>,
1208    #[props(default)] padding_role: Option<SpaceRole>,
1209    #[props(default)] background: Option<Paint>,
1210    #[props(default)] shape_role: Option<ShapeRole>,
1211    #[props(default)] corner_radius: Option<f32>,
1212    #[props(default)] border_width: Option<f32>,
1213    #[props(default)] border_color: Option<Paint>,
1214    #[props(default)] elevation: Option<f32>,
1215    #[props(default)] fill_max_width: bool,
1216    #[props(default)] fill_max_height: bool,
1217    /// Days since 1970-01-01.
1218    value: i64,
1219    #[props(default)] min: Option<i64>,
1220    #[props(default)] max: Option<i64>,
1221    #[props(default = true)] enabled: bool,
1222    #[props(default)] on_change: EventHandler<i64>,
1223) -> Element {
1224    rsx! {
1225        datepicker {
1226            weight: opt_dp(weight),
1227            width: opt_dp(width),
1228            height: opt_dp(height),
1229            padding: opt_dp(padding),
1230            padding_role: opt_role(padding_role),
1231            background: opt_paint(background),
1232            shape_role: opt_role(shape_role),
1233            corner_radius: opt_dp(corner_radius),
1234            border_width: opt_dp(border_width),
1235            border_color: opt_paint(border_color),
1236            elevation: opt_dp(elevation),
1237            fill_max_width,
1238            fill_max_height,
1239            value,
1240            min: min.unwrap_or(i64::MIN),
1241            max: max.unwrap_or(i64::MAX),
1242            enabled,
1243            onchange: move |event: dioxus_core::Event<f64>| on_change.call(*event.data() as i64),
1244        }
1245    }
1246}
1247
1248/// A time of day, as minutes since midnight.
1249///
1250/// Whether it is picked on a dial, a wheel or a list is the design system's decision, and
1251/// whether it reads as 12 or 24 hour is the platform's.
1252#[component]
1253pub fn TimePicker(
1254    #[props(default)] weight: Option<f32>,
1255    #[props(default)] width: Option<f32>,
1256    #[props(default)] height: Option<f32>,
1257    #[props(default)] padding: Option<f32>,
1258    #[props(default)] padding_role: Option<SpaceRole>,
1259    #[props(default)] background: Option<Paint>,
1260    #[props(default)] shape_role: Option<ShapeRole>,
1261    #[props(default)] corner_radius: Option<f32>,
1262    #[props(default)] border_width: Option<f32>,
1263    #[props(default)] border_color: Option<Paint>,
1264    #[props(default)] elevation: Option<f32>,
1265    #[props(default)] fill_max_width: bool,
1266    #[props(default)] fill_max_height: bool,
1267    /// Minutes since midnight.
1268    value: u32,
1269    #[props(default)] min: Option<u32>,
1270    #[props(default)] max: Option<u32>,
1271    #[props(default = true)] enabled: bool,
1272    #[props(default)] on_change: EventHandler<u32>,
1273) -> Element {
1274    rsx! {
1275        timepicker {
1276            weight: opt_dp(weight),
1277            width: opt_dp(width),
1278            height: opt_dp(height),
1279            padding: opt_dp(padding),
1280            padding_role: opt_role(padding_role),
1281            background: opt_paint(background),
1282            shape_role: opt_role(shape_role),
1283            corner_radius: opt_dp(corner_radius),
1284            border_width: opt_dp(border_width),
1285            border_color: opt_paint(border_color),
1286            elevation: opt_dp(elevation),
1287            fill_max_width,
1288            fill_max_height,
1289            value: i64::from(value),
1290            min: i64::from(min.unwrap_or(0)),
1291            max: i64::from(max.unwrap_or(MINUTES_IN_A_DAY - 1)),
1292            enabled,
1293            onchange: move |event: dioxus_core::Event<f64>| {
1294                on_change.call((*event.data() as i64).clamp(0, i64::from(MINUTES_IN_A_DAY - 1)) as u32)
1295            },
1296        }
1297    }
1298}
1299
1300const MINUTES_IN_A_DAY: u32 = 24 * 60;
1301
1302/// One choice out of a list. Each child is one option, and the Renderer reports the
1303/// position the user landed on.
1304#[component]
1305pub fn Dropdown(
1306    #[props(default)] weight: Option<f32>,
1307    #[props(default)] width: Option<f32>,
1308    #[props(default)] height: Option<f32>,
1309    #[props(default)] padding: Option<f32>,
1310    #[props(default)] padding_role: Option<SpaceRole>,
1311    #[props(default)] background: Option<Paint>,
1312    #[props(default)] shape_role: Option<ShapeRole>,
1313    #[props(default)] corner_radius: Option<f32>,
1314    #[props(default)] border_width: Option<f32>,
1315    #[props(default)] border_color: Option<Paint>,
1316    #[props(default)] elevation: Option<f32>,
1317    #[props(default)] fill_max_width: bool,
1318    #[props(default)] fill_max_height: bool,
1319    #[props(default)] selected_index: usize,
1320    #[props(default = true)] enabled: bool,
1321    #[props(default)] on_change: EventHandler<usize>,
1322    children: Element,
1323) -> Element {
1324    rsx! {
1325        dropdown {
1326            weight: opt_dp(weight),
1327            width: opt_dp(width),
1328            height: opt_dp(height),
1329            padding: opt_dp(padding),
1330            padding_role: opt_role(padding_role),
1331            background: opt_paint(background),
1332            shape_role: opt_role(shape_role),
1333            corner_radius: opt_dp(corner_radius),
1334            border_width: opt_dp(border_width),
1335            border_color: opt_paint(border_color),
1336            elevation: opt_dp(elevation),
1337            fill_max_width,
1338            fill_max_height,
1339            selected_index: selected_index as i64,
1340            enabled,
1341            onchange: move |event: dioxus_core::Event<f64>| {
1342                on_change.call((*event.data() as i64).max(0) as usize)
1343            },
1344            {children}
1345        }
1346    }
1347}
1348
1349/// A two-state box.
1350///
1351/// The widget is controlled: it draws exactly what `checked` says and reports what the
1352/// user asked for, so the value the Host holds and the box on screen cannot disagree.
1353/// What the mark looks like, and whether pressing it ripples or dims, is the design
1354/// system's rule.
1355#[component]
1356pub fn Checkbox(
1357    #[props(default)] weight: Option<f32>,
1358    #[props(default)] width: Option<f32>,
1359    #[props(default)] height: Option<f32>,
1360    #[props(default)] padding: Option<f32>,
1361    #[props(default)] padding_role: Option<SpaceRole>,
1362    #[props(default)] background: Option<Paint>,
1363    #[props(default)] shape_role: Option<ShapeRole>,
1364    #[props(default)] corner_radius: Option<f32>,
1365    #[props(default)] border_width: Option<f32>,
1366    #[props(default)] border_color: Option<Paint>,
1367    #[props(default)] elevation: Option<f32>,
1368    #[props(default)] fill_max_width: bool,
1369    #[props(default)] fill_max_height: bool,
1370    #[props(default)] checked: bool,
1371    #[props(default = true)] enabled: bool,
1372    #[props(default)] on_change: EventHandler<bool>,
1373) -> Element {
1374    rsx! {
1375        checkbox {
1376            weight: opt_dp(weight),
1377            width: opt_dp(width),
1378            height: opt_dp(height),
1379            padding: opt_dp(padding),
1380            padding_role: opt_role(padding_role),
1381            background: opt_paint(background),
1382            shape_role: opt_role(shape_role),
1383            corner_radius: opt_dp(corner_radius),
1384            border_width: opt_dp(border_width),
1385            border_color: opt_paint(border_color),
1386            elevation: opt_dp(elevation),
1387            fill_max_width,
1388            fill_max_height,
1389            checked,
1390            enabled,
1391            onchange: move |event: dioxus_core::Event<f64>| on_change.call(is_on(*event.data())),
1392        }
1393    }
1394}
1395
1396/// One choice out of several. Which siblings it excludes is the Host's business, so the
1397/// widget carries only whether this one is the chosen one.
1398///
1399/// `selected` is the Compose name for that state and is what the rsx attribute is called.
1400/// On the wire it is the same "is this on" boolean the other two toggles send, because a
1401/// selected radio button and a switch that is on are one fact.
1402#[component]
1403pub fn RadioButton(
1404    #[props(default)] weight: Option<f32>,
1405    #[props(default)] width: Option<f32>,
1406    #[props(default)] height: Option<f32>,
1407    #[props(default)] padding: Option<f32>,
1408    #[props(default)] padding_role: Option<SpaceRole>,
1409    #[props(default)] background: Option<Paint>,
1410    #[props(default)] shape_role: Option<ShapeRole>,
1411    #[props(default)] corner_radius: Option<f32>,
1412    #[props(default)] border_width: Option<f32>,
1413    #[props(default)] border_color: Option<Paint>,
1414    #[props(default)] elevation: Option<f32>,
1415    #[props(default)] fill_max_width: bool,
1416    #[props(default)] fill_max_height: bool,
1417    #[props(default)] selected: bool,
1418    #[props(default = true)] enabled: bool,
1419    #[props(default)] on_change: EventHandler<bool>,
1420) -> Element {
1421    rsx! {
1422        radiobutton {
1423            weight: opt_dp(weight),
1424            width: opt_dp(width),
1425            height: opt_dp(height),
1426            padding: opt_dp(padding),
1427            padding_role: opt_role(padding_role),
1428            background: opt_paint(background),
1429            shape_role: opt_role(shape_role),
1430            corner_radius: opt_dp(corner_radius),
1431            border_width: opt_dp(border_width),
1432            border_color: opt_paint(border_color),
1433            elevation: opt_dp(elevation),
1434            fill_max_width,
1435            fill_max_height,
1436            checked: selected,
1437            enabled,
1438            onchange: move |event: dioxus_core::Event<f64>| on_change.call(is_on(*event.data())),
1439        }
1440    }
1441}
1442
1443/// An on/off control. The same contract as `Checkbox`: the Host owns the value, and the
1444/// track, the thumb and the way the change is animated belong to the design system.
1445#[component]
1446pub fn Switch(
1447    #[props(default)] weight: Option<f32>,
1448    #[props(default)] width: Option<f32>,
1449    #[props(default)] height: Option<f32>,
1450    #[props(default)] padding: Option<f32>,
1451    #[props(default)] padding_role: Option<SpaceRole>,
1452    #[props(default)] background: Option<Paint>,
1453    #[props(default)] shape_role: Option<ShapeRole>,
1454    #[props(default)] corner_radius: Option<f32>,
1455    #[props(default)] border_width: Option<f32>,
1456    #[props(default)] border_color: Option<Paint>,
1457    #[props(default)] elevation: Option<f32>,
1458    #[props(default)] fill_max_width: bool,
1459    #[props(default)] fill_max_height: bool,
1460    #[props(default)] checked: bool,
1461    #[props(default = true)] enabled: bool,
1462    #[props(default)] on_change: EventHandler<bool>,
1463) -> Element {
1464    rsx! {
1465        switch {
1466            weight: opt_dp(weight),
1467            width: opt_dp(width),
1468            height: opt_dp(height),
1469            padding: opt_dp(padding),
1470            padding_role: opt_role(padding_role),
1471            background: opt_paint(background),
1472            shape_role: opt_role(shape_role),
1473            corner_radius: opt_dp(corner_radius),
1474            border_width: opt_dp(border_width),
1475            border_color: opt_paint(border_color),
1476            elevation: opt_dp(elevation),
1477            fill_max_width,
1478            fill_max_height,
1479            checked,
1480            enabled,
1481            onchange: move |event: dioxus_core::Event<f64>| on_change.call(is_on(*event.data())),
1482        }
1483    }
1484}
1485
1486/// A value picked from a range.
1487///
1488/// The position a drag is passing through stays in the Renderer, so following a finger
1489/// costs no boundary call, and `value` seeds that position and moves it when the change
1490/// came from elsewhere. `steps` is the number of stops between the two ends; zero leaves
1491/// the slider continuous.
1492#[component]
1493pub fn Slider(
1494    #[props(default)] weight: Option<f32>,
1495    #[props(default)] width: Option<f32>,
1496    #[props(default)] height: Option<f32>,
1497    #[props(default)] padding: Option<f32>,
1498    #[props(default)] padding_role: Option<SpaceRole>,
1499    #[props(default)] background: Option<Paint>,
1500    /// The colour of the part of the track that has been filled in, and of the thumb.
1501    ///
1502    /// The design system decides where this is unset, which is what an adaptive
1503    /// application wants. A unified one names it, because its reference may have no
1504    /// accent at all and a system asked the question answers with its own.
1505    #[props(default)]
1506    color: Option<Paint>,
1507    #[props(default)] shape_role: Option<ShapeRole>,
1508    #[props(default)] corner_radius: Option<f32>,
1509    #[props(default)] border_width: Option<f32>,
1510    #[props(default)] border_color: Option<Paint>,
1511    #[props(default)] elevation: Option<f32>,
1512    #[props(default)] fill_max_width: bool,
1513    #[props(default)] fill_max_height: bool,
1514    #[props(default)] value: f32,
1515    #[props(default = 0.0)] min: f32,
1516    #[props(default = 1.0)] max: f32,
1517    #[props(default)] steps: u32,
1518    #[props(default = true)] enabled: bool,
1519    #[props(default)] on_change: EventHandler<f32>,
1520) -> Element {
1521    rsx! {
1522        slider {
1523            weight: opt_dp(weight),
1524            width: opt_dp(width),
1525            height: opt_dp(height),
1526            padding: opt_dp(padding),
1527            padding_role: opt_role(padding_role),
1528            background: opt_paint(background),
1529            color: opt_paint(color),
1530            shape_role: opt_role(shape_role),
1531            corner_radius: opt_dp(corner_radius),
1532            border_width: opt_dp(border_width),
1533            border_color: opt_paint(border_color),
1534            elevation: opt_dp(elevation),
1535            fill_max_width,
1536            fill_max_height,
1537            value: f64::from(value),
1538            min: f64::from(min),
1539            max: f64::from(max),
1540            steps: i64::from(steps),
1541            enabled,
1542            onchange: move |event: dioxus_core::Event<f64>| on_change.call(*event.data() as f32),
1543        }
1544    }
1545}
1546
1547/// Work in progress. `determinate` says whether `value` means anything, and `circular`
1548/// picks between the two forms every supported design system has.
1549///
1550/// There is no speed, no easing and no track colour here: how an indeterminate indicator
1551/// travels is motion, and motion is the design system's rule.
1552#[component]
1553pub fn ProgressIndicator(
1554    #[props(default)] weight: Option<f32>,
1555    #[props(default)] width: Option<f32>,
1556    #[props(default)] height: Option<f32>,
1557    #[props(default)] padding: Option<f32>,
1558    #[props(default)] padding_role: Option<SpaceRole>,
1559    #[props(default)] background: Option<Paint>,
1560    #[props(default)] shape_role: Option<ShapeRole>,
1561    #[props(default)] corner_radius: Option<f32>,
1562    #[props(default)] border_width: Option<f32>,
1563    #[props(default)] border_color: Option<Paint>,
1564    #[props(default)] elevation: Option<f32>,
1565    #[props(default)] fill_max_width: bool,
1566    #[props(default)] fill_max_height: bool,
1567    #[props(default)] value: f32,
1568    #[props(default = true)] determinate: bool,
1569    #[props(default)] circular: bool,
1570) -> Element {
1571    rsx! {
1572        progressindicator {
1573            weight: opt_dp(weight),
1574            width: opt_dp(width),
1575            height: opt_dp(height),
1576            padding: opt_dp(padding),
1577            padding_role: opt_role(padding_role),
1578            background: opt_paint(background),
1579            shape_role: opt_role(shape_role),
1580            corner_radius: opt_dp(corner_radius),
1581            border_width: opt_dp(border_width),
1582            border_color: opt_paint(border_color),
1583            elevation: opt_dp(elevation),
1584            fill_max_width,
1585            fill_max_height,
1586            value: f64::from(value),
1587            determinate,
1588            circular,
1589        }
1590    }
1591}
1592
1593/// A line between two things. The axis is the only decision the Host makes: the
1594/// thickness, the colour and the inset all come from the design system.
1595#[component]
1596pub fn Divider(
1597    #[props(default)] weight: Option<f32>,
1598    #[props(default)] width: Option<f32>,
1599    #[props(default)] height: Option<f32>,
1600    #[props(default)] padding: Option<f32>,
1601    #[props(default)] padding_role: Option<SpaceRole>,
1602    #[props(default)] background: Option<Paint>,
1603    #[props(default)] shape_role: Option<ShapeRole>,
1604    #[props(default)] corner_radius: Option<f32>,
1605    #[props(default)] border_width: Option<f32>,
1606    #[props(default)] border_color: Option<Paint>,
1607    #[props(default)] elevation: Option<f32>,
1608    #[props(default)] fill_max_width: bool,
1609    #[props(default)] fill_max_height: bool,
1610    #[props(default)] vertical: bool,
1611) -> Element {
1612    rsx! {
1613        divider {
1614            weight: opt_dp(weight),
1615            width: opt_dp(width),
1616            height: opt_dp(height),
1617            padding: opt_dp(padding),
1618            padding_role: opt_role(padding_role),
1619            background: opt_paint(background),
1620            shape_role: opt_role(shape_role),
1621            corner_radius: opt_dp(corner_radius),
1622            border_width: opt_dp(border_width),
1623            border_color: opt_paint(border_color),
1624            elevation: opt_dp(elevation),
1625            fill_max_width,
1626            fill_max_height,
1627            vertical,
1628        }
1629    }
1630}
1631
1632/// A toggle's new state, as it arrives on the wire: off is 0.0 and on is 1.0.
1633///
1634/// The comparison is against zero rather than against 1.0 so a Renderer that reports a
1635/// half-finished transition still reads as on.
1636fn is_on(value: f64) -> bool {
1637    value != 0.0
1638}
1639
1640/// A set of destinations with one of them selected, plus the screen they lead to.
1641///
1642/// The children that are `NavigationItem` are the destinations; every other child is the
1643/// content of the selected screen. Splitting by kind rather than by index means a
1644/// destination that only exists under some condition does not force the Host to keep two
1645/// lists in step by hand.
1646///
1647/// **Nothing here says bar, rail or drawer.** The Renderer has already measured the window,
1648/// so it chooses: a bar across the bottom of a phone-shaped window, a rail down the side of
1649/// a tablet-shaped one, a drawer standing open on a desktop. Asking the Host to choose
1650/// would mean the width travelling up, a whole VirtualDom pass, and a tree of destinations
1651/// being destroyed and rebuilt every time the window crossed a boundary, to arrive at the
1652/// same three layouts the Renderer can reach by moving the nodes it already has.
1653///
1654/// `selected_index` counts destinations, not children. It seeds the Renderer's selection
1655/// and moves it when the change came from somewhere other than a tap; a tap moves the
1656/// selection in the Renderer and reports it by firing that destination's own `on_click`.
1657#[component]
1658pub fn Navigation(
1659    #[props(default)] weight: Option<f32>,
1660    #[props(default)] width: Option<f32>,
1661    #[props(default)] height: Option<f32>,
1662    #[props(default)] padding: Option<f32>,
1663    #[props(default)] padding_role: Option<SpaceRole>,
1664    #[props(default)] background: Option<Paint>,
1665    #[props(default)] shape_role: Option<ShapeRole>,
1666    #[props(default)] corner_radius: Option<f32>,
1667    #[props(default)] border_width: Option<f32>,
1668    #[props(default)] border_color: Option<Paint>,
1669    #[props(default)] elevation: Option<f32>,
1670    #[props(default)] fill_max_width: bool,
1671    #[props(default)] fill_max_height: bool,
1672    #[props(default)] selected_index: usize,
1673    /// What sits above the destinations in a strip: a mark, a name, a control.
1674    ///
1675    /// Drawn only where the destinations are a strip down the side. A bar along the bottom
1676    /// is five destinations standing side by side and has nowhere to put this.
1677    #[props(default)]
1678    head: Option<Element>,
1679    /// What sits below them. An account row, usually.
1680    #[props(default)]
1681    foot: Option<Element>,
1682    children: Element,
1683) -> Element {
1684    rsx! {
1685        navigation {
1686            weight: opt_dp(weight),
1687            width: opt_dp(width),
1688            height: opt_dp(height),
1689            padding: opt_dp(padding),
1690            padding_role: opt_role(padding_role),
1691            background: opt_paint(background),
1692            shape_role: opt_role(shape_role),
1693            corner_radius: opt_dp(corner_radius),
1694            border_width: opt_dp(border_width),
1695            border_color: opt_paint(border_color),
1696            elevation: opt_dp(elevation),
1697            fill_max_width,
1698            fill_max_height,
1699            selected_index: selected_index as i64,
1700            // The same slot widget a Scaffold's bars arrive in, because it says the same
1701            // thing: where inside my parent this content goes.
1702            if let Some(head) = head {
1703                scaffoldslot { slot: i64::from(u16::from(SlotRole::TopBar)), {head} }
1704            }
1705            if let Some(foot) = foot {
1706                scaffoldslot { slot: i64::from(u16::from(SlotRole::BottomBar)), {foot} }
1707            }
1708            {children}
1709        }
1710    }
1711}
1712
1713/// One destination inside a [`Navigation`].
1714///
1715/// The label and the icon are properties rather than a child tree, because the three
1716/// presentations disagree about how they go together: a bottom bar stacks a small label
1717/// under the icon, a rail may drop the label entirely, a drawer sets the label beside the
1718/// icon and left-aligns the row. A `Column { Icon, Text }` handed over as children would
1719/// have settled that question in the Host, where the window's width is not known.
1720///
1721/// The icon is a meaning, never a picture: `IconRole::Inbox` comes out as this design
1722/// system's inbox.
1723#[component]
1724pub fn NavigationItem(
1725    #[props(default)] weight: Option<f32>,
1726    #[props(default)] width: Option<f32>,
1727    #[props(default)] height: Option<f32>,
1728    #[props(default)] padding: Option<f32>,
1729    #[props(default)] padding_role: Option<SpaceRole>,
1730    #[props(default)] background: Option<Paint>,
1731    #[props(default)] shape_role: Option<ShapeRole>,
1732    #[props(default)] corner_radius: Option<f32>,
1733    #[props(default)] border_width: Option<f32>,
1734    #[props(default)] border_color: Option<Paint>,
1735    #[props(default)] elevation: Option<f32>,
1736    #[props(default)] fill_max_width: bool,
1737    #[props(default)] fill_max_height: bool,
1738    #[props(default)] text: String,
1739    #[props(default)] icon: Option<IconRole>,
1740    /// The colour of this destination's icon and label, in both states.
1741    ///
1742    /// The design system decides what selected looks like where this is not set, which is
1743    /// what an adaptive application wants. A unified one names it, because its reference
1744    /// may have no accent in the bar at all and a system asked the question answers with
1745    /// its own.
1746    #[props(default)]
1747    color: Option<Paint>,
1748    #[props(default = true)] enabled: bool,
1749    /// The named group of the strip this destination belongs to.
1750    ///
1751    /// Neighbouring destinations carrying the same name are one group and the name is its
1752    /// heading. A destination that names none belongs to no group and is drawn where it
1753    /// always was. What a heading looks like is the design system's.
1754    #[props(default)]
1755    section: Option<String>,
1756    #[props(default)] on_click: EventHandler<()>,
1757) -> Element {
1758    rsx! {
1759        navigationitem {
1760            weight: opt_dp(weight),
1761            width: opt_dp(width),
1762            height: opt_dp(height),
1763            padding: opt_dp(padding),
1764            padding_role: opt_role(padding_role),
1765            background: opt_paint(background),
1766            shape_role: opt_role(shape_role),
1767            corner_radius: opt_dp(corner_radius),
1768            border_width: opt_dp(border_width),
1769            border_color: opt_paint(border_color),
1770            elevation: opt_dp(elevation),
1771            fill_max_width,
1772            fill_max_height,
1773            text,
1774            icon: opt_role(icon),
1775            color: opt_paint(color),
1776            enabled,
1777            section,
1778            onclick: move |_| on_click.call(()),
1779        }
1780    }
1781}
1782
1783/// A temporary surface that slides in from an edge, holding whatever its children are.
1784///
1785/// It is a `Dialog` in every way that crosses the boundary: `open` seeds the Renderer's own
1786/// open state and carries a change that came from elsewhere, and `on_dismiss` says once
1787/// that the user asked to close it. The drag itself does not cross: while the sheet is
1788/// being pulled about, Rust hears nothing, and only a drag that ends in a dismissal is
1789/// reported, once.
1790///
1791/// Which edge it comes from is the Renderer's decision, made from the width it measured:
1792/// up from the bottom in a narrow window, in from the side in a wide one. There is no
1793/// property that could ask for one of them, for the same reason there is none that can ask
1794/// a date picker for a wheel.
1795#[component]
1796pub fn Sheet(
1797    #[props(default)] weight: Option<f32>,
1798    #[props(default)] width: Option<f32>,
1799    #[props(default)] height: Option<f32>,
1800    #[props(default)] padding: Option<f32>,
1801    #[props(default)] padding_role: Option<SpaceRole>,
1802    #[props(default)] background: Option<Paint>,
1803    #[props(default)] shape_role: Option<ShapeRole>,
1804    #[props(default)] corner_radius: Option<f32>,
1805    #[props(default)] border_width: Option<f32>,
1806    #[props(default)] border_color: Option<Paint>,
1807    #[props(default)] elevation: Option<f32>,
1808    #[props(default)] fill_max_width: bool,
1809    #[props(default)] fill_max_height: bool,
1810    #[props(default)] open: bool,
1811    #[props(default)] on_dismiss: EventHandler<()>,
1812    children: Element,
1813) -> Element {
1814    rsx! {
1815        sheet {
1816            weight: opt_dp(weight),
1817            width: opt_dp(width),
1818            height: opt_dp(height),
1819            padding: opt_dp(padding),
1820            padding_role: opt_role(padding_role),
1821            background: opt_paint(background),
1822            shape_role: opt_role(shape_role),
1823            corner_radius: opt_dp(corner_radius),
1824            border_width: opt_dp(border_width),
1825            border_color: opt_paint(border_color),
1826            elevation: opt_dp(elevation),
1827            fill_max_width,
1828            fill_max_height,
1829            open,
1830            ondismiss: move |_| on_dismiss.call(()),
1831            {children}
1832        }
1833    }
1834}
1835
1836/// The screen's frame: a top bar, a bottom bar, a floating action and the page.
1837///
1838/// Shaped after Material's `Scaffold`, because that is the vocabulary people already
1839/// write, and for the same reason: a screen's frame is the same few parts every time, and
1840/// assembling it by hand in every application is how every application ends up with a
1841/// different one.
1842///
1843/// What it buys here is more than the layout. **The application says which slot it filled
1844/// and nothing about what the slot becomes.** A top bar is the window's caption on a
1845/// desktop that hands its caption to the application and an ordinary bar where it does
1846/// not; a bottom bar is a bar on a phone, a rail on a tablet and a permanent drawer on a
1847/// desktop; a floating action floats, or sits in the toolbar, or folds into a menu. Those
1848/// are decisions about the running platform and the measured window, and both of those
1849/// are known in the Renderer and not here.
1850///
1851/// The page is the children, and it is laid out clear of whatever the other slots took.
1852/// Nothing about that arithmetic reaches the application, which is the point: a screen
1853/// that computed its own inset would be wrong the moment the Renderer chose differently.
1854///
1855/// ```ignore
1856/// rsx! {
1857///     Scaffold {
1858///         top_bar: rsx! { TopAppBar { title: "Standard" } },
1859///         bottom_bar: rsx! { Navigation { /* destinations */ } },
1860///         Column { /* the page */ }
1861///     }
1862/// }
1863/// ```
1864#[component]
1865pub fn Scaffold(
1866    /// The window's own colour, where this screen holds its own palette.
1867    ///
1868    /// Read off the root, so a frame that is the root is where it has to be said. A
1869    /// screen that says nothing gets the theme's page colour.
1870    #[props(default)]
1871    background: Option<Paint>,
1872    /// What the window itself is made of, where the screen wants it to be a material.
1873    ///
1874    /// Read off the root like the colour above, and asking for it is what stands the
1875    /// window up with something behind it to show. `MaterialRole::Chrome` is the window's
1876    /// own: on a platform that can put the system's material behind a window, the desktop
1877    /// shows through wherever the design system draws chrome; everywhere else the design
1878    /// system answers the role the way it answers it on any other node.
1879    #[props(default)]
1880    material: Option<MaterialRole>,
1881    /// The bar across the top, where this screen wants one.
1882    #[props(default)]
1883    top_bar: Option<Element>,
1884    /// The destinations, or the actions that belong at the bottom of a phone screen.
1885    #[props(default)]
1886    bottom_bar: Option<Element>,
1887    /// The one action a screen is mostly about, where it has one.
1888    #[props(default)]
1889    floating_action: Option<Element>,
1890    /// The page.
1891    children: Element,
1892) -> Element {
1893    rsx! {
1894        scaffold {
1895            background: opt_paint(background),
1896            material: opt_role(material),
1897            if let Some(bar) = top_bar {
1898                scaffoldslot { slot: i64::from(u16::from(SlotRole::TopBar)), {bar} }
1899            }
1900            if let Some(bar) = bottom_bar {
1901                scaffoldslot { slot: i64::from(u16::from(SlotRole::BottomBar)), {bar} }
1902            }
1903            if let Some(action) = floating_action {
1904                scaffoldslot {
1905                    slot: i64::from(u16::from(SlotRole::FloatingAction)),
1906                    {action}
1907                }
1908            }
1909            scaffoldslot { slot: i64::from(u16::from(SlotRole::Content)), {children} }
1910        }
1911    }
1912}
1913
1914/// A grid that only builds the rows it can show.
1915///
1916/// The windowing protocol is `LazyColumn`'s, unchanged: the Renderer asks for a range of
1917/// items and this materialises exactly that range. What a grid adds is that the range
1918/// arrives rounded to whole rows, because a row is what the Renderer lays out.
1919///
1920/// How wide a column is can be said two ways, and they are different questions. `columns`
1921/// is a count the screen insists on. `min_column_width` is a width below which a column
1922/// may not fall, and the Renderer divides its own width by it: that is the same judgement
1923/// as a size class, made where the width is known, so the Host does not make it.
1924///
1925/// Saying both is saying the count, because a count leaves nothing for the width to
1926/// decide.
1927#[component]
1928pub fn LazyGrid(
1929    #[props(default)] weight: Option<f32>,
1930    #[props(default)] width: Option<f32>,
1931    #[props(default)] height: Option<f32>,
1932    #[props(default)] padding: Option<f32>,
1933    #[props(default)] padding_role: Option<SpaceRole>,
1934    #[props(default)] background: Option<Paint>,
1935    #[props(default)] shape_role: Option<ShapeRole>,
1936    #[props(default)] corner_radius: Option<f32>,
1937    #[props(default)] border_width: Option<f32>,
1938    #[props(default)] border_color: Option<Paint>,
1939    #[props(default)] elevation: Option<f32>,
1940    #[props(default)] fill_max_width: bool,
1941    #[props(default)] fill_max_height: bool,
1942    /// A fixed number of columns.
1943    #[props(default)]
1944    columns: Option<u32>,
1945    /// The narrowest a column may be, where the count is the Renderer's to work out.
1946    #[props(default)]
1947    min_column_width: Option<f32>,
1948    item_count: usize,
1949    #[props(default)] key_of: Option<Callback<usize, String>>,
1950    item: Callback<usize, Element>,
1951) -> Element {
1952    let mut range = use_signal(|| (0_usize, 0_usize));
1953    let (start, count) = range();
1954    let first = start.min(item_count);
1955    let last = first.saturating_add(count).min(item_count);
1956    rsx! {
1957        lazygrid {
1958            weight: opt_dp(weight),
1959            width: opt_dp(width),
1960            height: opt_dp(height),
1961            padding: opt_dp(padding),
1962            padding_role: opt_role(padding_role),
1963            background: opt_paint(background),
1964            shape_role: opt_role(shape_role),
1965            corner_radius: opt_dp(corner_radius),
1966            border_width: opt_dp(border_width),
1967            border_color: opt_paint(border_color),
1968            elevation: opt_dp(elevation),
1969            fill_max_width,
1970            fill_max_height,
1971            item_count: item_count as i64,
1972            columns: columns.map(i64::from),
1973            min_column_width: min_column_width.map(f64::from),
1974            onrangerequest: move |event: dioxus_core::Event<RangeRequest>| {
1975                let requested = event.data();
1976                range.set((requested.start(), requested.count()));
1977            },
1978            for index in first..last {
1979                {
1980                    let item_key = key_of
1981                        .map_or_else(|| index.to_string(), |key_of| key_of.call(index));
1982                    rsx! {
1983                        composebox { key: "{item_key}", item_key, {item.call(index)} }
1984                    }
1985                }
1986            }
1987        }
1988    }
1989}
1990
1991/// A place files may be dropped.
1992///
1993/// A widget rather than a property on every container, because willingness has to be
1994/// absent by default and a handler cannot be: a listener is attached whether or not a
1995/// screen supplied one, so four containers each grew three records for saying nothing.
1996/// Being this widget is the willingness, and nothing else in the tree carries any of it.
1997///
1998/// Desktop only in effect. A phone has no notion of letting a file go over a window, so
1999/// the events never arrive there and the same screen compiles and runs unchanged.
2000#[component]
2001pub fn FileDropTarget(
2002    #[props(default)] weight: Option<f32>,
2003    #[props(default)] width: Option<f32>,
2004    #[props(default)] height: Option<f32>,
2005    #[props(default)] padding: Option<f32>,
2006    #[props(default)] padding_role: Option<SpaceRole>,
2007    #[props(default)] background: Option<Paint>,
2008    #[props(default)] shape_role: Option<ShapeRole>,
2009    #[props(default)] corner_radius: Option<f32>,
2010    #[props(default)] border_width: Option<f32>,
2011    #[props(default)] border_color: Option<Paint>,
2012    #[props(default)] elevation: Option<f32>,
2013    #[props(default)] fill_max_width: bool,
2014    #[props(default)] fill_max_height: bool,
2015    #[props(default)] alignment: Option<Alignment>,
2016    /// Files have come over this node. Nothing about what they are: the platforms
2017    /// disagree about what is knowable before a drop, and a screen that only lights up
2018    /// does not need to know.
2019    #[props(default)]
2020    on_files_entered: EventHandler<()>,
2021    /// Files were let go here.
2022    #[props(default)]
2023    on_files_dropped: EventHandler<FileDrop>,
2024    children: Element,
2025) -> Element {
2026    rsx! {
2027        filedroptarget {
2028            weight: opt_dp(weight),
2029            width: opt_dp(width),
2030            height: opt_dp(height),
2031            padding: opt_dp(padding),
2032            padding_role: opt_role(padding_role),
2033            background: opt_paint(background),
2034            shape_role: opt_role(shape_role),
2035            corner_radius: opt_dp(corner_radius),
2036            border_width: opt_dp(border_width),
2037            border_color: opt_paint(border_color),
2038            elevation: opt_dp(elevation),
2039            fill_max_width,
2040            fill_max_height,
2041            alignment: opt_role(alignment),
2042            onfilesentered: move |_: dioxus_core::Event<()>| on_files_entered.call(()),
2043            onfilesdropped: move |event: dioxus_core::Event<FileDrop>| {
2044                on_files_dropped.call(event.data().as_ref().clone());
2045            },
2046            {children}
2047        }
2048    }
2049}
2050
2051/// A small mark on something else that says how many, that there is something new, or one
2052/// short word.
2053///
2054/// With one child it is attached to that child: an icon, an avatar, a destination. With no
2055/// child it stands on its own, the count at the end of a list row. Compose calls the first
2056/// a `BadgedBox` and the second a `Badge`; here they are one widget, because whether there
2057/// is something to attach to is already said by the children.
2058///
2059/// `value` is a count and `text` is a short word. With neither it is a dot. Where it sits
2060/// on its child, what shape it is, and whether 120 is written out or shortened to `99+`
2061/// are the design system's, so there is no property for a corner or a ceiling.
2062///
2063/// The colour is a role and never a literal, and it defaults to the error role the way
2064/// every one of these systems draws an unread count. The content is read out as part of
2065/// the child it is attached to, and never on its own.
2066#[component]
2067pub fn Badge(
2068    #[props(default)] weight: Option<f32>,
2069    #[props(default)] width: Option<f32>,
2070    #[props(default)] height: Option<f32>,
2071    #[props(default)] padding: Option<f32>,
2072    #[props(default)] padding_role: Option<SpaceRole>,
2073    #[props(default)] background: Option<Paint>,
2074    #[props(default)] shape_role: Option<ShapeRole>,
2075    #[props(default)] corner_radius: Option<f32>,
2076    #[props(default)] border_width: Option<f32>,
2077    #[props(default)] border_color: Option<Paint>,
2078    #[props(default)] elevation: Option<f32>,
2079    #[props(default)] fill_max_width: bool,
2080    #[props(default)] fill_max_height: bool,
2081    /// How many. Leave it out for a dot or a word: no count is not the same as a count of
2082    /// nothing, and only the screen knows which one it means.
2083    #[props(default)]
2084    value: Option<u32>,
2085    /// One short word, shown where a count would be. A count wins where both are given.
2086    #[props(default)]
2087    text: Option<String>,
2088    /// The role the mark is filled with. The error role where it is not given.
2089    #[props(default)]
2090    color: Option<ColorRole>,
2091    #[props(default)] children: Element,
2092) -> Element {
2093    rsx! {
2094        badge {
2095            weight: opt_dp(weight),
2096            width: opt_dp(width),
2097            height: opt_dp(height),
2098            padding: opt_dp(padding),
2099            padding_role: opt_role(padding_role),
2100            background: opt_paint(background),
2101            shape_role: opt_role(shape_role),
2102            corner_radius: opt_dp(corner_radius),
2103            border_width: opt_dp(border_width),
2104            border_color: opt_paint(border_color),
2105            elevation: opt_dp(elevation),
2106            fill_max_width,
2107            fill_max_height,
2108            count: value.map(i64::from),
2109            text,
2110            color: opt_paint(color.map(Paint::Role)),
2111            {children}
2112        }
2113    }
2114}
2115
2116/// A region whose text can be dragged over and copied as one selection.
2117///
2118/// Every `Text` inside it, however deep and however many, is part of the same selection,
2119/// so a whole message of paragraphs, list items and code can be picked up in one drag and
2120/// copied with a line break between each block. Text outside one cannot be selected at all,
2121/// which is what keeps button labels and destinations from being dragged over as though
2122/// the screen were a document.
2123///
2124/// Buttons, checkboxes and the other controls inside it are left out of the selection and
2125/// keep working as they always do.
2126///
2127/// Nothing about selecting or copying reaches this side. What is selected, the copy
2128/// shortcut, the context menu and the clipboard all belong to the Renderer, so there is no
2129/// event to handle and no way to read or set the selection from here.
2130#[component]
2131pub fn SelectionContainer(
2132    #[props(default)] weight: Option<f32>,
2133    #[props(default)] width: Option<f32>,
2134    #[props(default)] height: Option<f32>,
2135    #[props(default)] padding: Option<f32>,
2136    #[props(default)] padding_role: Option<SpaceRole>,
2137    #[props(default)] background: Option<Paint>,
2138    #[props(default)] shape_role: Option<ShapeRole>,
2139    #[props(default)] corner_radius: Option<f32>,
2140    #[props(default)] border_width: Option<f32>,
2141    #[props(default)] border_color: Option<Paint>,
2142    #[props(default)] elevation: Option<f32>,
2143    #[props(default)] fill_max_width: bool,
2144    #[props(default)] fill_max_height: bool,
2145    children: Element,
2146) -> Element {
2147    rsx! {
2148        selectioncontainer {
2149            weight: opt_dp(weight),
2150            width: opt_dp(width),
2151            height: opt_dp(height),
2152            padding: opt_dp(padding),
2153            padding_role: opt_role(padding_role),
2154            background: opt_paint(background),
2155            shape_role: opt_role(shape_role),
2156            corner_radius: opt_dp(corner_radius),
2157            border_width: opt_dp(border_width),
2158            border_color: opt_paint(border_color),
2159            elevation: opt_dp(elevation),
2160            fill_max_width,
2161            fill_max_height,
2162            {children}
2163        }
2164    }
2165}