Skip to main content

compose_rust/ui/
widgets.rs

1//! One composable per widget in the schema.
2//!
3//! Each is a function with the widget's Compose name that takes what the widget cannot do
4//! without, and returns a description whose methods set the rest. The description is
5//! composed when the statement it is written in ends, or at once by `content` for a
6//! widget that has children.
7//!
8//! Every attribute list below is the `rsx!` widget's, in its order, with its defaults. A
9//! difference would be a widget that draws differently depending on which API wrote it.
10
11#![allow(non_snake_case)]
12
13use super::{
14    Attrs, Modifier, Mods, dp_or_zero, emit, optional_text, paint_or_zero, push_modifiers,
15    role_or_zero, widget_key,
16};
17use crate::drawing::DrawList;
18use crate::runtime::composer::{EventCallback, OwnedAttr};
19use crate::schema::{
20    Alignment, Arrangement, ButtonKind, ButtonVariant, ColorRole, IconRole, Paint, SlotRole,
21    SpaceRole, TextAlign, TextOverflow, TypeRole, WidgetKind,
22};
23use crate::spans::TextSpans;
24use crate::{FileDrop, KeyEvent, PointerEvent, PointerKinds};
25use std::cell::RefCell;
26use std::rc::Rc;
27
28/// The empty handler every listener has until one is given. The `rsx!` widgets write
29/// their listeners whether or not a handler was supplied, so these do too.
30fn unit(handler: Option<Box<dyn FnMut()>>) -> EventCallback {
31    match handler {
32        Some(handler) => EventCallback::Unit(Rc::new(RefCell::new(handler))),
33        None => EventCallback::Unit(Rc::new(RefCell::new(|| {}))),
34    }
35}
36
37fn text_handler(handler: Option<Box<dyn FnMut(String)>>) -> EventCallback {
38    match handler {
39        Some(handler) => EventCallback::Text(Rc::new(RefCell::new(handler))),
40        None => EventCallback::Text(Rc::new(RefCell::new(|_: String| {}))),
41    }
42}
43
44fn value_handler(handler: Option<Box<dyn FnMut(f64)>>) -> EventCallback {
45    match handler {
46        Some(handler) => EventCallback::Value(Rc::new(RefCell::new(handler))),
47        None => EventCallback::Value(Rc::new(RefCell::new(|_: f64| {}))),
48    }
49}
50
51/// A node's subscription to pointer input: the kinds it wants and the handler that gets
52/// them. A node without one has no pointer input at all.
53struct PointerSubscription {
54    kinds: PointerKinds,
55    handler: Box<dyn FnMut(PointerEvent)>,
56}
57
58/// Writes a subscription as the two things the wire needs: the handler, and the mask. A
59/// subscription of no kinds writes neither, so it is not a subscription.
60fn pointer_input(
61    subscription: Option<PointerSubscription>,
62    attrs: &mut Attrs,
63    listeners: &mut Vec<(&'static str, EventCallback)>,
64) {
65    let Some(subscription) = subscription else {
66        return;
67    };
68    if subscription.kinds == PointerKinds::NONE {
69        return;
70    }
71    attrs.push((
72        "pointer_kinds",
73        OwnedAttr::Int(i64::from(subscription.kinds.bits())),
74    ));
75    listeners.push((
76        "onpointer",
77        EventCallback::Pointer(Rc::new(RefCell::new(subscription.handler))),
78    ));
79}
80
81/// Adds `on_pointer` to a builder that has a `pointer` field.
82macro_rules! pointer_subscription {
83    ($name:ident) => {
84        impl $name {
85            /// Subscribes the node to the kinds of pointer input in `kinds`. The handler runs
86            /// on the same call as the input; calling `consume` on the event keeps it from
87            /// the nearest subscribed ancestor and from the Renderer's own handling.
88            ///
89            /// A node that never calls this has no pointer input at all, and costs nothing.
90            pub fn on_pointer(
91                mut self,
92                kinds: PointerKinds,
93                handler: impl FnMut(PointerEvent) + 'static,
94            ) -> Self {
95                self.pointer = Some(PointerSubscription {
96                    kinds,
97                    handler: std::boxed::Box::new(handler),
98                });
99                self
100            }
101        }
102    };
103}
104
105/// Composes the description when the statement it was written in ends.
106macro_rules! composed_on_drop {
107    ($name:ident $(<$lifetime:lifetime>)?) => {
108        impl$(<$lifetime>)? Drop for $name$(<$lifetime>)? {
109            fn drop(&mut self) {
110                // A panic is already unwinding the composition, and composing on the way
111                // out would only risk a second one.
112                if !self.done && !std::thread::panicking() {
113                    self.done = true;
114                    self.compose(|| {});
115                }
116            }
117        }
118
119        impl$(<$lifetime>)? $name$(<$lifetime>)? {
120            /// What sits around it. Compose's `modifier` parameter.
121            pub fn modifier(mut self, modifier: Modifier) -> Self {
122                self.modifier = modifier;
123                self
124            }
125        }
126    };
127}
128
129/// A container's `content`: composes it now, with these children.
130macro_rules! container {
131    ($name:ident $(<$lifetime:lifetime>)?) => {
132        composed_on_drop!($name $(<$lifetime>)?);
133
134        impl$(<$lifetime>)? $name$(<$lifetime>)? {
135            /// Composes this widget with `content` as its children.
136            pub fn content(mut self, content: impl FnOnce()) {
137                self.done = true;
138                self.compose(content);
139            }
140        }
141    };
142}
143
144// ----- layout ---------------------------------------------------------------------------
145
146/// The four properties a linear layout takes.
147#[derive(Default)]
148struct Linear {
149    arrangement: Option<Arrangement>,
150    spacing: Option<f32>,
151    space_role: Option<SpaceRole>,
152    alignment: Option<Alignment>,
153}
154
155impl Linear {
156    fn push(&self, attrs: &mut Attrs) {
157        attrs.push(("arrangement", role_or_zero(self.arrangement)));
158        attrs.push(("spacing", dp_or_zero(self.spacing)));
159        attrs.push(("space_role", role_or_zero(self.space_role)));
160        attrs.push(("alignment", role_or_zero(self.alignment)));
161    }
162}
163
164macro_rules! linear {
165    ($name:ident, $widget:ident, $key:literal) => {
166        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
167        pub struct $name {
168            modifier: Modifier,
169            linear: Linear,
170            pointer: Option<PointerSubscription>,
171            done: bool,
172        }
173
174        #[doc = concat!("Lays its children out one after another. Compose's `", stringify!($widget), "`.")]
175        pub fn $widget() -> $name {
176            $name {
177                modifier: Modifier::new(),
178                linear: Linear::default(),
179                pointer: None,
180                done: false,
181            }
182        }
183
184        impl $name {
185            /// How the children share the main axis.
186            pub fn arrangement(mut self, arrangement: Arrangement) -> Self {
187                self.linear.arrangement = Some(arrangement);
188                self
189            }
190
191            /// A literal gap between children, in dp.
192            pub fn spacing(mut self, dp: f32) -> Self {
193                self.linear.spacing = Some(dp);
194                self
195            }
196
197            /// A gap from the design system's spacing ladder.
198            pub fn space_role(mut self, role: SpaceRole) -> Self {
199                self.linear.space_role = Some(role);
200                self
201            }
202
203            /// Where the children sit on the cross axis.
204            pub fn alignment(mut self, alignment: Alignment) -> Self {
205                self.linear.alignment = Some(alignment);
206                self
207            }
208
209            fn compose(&mut self, content: impl FnOnce()) {
210                let modifier = self.modifier;
211                let linear = std::mem::take(&mut self.linear);
212                let pointer = self.pointer.take();
213                let mut listeners = Vec::new();
214                let mut subscription = Attrs::new();
215                pointer_input(pointer, &mut subscription, &mut listeners);
216                emit(
217                    widget_key($key),
218                    WidgetKind::$widget,
219                    |attrs| {
220                        push_modifiers(&modifier, Mods::Container, attrs);
221                        linear.push(attrs);
222                        attrs.append(&mut subscription);
223                    },
224                    listeners,
225                    content,
226                );
227            }
228        }
229
230        container!($name);
231        pointer_subscription!($name);
232    };
233}
234
235linear!(ColumnScope, Column, "compose_rust::ui::Column");
236linear!(RowScope, Row, "compose_rust::ui::Row");
237
238/// A `Box` and its builder. Named apart from `std::boxed::Box`, which a glob import of
239/// this module would otherwise hide.
240pub struct ComposeBox {
241    modifier: Modifier,
242    alignment: Option<Alignment>,
243    pointer: Option<PointerSubscription>,
244    done: bool,
245}
246
247/// Stacks its children on top of each other. Compose's `Box`.
248pub fn Box() -> ComposeBox {
249    ComposeBox {
250        modifier: Modifier::new(),
251        alignment: None,
252        pointer: None,
253        done: false,
254    }
255}
256
257impl ComposeBox {
258    /// Where the children sit inside it.
259    pub fn alignment(mut self, alignment: Alignment) -> Self {
260        self.alignment = Some(alignment);
261        self
262    }
263
264    fn compose(&mut self, content: impl FnOnce()) {
265        let (modifier, alignment) = (self.modifier, self.alignment);
266        let pointer = self.pointer.take();
267        let mut listeners = Vec::new();
268        let mut subscription = Attrs::new();
269        pointer_input(pointer, &mut subscription, &mut listeners);
270        emit(
271            widget_key("compose_rust::ui::Box"),
272            WidgetKind::Box,
273            |attrs| {
274                push_modifiers(&modifier, Mods::Container, attrs);
275                attrs.push(("alignment", role_or_zero(alignment)));
276                attrs.append(&mut subscription);
277            },
278            listeners,
279            content,
280        );
281    }
282}
283
284container!(ComposeBox);
285pointer_subscription!(ComposeBox);
286
287/// A plain container with the standard Modifiers and nothing of its own.
288macro_rules! plain_container {
289    ($name:ident, $widget:ident, $mods:expr, $key:literal, $doc:literal) => {
290        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
291        pub struct $name {
292            modifier: Modifier,
293            pointer: Option<PointerSubscription>,
294            done: bool,
295        }
296
297        #[doc = $doc]
298        pub fn $widget() -> $name {
299            $name {
300                modifier: Modifier::new(),
301                pointer: None,
302                done: false,
303            }
304        }
305
306        impl $name {
307            fn compose(&mut self, content: impl FnOnce()) {
308                let modifier = self.modifier;
309                let pointer = self.pointer.take();
310                let mut listeners = Vec::new();
311                let mut subscription = Attrs::new();
312                pointer_input(pointer, &mut subscription, &mut listeners);
313                emit(
314                    widget_key($key),
315                    WidgetKind::$widget,
316                    |attrs| {
317                        push_modifiers(&modifier, $mods, attrs);
318                        attrs.append(&mut subscription);
319                    },
320                    listeners,
321                    content,
322                );
323            }
324        }
325
326        container!($name);
327        pointer_subscription!($name);
328    };
329}
330
331plain_container!(
332    ScrollColumnScope,
333    ScrollColumn,
334    Mods::Standard,
335    "compose_rust::ui::ScrollColumn",
336    "All of its content, with a vertical scroll the Renderer owns."
337);
338plain_container!(
339    ScrollRowScope,
340    ScrollRow,
341    Mods::Standard,
342    "compose_rust::ui::ScrollRow",
343    "All of its content, with a horizontal scroll the Renderer owns."
344);
345plain_container!(
346    CardScope,
347    Card,
348    Mods::Container,
349    "compose_rust::ui::Card",
350    "A grouped container. What it looks like is the design system's."
351);
352plain_container!(
353    SurfaceScope,
354    Surface,
355    Mods::Container,
356    "compose_rust::ui::Surface",
357    "A plain background and elevation, without a card's meaning."
358);
359plain_container!(
360    SelectionContainerScope,
361    SelectionContainer,
362    Mods::Standard,
363    "compose_rust::ui::SelectionContainer",
364    "Every `Text` inside can be selected and copied as one selection."
365);
366plain_container!(
367    SpacerScope,
368    Spacer,
369    Mods::Standard,
370    "compose_rust::ui::Spacer",
371    "Empty space, sized by its Modifier."
372);
373
374// ----- text -----------------------------------------------------------------------------
375
376/// A `Text` and its builder.
377pub struct TextScope {
378    modifier: Modifier,
379    text: String,
380    spans: TextSpans,
381    type_role: Option<TypeRole>,
382    font_size: Option<f32>,
383    font_weight: Option<u16>,
384    line_height: Option<f32>,
385    letter_spacing: Option<f32>,
386    color: Option<Paint>,
387    text_align: Option<TextAlign>,
388    max_lines: Option<u32>,
389    overflow: Option<TextOverflow>,
390    pointer: Option<PointerSubscription>,
391    done: bool,
392}
393
394/// Text. Compose's `Text`.
395///
396/// `type_role` alone takes the design system's size, weight, line height and letter
397/// spacing; each other style method overrides one axis of it.
398pub fn Text(text: impl Into<String>) -> TextScope {
399    TextScope {
400        modifier: Modifier::new(),
401        text: text.into(),
402        spans: TextSpans::default(),
403        type_role: None,
404        font_size: None,
405        font_weight: None,
406        line_height: None,
407        letter_spacing: None,
408        color: None,
409        text_align: None,
410        max_lines: None,
411        overflow: None,
412        pointer: None,
413        done: false,
414    }
415}
416
417impl TextScope {
418    /// Runs of different treatment inside the text: a bold phrase, a link, some code.
419    pub fn spans(mut self, spans: TextSpans) -> Self {
420        self.spans = spans;
421        self
422    }
423
424    pub fn type_role(mut self, role: TypeRole) -> Self {
425        self.type_role = Some(role);
426        self
427    }
428
429    pub fn font_size(mut self, sp: f32) -> Self {
430        self.font_size = Some(sp);
431        self
432    }
433
434    pub fn font_weight(mut self, weight: u16) -> Self {
435        self.font_weight = Some(weight);
436        self
437    }
438
439    pub fn line_height(mut self, sp: f32) -> Self {
440        self.line_height = Some(sp);
441        self
442    }
443
444    pub fn letter_spacing(mut self, sp: f32) -> Self {
445        self.letter_spacing = Some(sp);
446        self
447    }
448
449    pub fn color(mut self, paint: Paint) -> Self {
450        self.color = Some(paint);
451        self
452    }
453
454    pub fn text_align(mut self, align: TextAlign) -> Self {
455        self.text_align = Some(align);
456        self
457    }
458
459    pub fn max_lines(mut self, lines: u32) -> Self {
460        self.max_lines = Some(lines);
461        self
462    }
463
464    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
465        self.overflow = Some(overflow);
466        self
467    }
468
469    fn compose(&mut self, content: impl FnOnce()) {
470        let modifier = self.modifier;
471        let text = std::mem::take(&mut self.text);
472        let spans = std::mem::take(&mut self.spans);
473        let (type_role, font_size, font_weight, line_height, letter_spacing) = (
474            self.type_role,
475            self.font_size,
476            self.font_weight,
477            self.line_height,
478            self.letter_spacing,
479        );
480        let (color, text_align, max_lines, overflow) =
481            (self.color, self.text_align, self.max_lines, self.overflow);
482        let pointer = self.pointer.take();
483        let mut listeners = Vec::new();
484        let mut subscription = Attrs::new();
485        pointer_input(pointer, &mut subscription, &mut listeners);
486        emit(
487            widget_key("compose_rust::ui::Text"),
488            WidgetKind::Text,
489            |attrs| {
490                push_modifiers(&modifier, Mods::Standard, attrs);
491                // Left out entirely when there are none, as the rsx widget leaves it out.
492                attrs.push((
493                    "spans",
494                    if spans.is_empty() {
495                        OwnedAttr::None
496                    } else {
497                        OwnedAttr::Spans(spans)
498                    },
499                ));
500                attrs.push(("text", OwnedAttr::Text(text)));
501                attrs.push(("type_role", role_or_zero(type_role)));
502                attrs.push(("font_size", dp_or_zero(font_size)));
503                attrs.push((
504                    "font_weight",
505                    OwnedAttr::Int(i64::from(font_weight.unwrap_or(0))),
506                ));
507                attrs.push(("line_height", dp_or_zero(line_height)));
508                attrs.push(("letter_spacing", dp_or_zero(letter_spacing)));
509                attrs.push(("color", paint_or_zero(color)));
510                attrs.push(("text_align", role_or_zero(text_align)));
511                attrs.push((
512                    "max_lines",
513                    OwnedAttr::Int(i64::from(max_lines.unwrap_or(0))),
514                ));
515                attrs.push(("overflow", role_or_zero(overflow)));
516                attrs.append(&mut subscription);
517            },
518            listeners,
519            content,
520        );
521    }
522}
523
524composed_on_drop!(TextScope);
525pointer_subscription!(TextScope);
526
527/// A `TextField` and its builder.
528pub struct TextFieldScope {
529    modifier: Modifier,
530    text: String,
531    placeholder: String,
532    enabled: bool,
533    multiline: bool,
534    type_role: Option<TypeRole>,
535    on_value_change: Option<Box<dyn FnMut(String)>>,
536    on_submit: Option<Box<dyn FnMut(String)>>,
537    on_focus_lost: Option<Box<dyn FnMut()>>,
538    on_key_down: Option<Box<dyn FnMut(KeyEvent)>>,
539    done: bool,
540}
541
542/// An uncontrolled text field. Compose's `TextField`, with the value kept where the
543/// platform's text input keeps it: the Renderer reports each change and the Host never
544/// writes the value back while the user types.
545pub fn TextField() -> TextFieldScope {
546    TextFieldScope {
547        modifier: Modifier::new(),
548        text: String::new(),
549        placeholder: String::new(),
550        enabled: true,
551        multiline: false,
552        type_role: None,
553        on_value_change: None,
554        on_submit: None,
555        on_focus_lost: None,
556        on_key_down: None,
557        done: false,
558    }
559}
560
561impl TextFieldScope {
562    /// The text the field starts with. Only the start: the field stays uncontrolled, so
563    /// what the user types is never written back, and a later value here changes nothing
564    /// on the field that is already there. To show other text, such as another document,
565    /// give the field a new identity with `key`, and the new field starts with this text.
566    pub fn text(mut self, text: impl Into<String>) -> Self {
567        self.text = text.into();
568        self
569    }
570
571    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
572        self.placeholder = placeholder.into();
573        self
574    }
575
576    pub fn enabled(mut self, enabled: bool) -> Self {
577        self.enabled = enabled;
578        self
579    }
580
581    pub fn multiline(mut self, multiline: bool) -> Self {
582        self.multiline = multiline;
583        self
584    }
585
586    /// The rung of the type ladder the field's own text is set in.
587    pub fn type_role(mut self, role: TypeRole) -> Self {
588        self.type_role = Some(role);
589        self
590    }
591
592    pub fn on_value_change(mut self, handler: impl FnMut(String) + 'static) -> Self {
593        self.on_value_change = Some(std::boxed::Box::new(handler));
594        self
595    }
596
597    pub fn on_submit(mut self, handler: impl FnMut(String) + 'static) -> Self {
598        self.on_submit = Some(std::boxed::Box::new(handler));
599        self
600    }
601
602    pub fn on_focus_lost(mut self, handler: impl FnMut() + 'static) -> Self {
603        self.on_focus_lost = Some(std::boxed::Box::new(handler));
604        self
605    }
606
607    /// Sees each key the field is pressed with; calling `consume` on the event keeps the
608    /// field from acting on it.
609    pub fn on_key_down(mut self, handler: impl FnMut(KeyEvent) + 'static) -> Self {
610        self.on_key_down = Some(std::boxed::Box::new(handler));
611        self
612    }
613
614    fn compose(&mut self, content: impl FnOnce()) {
615        let modifier = self.modifier;
616        let text = std::mem::take(&mut self.text);
617        let placeholder = std::mem::take(&mut self.placeholder);
618        let (enabled, multiline, type_role) = (self.enabled, self.multiline, self.type_role);
619        let key_down: EventCallback = match self.on_key_down.take() {
620            Some(handler) => EventCallback::Key(Rc::new(RefCell::new(handler))),
621            None => EventCallback::Key(Rc::new(RefCell::new(|_: KeyEvent| {}))),
622        };
623        let listeners = vec![
624            ("onvaluechange", text_handler(self.on_value_change.take())),
625            ("onsubmit", text_handler(self.on_submit.take())),
626            ("onfocuslost", unit(self.on_focus_lost.take())),
627            ("onkeydown", key_down),
628        ];
629        emit(
630            widget_key("compose_rust::ui::TextField"),
631            WidgetKind::TextField,
632            |attrs| {
633                push_modifiers(&modifier, Mods::Standard, attrs);
634                // Written only when there is a starting text, so a field without one sends
635                // the same records it always did.
636                if !text.is_empty() {
637                    attrs.push(("text", OwnedAttr::Text(text)));
638                }
639                attrs.push(("placeholder", OwnedAttr::Text(placeholder)));
640                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
641                attrs.push(("multiline", OwnedAttr::Bool(multiline)));
642                attrs.push(("type_role", role_or_zero(type_role)));
643            },
644            listeners,
645            content,
646        );
647    }
648}
649
650composed_on_drop!(TextFieldScope);
651
652// ----- buttons and controls ---------------------------------------------------------------
653
654/// A `Button` and its builder.
655pub struct ButtonScope {
656    modifier: Modifier,
657    text: String,
658    icon: Option<IconRole>,
659    enabled: bool,
660    variant: Option<ButtonVariant>,
661    kind: Option<ButtonKind>,
662    color: Option<Paint>,
663    on_click: Option<Box<dyn FnMut()>>,
664    done: bool,
665}
666
667/// A button with a label. Compose's `Button`; the variant is the design system's seam,
668/// so `ButtonVariant::Text` is Compose's `TextButton` and `Outlined` its `OutlinedButton`.
669///
670/// An empty label with an icon is an icon button, named for assistive technology by its
671/// icon's meaning.
672pub fn Button(text: impl Into<String>) -> ButtonScope {
673    ButtonScope {
674        modifier: Modifier::new(),
675        text: text.into(),
676        icon: None,
677        enabled: true,
678        variant: None,
679        kind: None,
680        color: None,
681        on_click: None,
682        done: false,
683    }
684}
685
686impl ButtonScope {
687    /// The meaning of the glyph on it, never a picture.
688    pub fn icon(mut self, icon: IconRole) -> Self {
689        self.icon = Some(icon);
690        self
691    }
692
693    pub fn enabled(mut self, enabled: bool) -> Self {
694        self.enabled = enabled;
695        self
696    }
697
698    pub fn variant(mut self, variant: ButtonVariant) -> Self {
699        self.variant = Some(variant);
700        self
701    }
702
703    /// What the button is, apart from how loud it is: `ButtonKind::ActionKey` is one key of
704    /// a dense grid, drawn in the design system's own calculator-key shape.
705    pub fn kind(mut self, kind: ButtonKind) -> Self {
706        self.kind = Some(kind);
707        self
708    }
709
710    /// The label's colour, for the rare button whose meaning is not its variant's.
711    pub fn color(mut self, paint: Paint) -> Self {
712        self.color = Some(paint);
713        self
714    }
715
716    /// The same, when there may be no colour of its own.
717    pub fn color_if(mut self, paint: Option<Paint>) -> Self {
718        self.color = paint;
719        self
720    }
721
722    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
723        self.on_click = Some(std::boxed::Box::new(handler));
724        self
725    }
726
727    fn compose(&mut self, content: impl FnOnce()) {
728        let modifier = self.modifier;
729        let text = std::mem::take(&mut self.text);
730        let (icon, enabled, variant, kind, color) =
731            (self.icon, self.enabled, self.variant, self.kind, self.color);
732        let listeners = vec![("onclick", unit(self.on_click.take()))];
733        emit(
734            widget_key("compose_rust::ui::Button"),
735            WidgetKind::Button,
736            |attrs| {
737                push_modifiers(&modifier, Mods::Standard, attrs);
738                attrs.push(("text", OwnedAttr::Text(text)));
739                attrs.push(("icon", super::role(icon)));
740                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
741                attrs.push(("variant", role_or_zero(variant)));
742                attrs.push(("button_kind", role_or_zero(kind)));
743                attrs.push(("color", super::paint(color)));
744            },
745            listeners,
746            content,
747        );
748    }
749}
750
751composed_on_drop!(ButtonScope);
752
753/// A two-state control, for Checkbox, RadioButton and Switch alike: the Host owns the
754/// value, the control draws exactly what it is told and reports what the user asked for.
755macro_rules! toggle {
756    ($name:ident, $widget:ident, $setter:ident, $key:literal, $doc:literal) => {
757        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
758        pub struct $name {
759            modifier: Modifier,
760            checked: bool,
761            enabled: bool,
762            on_change: Option<Box<dyn FnMut(bool)>>,
763            done: bool,
764        }
765
766        #[doc = $doc]
767        pub fn $widget($setter: bool) -> $name {
768            $name {
769                modifier: Modifier::new(),
770                checked: $setter,
771                enabled: true,
772                on_change: None,
773                done: false,
774            }
775        }
776
777        impl $name {
778            pub fn enabled(mut self, enabled: bool) -> Self {
779                self.enabled = enabled;
780                self
781            }
782
783            /// Called with the state the user asked for. The control changes only when the
784            /// value passed in does.
785            pub fn on_change(mut self, handler: impl FnMut(bool) + 'static) -> Self {
786                self.on_change = Some(std::boxed::Box::new(handler));
787                self
788            }
789
790            fn compose(&mut self, content: impl FnOnce()) {
791                let modifier = self.modifier;
792                let (checked, enabled) = (self.checked, self.enabled);
793                let handler: EventCallback = match self.on_change.take() {
794                    Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(
795                        // Off is 0.0 and on is anything else, so a Renderer that reports a
796                        // half-finished transition still reads as on.
797                        move |value: f64| handler(value != 0.0),
798                    ))),
799                    None => value_handler(None),
800                };
801                emit(
802                    widget_key($key),
803                    WidgetKind::$widget,
804                    |attrs| {
805                        push_modifiers(&modifier, Mods::Standard, attrs);
806                        attrs.push(("checked", OwnedAttr::Bool(checked)));
807                        attrs.push(("enabled", OwnedAttr::Bool(enabled)));
808                    },
809                    vec![("onchange", handler)],
810                    content,
811                );
812            }
813        }
814
815        composed_on_drop!($name);
816    };
817}
818
819toggle!(
820    CheckboxScope,
821    Checkbox,
822    checked,
823    "compose_rust::ui::Checkbox",
824    "A box that is ticked or not. Compose's `Checkbox(checked, onCheckedChange)`."
825);
826toggle!(
827    RadioButtonScope,
828    RadioButton,
829    selected,
830    "compose_rust::ui::RadioButton",
831    "One choice out of several. Compose's `RadioButton(selected, onClick)`."
832);
833toggle!(
834    SwitchScope,
835    Switch,
836    checked,
837    "compose_rust::ui::Switch",
838    "An on and off control. Compose's `Switch(checked, onCheckedChange)`."
839);
840
841/// A `Slider` and its builder.
842pub struct SliderScope {
843    modifier: Modifier,
844    color: Option<Paint>,
845    value: f32,
846    min: f32,
847    max: f32,
848    steps: u32,
849    enabled: bool,
850    on_change: Option<Box<dyn FnMut(f32)>>,
851    done: bool,
852}
853
854/// A value picked from a range. Compose's `Slider`. The drag is the Renderer's; `value`
855/// seeds it and moves it when the change came from elsewhere.
856pub fn Slider(value: f32) -> SliderScope {
857    SliderScope {
858        modifier: Modifier::new(),
859        color: None,
860        value,
861        min: 0.0,
862        max: 1.0,
863        steps: 0,
864        enabled: true,
865        on_change: None,
866        done: false,
867    }
868}
869
870impl SliderScope {
871    /// The two ends. Compose's `valueRange`.
872    pub fn range(mut self, min: f32, max: f32) -> Self {
873        self.min = min;
874        self.max = max;
875        self
876    }
877
878    /// Stops between the ends; zero is continuous.
879    pub fn steps(mut self, steps: u32) -> Self {
880        self.steps = steps;
881        self
882    }
883
884    pub fn enabled(mut self, enabled: bool) -> Self {
885        self.enabled = enabled;
886        self
887    }
888
889    /// The filled part of the track and the thumb.
890    pub fn color(mut self, paint: Paint) -> Self {
891        self.color = Some(paint);
892        self
893    }
894
895    pub fn on_change(mut self, handler: impl FnMut(f32) + 'static) -> Self {
896        self.on_change = Some(std::boxed::Box::new(handler));
897        self
898    }
899
900    fn compose(&mut self, content: impl FnOnce()) {
901        let modifier = self.modifier;
902        let (color, value, min, max, steps, enabled) = (
903            self.color,
904            self.value,
905            self.min,
906            self.max,
907            self.steps,
908            self.enabled,
909        );
910        let handler = match self.on_change.take() {
911            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
912                handler(value as f32)
913            }))),
914            None => value_handler(None),
915        };
916        emit(
917            widget_key("compose_rust::ui::Slider"),
918            WidgetKind::Slider,
919            |attrs| {
920                push_modifiers(&modifier, Mods::Standard, attrs);
921                attrs.push(("color", super::paint(color)));
922                attrs.push(("value", OwnedAttr::Float(f64::from(value))));
923                attrs.push(("min", OwnedAttr::Float(f64::from(min))));
924                attrs.push(("max", OwnedAttr::Float(f64::from(max))));
925                attrs.push(("steps", OwnedAttr::Int(i64::from(steps))));
926                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
927            },
928            vec![("onchange", handler)],
929            content,
930        );
931    }
932}
933
934composed_on_drop!(SliderScope);
935
936/// A `ProgressIndicator` and its builder.
937pub struct ProgressIndicatorScope {
938    modifier: Modifier,
939    value: f32,
940    determinate: bool,
941    circular: bool,
942    done: bool,
943}
944
945/// How far along something is, as a fraction. Compose's `LinearProgressIndicator(progress)`.
946pub fn ProgressIndicator(value: f32) -> ProgressIndicatorScope {
947    ProgressIndicatorScope {
948        modifier: Modifier::new(),
949        value,
950        determinate: true,
951        circular: false,
952        done: false,
953    }
954}
955
956/// Work in progress of no known length.
957pub fn IndeterminateProgressIndicator() -> ProgressIndicatorScope {
958    ProgressIndicator(0.0).determinate(false)
959}
960
961impl ProgressIndicatorScope {
962    pub fn determinate(mut self, determinate: bool) -> Self {
963        self.determinate = determinate;
964        self
965    }
966
967    /// The circular form rather than the linear one.
968    pub fn circular(mut self, circular: bool) -> Self {
969        self.circular = circular;
970        self
971    }
972
973    fn compose(&mut self, content: impl FnOnce()) {
974        let modifier = self.modifier;
975        let (value, determinate, circular) = (self.value, self.determinate, self.circular);
976        emit(
977            widget_key("compose_rust::ui::ProgressIndicator"),
978            WidgetKind::ProgressIndicator,
979            |attrs| {
980                push_modifiers(&modifier, Mods::Standard, attrs);
981                attrs.push(("value", OwnedAttr::Float(f64::from(value))));
982                attrs.push(("determinate", OwnedAttr::Bool(determinate)));
983                attrs.push(("circular", OwnedAttr::Bool(circular)));
984            },
985            Vec::new(),
986            content,
987        );
988    }
989}
990
991composed_on_drop!(ProgressIndicatorScope);
992
993/// A `Divider` and its builder.
994pub struct DividerScope {
995    modifier: Modifier,
996    vertical: bool,
997    done: bool,
998}
999
1000/// A line between two things. Compose's `HorizontalDivider`; `vertical` makes it the other.
1001pub fn Divider() -> DividerScope {
1002    DividerScope {
1003        modifier: Modifier::new(),
1004        vertical: false,
1005        done: false,
1006    }
1007}
1008
1009/// A hairline between two rows of a grouped list, in the design system's quiet edge or
1010/// the given colour. The same node as `Divider().modifier(Modifier.background(color))`.
1011pub fn Separator(color: Option<Paint>) -> DividerScope {
1012    Divider().modifier(Modifier::new().background_if(color))
1013}
1014
1015impl DividerScope {
1016    pub fn vertical(mut self, vertical: bool) -> Self {
1017        self.vertical = vertical;
1018        self
1019    }
1020
1021    fn compose(&mut self, content: impl FnOnce()) {
1022        let modifier = self.modifier;
1023        let vertical = self.vertical;
1024        emit(
1025            widget_key("compose_rust::ui::Divider"),
1026            WidgetKind::Divider,
1027            |attrs| {
1028                push_modifiers(&modifier, Mods::Standard, attrs);
1029                attrs.push(("vertical", OwnedAttr::Bool(vertical)));
1030            },
1031            Vec::new(),
1032            content,
1033        );
1034    }
1035}
1036
1037composed_on_drop!(DividerScope);
1038
1039// ----- pictures -------------------------------------------------------------------------
1040
1041/// An `Image` or an `Icon` and its builder.
1042pub struct AssetScope {
1043    modifier: Modifier,
1044    widget: WidgetKind,
1045    asset_id: u32,
1046    color: Option<Paint>,
1047    done: bool,
1048}
1049
1050/// A registered asset drawn as a picture. Compose's `Image`.
1051pub fn Image(asset_id: u32) -> AssetScope {
1052    AssetScope {
1053        modifier: Modifier::new(),
1054        widget: WidgetKind::Image,
1055        asset_id,
1056        color: None,
1057        done: false,
1058    }
1059}
1060
1061/// A registered icon, tinted. Compose's `Icon`.
1062pub fn Icon(asset_id: u32) -> AssetScope {
1063    AssetScope {
1064        modifier: Modifier::new(),
1065        widget: WidgetKind::Icon,
1066        asset_id,
1067        color: None,
1068        done: false,
1069    }
1070}
1071
1072impl AssetScope {
1073    /// The tint. An icon only; an image ignores it, as the rsx widget has no such property.
1074    pub fn color(mut self, paint: Paint) -> Self {
1075        self.color = Some(paint);
1076        self
1077    }
1078
1079    fn compose(&mut self, content: impl FnOnce()) {
1080        let modifier = self.modifier;
1081        let (widget, asset_id, color) = (self.widget, self.asset_id, self.color);
1082        let key = if widget == WidgetKind::Icon {
1083            widget_key("compose_rust::ui::Icon")
1084        } else {
1085            widget_key("compose_rust::ui::Image")
1086        };
1087        emit(
1088            key,
1089            widget,
1090            |attrs| {
1091                push_modifiers(&modifier, Mods::Standard, attrs);
1092                attrs.push(("asset", OwnedAttr::Int(i64::from(asset_id))));
1093                if widget == WidgetKind::Icon {
1094                    attrs.push(("color", paint_or_zero(color)));
1095                }
1096            },
1097            Vec::new(),
1098            content,
1099        );
1100    }
1101}
1102
1103composed_on_drop!(AssetScope);
1104
1105/// A `Canvas` and its builder.
1106pub struct CanvasScope {
1107    modifier: Modifier,
1108    commands: DrawList,
1109    done: bool,
1110}
1111
1112/// Pixels the widget vocabulary cannot produce. Compose's `Canvas`, with the drawing as a
1113/// value rather than a callback: drawing code cannot cross into the Renderer, and an equal
1114/// list costs no record.
1115pub fn Canvas(commands: DrawList) -> CanvasScope {
1116    CanvasScope {
1117        modifier: Modifier::new(),
1118        commands,
1119        done: false,
1120    }
1121}
1122
1123impl CanvasScope {
1124    fn compose(&mut self, content: impl FnOnce()) {
1125        let modifier = self.modifier;
1126        let commands = std::mem::take(&mut self.commands);
1127        emit(
1128            widget_key("compose_rust::ui::Canvas"),
1129            WidgetKind::Canvas,
1130            |attrs| {
1131                push_modifiers(&modifier, Mods::Standard, attrs);
1132                attrs.push(("commands", OwnedAttr::Draw(commands)));
1133            },
1134            Vec::new(),
1135            content,
1136        );
1137    }
1138}
1139
1140composed_on_drop!(CanvasScope);
1141
1142// ----- pickers ----------------------------------------------------------------------------
1143
1144/// A `DatePicker` and its builder.
1145pub struct DatePickerScope {
1146    modifier: Modifier,
1147    value: i64,
1148    min: Option<i64>,
1149    max: Option<i64>,
1150    enabled: bool,
1151    on_change: Option<Box<dyn FnMut(i64)>>,
1152    done: bool,
1153}
1154
1155/// A date, as whole days since 1970-01-01. How it is picked is the design system's.
1156pub fn DatePicker(value: i64) -> DatePickerScope {
1157    DatePickerScope {
1158        modifier: Modifier::new(),
1159        value,
1160        min: None,
1161        max: None,
1162        enabled: true,
1163        on_change: None,
1164        done: false,
1165    }
1166}
1167
1168impl DatePickerScope {
1169    pub fn min(mut self, days: i64) -> Self {
1170        self.min = Some(days);
1171        self
1172    }
1173
1174    pub fn max(mut self, days: i64) -> Self {
1175        self.max = Some(days);
1176        self
1177    }
1178
1179    pub fn enabled(mut self, enabled: bool) -> Self {
1180        self.enabled = enabled;
1181        self
1182    }
1183
1184    pub fn on_change(mut self, handler: impl FnMut(i64) + 'static) -> Self {
1185        self.on_change = Some(std::boxed::Box::new(handler));
1186        self
1187    }
1188
1189    fn compose(&mut self, content: impl FnOnce()) {
1190        let modifier = self.modifier;
1191        let (value, min, max, enabled) = (self.value, self.min, self.max, self.enabled);
1192        let handler = match self.on_change.take() {
1193            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
1194                handler(value as i64)
1195            }))),
1196            None => value_handler(None),
1197        };
1198        emit(
1199            widget_key("compose_rust::ui::DatePicker"),
1200            WidgetKind::DatePicker,
1201            |attrs| {
1202                push_modifiers(&modifier, Mods::Standard, attrs);
1203                attrs.push(("value", OwnedAttr::Int(value)));
1204                attrs.push(("min", OwnedAttr::Int(min.unwrap_or(i64::MIN))));
1205                attrs.push(("max", OwnedAttr::Int(max.unwrap_or(i64::MAX))));
1206                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1207            },
1208            vec![("onchange", handler)],
1209            content,
1210        );
1211    }
1212}
1213
1214composed_on_drop!(DatePickerScope);
1215
1216const MINUTES_IN_A_DAY: u32 = 24 * 60;
1217
1218/// A `TimePicker` and its builder.
1219pub struct TimePickerScope {
1220    modifier: Modifier,
1221    value: u32,
1222    min: Option<u32>,
1223    max: Option<u32>,
1224    enabled: bool,
1225    on_change: Option<Box<dyn FnMut(u32)>>,
1226    done: bool,
1227}
1228
1229/// A time of day, as minutes since midnight.
1230pub fn TimePicker(value: u32) -> TimePickerScope {
1231    TimePickerScope {
1232        modifier: Modifier::new(),
1233        value,
1234        min: None,
1235        max: None,
1236        enabled: true,
1237        on_change: None,
1238        done: false,
1239    }
1240}
1241
1242impl TimePickerScope {
1243    pub fn min(mut self, minutes: u32) -> Self {
1244        self.min = Some(minutes);
1245        self
1246    }
1247
1248    pub fn max(mut self, minutes: u32) -> Self {
1249        self.max = Some(minutes);
1250        self
1251    }
1252
1253    pub fn enabled(mut self, enabled: bool) -> Self {
1254        self.enabled = enabled;
1255        self
1256    }
1257
1258    pub fn on_change(mut self, handler: impl FnMut(u32) + 'static) -> Self {
1259        self.on_change = Some(std::boxed::Box::new(handler));
1260        self
1261    }
1262
1263    fn compose(&mut self, content: impl FnOnce()) {
1264        let modifier = self.modifier;
1265        let (value, min, max, enabled) = (self.value, self.min, self.max, self.enabled);
1266        let handler = match self.on_change.take() {
1267            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
1268                handler((value as i64).clamp(0, i64::from(MINUTES_IN_A_DAY - 1)) as u32)
1269            }))),
1270            None => value_handler(None),
1271        };
1272        emit(
1273            widget_key("compose_rust::ui::TimePicker"),
1274            WidgetKind::TimePicker,
1275            |attrs| {
1276                push_modifiers(&modifier, Mods::Standard, attrs);
1277                attrs.push(("value", OwnedAttr::Int(i64::from(value))));
1278                attrs.push(("min", OwnedAttr::Int(i64::from(min.unwrap_or(0)))));
1279                attrs.push((
1280                    "max",
1281                    OwnedAttr::Int(i64::from(max.unwrap_or(MINUTES_IN_A_DAY - 1))),
1282                ));
1283                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1284            },
1285            vec![("onchange", handler)],
1286            content,
1287        );
1288    }
1289}
1290
1291composed_on_drop!(TimePickerScope);
1292
1293/// A `Dropdown` and its builder.
1294pub struct DropdownScope {
1295    modifier: Modifier,
1296    selected_index: usize,
1297    enabled: bool,
1298    on_change: Option<Box<dyn FnMut(usize)>>,
1299    done: bool,
1300}
1301
1302/// One choice out of a list; each child is one option.
1303pub fn Dropdown(selected_index: usize) -> DropdownScope {
1304    DropdownScope {
1305        modifier: Modifier::new(),
1306        selected_index,
1307        enabled: true,
1308        on_change: None,
1309        done: false,
1310    }
1311}
1312
1313impl DropdownScope {
1314    pub fn enabled(mut self, enabled: bool) -> Self {
1315        self.enabled = enabled;
1316        self
1317    }
1318
1319    pub fn on_change(mut self, handler: impl FnMut(usize) + 'static) -> Self {
1320        self.on_change = Some(std::boxed::Box::new(handler));
1321        self
1322    }
1323
1324    fn compose(&mut self, content: impl FnOnce()) {
1325        let modifier = self.modifier;
1326        let (selected_index, enabled) = (self.selected_index, self.enabled);
1327        let handler = match self.on_change.take() {
1328            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
1329                handler((value as i64).max(0) as usize)
1330            }))),
1331            None => value_handler(None),
1332        };
1333        emit(
1334            widget_key("compose_rust::ui::Dropdown"),
1335            WidgetKind::Dropdown,
1336            |attrs| {
1337                push_modifiers(&modifier, Mods::Standard, attrs);
1338                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
1339                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1340            },
1341            vec![("onchange", handler)],
1342            content,
1343        );
1344    }
1345}
1346
1347container!(DropdownScope);
1348
1349// ----- surfaces that come and go ----------------------------------------------------------
1350
1351/// A `Dialog` or a `Sheet` and its builder: `open` seeds the Renderer's own open state,
1352/// and `on_dismiss` says once that the user asked to close it.
1353pub struct ModalScope {
1354    modifier: Modifier,
1355    widget: WidgetKind,
1356    open: bool,
1357    on_dismiss: Option<Box<dyn FnMut()>>,
1358    done: bool,
1359}
1360
1361/// A modal. Compose's `Dialog`, kept in the tree and opened by `open`, so its enter and
1362/// exit belong to the Renderer.
1363pub fn Dialog(open: bool) -> ModalScope {
1364    ModalScope {
1365        modifier: Modifier::new(),
1366        widget: WidgetKind::Dialog,
1367        open,
1368        on_dismiss: None,
1369        done: false,
1370    }
1371}
1372
1373/// A surface that slides in from the edge the Renderer chooses. Compose's
1374/// `ModalBottomSheet`, without a promise about which edge.
1375pub fn Sheet(open: bool) -> ModalScope {
1376    ModalScope {
1377        modifier: Modifier::new(),
1378        widget: WidgetKind::Sheet,
1379        open,
1380        on_dismiss: None,
1381        done: false,
1382    }
1383}
1384
1385impl ModalScope {
1386    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
1387        self.on_dismiss = Some(std::boxed::Box::new(handler));
1388        self
1389    }
1390
1391    fn compose(&mut self, content: impl FnOnce()) {
1392        let modifier = self.modifier;
1393        let (widget, open) = (self.widget, self.open);
1394        let key = if widget == WidgetKind::Dialog {
1395            widget_key("compose_rust::ui::Dialog")
1396        } else {
1397            widget_key("compose_rust::ui::Sheet")
1398        };
1399        emit(
1400            key,
1401            widget,
1402            |attrs| {
1403                push_modifiers(&modifier, Mods::Standard, attrs);
1404                attrs.push(("open", OwnedAttr::Bool(open)));
1405            },
1406            vec![("ondismiss", unit(self.on_dismiss.take()))],
1407            content,
1408        );
1409    }
1410}
1411
1412container!(ModalScope);
1413
1414/// A `Menu` and its builder.
1415pub struct MenuScope<'a> {
1416    modifier: Modifier,
1417    expanded: bool,
1418    on_dismiss: Option<Box<dyn FnMut()>>,
1419    anchor: Option<Box<dyn FnOnce() + 'a>>,
1420    done: bool,
1421}
1422
1423/// A popup hanging off its anchor. Compose's `DropdownMenu`, with the anchor said rather
1424/// than positioned: the Renderer places the popup.
1425pub fn Menu<'a>(expanded: bool) -> MenuScope<'a> {
1426    MenuScope {
1427        modifier: Modifier::new(),
1428        expanded,
1429        on_dismiss: None,
1430        anchor: None,
1431        done: false,
1432    }
1433}
1434
1435impl<'a> MenuScope<'a> {
1436    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
1437        self.on_dismiss = Some(std::boxed::Box::new(handler));
1438        self
1439    }
1440
1441    /// The widget the menu hangs off. It is the first child on the wire.
1442    pub fn anchor(mut self, anchor: impl FnOnce() + 'a) -> Self {
1443        self.anchor = Some(std::boxed::Box::new(anchor));
1444        self
1445    }
1446
1447    fn compose(&mut self, content: impl FnOnce()) {
1448        let modifier = self.modifier;
1449        let expanded = self.expanded;
1450        let anchor = self.anchor.take();
1451        emit(
1452            widget_key("compose_rust::ui::Menu"),
1453            WidgetKind::Menu,
1454            |attrs| {
1455                push_modifiers(&modifier, Mods::Standard, attrs);
1456                attrs.push(("open", OwnedAttr::Bool(expanded)));
1457            },
1458            vec![("ondismiss", unit(self.on_dismiss.take()))],
1459            || {
1460                if let Some(anchor) = anchor {
1461                    let _anchor =
1462                        crate::runtime::group_guard(widget_key("compose_rust::ui::Menu::anchor"));
1463                    anchor();
1464                }
1465                let _entries =
1466                    crate::runtime::group_guard(widget_key("compose_rust::ui::Menu::entries"));
1467                content();
1468            },
1469        );
1470    }
1471}
1472
1473container!(MenuScope<'a>);
1474
1475/// A `Tabs` and its builder.
1476pub struct TabsScope {
1477    modifier: Modifier,
1478    color: Option<Paint>,
1479    selected_index: usize,
1480    done: bool,
1481}
1482
1483/// A row of tabs; each child is one tab, and a tap is that child's own click. Compose's
1484/// `TabRow`.
1485pub fn Tabs(selected_index: usize) -> TabsScope {
1486    TabsScope {
1487        modifier: Modifier::new(),
1488        color: None,
1489        selected_index,
1490        done: false,
1491    }
1492}
1493
1494impl TabsScope {
1495    /// The mark that shows which tab is selected.
1496    pub fn color(mut self, paint: Paint) -> Self {
1497        self.color = Some(paint);
1498        self
1499    }
1500
1501    fn compose(&mut self, content: impl FnOnce()) {
1502        let modifier = self.modifier;
1503        let (color, selected_index) = (self.color, self.selected_index);
1504        emit(
1505            widget_key("compose_rust::ui::Tabs"),
1506            WidgetKind::Tabs,
1507            |attrs| {
1508                push_modifiers(&modifier, Mods::Standard, attrs);
1509                attrs.push(("color", super::paint(color)));
1510                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
1511            },
1512            Vec::new(),
1513            content,
1514        );
1515    }
1516}
1517
1518container!(TabsScope);
1519
1520/// A `TopAppBar` and its builder.
1521pub struct TopAppBarScope {
1522    modifier: Modifier,
1523    title: String,
1524    done: bool,
1525}
1526
1527/// The bar across the top of a screen; its children are its content. Compose's
1528/// `TopAppBar`, with the title a property so the Renderer can find it.
1529pub fn TopAppBar() -> TopAppBarScope {
1530    TopAppBarScope {
1531        modifier: Modifier::new(),
1532        title: String::new(),
1533        done: false,
1534    }
1535}
1536
1537impl TopAppBarScope {
1538    pub fn title(mut self, title: impl Into<String>) -> Self {
1539        self.title = title.into();
1540        self
1541    }
1542
1543    fn compose(&mut self, content: impl FnOnce()) {
1544        let modifier = self.modifier;
1545        let title = std::mem::take(&mut self.title);
1546        emit(
1547            widget_key("compose_rust::ui::TopAppBar"),
1548            WidgetKind::TopAppBar,
1549            |attrs| {
1550                push_modifiers(&modifier, Mods::Standard, attrs);
1551                attrs.push(("text", OwnedAttr::Text(title)));
1552            },
1553            Vec::new(),
1554            content,
1555        );
1556    }
1557}
1558
1559container!(TopAppBarScope);
1560
1561/// A `Tooltip` and its builder.
1562pub struct TooltipScope {
1563    modifier: Modifier,
1564    text: String,
1565    done: bool,
1566}
1567
1568/// An explanation attached to its child, which assistive technology reads too. Compose's
1569/// `TooltipBox`.
1570pub fn Tooltip(text: impl Into<String>) -> TooltipScope {
1571    TooltipScope {
1572        modifier: Modifier::new(),
1573        text: text.into(),
1574        done: false,
1575    }
1576}
1577
1578impl TooltipScope {
1579    fn compose(&mut self, content: impl FnOnce()) {
1580        let modifier = self.modifier;
1581        let text = std::mem::take(&mut self.text);
1582        emit(
1583            widget_key("compose_rust::ui::Tooltip"),
1584            WidgetKind::Tooltip,
1585            |attrs| {
1586                push_modifiers(&modifier, Mods::Standard, attrs);
1587                attrs.push(("text", OwnedAttr::Text(text)));
1588            },
1589            Vec::new(),
1590            content,
1591        );
1592    }
1593}
1594
1595container!(TooltipScope);
1596
1597// ----- frames and navigation --------------------------------------------------------------
1598
1599/// Composes one slot of a frame: a ScaffoldSlot node holding `content`, in a group of its
1600/// own so that a slot coming and going leaves the others where they were.
1601fn slot(role: SlotRole, key: u64, content: impl FnOnce()) {
1602    emit(
1603        key,
1604        WidgetKind::ScaffoldSlot,
1605        |attrs| attrs.push(("slot", OwnedAttr::Int(i64::from(u16::from(role))))),
1606        Vec::new(),
1607        content,
1608    );
1609}
1610
1611type Slot<'a> = Option<Box<dyn FnOnce() + 'a>>;
1612
1613/// A `Scaffold` and its builder.
1614pub struct ScaffoldScope<'a> {
1615    modifier: Modifier,
1616    top_bar: Slot<'a>,
1617    bottom_bar: Slot<'a>,
1618    floating_action: Slot<'a>,
1619    done: bool,
1620}
1621
1622/// The screen's frame: which slots it has filled, and the page. Compose's `Scaffold`.
1623/// What each slot becomes on this platform at this width is the Renderer's decision.
1624///
1625/// The Modifier carries the two things a frame takes: `background` and `material`.
1626pub fn Scaffold<'a>() -> ScaffoldScope<'a> {
1627    ScaffoldScope {
1628        modifier: Modifier::new(),
1629        top_bar: None,
1630        bottom_bar: None,
1631        floating_action: None,
1632        done: false,
1633    }
1634}
1635
1636impl<'a> ScaffoldScope<'a> {
1637    pub fn top_bar(mut self, bar: impl FnOnce() + 'a) -> Self {
1638        self.top_bar = Some(std::boxed::Box::new(bar));
1639        self
1640    }
1641
1642    pub fn bottom_bar(mut self, bar: impl FnOnce() + 'a) -> Self {
1643        self.bottom_bar = Some(std::boxed::Box::new(bar));
1644        self
1645    }
1646
1647    pub fn floating_action(mut self, action: impl FnOnce() + 'a) -> Self {
1648        self.floating_action = Some(std::boxed::Box::new(action));
1649        self
1650    }
1651
1652    fn compose(&mut self, content: impl FnOnce()) {
1653        let modifier = self.modifier;
1654        let (top_bar, bottom_bar, floating_action) = (
1655            self.top_bar.take(),
1656            self.bottom_bar.take(),
1657            self.floating_action.take(),
1658        );
1659        emit(
1660            widget_key("compose_rust::ui::Scaffold"),
1661            WidgetKind::Scaffold,
1662            |attrs| push_modifiers(&modifier, Mods::Scaffold, attrs),
1663            Vec::new(),
1664            || {
1665                if let Some(bar) = top_bar {
1666                    slot(
1667                        SlotRole::TopBar,
1668                        widget_key("compose_rust::ui::Scaffold::top_bar"),
1669                        bar,
1670                    );
1671                }
1672                if let Some(bar) = bottom_bar {
1673                    slot(
1674                        SlotRole::BottomBar,
1675                        widget_key("compose_rust::ui::Scaffold::bottom_bar"),
1676                        bar,
1677                    );
1678                }
1679                if let Some(action) = floating_action {
1680                    slot(
1681                        SlotRole::FloatingAction,
1682                        widget_key("compose_rust::ui::Scaffold::floating_action"),
1683                        action,
1684                    );
1685                }
1686                slot(
1687                    SlotRole::Content,
1688                    widget_key("compose_rust::ui::Scaffold::content"),
1689                    content,
1690                );
1691            },
1692        );
1693    }
1694}
1695
1696container!(ScaffoldScope<'a>);
1697
1698/// A `Navigation` and its builder.
1699pub struct NavigationScope<'a> {
1700    modifier: Modifier,
1701    selected_index: usize,
1702    head: Slot<'a>,
1703    foot: Slot<'a>,
1704    done: bool,
1705}
1706
1707/// A set of destinations and the screen they lead to. The children that are
1708/// `NavigationItem`s are the destinations; whether they are a bar, a rail or a drawer is
1709/// the Renderer's choice. Compose's `NavigationSuiteScaffold`.
1710pub fn Navigation<'a>(selected_index: usize) -> NavigationScope<'a> {
1711    NavigationScope {
1712        modifier: Modifier::new(),
1713        selected_index,
1714        head: None,
1715        foot: None,
1716        done: false,
1717    }
1718}
1719
1720impl<'a> NavigationScope<'a> {
1721    /// What sits above the destinations in a strip.
1722    pub fn head(mut self, head: impl FnOnce() + 'a) -> Self {
1723        self.head = Some(std::boxed::Box::new(head));
1724        self
1725    }
1726
1727    /// What sits below them.
1728    pub fn foot(mut self, foot: impl FnOnce() + 'a) -> Self {
1729        self.foot = Some(std::boxed::Box::new(foot));
1730        self
1731    }
1732
1733    fn compose(&mut self, content: impl FnOnce()) {
1734        let modifier = self.modifier;
1735        let selected_index = self.selected_index;
1736        let (head, foot) = (self.head.take(), self.foot.take());
1737        emit(
1738            widget_key("compose_rust::ui::Navigation"),
1739            WidgetKind::Navigation,
1740            |attrs| {
1741                push_modifiers(&modifier, Mods::Standard, attrs);
1742                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
1743            },
1744            Vec::new(),
1745            || {
1746                if let Some(head) = head {
1747                    slot(
1748                        SlotRole::TopBar,
1749                        widget_key("compose_rust::ui::Navigation::head"),
1750                        head,
1751                    );
1752                }
1753                if let Some(foot) = foot {
1754                    slot(
1755                        SlotRole::BottomBar,
1756                        widget_key("compose_rust::ui::Navigation::foot"),
1757                        foot,
1758                    );
1759                }
1760                let _destinations = crate::runtime::group_guard(widget_key(
1761                    "compose_rust::ui::Navigation::content",
1762                ));
1763                content();
1764            },
1765        );
1766    }
1767}
1768
1769container!(NavigationScope<'a>);
1770
1771/// A `NavigationItem` and its builder.
1772pub struct NavigationItemScope {
1773    modifier: Modifier,
1774    text: String,
1775    icon: Option<IconRole>,
1776    color: Option<Paint>,
1777    enabled: bool,
1778    section: Option<String>,
1779    on_click: Option<Box<dyn FnMut()>>,
1780    done: bool,
1781}
1782
1783/// One destination inside a `Navigation`. Compose's `NavigationSuiteItem`.
1784pub fn NavigationItem(text: impl Into<String>) -> NavigationItemScope {
1785    NavigationItemScope {
1786        modifier: Modifier::new(),
1787        text: text.into(),
1788        icon: None,
1789        color: None,
1790        enabled: true,
1791        section: None,
1792        on_click: None,
1793        done: false,
1794    }
1795}
1796
1797impl NavigationItemScope {
1798    pub fn icon(mut self, icon: IconRole) -> Self {
1799        self.icon = Some(icon);
1800        self
1801    }
1802
1803    pub fn color(mut self, paint: Paint) -> Self {
1804        self.color = Some(paint);
1805        self
1806    }
1807
1808    pub fn enabled(mut self, enabled: bool) -> Self {
1809        self.enabled = enabled;
1810        self
1811    }
1812
1813    /// The named group of the strip this destination belongs to.
1814    pub fn section(mut self, section: impl Into<String>) -> Self {
1815        self.section = Some(section.into());
1816        self
1817    }
1818
1819    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
1820        self.on_click = Some(std::boxed::Box::new(handler));
1821        self
1822    }
1823
1824    fn compose(&mut self, content: impl FnOnce()) {
1825        let modifier = self.modifier;
1826        let text = std::mem::take(&mut self.text);
1827        let section = self.section.take();
1828        let (icon, color, enabled) = (self.icon, self.color, self.enabled);
1829        emit(
1830            widget_key("compose_rust::ui::NavigationItem"),
1831            WidgetKind::NavigationItem,
1832            |attrs| {
1833                push_modifiers(&modifier, Mods::Standard, attrs);
1834                attrs.push(("text", OwnedAttr::Text(text)));
1835                attrs.push(("icon", super::role(icon)));
1836                attrs.push(("color", super::paint(color)));
1837                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1838                attrs.push(("section", optional_text(section)));
1839            },
1840            vec![("onclick", unit(self.on_click.take()))],
1841            content,
1842        );
1843    }
1844}
1845
1846composed_on_drop!(NavigationItemScope);
1847
1848// ----- small things ---------------------------------------------------------------------
1849
1850/// A `FileDropTarget` and its builder.
1851pub struct FileDropTargetScope {
1852    modifier: Modifier,
1853    alignment: Option<Alignment>,
1854    on_files_entered: Option<Box<dyn FnMut()>>,
1855    on_files_dropped: Option<Box<dyn FnMut(FileDrop)>>,
1856    done: bool,
1857}
1858
1859/// A place files may be dropped. Desktop only in effect.
1860pub fn FileDropTarget() -> FileDropTargetScope {
1861    FileDropTargetScope {
1862        modifier: Modifier::new(),
1863        alignment: None,
1864        on_files_entered: None,
1865        on_files_dropped: None,
1866        done: false,
1867    }
1868}
1869
1870impl FileDropTargetScope {
1871    pub fn alignment(mut self, alignment: Alignment) -> Self {
1872        self.alignment = Some(alignment);
1873        self
1874    }
1875
1876    pub fn on_files_entered(mut self, handler: impl FnMut() + 'static) -> Self {
1877        self.on_files_entered = Some(std::boxed::Box::new(handler));
1878        self
1879    }
1880
1881    pub fn on_files_dropped(mut self, handler: impl FnMut(FileDrop) + 'static) -> Self {
1882        self.on_files_dropped = Some(std::boxed::Box::new(handler));
1883        self
1884    }
1885
1886    fn compose(&mut self, content: impl FnOnce()) {
1887        let modifier = self.modifier;
1888        let alignment = self.alignment;
1889        let dropped: EventCallback = match self.on_files_dropped.take() {
1890            Some(handler) => EventCallback::Files(Rc::new(RefCell::new(handler))),
1891            None => EventCallback::Files(Rc::new(RefCell::new(|_: FileDrop| {}))),
1892        };
1893        emit(
1894            widget_key("compose_rust::ui::FileDropTarget"),
1895            WidgetKind::FileDropTarget,
1896            |attrs| {
1897                push_modifiers(&modifier, Mods::Standard, attrs);
1898                attrs.push(("alignment", super::role(alignment)));
1899            },
1900            vec![
1901                ("onfilesentered", unit(self.on_files_entered.take())),
1902                ("onfilesdropped", dropped),
1903            ],
1904            content,
1905        );
1906    }
1907}
1908
1909container!(FileDropTargetScope);
1910
1911/// A `Chip` and its builder.
1912pub struct ChipScope {
1913    modifier: Modifier,
1914    text: String,
1915    icon: Option<IconRole>,
1916    selected: bool,
1917    enabled: bool,
1918    on_click: Option<Box<dyn FnMut()>>,
1919    done: bool,
1920}
1921
1922/// A small token that is chosen or filters. Compose's `FilterChip`.
1923pub fn Chip(text: impl Into<String>) -> ChipScope {
1924    ChipScope {
1925        modifier: Modifier::new(),
1926        text: text.into(),
1927        icon: None,
1928        selected: false,
1929        enabled: true,
1930        on_click: None,
1931        done: false,
1932    }
1933}
1934
1935impl ChipScope {
1936    pub fn icon(mut self, icon: IconRole) -> Self {
1937        self.icon = Some(icon);
1938        self
1939    }
1940
1941    pub fn selected(mut self, selected: bool) -> Self {
1942        self.selected = selected;
1943        self
1944    }
1945
1946    pub fn enabled(mut self, enabled: bool) -> Self {
1947        self.enabled = enabled;
1948        self
1949    }
1950
1951    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
1952        self.on_click = Some(std::boxed::Box::new(handler));
1953        self
1954    }
1955
1956    fn compose(&mut self, content: impl FnOnce()) {
1957        let modifier = self.modifier;
1958        let text = std::mem::take(&mut self.text);
1959        let (icon, selected, enabled) = (self.icon, self.selected, self.enabled);
1960        emit(
1961            widget_key("compose_rust::ui::Chip"),
1962            WidgetKind::Chip,
1963            |attrs| {
1964                push_modifiers(&modifier, Mods::Chip, attrs);
1965                attrs.push(("text", OwnedAttr::Text(text)));
1966                attrs.push(("icon", super::role(icon)));
1967                attrs.push(("checked", OwnedAttr::Bool(selected)));
1968                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1969            },
1970            vec![("onclick", unit(self.on_click.take()))],
1971            content,
1972        );
1973    }
1974}
1975
1976composed_on_drop!(ChipScope);
1977
1978/// A `FloatingAction` and its builder.
1979pub struct FloatingActionScope {
1980    modifier: Modifier,
1981    text: String,
1982    icon: Option<IconRole>,
1983    on_click: Option<Box<dyn FnMut()>>,
1984    done: bool,
1985}
1986
1987/// The one action a screen is about. Compose's `ExtendedFloatingActionButton`; where it
1988/// goes is the design system's answer.
1989pub fn FloatingAction(text: impl Into<String>) -> FloatingActionScope {
1990    FloatingActionScope {
1991        modifier: Modifier::new(),
1992        text: text.into(),
1993        icon: None,
1994        on_click: None,
1995        done: false,
1996    }
1997}
1998
1999impl FloatingActionScope {
2000    pub fn icon(mut self, icon: IconRole) -> Self {
2001        self.icon = Some(icon);
2002        self
2003    }
2004
2005    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
2006        self.on_click = Some(std::boxed::Box::new(handler));
2007        self
2008    }
2009
2010    fn compose(&mut self, content: impl FnOnce()) {
2011        let modifier = self.modifier;
2012        let text = std::mem::take(&mut self.text);
2013        let icon = self.icon;
2014        emit(
2015            widget_key("compose_rust::ui::FloatingAction"),
2016            WidgetKind::FloatingAction,
2017            |attrs| {
2018                push_modifiers(&modifier, Mods::None, attrs);
2019                attrs.push(("text", OwnedAttr::Text(text)));
2020                attrs.push(("icon", super::role(icon)));
2021            },
2022            vec![("onclick", unit(self.on_click.take()))],
2023            content,
2024        );
2025    }
2026}
2027
2028composed_on_drop!(FloatingActionScope);
2029
2030/// A `Badge` and its builder.
2031pub struct BadgeScope {
2032    modifier: Modifier,
2033    value: Option<u32>,
2034    text: Option<String>,
2035    color: Option<ColorRole>,
2036    done: bool,
2037}
2038
2039/// A count, a word or a dot, on its child or on its own. Compose's `BadgedBox` and `Badge`.
2040pub fn Badge() -> BadgeScope {
2041    BadgeScope {
2042        modifier: Modifier::new(),
2043        value: None,
2044        text: None,
2045        color: None,
2046        done: false,
2047    }
2048}
2049
2050impl BadgeScope {
2051    /// How many.
2052    pub fn value(mut self, value: u32) -> Self {
2053        self.value = Some(value);
2054        self
2055    }
2056
2057    /// One short word, shown where a count would be.
2058    pub fn text(mut self, text: impl Into<String>) -> Self {
2059        self.text = Some(text.into());
2060        self
2061    }
2062
2063    /// The role the mark is filled with; the error role where it is not given.
2064    pub fn color(mut self, role: ColorRole) -> Self {
2065        self.color = Some(role);
2066        self
2067    }
2068
2069    fn compose(&mut self, content: impl FnOnce()) {
2070        let modifier = self.modifier;
2071        let text = self.text.take();
2072        let (value, color) = (self.value, self.color);
2073        emit(
2074            widget_key("compose_rust::ui::Badge"),
2075            WidgetKind::Badge,
2076            |attrs| {
2077                push_modifiers(&modifier, Mods::Standard, attrs);
2078                attrs.push((
2079                    "count",
2080                    value.map_or(OwnedAttr::None, |value| OwnedAttr::Int(i64::from(value))),
2081                ));
2082                attrs.push(("text", optional_text(text)));
2083                attrs.push(("color", super::paint(color.map(Paint::Role))));
2084            },
2085            Vec::new(),
2086            content,
2087        );
2088    }
2089}
2090
2091container!(BadgeScope);
2092
2093/// A `SplitPane` and its builder.
2094pub struct SplitPaneScope {
2095    modifier: Modifier,
2096    value: Option<f32>,
2097    min: Option<f32>,
2098    max: Option<f32>,
2099    collapsible: bool,
2100    selected_index: usize,
2101    label: Option<String>,
2102    on_change: Option<Box<dyn FnMut(f32)>>,
2103    on_dismiss: Option<Box<dyn FnMut()>>,
2104    done: bool,
2105}
2106
2107/// A side pane and a body with a divider the user drags. Exactly two children, side pane
2108/// first. The drag is reported once, when it ends.
2109pub fn SplitPane() -> SplitPaneScope {
2110    SplitPaneScope {
2111        modifier: Modifier::new(),
2112        value: None,
2113        min: None,
2114        max: None,
2115        collapsible: false,
2116        selected_index: 0,
2117        label: None,
2118        on_change: None,
2119        on_dismiss: None,
2120        done: false,
2121    }
2122}
2123
2124impl SplitPaneScope {
2125    /// The side pane's width in dp, or 0 to open it folded.
2126    pub fn value(mut self, dp: f32) -> Self {
2127        self.value = Some(dp);
2128        self
2129    }
2130
2131    pub fn min(mut self, dp: f32) -> Self {
2132        self.min = Some(dp);
2133        self
2134    }
2135
2136    pub fn max(mut self, dp: f32) -> Self {
2137        self.max = Some(dp);
2138        self
2139    }
2140
2141    pub fn collapsible(mut self, collapsible: bool) -> Self {
2142        self.collapsible = collapsible;
2143        self
2144    }
2145
2146    /// Which pane shows when only one fits: 0 the side pane, 1 the body.
2147    pub fn selected_index(mut self, index: usize) -> Self {
2148        self.selected_index = index;
2149        self
2150    }
2151
2152    /// What a screen reader calls the divider.
2153    pub fn label(mut self, label: impl Into<String>) -> Self {
2154        self.label = Some(label.into());
2155        self
2156    }
2157
2158    pub fn on_change(mut self, handler: impl FnMut(f32) + 'static) -> Self {
2159        self.on_change = Some(std::boxed::Box::new(handler));
2160        self
2161    }
2162
2163    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
2164        self.on_dismiss = Some(std::boxed::Box::new(handler));
2165        self
2166    }
2167
2168    fn compose(&mut self, content: impl FnOnce()) {
2169        let modifier = self.modifier;
2170        let label = self.label.take();
2171        let (value, min, max, collapsible, selected_index) = (
2172            self.value,
2173            self.min,
2174            self.max,
2175            self.collapsible,
2176            self.selected_index,
2177        );
2178        let change = match self.on_change.take() {
2179            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
2180                handler(value as f32)
2181            }))),
2182            None => value_handler(None),
2183        };
2184        let as_float = |value: Option<f32>| {
2185            value.map_or(OwnedAttr::None, |value| OwnedAttr::Float(f64::from(value)))
2186        };
2187        emit(
2188            widget_key("compose_rust::ui::SplitPane"),
2189            WidgetKind::SplitPane,
2190            |attrs| {
2191                push_modifiers(&modifier, Mods::Standard, attrs);
2192                attrs.push(("value", as_float(value)));
2193                attrs.push(("min", as_float(min)));
2194                attrs.push(("max", as_float(max)));
2195                attrs.push(("collapsible", OwnedAttr::Bool(collapsible)));
2196                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
2197                attrs.push(("text", optional_text(label)));
2198            },
2199            vec![
2200                ("onchange", change),
2201                ("ondismiss", unit(self.on_dismiss.take())),
2202            ],
2203            content,
2204        );
2205    }
2206}
2207
2208container!(SplitPaneScope);
2209
2210/// The worked extension widget: Material 3's determinate linear progress indicator.
2211pub struct LinearProgressIndicatorScope {
2212    modifier: Modifier,
2213    progress: f32,
2214    done: bool,
2215}
2216
2217/// The extension widget the schema carries as its example.
2218pub fn LinearProgressIndicator(progress: f32) -> LinearProgressIndicatorScope {
2219    LinearProgressIndicatorScope {
2220        modifier: Modifier::new(),
2221        progress,
2222        done: false,
2223    }
2224}
2225
2226impl LinearProgressIndicatorScope {
2227    fn compose(&mut self, content: impl FnOnce()) {
2228        let modifier = self.modifier;
2229        let progress = self.progress;
2230        emit(
2231            widget_key("compose_rust::ui::LinearProgressIndicator"),
2232            WidgetKind::LinearProgressIndicator,
2233            |attrs| {
2234                push_modifiers(&modifier, Mods::None, attrs);
2235                attrs.push(("progress", OwnedAttr::Float(f64::from(progress))));
2236            },
2237            Vec::new(),
2238            content,
2239        );
2240    }
2241}
2242
2243composed_on_drop!(LinearProgressIndicatorScope);