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::font_features::FontFeature;
19use crate::fonts::FontRefs;
20use crate::runtime::composer::{EventCallback, OwnedAttr};
21use crate::schema::{
22    Alignment, Arrangement, ButtonKind, ButtonVariant, ColorRole, CompactNavigation, ContentScale,
23    HorizontalAlignment, IconRole, OverflowWrap, Paint, PopoverAlign, PopoverPlacement, SlotRole,
24    Snap, SpaceRole, TextAlign, TextOverflow, TypeRole, VerticalAlignment, WidgetKind, WordBreak,
25};
26use crate::spans::TextSpans;
27use crate::{FileDrop, KeyEvent, MotionFinished, PointerEvent, PointerKinds};
28use std::cell::RefCell;
29use std::rc::Rc;
30
31/// The empty handler every listener has until one is given. The `rsx!` widgets write
32/// their listeners whether or not a handler was supplied, so these do too.
33fn unit(handler: Option<Box<dyn FnMut()>>) -> EventCallback {
34    match handler {
35        Some(handler) => EventCallback::Unit(Rc::new(RefCell::new(handler))),
36        None => EventCallback::Unit(Rc::new(RefCell::new(|| {}))),
37    }
38}
39
40fn text_handler(handler: Option<Box<dyn FnMut(String)>>) -> EventCallback {
41    match handler {
42        Some(handler) => EventCallback::Text(Rc::new(RefCell::new(handler))),
43        None => EventCallback::Text(Rc::new(RefCell::new(|_: String| {}))),
44    }
45}
46
47fn value_handler(handler: Option<Box<dyn FnMut(f64)>>) -> EventCallback {
48    match handler {
49        Some(handler) => EventCallback::Value(Rc::new(RefCell::new(handler))),
50        None => EventCallback::Value(Rc::new(RefCell::new(|_: f64| {}))),
51    }
52}
53
54/// A node's subscription to pointer input: the kinds it wants and the handler that gets
55/// them. A node without one has no pointer input at all.
56struct PointerSubscription {
57    kinds: PointerKinds,
58    handler: Box<dyn FnMut(PointerEvent)>,
59}
60
61/// Writes a subscription as the two things the wire needs: the handler, and the mask. A
62/// subscription of no kinds writes neither, so it is not a subscription.
63fn pointer_input(
64    subscription: Option<PointerSubscription>,
65    attrs: &mut Attrs,
66    listeners: &mut Vec<(&'static str, EventCallback)>,
67) {
68    let Some(subscription) = subscription else {
69        return;
70    };
71    if subscription.kinds == PointerKinds::NONE {
72        return;
73    }
74    attrs.push((
75        "pointer_kinds",
76        OwnedAttr::Int(i64::from(subscription.kinds.bits())),
77    ));
78    listeners.push((
79        "onpointer",
80        EventCallback::Pointer(Rc::new(RefCell::new(subscription.handler))),
81    ));
82}
83
84/// What a node says about windows that is not a window's own: the key it is torn
85/// off under, and the handler for a dragged window passing over it.
86struct TearOff {
87    key: Option<String>,
88    drag_over: Option<Box<dyn FnMut(crate::windows::WindowDragOver)>>,
89}
90
91/// Writes a node's tear-off key and drag-over handler as what the wire needs. A node with
92/// neither writes nothing.
93fn tear_off_input(
94    tear_off: Option<TearOff>,
95    attrs: &mut Attrs,
96    listeners: &mut Vec<(&'static str, EventCallback)>,
97) {
98    let Some(tear_off) = tear_off else {
99        return;
100    };
101    if let Some(key) = tear_off.key {
102        attrs.push(("tear_off_key", OwnedAttr::Text(key)));
103    }
104    if let Some(mut handler) = tear_off.drag_over {
105        listeners.push((
106            "onwindowdragover",
107            EventCallback::Window(Rc::new(RefCell::new(
108                move |event: crate::windows::WindowEvent| {
109                    if let crate::windows::WindowEvent::DragOver { kind, window, x, y } = event {
110                        handler(crate::windows::WindowDragOver { kind, window, x, y });
111                    }
112                },
113            ))),
114        ));
115    }
116}
117
118/// Adds `tear_off_key` and `on_window_drag_over` to a builder that has a `tear_off` field.
119macro_rules! tear_off {
120    ($name:ident) => {
121        impl $name {
122            /// Lets the user pull this node out into a window of its own, named by `key`:
123            /// the `RestoreKey` of the window that results.
124            pub fn tear_off_key(mut self, key: impl Into<String>) -> Self {
125                self.tear_off
126                    .get_or_insert_with(|| TearOff {
127                        key: None,
128                        drag_over: None,
129                    })
130                    .key = Some(key.into());
131                self
132            }
133
134            /// Hears a dragged window pass over this node: where a tab row shows the place
135            /// a dropped tab would go.
136            pub fn on_window_drag_over(
137                mut self,
138                handler: impl FnMut(crate::windows::WindowDragOver) + 'static,
139            ) -> Self {
140                self.tear_off
141                    .get_or_insert_with(|| TearOff {
142                        key: None,
143                        drag_over: None,
144                    })
145                    .drag_over = Some(std::boxed::Box::new(handler));
146                self
147            }
148        }
149    };
150}
151
152/// Adds `on_pointer` to a builder that has a `pointer` field.
153macro_rules! pointer_subscription {
154    ($name:ident) => {
155        impl $name {
156            /// Subscribes the node to the kinds of pointer input in `kinds`. The handler runs
157            /// on the same call as the input; calling `consume` on the event keeps it from
158            /// the nearest subscribed ancestor and from the Renderer's own handling.
159            ///
160            /// A node that never calls this has no pointer input at all, and costs nothing.
161            pub fn on_pointer(
162                mut self,
163                kinds: PointerKinds,
164                handler: impl FnMut(PointerEvent) + 'static,
165            ) -> Self {
166                self.pointer = Some(PointerSubscription {
167                    kinds,
168                    handler: std::boxed::Box::new(handler),
169                });
170                self
171            }
172        }
173    };
174}
175
176/// Writes a container's click handler as the listener the wire needs. A container that was
177/// given none has no `OnClick` at all, so it is not a control: it takes no focus, no key and
178/// no pointer press, and says nothing to assistive technology.
179fn click_input(
180    on_click: Option<Box<dyn FnMut()>>,
181    listeners: &mut Vec<(&'static str, EventCallback)>,
182) {
183    if let Some(handler) = on_click {
184        listeners.push(("onclick", unit(Some(handler))));
185    }
186}
187
188/// Adds `on_click` to a builder that has an `on_click` field.
189macro_rules! click_handler {
190    ($name:ident) => {
191        impl $name {
192            /// Makes the container a button. The Renderer gives it keyboard focus, runs
193            /// the handler on a click and on Enter or Space, announces it to assistive
194            /// technology as a button, and shows hover, pressed and focus state.
195            ///
196            /// A container that never calls this is not a control and costs nothing.
197            pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
198                self.on_click = Some(std::boxed::Box::new(handler));
199                self
200            }
201        }
202    };
203}
204
205/// Optional focus and key handlers on a node. A node that registers none of these
206/// emits no focus or key listener property on the wire.
207#[derive(Default)]
208struct FocusAndKeyHandlers {
209    on_focus_gained: Option<Box<dyn FnMut()>>,
210    on_focus_lost: Option<Box<dyn FnMut()>>,
211    on_key_down: Option<Box<dyn FnMut(KeyEvent)>>,
212    on_key_up: Option<Box<dyn FnMut(KeyEvent)>>,
213    on_motion_finished: Option<Box<dyn FnMut(MotionFinished)>>,
214    focus_handle: Option<super::FocusHandle>,
215}
216
217/// Writes a motion handler as the listener the wire needs. A node that was given none has
218/// no `OnMotionFinished` at all, and the Renderer attaches nothing to its size motion.
219fn motion_input(
220    handler: Option<Box<dyn FnMut(MotionFinished)>>,
221    listeners: &mut Vec<(&'static str, EventCallback)>,
222) {
223    if let Some(handler) = handler {
224        listeners.push((
225            "onmotionfinished",
226            EventCallback::Motion(Rc::new(RefCell::new(handler))),
227        ));
228    }
229}
230
231/// Turns the handlers into listeners and binds the focus handle, if there is one, to the
232/// node the widget is about to emit, so it is called right before that `emit`.
233fn focus_and_key_input(
234    mut handlers: FocusAndKeyHandlers,
235    listeners: &mut Vec<(&'static str, EventCallback)>,
236) {
237    super::focus::bind_focus_handle(handlers.focus_handle.take());
238    if let Some(handler) = handlers.on_focus_gained.take() {
239        listeners.push(("onfocusgained", unit(Some(handler))));
240    }
241    if let Some(handler) = handlers.on_focus_lost.take() {
242        listeners.push(("onfocuslost", unit(Some(handler))));
243    }
244    if let Some(handler) = handlers.on_key_down.take() {
245        listeners.push((
246            "onkeydown",
247            EventCallback::Key(Rc::new(RefCell::new(handler))),
248        ));
249    }
250    if let Some(handler) = handlers.on_key_up.take() {
251        listeners.push((
252            "onkeyup",
253            EventCallback::Key(Rc::new(RefCell::new(handler))),
254        ));
255    }
256    motion_input(handlers.on_motion_finished.take(), listeners);
257}
258
259macro_rules! focus_and_key_handlers {
260    ($name:ident) => {
261        impl $name {
262            /// Runs when this node or a descendant inside it gains keyboard focus.
263            pub fn on_focus_gained(mut self, handler: impl FnMut() + 'static) -> Self {
264                self.focus_and_keys.on_focus_gained = Some(std::boxed::Box::new(handler));
265                self
266            }
267
268            /// Runs when this node and its descendants lose keyboard focus.
269            pub fn on_focus_lost(mut self, handler: impl FnMut() + 'static) -> Self {
270                self.focus_and_keys.on_focus_lost = Some(std::boxed::Box::new(handler));
271                self
272            }
273
274            /// Sees each key pressed while this node or a descendant inside it holds focus;
275            /// calling `consume` on the event keeps the Renderer from acting on it.
276            pub fn on_key_down(mut self, handler: impl FnMut(KeyEvent) + 'static) -> Self {
277                self.focus_and_keys.on_key_down = Some(std::boxed::Box::new(handler));
278                self
279            }
280
281            /// Sees each key released while this node or a descendant inside it holds focus.
282            pub fn on_key_up(mut self, handler: impl FnMut(KeyEvent) + 'static) -> Self {
283                self.focus_and_keys.on_key_up = Some(std::boxed::Box::new(handler));
284                self
285            }
286
287            /// Binds `handle` to this node, so `handle.request()` puts keyboard focus here.
288            pub fn focus_handle(mut self, handle: &super::FocusHandle) -> Self {
289                self.focus_and_keys.focus_handle = Some(handle.clone());
290                self
291            }
292
293            /// Runs once when this node's size motion comes to rest, with the size it settled
294            /// at in dp. The node needs `Modifier::motion` for its size to move; without it
295            /// nothing is reported. A run interrupted by a newer size is not reported, only
296            /// the one that settles, and under reduced motion it arrives in the frame the
297            /// size changes in.
298            pub fn on_motion_finished(
299                mut self,
300                handler: impl FnMut(MotionFinished) + 'static,
301            ) -> Self {
302                self.focus_and_keys.on_motion_finished = Some(std::boxed::Box::new(handler));
303                self
304            }
305        }
306    };
307}
308
309/// Composes the description when the statement it was written in ends.
310macro_rules! composed_on_drop {
311    ($name:ident $(<$lifetime:lifetime>)?) => {
312        impl$(<$lifetime>)? Drop for $name$(<$lifetime>)? {
313            fn drop(&mut self) {
314                // A panic is already unwinding the composition, and composing on the way
315                // out would only risk a second one.
316                if !self.done && !std::thread::panicking() {
317                    self.done = true;
318                    self.compose(|| {});
319                }
320            }
321        }
322
323        impl$(<$lifetime>)? $name$(<$lifetime>)? {
324            /// What sits around it. Compose's `modifier` parameter.
325            pub fn modifier(mut self, modifier: Modifier) -> Self {
326                self.modifier = modifier;
327                self
328            }
329        }
330    };
331}
332
333/// A container's `content`: composes it now, with these children.
334macro_rules! container {
335    ($name:ident $(<$lifetime:lifetime>)?) => {
336        composed_on_drop!($name $(<$lifetime>)?);
337
338        impl$(<$lifetime>)? $name$(<$lifetime>)? {
339            /// Composes this widget with `content` as its children.
340            pub fn content(mut self, content: impl FnOnce()) {
341                self.done = true;
342                self.compose(content);
343            }
344        }
345    };
346}
347
348// ----- layout ---------------------------------------------------------------------------
349
350/// The properties a linear layout takes.
351#[derive(Default)]
352struct Linear {
353    arrangement: Option<Arrangement>,
354    spacing: Option<f32>,
355    space_role: Option<SpaceRole>,
356    alignment: Option<Alignment>,
357    intrinsic_size: Option<bool>,
358    radio_group: bool,
359}
360
361impl Linear {
362    fn push(&self, attrs: &mut Attrs) {
363        attrs.push(("arrangement", role_or_zero(self.arrangement)));
364        attrs.push(("spacing", dp_or_zero(self.spacing)));
365        attrs.push(("space_role", role_or_zero(self.space_role)));
366        attrs.push(("alignment", role_or_zero(self.alignment)));
367        if let Some(intrinsic) = self.intrinsic_size {
368            attrs.push(("collapsible", OwnedAttr::Bool(intrinsic)));
369        }
370        // Written only for a group, so a plain layout sends what it always did.
371        if self.radio_group {
372            attrs.push(("radio_group", OwnedAttr::Bool(true)));
373        }
374    }
375}
376
377macro_rules! linear {
378    ($name:ident, $widget:ident, $key:literal) => {
379        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
380        pub struct $name {
381            modifier: Modifier,
382            linear: Linear,
383            pointer: Option<PointerSubscription>,
384            on_click: Option<Box<dyn FnMut()>>,
385            focus_and_keys: FocusAndKeyHandlers,
386            tear_off: Option<TearOff>,
387            done: bool,
388        }
389
390        #[doc = concat!("Lays its children out one after another. Compose's `", stringify!($widget), "`.")]
391        pub fn $widget() -> $name {
392            $name {
393                modifier: Modifier::new(),
394                linear: Linear::default(),
395                pointer: None,
396                on_click: None,
397                focus_and_keys: FocusAndKeyHandlers::default(),
398                tear_off: None,
399                done: false,
400            }
401        }
402
403        impl $name {
404            /// How the children share the main axis.
405            pub fn arrangement(mut self, arrangement: Arrangement) -> Self {
406                self.linear.arrangement = Some(arrangement);
407                self
408            }
409
410            /// A literal gap between children, in dp.
411            pub fn spacing(mut self, dp: f32) -> Self {
412                self.linear.spacing = Some(dp);
413                self
414            }
415
416            /// A gap from the design system's spacing ladder.
417            pub fn space_role(mut self, role: SpaceRole) -> Self {
418                self.linear.space_role = Some(role);
419                self
420            }
421
422            /// Where the children sit on the cross axis.
423            pub fn alignment(mut self, alignment: Alignment) -> Self {
424                self.linear.alignment = Some(alignment);
425                self
426            }
427
428            /// Sizes the cross axis to the tallest child in a `Row` or the widest child in
429            /// a `Column`, so children with `fill_max_height` or `fill_max_width` match the
430            /// largest sibling without an explicit container size.
431            pub fn intrinsic_size(mut self, enabled: bool) -> Self {
432                self.linear.intrinsic_size = Some(enabled);
433                self
434            }
435
436            /// Makes this layout a group of the `RadioButton`s inside it.
437            ///
438            /// The group is one stop for Tab: the selected button, or the first when none
439            /// is. The arrow keys move focus to the next or the previous button, wrapping
440            /// at the ends, and each move sends that button's `on_change(true)` once, the
441            /// same report a click sends. Which button is selected is still the
442            /// application's to decide. Assistive technology hears the layout as a radio
443            /// group.
444            pub fn radio_group(mut self, enabled: bool) -> Self {
445                self.linear.radio_group = enabled;
446                self
447            }
448
449            fn compose(&mut self, content: impl FnOnce()) {
450                let modifier = self.modifier;
451                let linear = std::mem::take(&mut self.linear);
452                let pointer = self.pointer.take();
453                let tear_off = self.tear_off.take();
454                let mut listeners = Vec::new();
455                let mut subscription = Attrs::new();
456                pointer_input(pointer, &mut subscription, &mut listeners);
457                click_input(self.on_click.take(), &mut listeners);
458                focus_and_key_input(std::mem::take(&mut self.focus_and_keys), &mut listeners);
459                tear_off_input(tear_off, &mut subscription, &mut listeners);
460                emit(
461                    widget_key($key),
462                    WidgetKind::$widget,
463                    |attrs| {
464                        push_modifiers(&modifier, Mods::Container, attrs);
465                        linear.push(attrs);
466                        attrs.append(&mut subscription);
467                    },
468                    listeners,
469                    content,
470                );
471            }
472        }
473
474        container!($name);
475        pointer_subscription!($name);
476        click_handler!($name);
477        focus_and_key_handlers!($name);
478        tear_off!($name);
479    };
480}
481
482linear!(ColumnScope, Column, "compose_rust::ui::Column");
483linear!(RowScope, Row, "compose_rust::ui::Row");
484
485impl RowScope {
486    /// Sizes the row to its tallest child so children with `fill_max_height` match their
487    /// siblings' height.
488    pub fn equal_height(self, enabled: bool) -> Self {
489        self.intrinsic_size(enabled)
490    }
491}
492
493impl ColumnScope {
494    /// Sizes the column to its widest child so children with `fill_max_width` match their
495    /// siblings' width.
496    pub fn equal_width(self, enabled: bool) -> Self {
497        self.intrinsic_size(enabled)
498    }
499}
500
501/// A `FlowRow` and its builder.
502pub struct FlowRowScope {
503    modifier: Modifier,
504    horizontal_spacing: Option<f32>,
505    vertical_spacing: Option<f32>,
506    horizontal_alignment: Option<HorizontalAlignment>,
507    vertical_alignment: Option<VerticalAlignment>,
508    max_items_in_each_row: Option<u32>,
509    done: bool,
510}
511
512/// Lays its children out on a line and starts a new line when the next child does not fit
513/// in what is left of it. Each child is measured at its own width, so a line breaks where
514/// the text really ends rather than where an estimate put it. A child wider than the whole
515/// line has the line to itself and is measured at the line's width. Compose's `FlowRow`.
516pub fn FlowRow() -> FlowRowScope {
517    FlowRowScope {
518        modifier: Modifier::new(),
519        horizontal_spacing: None,
520        vertical_spacing: None,
521        horizontal_alignment: None,
522        vertical_alignment: None,
523        max_items_in_each_row: None,
524        done: false,
525    }
526}
527
528impl FlowRowScope {
529    /// The gap between two children on the same line, in dp.
530    pub fn horizontal_spacing(mut self, dp: f32) -> Self {
531        self.horizontal_spacing = Some(dp);
532        self
533    }
534
535    /// The gap between two lines, in dp.
536    pub fn vertical_spacing(mut self, dp: f32) -> Self {
537        self.vertical_spacing = Some(dp);
538        self
539    }
540
541    /// Where the children of a line sit along it when they do not fill it. `Start` when
542    /// not set.
543    pub fn horizontal_alignment(mut self, alignment: HorizontalAlignment) -> Self {
544        self.horizontal_alignment = Some(alignment);
545        self
546    }
547
548    /// Where a child sits inside the height of its line. `Top` when not set.
549    pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
550        self.vertical_alignment = Some(alignment);
551        self
552    }
553
554    /// The most children one line holds, even when more would fit. No limit when not set.
555    pub fn max_items_in_each_row(mut self, count: u32) -> Self {
556        self.max_items_in_each_row = Some(count);
557        self
558    }
559
560    fn compose(&mut self, content: impl FnOnce()) {
561        let modifier = self.modifier;
562        let (horizontal_spacing, vertical_spacing) =
563            (self.horizontal_spacing, self.vertical_spacing);
564        let (horizontal_alignment, vertical_alignment) =
565            (self.horizontal_alignment, self.vertical_alignment);
566        let max_items = self.max_items_in_each_row;
567        emit(
568            widget_key("compose_rust::ui::FlowRow"),
569            WidgetKind::FlowRow,
570            |attrs| {
571                push_modifiers(&modifier, Mods::Container, attrs);
572                // Only what was set is written: an absent property is the Renderer's
573                // default, and a zero gap or tag would say nothing an absent one does not.
574                if let Some(dp) = horizontal_spacing {
575                    attrs.push(("horizontal_spacing", dp_or_zero(Some(dp))));
576                }
577                if let Some(dp) = vertical_spacing {
578                    attrs.push(("vertical_spacing", dp_or_zero(Some(dp))));
579                }
580                if let Some(alignment) = horizontal_alignment {
581                    attrs.push(("horizontal_alignment", role_or_zero(Some(alignment))));
582                }
583                if let Some(alignment) = vertical_alignment {
584                    attrs.push(("vertical_alignment", role_or_zero(Some(alignment))));
585                }
586                if let Some(count) = max_items {
587                    attrs.push(("max_items_in_each_row", OwnedAttr::Int(i64::from(count))));
588                }
589            },
590            Vec::new(),
591            content,
592        );
593    }
594}
595
596container!(FlowRowScope);
597
598/// A `Box` and its builder. Named apart from `std::boxed::Box`, which a glob import of
599/// this module would otherwise hide.
600pub struct ComposeBox {
601    modifier: Modifier,
602    alignment: Option<Alignment>,
603    pointer: Option<PointerSubscription>,
604    on_click: Option<Box<dyn FnMut()>>,
605    focus_and_keys: FocusAndKeyHandlers,
606    tear_off: Option<TearOff>,
607    done: bool,
608}
609
610/// Stacks its children on top of each other. Compose's `Box`.
611pub fn Box() -> ComposeBox {
612    ComposeBox {
613        modifier: Modifier::new(),
614        alignment: None,
615        pointer: None,
616        on_click: None,
617        focus_and_keys: FocusAndKeyHandlers::default(),
618        tear_off: None,
619        done: false,
620    }
621}
622
623impl ComposeBox {
624    /// Where the children sit inside it.
625    pub fn alignment(mut self, alignment: Alignment) -> Self {
626        self.alignment = Some(alignment);
627        self
628    }
629
630    fn compose(&mut self, content: impl FnOnce()) {
631        let (modifier, alignment) = (self.modifier, self.alignment);
632        let pointer = self.pointer.take();
633        let tear_off = self.tear_off.take();
634        let mut listeners = Vec::new();
635        let mut subscription = Attrs::new();
636        pointer_input(pointer, &mut subscription, &mut listeners);
637        click_input(self.on_click.take(), &mut listeners);
638        focus_and_key_input(std::mem::take(&mut self.focus_and_keys), &mut listeners);
639        tear_off_input(tear_off, &mut subscription, &mut listeners);
640        emit(
641            widget_key("compose_rust::ui::Box"),
642            WidgetKind::Box,
643            |attrs| {
644                push_modifiers(&modifier, Mods::Container, attrs);
645                attrs.push(("alignment", role_or_zero(alignment)));
646                attrs.append(&mut subscription);
647            },
648            listeners,
649            content,
650        );
651    }
652}
653
654container!(ComposeBox);
655pointer_subscription!(ComposeBox);
656click_handler!(ComposeBox);
657focus_and_key_handlers!(ComposeBox);
658tear_off!(ComposeBox);
659
660/// A plain container with the standard Modifiers and nothing of its own.
661macro_rules! plain_container {
662    ($name:ident, $widget:ident, $mods:expr, $key:literal, $doc:literal) => {
663        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
664        pub struct $name {
665            modifier: Modifier,
666            pointer: Option<PointerSubscription>,
667            on_click: Option<Box<dyn FnMut()>>,
668            focus_and_keys: FocusAndKeyHandlers,
669            done: bool,
670        }
671
672        #[doc = $doc]
673        pub fn $widget() -> $name {
674            $name {
675                modifier: Modifier::new(),
676                pointer: None,
677                on_click: None,
678                focus_and_keys: FocusAndKeyHandlers::default(),
679                done: false,
680            }
681        }
682
683        impl $name {
684            fn compose(&mut self, content: impl FnOnce()) {
685                let modifier = self.modifier;
686                let pointer = self.pointer.take();
687                let mut listeners = Vec::new();
688                let mut subscription = Attrs::new();
689                pointer_input(pointer, &mut subscription, &mut listeners);
690                click_input(self.on_click.take(), &mut listeners);
691                focus_and_key_input(std::mem::take(&mut self.focus_and_keys), &mut listeners);
692                emit(
693                    widget_key($key),
694                    WidgetKind::$widget,
695                    |attrs| {
696                        push_modifiers(&modifier, $mods, attrs);
697                        attrs.append(&mut subscription);
698                    },
699                    listeners,
700                    content,
701                );
702            }
703        }
704
705        container!($name);
706        pointer_subscription!($name);
707        click_handler!($name);
708        focus_and_key_handlers!($name);
709    };
710}
711
712/// The scroll position of a [`ScrollColumn`] or [`ScrollRow`], and a handle to move it.
713///
714/// The Renderer owns the scroll physics and the scrollbar; this handle reads the current
715/// offset and maximum offset in dp as observable states and sends target scroll requests.
716#[derive(Clone, Debug, PartialEq, Eq)]
717pub struct ScrollState {
718    offset: crate::runtime::MutableState<f32>,
719    max_offset: crate::runtime::MutableState<f32>,
720    command: crate::runtime::MutableState<(i64, f32, bool)>,
721}
722
723impl Default for ScrollState {
724    fn default() -> Self {
725        Self::new()
726    }
727}
728
729impl ScrollState {
730    /// Creates a new scroll state starting at 0 dp.
731    pub fn new() -> Self {
732        Self {
733            offset: crate::runtime::mutable_state_of(0.0),
734            max_offset: crate::runtime::mutable_state_of(0.0),
735            command: crate::runtime::mutable_state_of((0, 0.0, false)),
736        }
737    }
738
739    /// The current scroll offset in dp. A composable that reads this runs again when the
740    /// user or a programmatic scroll moves the container.
741    pub fn offset(&self) -> f32 {
742        self.offset.get()
743    }
744
745    /// The furthest offset in dp the container can scroll to (`0.0` when the content fits
746    /// inside the viewport). A composable that reads this runs again when the content or
747    /// viewport size changes the scrollable distance.
748    pub fn max_offset(&self) -> f32 {
749        self.max_offset.get()
750    }
751
752    /// Jumps to `dp` (clamped by the Renderer to `0.0..=max_offset`).
753    pub fn scroll_to(&self, dp: f32) {
754        let (revision, _, _) = self.command.get_untracked();
755        self.command
756            .set((revision.wrapping_add(1).max(1), dp.max(0.0), false));
757    }
758
759    /// Smoothly scrolls to `dp` (clamped by the Renderer to `0.0..=max_offset`).
760    pub fn animate_scroll_to(&self, dp: f32) {
761        let (revision, _, _) = self.command.get_untracked();
762        self.command
763            .set((revision.wrapping_add(1).max(1), dp.max(0.0), true));
764    }
765}
766
767/// Remembers a [`ScrollState`] across recompositions. Compose's `rememberScrollState`.
768pub fn remember_scroll_state() -> ScrollState {
769    crate::runtime::remember(ScrollState::new)
770}
771
772fn scroll_state_input(
773    state: Option<ScrollState>,
774    attrs: &mut Attrs,
775    listeners: &mut Vec<(&'static str, EventCallback)>,
776) {
777    let Some(state) = state else {
778        return;
779    };
780    let (revision, target, animate) = state.command.get();
781    if revision > 0 {
782        attrs.push(("value", OwnedAttr::Float(f64::from(target))));
783        attrs.push(("determinate", OwnedAttr::Bool(animate)));
784        attrs.push(("count", OwnedAttr::Int(revision)));
785    }
786    let offset = state.offset;
787    let max_offset = state.max_offset;
788    listeners.push((
789        "onrangerequest",
790        EventCallback::Range(Rc::new(RefCell::new(move |range: crate::RangeRequest| {
791            let next_offset = f32::from_bits(range.start() as u32);
792            let next_max = f32::from_bits(range.count() as u32);
793            if next_offset.is_finite() {
794                offset.set(next_offset);
795            }
796            if next_max.is_finite() {
797                max_offset.set(next_max);
798            }
799        }))),
800    ));
801}
802
803macro_rules! scroll_container {
804    ($name:ident, $widget:ident, $key:literal, $doc:literal) => {
805        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
806        pub struct $name {
807            modifier: Modifier,
808            scroll_state: Option<ScrollState>,
809            snap: Snap,
810            pointer: Option<PointerSubscription>,
811            on_click: Option<Box<dyn FnMut()>>,
812            focus_and_keys: FocusAndKeyHandlers,
813            done: bool,
814        }
815
816        #[doc = $doc]
817        pub fn $widget() -> $name {
818            $name {
819                modifier: Modifier::new(),
820                scroll_state: None,
821                snap: Snap::None,
822                pointer: None,
823                on_click: None,
824                focus_and_keys: FocusAndKeyHandlers::default(),
825                done: false,
826            }
827        }
828
829        impl $name {
830            /// Binds a [`ScrollState`] so the application can read `offset()` and
831            /// `max_offset()` or call `scroll_to` and `animate_scroll_to`.
832            pub fn scroll_state(mut self, state: &ScrollState) -> Self {
833                self.scroll_state = Some(state.clone());
834                self
835            }
836
837            /// Where the content comes to rest once a drag or a fling ends.
838            ///
839            /// `Snap::None`, the default, stops wherever the user left it.
840            /// `Snap::ItemStart` stops with the start edge of the nearest direct child on
841            /// the start of the visible area, like CSS `scroll-snap-align: start`. The
842            /// Renderer runs the snap, so it follows the platform's fling physics.
843            pub fn snap(mut self, snap: Snap) -> Self {
844                self.snap = snap;
845                self
846            }
847
848            fn compose(&mut self, content: impl FnOnce()) {
849                let modifier = self.modifier;
850                let snap = self.snap;
851                let scroll_state = self.scroll_state.take();
852                let pointer = self.pointer.take();
853                let mut listeners = Vec::new();
854                let mut subscription = Attrs::new();
855                scroll_state_input(scroll_state, &mut subscription, &mut listeners);
856                pointer_input(pointer, &mut subscription, &mut listeners);
857                click_input(self.on_click.take(), &mut listeners);
858                focus_and_key_input(std::mem::take(&mut self.focus_and_keys), &mut listeners);
859                emit(
860                    widget_key($key),
861                    WidgetKind::$widget,
862                    |attrs| {
863                        push_modifiers(&modifier, Mods::Standard, attrs);
864                        // Free scrolling is the default, and the default is not sent: a
865                        // container that says nothing travels as it did before snapping.
866                        if snap != Snap::None {
867                            attrs.push(("snap", OwnedAttr::Int(i64::from(u16::from(snap)))));
868                        }
869                        attrs.append(&mut subscription);
870                    },
871                    listeners,
872                    content,
873                );
874            }
875        }
876
877        container!($name);
878        pointer_subscription!($name);
879        click_handler!($name);
880        focus_and_key_handlers!($name);
881    };
882}
883
884scroll_container!(
885    ScrollColumnScope,
886    ScrollColumn,
887    "compose_rust::ui::ScrollColumn",
888    "All of its content, with a vertical scroll the Renderer owns."
889);
890scroll_container!(
891    ScrollRowScope,
892    ScrollRow,
893    "compose_rust::ui::ScrollRow",
894    "All of its content, with a horizontal scroll the Renderer owns."
895);
896plain_container!(
897    CardScope,
898    Card,
899    Mods::Container,
900    "compose_rust::ui::Card",
901    "A grouped container. What it looks like is the design system's."
902);
903plain_container!(
904    SurfaceScope,
905    Surface,
906    Mods::Container,
907    "compose_rust::ui::Surface",
908    "A plain background and elevation, without a card's meaning."
909);
910plain_container!(
911    SelectionContainerScope,
912    SelectionContainer,
913    Mods::Standard,
914    "compose_rust::ui::SelectionContainer",
915    "Every `Text` inside can be selected and copied as one selection."
916);
917plain_container!(
918    SpacerScope,
919    Spacer,
920    Mods::Standard,
921    "compose_rust::ui::Spacer",
922    "Empty space, sized by its Modifier."
923);
924
925// ----- text -----------------------------------------------------------------------------
926
927/// A font list as the property blob, or nothing when it is empty.
928fn font_bytes(font: &FontRefs) -> OwnedAttr {
929    if font.is_empty() {
930        OwnedAttr::None
931    } else {
932        OwnedAttr::Bytes(font.as_bytes().to_vec())
933    }
934}
935
936/// A `Text` and its builder.
937pub struct TextScope {
938    modifier: Modifier,
939    text: String,
940    spans: TextSpans,
941    type_role: Option<TypeRole>,
942    font_size: Option<f32>,
943    font_weight: Option<u16>,
944    line_height: Option<f32>,
945    letter_spacing: Option<f32>,
946    color: Option<Paint>,
947    text_align: Option<TextAlign>,
948    max_lines: Option<u32>,
949    overflow: Option<TextOverflow>,
950    font: FontRefs,
951    font_features: OwnedAttr,
952    word_break: Option<WordBreak>,
953    overflow_wrap: Option<OverflowWrap>,
954    pointer: Option<PointerSubscription>,
955    done: bool,
956}
957
958/// Text. Compose's `Text`.
959///
960/// `type_role` alone takes the design system's size, weight, line height and letter
961/// spacing; each other style method overrides one axis of it.
962pub fn Text(text: impl Into<String>) -> TextScope {
963    TextScope {
964        modifier: Modifier::new(),
965        text: text.into(),
966        spans: TextSpans::default(),
967        type_role: None,
968        font_size: None,
969        font_weight: None,
970        line_height: None,
971        letter_spacing: None,
972        color: None,
973        text_align: None,
974        max_lines: None,
975        overflow: None,
976        font: FontRefs::default(),
977        font_features: OwnedAttr::None,
978        word_break: None,
979        overflow_wrap: None,
980        pointer: None,
981        done: false,
982    }
983}
984
985impl TextScope {
986    /// Runs of different treatment inside the text: a bold phrase, a link, some code.
987    pub fn spans(mut self, spans: TextSpans) -> Self {
988        self.spans = spans;
989        self
990    }
991
992    pub fn type_role(mut self, role: TypeRole) -> Self {
993        self.type_role = Some(role);
994        self
995    }
996
997    pub fn font_size(mut self, sp: f32) -> Self {
998        self.font_size = Some(sp);
999        self
1000    }
1001
1002    pub fn font_weight(mut self, weight: u16) -> Self {
1003        self.font_weight = Some(weight);
1004        self
1005    }
1006
1007    pub fn line_height(mut self, sp: f32) -> Self {
1008        self.line_height = Some(sp);
1009        self
1010    }
1011
1012    /// Extra space between letters, in sp. 0 draws no extra spacing; NaN, like never
1013    /// calling this, takes the type role's spacing.
1014    pub fn letter_spacing(mut self, sp: f32) -> Self {
1015        self.letter_spacing = Some(sp);
1016        self
1017    }
1018
1019    pub fn color(mut self, paint: Paint) -> Self {
1020        self.color = Some(paint);
1021        self
1022    }
1023
1024    pub fn text_align(mut self, align: TextAlign) -> Self {
1025        self.text_align = Some(align);
1026        self
1027    }
1028
1029    pub fn max_lines(mut self, lines: u32) -> Self {
1030        self.max_lines = Some(lines);
1031        self
1032    }
1033
1034    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
1035        self.overflow = Some(overflow);
1036        self
1037    }
1038
1039    /// The fonts to try, in order. Only text with `TypeRole::None` takes its own fonts;
1040    /// text with a role is set in the theme's font for that role.
1041    pub fn font(mut self, font: FontRefs) -> Self {
1042        self.font = font;
1043        self
1044    }
1045
1046    /// OpenType features of the font the text is set in, such as
1047    /// `FontFeature::TABULAR_NUMS` for digits of one width. Compose's
1048    /// `TextStyle.fontFeatureSettings`, CSS's `font-feature-settings`.
1049    ///
1050    /// Taken with or without a type role: a feature picks one of the forms the font already
1051    /// has and does not change the font. A tag given twice takes the later value, an empty
1052    /// list is no features, and at most `MAX_FONT_FEATURES` (16) are taken. A font without
1053    /// the feature draws as it would have.
1054    pub fn font_feature_settings(mut self, features: &[FontFeature]) -> Self {
1055        self.font_features = crate::font_features::font_feature_attr(features);
1056        self
1057    }
1058
1059    /// Where a line may break inside a word. CSS `word-break`.
1060    pub fn word_break(mut self, word_break: WordBreak) -> Self {
1061        self.word_break = Some(word_break);
1062        self
1063    }
1064
1065    /// Whether a token too long for its line, such as a path or a URL, may break inside
1066    /// itself. CSS `overflow-wrap`. With `Anywhere` such a token breaks between its letters
1067    /// even under `WordBreak::KeepAll`; tokens that fit keep the breaking rule they had.
1068    pub fn overflow_wrap(mut self, overflow_wrap: OverflowWrap) -> Self {
1069        self.overflow_wrap = Some(overflow_wrap);
1070        self
1071    }
1072
1073    fn compose(&mut self, content: impl FnOnce()) {
1074        let modifier = self.modifier;
1075        let text = std::mem::take(&mut self.text);
1076        let spans = std::mem::take(&mut self.spans);
1077        let font = std::mem::take(&mut self.font);
1078        let font_features = std::mem::replace(&mut self.font_features, OwnedAttr::None);
1079        let (word_break, overflow_wrap) = (self.word_break, self.overflow_wrap);
1080        let (type_role, font_size, font_weight, line_height, letter_spacing) = (
1081            self.type_role,
1082            self.font_size,
1083            self.font_weight,
1084            self.line_height,
1085            self.letter_spacing,
1086        );
1087        let (color, text_align, max_lines, overflow) =
1088            (self.color, self.text_align, self.max_lines, self.overflow);
1089        let pointer = self.pointer.take();
1090        let mut listeners = Vec::new();
1091        let mut subscription = Attrs::new();
1092        pointer_input(pointer, &mut subscription, &mut listeners);
1093        emit(
1094            widget_key("compose_rust::ui::Text"),
1095            WidgetKind::Text,
1096            |attrs| {
1097                push_modifiers(&modifier, Mods::Standard, attrs);
1098                // Left out entirely when there are none, as the rsx widget leaves it out.
1099                let span_fonts = spans.font_table().to_vec();
1100                attrs.push((
1101                    "spans",
1102                    if spans.is_empty() {
1103                        OwnedAttr::None
1104                    } else {
1105                        OwnedAttr::Spans(spans)
1106                    },
1107                ));
1108                // The fonts each styled run names travel beside the runs, and are left out
1109                // when no run names any.
1110                attrs.push((
1111                    "span_fonts",
1112                    if span_fonts.is_empty() {
1113                        OwnedAttr::None
1114                    } else {
1115                        OwnedAttr::Bytes(span_fonts)
1116                    },
1117                ));
1118                attrs.push(("font", font_bytes(&font)));
1119                // Left out when no feature was asked for, so such a text sends what it
1120                // always did. A first composition writes every attribute in the list, an
1121                // empty one as well, so leaving it out is what keeps it off the wire.
1122                if font_features != OwnedAttr::None {
1123                    attrs.push(("font_feature_settings", font_features));
1124                }
1125                attrs.push(("word_break", super::role(word_break)));
1126                attrs.push(("overflow_wrap", super::role(overflow_wrap)));
1127                attrs.push(("text", OwnedAttr::Text(text)));
1128                // `TypeRole::None` is tag 0, so it travels as an explicit 0 and a role that
1129                // was never set travels as nothing. Sent the other way, the text that names
1130                // its own fonts could not say it has no role, and the Renderer set it in
1131                // the default role's font with its fonts ignored.
1132                attrs.push(("type_role", super::role(type_role)));
1133                attrs.push(("font_size", dp_or_zero(font_size)));
1134                attrs.push((
1135                    "font_weight",
1136                    OwnedAttr::Int(i64::from(font_weight.unwrap_or(0))),
1137                ));
1138                attrs.push(("line_height", dp_or_zero(line_height)));
1139                // Unset is NaN, so an explicit 0 stays a 0 and draws no extra spacing.
1140                attrs.push((
1141                    "letter_spacing",
1142                    OwnedAttr::Float(letter_spacing.map_or(f64::NAN, f64::from)),
1143                ));
1144                attrs.push(("color", paint_or_zero(color)));
1145                attrs.push(("text_align", role_or_zero(text_align)));
1146                attrs.push((
1147                    "max_lines",
1148                    OwnedAttr::Int(i64::from(max_lines.unwrap_or(0))),
1149                ));
1150                attrs.push(("overflow", role_or_zero(overflow)));
1151                attrs.append(&mut subscription);
1152            },
1153            listeners,
1154            content,
1155        );
1156    }
1157}
1158
1159composed_on_drop!(TextScope);
1160pointer_subscription!(TextScope);
1161
1162/// A `TextField` and its builder.
1163pub struct TextFieldScope {
1164    modifier: Modifier,
1165    text: String,
1166    placeholder: String,
1167    enabled: bool,
1168    multiline: bool,
1169    type_role: Option<TypeRole>,
1170    font_size: Option<f32>,
1171    font_weight: Option<u16>,
1172    line_height: Option<f32>,
1173    letter_spacing: Option<f32>,
1174    color: Option<Paint>,
1175    font: FontRefs,
1176    font_features: OwnedAttr,
1177    word_break: Option<WordBreak>,
1178    overflow_wrap: Option<OverflowWrap>,
1179    on_value_change: Option<Box<dyn FnMut(String)>>,
1180    on_submit: Option<Box<dyn FnMut(String)>>,
1181    on_focus_gained: Option<Box<dyn FnMut()>>,
1182    on_focus_lost: Option<Box<dyn FnMut()>>,
1183    on_key_down: Option<Box<dyn FnMut(KeyEvent)>>,
1184    on_key_up: Option<Box<dyn FnMut(KeyEvent)>>,
1185    on_composition_change: Option<Box<dyn FnMut(bool)>>,
1186    on_motion_finished: Option<Box<dyn FnMut(MotionFinished)>>,
1187    focus_handle: Option<super::FocusHandle>,
1188    bare: bool,
1189    done: bool,
1190}
1191
1192/// An uncontrolled text field. Compose's `TextField`, with the value kept where the
1193/// platform's text input keeps it: the Renderer reports each change and the Host never
1194/// writes the value back while the user types.
1195pub fn TextField() -> TextFieldScope {
1196    TextFieldScope {
1197        modifier: Modifier::new(),
1198        text: String::new(),
1199        placeholder: String::new(),
1200        enabled: true,
1201        multiline: false,
1202        type_role: None,
1203        font_size: None,
1204        font_weight: None,
1205        line_height: None,
1206        letter_spacing: None,
1207        color: None,
1208        font: FontRefs::default(),
1209        font_features: OwnedAttr::None,
1210        word_break: None,
1211        overflow_wrap: None,
1212        on_value_change: None,
1213        on_submit: None,
1214        on_focus_gained: None,
1215        on_focus_lost: None,
1216        on_key_down: None,
1217        on_key_up: None,
1218        on_composition_change: None,
1219        on_motion_finished: None,
1220        focus_handle: None,
1221        bare: false,
1222        done: false,
1223    }
1224}
1225
1226impl TextFieldScope {
1227    /// The text the field starts with. Only the start: the field stays uncontrolled, so
1228    /// what the user types is never written back, and a later value here changes nothing
1229    /// on the field that is already there. To show other text, such as another document,
1230    /// give the field a new identity with `key`, and the new field starts with this text.
1231    pub fn text(mut self, text: impl Into<String>) -> Self {
1232        self.text = text.into();
1233        self
1234    }
1235
1236    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1237        self.placeholder = placeholder.into();
1238        self
1239    }
1240
1241    pub fn enabled(mut self, enabled: bool) -> Self {
1242        self.enabled = enabled;
1243        self
1244    }
1245
1246    pub fn multiline(mut self, multiline: bool) -> Self {
1247        self.multiline = multiline;
1248        self
1249    }
1250
1251    /// The rung of the type ladder the field's own text is set in.
1252    pub fn type_role(mut self, role: TypeRole) -> Self {
1253        self.type_role = Some(role);
1254        self
1255    }
1256
1257    /// Explicit font size in sp. Overrides the type role's size.
1258    pub fn font_size(mut self, sp: f32) -> Self {
1259        self.font_size = Some(sp);
1260        self
1261    }
1262
1263    /// Explicit font weight (e.g. 400, 500, 600, 700). Overrides the type role's weight.
1264    pub fn font_weight(mut self, weight: u16) -> Self {
1265        self.font_weight = Some(weight);
1266        self
1267    }
1268
1269    /// Explicit line height in sp.
1270    pub fn line_height(mut self, sp: f32) -> Self {
1271        self.line_height = Some(sp);
1272        self
1273    }
1274
1275    /// Explicit letter spacing in sp.
1276    pub fn letter_spacing(mut self, sp: f32) -> Self {
1277        self.letter_spacing = Some(sp);
1278        self
1279    }
1280
1281    /// Explicit text colour.
1282    pub fn color(mut self, paint: Paint) -> Self {
1283        self.color = Some(paint);
1284        self
1285    }
1286
1287    /// The fonts to try for the field's text, in order. Taken only with `TypeRole::None`.
1288    pub fn font(mut self, font: FontRefs) -> Self {
1289        self.font = font;
1290        self
1291    }
1292
1293    /// OpenType features for the typed text and the placeholder, as
1294    /// `Text::font_feature_settings` takes them. Taken with or without a type role.
1295    pub fn font_feature_settings(mut self, features: &[FontFeature]) -> Self {
1296        self.font_features = crate::font_features::font_feature_attr(features);
1297        self
1298    }
1299
1300    /// Where a line of the typed text and of the placeholder may break inside a word, as
1301    /// `Text::word_break` does. CSS `word-break`.
1302    pub fn word_break(mut self, word_break: WordBreak) -> Self {
1303        self.word_break = Some(word_break);
1304        self
1305    }
1306
1307    /// Whether a token too long for its line may break inside itself, in the typed text
1308    /// and the placeholder, as `Text::overflow_wrap` does. CSS `overflow-wrap`.
1309    pub fn overflow_wrap(mut self, overflow_wrap: OverflowWrap) -> Self {
1310        self.overflow_wrap = Some(overflow_wrap);
1311        self
1312    }
1313
1314    pub fn on_value_change(mut self, handler: impl FnMut(String) + 'static) -> Self {
1315        self.on_value_change = Some(std::boxed::Box::new(handler));
1316        self
1317    }
1318
1319    pub fn on_submit(mut self, handler: impl FnMut(String) + 'static) -> Self {
1320        self.on_submit = Some(std::boxed::Box::new(handler));
1321        self
1322    }
1323
1324    pub fn on_focus_gained(mut self, handler: impl FnMut() + 'static) -> Self {
1325        self.on_focus_gained = Some(std::boxed::Box::new(handler));
1326        self
1327    }
1328
1329    pub fn on_focus_lost(mut self, handler: impl FnMut() + 'static) -> Self {
1330        self.on_focus_lost = Some(std::boxed::Box::new(handler));
1331        self
1332    }
1333
1334    /// Sees each key the field is pressed with; calling `consume` on the event keeps the
1335    /// field from acting on it.
1336    pub fn on_key_down(mut self, handler: impl FnMut(KeyEvent) + 'static) -> Self {
1337        self.on_key_down = Some(std::boxed::Box::new(handler));
1338        self
1339    }
1340
1341    /// Sees each key released while the field holds focus.
1342    pub fn on_key_up(mut self, handler: impl FnMut(KeyEvent) + 'static) -> Self {
1343        self.on_key_up = Some(std::boxed::Box::new(handler));
1344        self
1345    }
1346
1347    /// Reports when an input method begins or ends an active composition in the field.
1348    pub fn on_composition_change(mut self, handler: impl FnMut(bool) + 'static) -> Self {
1349        self.on_composition_change = Some(std::boxed::Box::new(handler));
1350        self
1351    }
1352
1353    /// Binds `handle` to this field, so `handle.request()` puts keyboard focus here.
1354    pub fn focus_handle(mut self, handle: &super::FocusHandle) -> Self {
1355        self.focus_handle = Some(handle.clone());
1356        self
1357    }
1358
1359    /// Runs once when the field's size motion comes to rest, with the size it settled at
1360    /// in dp. The field needs `Modifier::motion` for its size to move; without it nothing
1361    /// is reported. A run interrupted by a newer size is not reported, only the one that
1362    /// settles.
1363    pub fn on_motion_finished(mut self, handler: impl FnMut(MotionFinished) + 'static) -> Self {
1364        self.on_motion_finished = Some(std::boxed::Box::new(handler));
1365        self
1366    }
1367
1368    /// Draws the field without the design system's frame: no fill, line, underline,
1369    /// corner clip, inner padding, minimum height or focus line. Text, placeholder, caret,
1370    /// selection and input methods stay as they are. For a field that sits inside a box
1371    /// which already draws its own frame, so the frame is not drawn twice. A `background`
1372    /// or `border` given on the node is still drawn.
1373    pub fn bare(mut self) -> Self {
1374        self.bare = true;
1375        self
1376    }
1377
1378    fn compose(&mut self, content: impl FnOnce()) {
1379        let modifier = self.modifier;
1380        let text = std::mem::take(&mut self.text);
1381        let placeholder = std::mem::take(&mut self.placeholder);
1382        let (enabled, multiline, type_role) = (self.enabled, self.multiline, self.type_role);
1383        let bare = self.bare;
1384        let (font_size, font_weight, line_height) =
1385            (self.font_size, self.font_weight, self.line_height);
1386        let (letter_spacing, color) = (self.letter_spacing, self.color);
1387        let font = std::mem::take(&mut self.font);
1388        let font_features = std::mem::replace(&mut self.font_features, OwnedAttr::None);
1389        let (word_break, overflow_wrap) = (self.word_break, self.overflow_wrap);
1390        let key_down: EventCallback = match self.on_key_down.take() {
1391            Some(handler) => EventCallback::Key(Rc::new(RefCell::new(handler))),
1392            None => EventCallback::Key(Rc::new(RefCell::new(|_: KeyEvent| {}))),
1393        };
1394        let mut listeners = vec![
1395            ("onvaluechange", text_handler(self.on_value_change.take())),
1396            ("onsubmit", text_handler(self.on_submit.take())),
1397            ("onfocuslost", unit(self.on_focus_lost.take())),
1398            ("onkeydown", key_down),
1399        ];
1400        if let Some(handler) = self.on_focus_gained.take() {
1401            listeners.push(("onfocusgained", unit(Some(handler))));
1402        }
1403        if let Some(handler) = self.on_key_up.take() {
1404            listeners.push((
1405                "onkeyup",
1406                EventCallback::Key(Rc::new(RefCell::new(handler))),
1407            ));
1408        }
1409        if let Some(mut handler) = self.on_composition_change.take() {
1410            listeners.push((
1411                "oncompositionchange",
1412                EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
1413                    handler(value != 0.0);
1414                }))),
1415            ));
1416        }
1417        motion_input(self.on_motion_finished.take(), &mut listeners);
1418        super::focus::bind_focus_handle(self.focus_handle.take());
1419        emit(
1420            widget_key("compose_rust::ui::TextField"),
1421            WidgetKind::TextField,
1422            |attrs| {
1423                push_modifiers(&modifier, Mods::Standard, attrs);
1424                // Written only when there is a starting text, so a field without one sends
1425                // the same records it always did.
1426                if !text.is_empty() {
1427                    attrs.push(("text", OwnedAttr::Text(text)));
1428                }
1429                attrs.push(("placeholder", OwnedAttr::Text(placeholder)));
1430                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1431                attrs.push(("multiline", OwnedAttr::Bool(multiline)));
1432                // An explicit `TypeRole::None` is a 0 on the wire, an unset role nothing,
1433                // as for `Text`.
1434                attrs.push(("type_role", super::role(type_role)));
1435                if let Some(font_size) = font_size {
1436                    attrs.push(("font_size", OwnedAttr::Float(font_size as f64)));
1437                }
1438                if let Some(font_weight) = font_weight {
1439                    attrs.push(("font_weight", OwnedAttr::Int(font_weight as i64)));
1440                }
1441                if let Some(line_height) = line_height {
1442                    attrs.push(("line_height", OwnedAttr::Float(line_height as f64)));
1443                }
1444                if let Some(letter_spacing) = letter_spacing {
1445                    attrs.push(("letter_spacing", OwnedAttr::Float(letter_spacing as f64)));
1446                }
1447                if let Some(color) = color {
1448                    attrs.push(("color", super::paint(Some(color))));
1449                }
1450                attrs.push(("font", font_bytes(&font)));
1451                // Left out when no feature was asked for, as `Text` leaves it out.
1452                if font_features != OwnedAttr::None {
1453                    attrs.push(("font_feature_settings", font_features));
1454                }
1455                attrs.push(("word_break", super::role(word_break)));
1456                attrs.push(("overflow_wrap", super::role(overflow_wrap)));
1457                // Written only for a bare field, so a framed one sends what it always did.
1458                if bare {
1459                    attrs.push(("bare", OwnedAttr::Bool(true)));
1460                }
1461            },
1462            listeners,
1463            content,
1464        );
1465    }
1466}
1467
1468composed_on_drop!(TextFieldScope);
1469
1470// ----- buttons and controls ---------------------------------------------------------------
1471
1472/// The label's line limit and overflow, written only when they were set, so a widget that
1473/// never asks for them sends what it always sent.
1474fn lines_and_overflow(max_lines: Option<u32>, overflow: Option<TextOverflow>, attrs: &mut Attrs) {
1475    if let Some(lines) = max_lines {
1476        attrs.push(("max_lines", OwnedAttr::Int(i64::from(lines))));
1477    }
1478    if overflow.is_some() {
1479        attrs.push(("overflow", role_or_zero(overflow)));
1480    }
1481}
1482
1483/// The colours and size a Switch, Slider, Checkbox or RadioButton was asked to draw with. Each is sent
1484/// only when it was set, so a control that sets none sends the properties it always has
1485/// and the design system draws it.
1486#[derive(Clone, Copy, Default)]
1487struct ControlLook {
1488    track: Option<Paint>,
1489    thumb: Option<Paint>,
1490    active_track: Option<Paint>,
1491    container: Option<Paint>,
1492    check: Option<Paint>,
1493    size: Option<(f32, f32)>,
1494}
1495
1496impl ControlLook {
1497    fn push(&self, attrs: &mut Attrs) {
1498        let paints = [
1499            ("track_color", self.track),
1500            ("thumb_color", self.thumb),
1501            ("active_track_color", self.active_track),
1502            ("container_color", self.container),
1503            ("check_color", self.check),
1504        ];
1505        for (name, paint) in paints {
1506            if let Some(paint) = paint {
1507                attrs.push((name, super::paint(Some(paint))));
1508            }
1509        }
1510        if let Some((width, height)) = self.size {
1511            let mut bytes = Vec::with_capacity(8);
1512            bytes.extend_from_slice(&width.to_bits().to_le_bytes());
1513            bytes.extend_from_slice(&height.to_bits().to_le_bytes());
1514            attrs.push(("control_size", OwnedAttr::Bytes(bytes)));
1515        }
1516    }
1517}
1518
1519/// Adds a colour setter that fills one slot of `look`.
1520macro_rules! control_colour {
1521    ($name:ident, $slot:ident, $setter:ident, $doc:literal) => {
1522        impl $name {
1523            #[doc = $doc]
1524            pub fn $setter(mut self, paint: Paint) -> Self {
1525                self.look.$slot = Some(paint);
1526                self
1527            }
1528        }
1529    };
1530}
1531
1532/// Writes a widget's own accessible name. It replaces one the Modifier gave, so the name
1533/// said on the widget is the one that reaches assistive technology.
1534fn push_accessible_name(name: Option<String>, attrs: &mut Attrs) {
1535    if let Some(name) = name.filter(|name| !name.is_empty()) {
1536        attrs.retain(|(key, _)| *key != "accessible_name");
1537        attrs.push(("accessible_name", OwnedAttr::Text(name)));
1538    }
1539}
1540
1541/// Adds `accessible_name` to a builder that has an `accessible_name` field.
1542macro_rules! accessible_name {
1543    ($name:ident) => {
1544        impl $name {
1545            /// The name assistive technology reads. With no visible label it is the
1546            /// control's name; with one it replaces the label, and on an icon-only button
1547            /// it is read instead of the icon's meaning.
1548            pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
1549                self.accessible_name = Some(name.into());
1550                self
1551            }
1552        }
1553    };
1554}
1555
1556/// A `Button` and its builder.
1557pub struct ButtonScope {
1558    modifier: Modifier,
1559    text: String,
1560    icon: Option<IconRole>,
1561    enabled: bool,
1562    variant: Option<ButtonVariant>,
1563    kind: Option<ButtonKind>,
1564    color: Option<Paint>,
1565    max_lines: Option<u32>,
1566    overflow: Option<TextOverflow>,
1567    accessible_name: Option<String>,
1568    text_align: Option<TextAlign>,
1569    font_size: Option<f32>,
1570    font_weight: Option<u16>,
1571    type_role: Option<TypeRole>,
1572    on_click: Option<Box<dyn FnMut()>>,
1573    focus_handle: Option<super::FocusHandle>,
1574    done: bool,
1575}
1576
1577/// A button with a label. Compose's `Button`; the variant is the design system's seam,
1578/// so `ButtonVariant::Text` is Compose's `TextButton` and `Outlined` its `OutlinedButton`.
1579///
1580/// An empty label with an icon is an icon button, named for assistive technology by its
1581/// icon's meaning.
1582pub fn Button(text: impl Into<String>) -> ButtonScope {
1583    ButtonScope {
1584        modifier: Modifier::new(),
1585        text: text.into(),
1586        icon: None,
1587        enabled: true,
1588        variant: None,
1589        kind: None,
1590        color: None,
1591        max_lines: None,
1592        overflow: None,
1593        accessible_name: None,
1594        text_align: None,
1595        font_size: None,
1596        font_weight: None,
1597        type_role: None,
1598        on_click: None,
1599        focus_handle: None,
1600        done: false,
1601    }
1602}
1603
1604accessible_name!(ButtonScope);
1605
1606impl ButtonScope {
1607    /// Where the label sits in the button. A button that never calls this sends nothing
1608    /// and keeps its label centred.
1609    pub fn text_align(mut self, align: TextAlign) -> Self {
1610        self.text_align = Some(align);
1611        self
1612    }
1613
1614    /// The label's size in sp. A button that never calls this sends nothing and takes the
1615    /// design system's size for its variant.
1616    pub fn font_size(mut self, sp: f32) -> Self {
1617        self.font_size = Some(sp);
1618        self
1619    }
1620
1621    /// The label's weight (e.g. 400, 500, 600, 700). Overrides the type role's weight. A
1622    /// button that never calls this sends nothing.
1623    pub fn font_weight(mut self, weight: u16) -> Self {
1624        self.font_weight = Some(weight);
1625        self
1626    }
1627
1628    /// The rung of the type ladder the label is set in: its size, weight and line height.
1629    /// `font_size` and `font_weight` win over the role's values. A button that never calls
1630    /// this sends nothing and keeps the design system's label style for its variant.
1631    pub fn type_role(mut self, role: TypeRole) -> Self {
1632        self.type_role = Some(role);
1633        self
1634    }
1635
1636    /// The meaning of the glyph on it, never a picture.
1637    pub fn icon(mut self, icon: IconRole) -> Self {
1638        self.icon = Some(icon);
1639        self
1640    }
1641
1642    pub fn enabled(mut self, enabled: bool) -> Self {
1643        self.enabled = enabled;
1644        self
1645    }
1646
1647    pub fn variant(mut self, variant: ButtonVariant) -> Self {
1648        self.variant = Some(variant);
1649        self
1650    }
1651
1652    /// What the button is, apart from how loud it is: `ButtonKind::ActionKey` is one key of
1653    /// a dense grid, drawn in the design system's own calculator-key shape.
1654    pub fn kind(mut self, kind: ButtonKind) -> Self {
1655        self.kind = Some(kind);
1656        self
1657    }
1658
1659    /// How many lines the label may take. A label is one line unless this says more, so a
1660    /// Korean label is never broken between syllables by a squeezed parent.
1661    pub fn max_lines(mut self, lines: u32) -> Self {
1662        self.max_lines = Some(lines);
1663        self
1664    }
1665
1666    /// What happens to a label longer than its lines.
1667    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
1668        self.overflow = Some(overflow);
1669        self
1670    }
1671
1672    /// The label's colour, for the rare button whose meaning is not its variant's.
1673    pub fn color(mut self, paint: Paint) -> Self {
1674        self.color = Some(paint);
1675        self
1676    }
1677
1678    /// The same, when there may be no colour of its own.
1679    pub fn color_if(mut self, paint: Option<Paint>) -> Self {
1680        self.color = paint;
1681        self
1682    }
1683
1684    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
1685        self.on_click = Some(std::boxed::Box::new(handler));
1686        self
1687    }
1688
1689    /// Binds `handle` to this button, so `handle.request()` puts keyboard focus here.
1690    pub fn focus_handle(mut self, handle: &super::FocusHandle) -> Self {
1691        self.focus_handle = Some(handle.clone());
1692        self
1693    }
1694
1695    fn compose(&mut self, content: impl FnOnce()) {
1696        let modifier = self.modifier;
1697        let text = std::mem::take(&mut self.text);
1698        let (icon, enabled, variant, kind, color) =
1699            (self.icon, self.enabled, self.variant, self.kind, self.color);
1700        let (max_lines, overflow) = (self.max_lines, self.overflow);
1701        let accessible_name = self.accessible_name.take();
1702        let (text_align, font_size) = (self.text_align, self.font_size);
1703        let (font_weight, type_role) = (self.font_weight, self.type_role);
1704        let listeners = vec![("onclick", unit(self.on_click.take()))];
1705        super::focus::bind_focus_handle(self.focus_handle.take());
1706        emit(
1707            widget_key("compose_rust::ui::Button"),
1708            WidgetKind::Button,
1709            |attrs| {
1710                push_modifiers(&modifier, Mods::Standard, attrs);
1711                attrs.push(("text", OwnedAttr::Text(text)));
1712                attrs.push(("icon", super::role(icon)));
1713                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1714                attrs.push(("variant", role_or_zero(variant)));
1715                attrs.push(("button_kind", role_or_zero(kind)));
1716                attrs.push(("color", super::paint(color)));
1717                lines_and_overflow(max_lines, overflow, attrs);
1718                push_accessible_name(accessible_name, attrs);
1719                if text_align.is_some() {
1720                    attrs.push(("text_align", role_or_zero(text_align)));
1721                }
1722                if font_size.is_some() {
1723                    attrs.push(("font_size", dp_or_zero(font_size)));
1724                }
1725                if let Some(font_weight) = font_weight {
1726                    attrs.push(("font_weight", OwnedAttr::Int(i64::from(font_weight))));
1727                }
1728                if type_role.is_some() {
1729                    attrs.push(("type_role", super::role(type_role)));
1730                }
1731            },
1732            listeners,
1733            content,
1734        );
1735    }
1736}
1737
1738container!(ButtonScope);
1739
1740/// A two-state control, for Checkbox, RadioButton and Switch alike: the Host owns the
1741/// value, the control draws exactly what it is told and reports what the user asked for.
1742macro_rules! toggle {
1743    ($name:ident, $widget:ident, $setter:ident, $key:literal, $doc:literal) => {
1744        #[doc = concat!("A `", stringify!($widget), "` and its builder.")]
1745        pub struct $name {
1746            modifier: Modifier,
1747            checked: bool,
1748            enabled: bool,
1749            accessible_name: Option<String>,
1750            look: ControlLook,
1751            on_change: Option<Box<dyn FnMut(bool)>>,
1752            done: bool,
1753        }
1754
1755        #[doc = $doc]
1756        pub fn $widget($setter: bool) -> $name {
1757            $name {
1758                modifier: Modifier::new(),
1759                checked: $setter,
1760                enabled: true,
1761                accessible_name: None,
1762                look: ControlLook::default(),
1763                on_change: None,
1764                done: false,
1765            }
1766        }
1767
1768        accessible_name!($name);
1769
1770        impl $name {
1771            pub fn enabled(mut self, enabled: bool) -> Self {
1772                self.enabled = enabled;
1773                self
1774            }
1775
1776            /// Called with the state the user asked for. The control changes only when the
1777            /// value passed in does.
1778            pub fn on_change(mut self, handler: impl FnMut(bool) + 'static) -> Self {
1779                self.on_change = Some(std::boxed::Box::new(handler));
1780                self
1781            }
1782
1783            fn compose(&mut self, content: impl FnOnce()) {
1784                let modifier = self.modifier;
1785                let (checked, enabled) = (self.checked, self.enabled);
1786                let accessible_name = self.accessible_name.take();
1787                let look = self.look;
1788                let handler: EventCallback = match self.on_change.take() {
1789                    Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(
1790                        // Off is 0.0 and on is anything else, so a Renderer that reports a
1791                        // half-finished transition still reads as on.
1792                        move |value: f64| handler(value != 0.0),
1793                    ))),
1794                    None => value_handler(None),
1795                };
1796                emit(
1797                    widget_key($key),
1798                    WidgetKind::$widget,
1799                    |attrs| {
1800                        push_modifiers(&modifier, Mods::Standard, attrs);
1801                        attrs.push(("checked", OwnedAttr::Bool(checked)));
1802                        attrs.push(("enabled", OwnedAttr::Bool(enabled)));
1803                        push_accessible_name(accessible_name, attrs);
1804                        look.push(attrs);
1805                    },
1806                    vec![("onchange", handler)],
1807                    content,
1808                );
1809            }
1810        }
1811
1812        composed_on_drop!($name);
1813    };
1814}
1815
1816control_colour!(
1817    SwitchScope,
1818    track,
1819    track_color,
1820    "The track, whichever state it is in. The Host owns the state, so it passes the colour \
1821     that goes with it."
1822);
1823control_colour!(
1824    SwitchScope,
1825    thumb,
1826    thumb_color,
1827    "The thumb that travels along the track."
1828);
1829
1830impl SwitchScope {
1831    /// The track's width and height in dp. The thumb scales with the height.
1832    pub fn size(mut self, width: f32, height: f32) -> Self {
1833        self.look.size = Some((width, height));
1834        self
1835    }
1836}
1837
1838control_colour!(
1839    CheckboxScope,
1840    container,
1841    container_color,
1842    "The box when it is checked."
1843);
1844control_colour!(CheckboxScope, check, check_color, "The check mark.");
1845
1846impl CheckboxScope {
1847    /// The length of the box's side in dp.
1848    pub fn size(mut self, side: f32) -> Self {
1849        self.look.size = Some((side, side));
1850        self
1851    }
1852}
1853
1854control_colour!(
1855    RadioButtonScope,
1856    container,
1857    ring_color,
1858    "The ring around the button, selected or not. Where the design system fills a selected \
1859     button, the fill takes this colour as well."
1860);
1861control_colour!(
1862    RadioButtonScope,
1863    check,
1864    dot_color,
1865    "The dot inside a selected button."
1866);
1867
1868impl RadioButtonScope {
1869    /// The diameter of the ring in dp. The dot scales with it.
1870    pub fn size(mut self, side: f32) -> Self {
1871        self.look.size = Some((side, side));
1872        self
1873    }
1874}
1875
1876toggle!(
1877    CheckboxScope,
1878    Checkbox,
1879    checked,
1880    "compose_rust::ui::Checkbox",
1881    "A box that is ticked or not. Compose's `Checkbox(checked, onCheckedChange)`."
1882);
1883toggle!(
1884    RadioButtonScope,
1885    RadioButton,
1886    selected,
1887    "compose_rust::ui::RadioButton",
1888    "One choice out of several. Compose's `RadioButton(selected, onClick)`."
1889);
1890toggle!(
1891    SwitchScope,
1892    Switch,
1893    checked,
1894    "compose_rust::ui::Switch",
1895    "An on and off control. Compose's `Switch(checked, onCheckedChange)`."
1896);
1897
1898/// A `Slider` and its builder.
1899pub struct SliderScope {
1900    modifier: Modifier,
1901    color: Option<Paint>,
1902    value: f32,
1903    min: f32,
1904    max: f32,
1905    steps: u32,
1906    enabled: bool,
1907    accessible_name: Option<String>,
1908    look: ControlLook,
1909    thumb_size: Option<f32>,
1910    on_change: Option<Box<dyn FnMut(f32)>>,
1911    done: bool,
1912}
1913
1914/// A value picked from a range. Compose's `Slider`. The drag is the Renderer's; `value`
1915/// seeds it and moves it when the change came from elsewhere.
1916pub fn Slider(value: f32) -> SliderScope {
1917    SliderScope {
1918        modifier: Modifier::new(),
1919        color: None,
1920        value,
1921        min: 0.0,
1922        max: 1.0,
1923        steps: 0,
1924        enabled: true,
1925        accessible_name: None,
1926        look: ControlLook::default(),
1927        thumb_size: None,
1928        on_change: None,
1929        done: false,
1930    }
1931}
1932
1933accessible_name!(SliderScope);
1934control_colour!(
1935    SliderScope,
1936    track,
1937    track_color,
1938    "The part of the track that is not filled."
1939);
1940control_colour!(SliderScope, thumb, thumb_color, "The thumb.");
1941control_colour!(
1942    SliderScope,
1943    active_track,
1944    active_track_color,
1945    "The part of the track up to the thumb."
1946);
1947
1948impl SliderScope {
1949    /// The two ends. Compose's `valueRange`.
1950    pub fn range(mut self, min: f32, max: f32) -> Self {
1951        self.min = min;
1952        self.max = max;
1953        self
1954    }
1955
1956    /// Stops between the ends; zero is continuous.
1957    pub fn steps(mut self, steps: u32) -> Self {
1958        self.steps = steps;
1959        self
1960    }
1961
1962    pub fn enabled(mut self, enabled: bool) -> Self {
1963        self.enabled = enabled;
1964        self
1965    }
1966
1967    /// The filled part of the track and the thumb.
1968    pub fn color(mut self, paint: Paint) -> Self {
1969        self.color = Some(paint);
1970        self
1971    }
1972
1973    /// The thumb's diameter in dp. Without it the design system picks the size. The track
1974    /// and the area that takes a press do not change with it, so a small thumb is still
1975    /// easy to grab.
1976    pub fn thumb_size(mut self, dp: f32) -> Self {
1977        self.thumb_size = Some(dp);
1978        self
1979    }
1980
1981    pub fn on_change(mut self, handler: impl FnMut(f32) + 'static) -> Self {
1982        self.on_change = Some(std::boxed::Box::new(handler));
1983        self
1984    }
1985
1986    fn compose(&mut self, content: impl FnOnce()) {
1987        let modifier = self.modifier;
1988        let thumb_size = self.thumb_size;
1989        let (color, value, min, max, steps, enabled) = (
1990            self.color,
1991            self.value,
1992            self.min,
1993            self.max,
1994            self.steps,
1995            self.enabled,
1996        );
1997        let accessible_name = self.accessible_name.take();
1998        let look = self.look;
1999        let handler = match self.on_change.take() {
2000            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
2001                handler(value as f32)
2002            }))),
2003            None => value_handler(None),
2004        };
2005        emit(
2006            widget_key("compose_rust::ui::Slider"),
2007            WidgetKind::Slider,
2008            |attrs| {
2009                push_modifiers(&modifier, Mods::Standard, attrs);
2010                attrs.push(("color", super::paint(color)));
2011                attrs.push(("value", OwnedAttr::Float(f64::from(value))));
2012                attrs.push(("min", OwnedAttr::Float(f64::from(min))));
2013                attrs.push(("max", OwnedAttr::Float(f64::from(max))));
2014                attrs.push(("steps", OwnedAttr::Int(i64::from(steps))));
2015                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
2016                push_accessible_name(accessible_name, attrs);
2017                look.push(attrs);
2018                if let Some(dp) = thumb_size {
2019                    attrs.push(("thumb_size", OwnedAttr::Float(f64::from(dp))));
2020                }
2021            },
2022            vec![("onchange", handler)],
2023            content,
2024        );
2025    }
2026}
2027
2028composed_on_drop!(SliderScope);
2029
2030/// A `ProgressIndicator` and its builder.
2031pub struct ProgressIndicatorScope {
2032    modifier: Modifier,
2033    value: f32,
2034    determinate: bool,
2035    circular: bool,
2036    done: bool,
2037}
2038
2039/// How far along something is, as a fraction. Compose's `LinearProgressIndicator(progress)`.
2040pub fn ProgressIndicator(value: f32) -> ProgressIndicatorScope {
2041    ProgressIndicatorScope {
2042        modifier: Modifier::new(),
2043        value,
2044        determinate: true,
2045        circular: false,
2046        done: false,
2047    }
2048}
2049
2050/// Work in progress of no known length.
2051pub fn IndeterminateProgressIndicator() -> ProgressIndicatorScope {
2052    ProgressIndicator(0.0).determinate(false)
2053}
2054
2055impl ProgressIndicatorScope {
2056    pub fn determinate(mut self, determinate: bool) -> Self {
2057        self.determinate = determinate;
2058        self
2059    }
2060
2061    /// The circular form rather than the linear one.
2062    pub fn circular(mut self, circular: bool) -> Self {
2063        self.circular = circular;
2064        self
2065    }
2066
2067    fn compose(&mut self, content: impl FnOnce()) {
2068        let modifier = self.modifier;
2069        let (value, determinate, circular) = (self.value, self.determinate, self.circular);
2070        emit(
2071            widget_key("compose_rust::ui::ProgressIndicator"),
2072            WidgetKind::ProgressIndicator,
2073            |attrs| {
2074                push_modifiers(&modifier, Mods::Standard, attrs);
2075                attrs.push(("value", OwnedAttr::Float(f64::from(value))));
2076                attrs.push(("determinate", OwnedAttr::Bool(determinate)));
2077                attrs.push(("circular", OwnedAttr::Bool(circular)));
2078            },
2079            Vec::new(),
2080            content,
2081        );
2082    }
2083}
2084
2085composed_on_drop!(ProgressIndicatorScope);
2086
2087/// A `Divider` and its builder.
2088pub struct DividerScope {
2089    modifier: Modifier,
2090    vertical: bool,
2091    done: bool,
2092}
2093
2094/// A line between two things. Compose's `HorizontalDivider`; `vertical` makes it the other.
2095pub fn Divider() -> DividerScope {
2096    DividerScope {
2097        modifier: Modifier::new(),
2098        vertical: false,
2099        done: false,
2100    }
2101}
2102
2103/// A hairline between two rows of a grouped list, in the design system's quiet edge or
2104/// the given colour. The same node as `Divider().modifier(Modifier.background(color))`.
2105pub fn Separator(color: Option<Paint>) -> DividerScope {
2106    Divider().modifier(Modifier::new().background_if(color))
2107}
2108
2109impl DividerScope {
2110    pub fn vertical(mut self, vertical: bool) -> Self {
2111        self.vertical = vertical;
2112        self
2113    }
2114
2115    fn compose(&mut self, content: impl FnOnce()) {
2116        let modifier = self.modifier;
2117        let vertical = self.vertical;
2118        emit(
2119            widget_key("compose_rust::ui::Divider"),
2120            WidgetKind::Divider,
2121            |attrs| {
2122                push_modifiers(&modifier, Mods::Standard, attrs);
2123                attrs.push(("vertical", OwnedAttr::Bool(vertical)));
2124            },
2125            Vec::new(),
2126            content,
2127        );
2128    }
2129}
2130
2131composed_on_drop!(DividerScope);
2132
2133// ----- pictures -------------------------------------------------------------------------
2134
2135/// An `Image` or an `Icon` and its builder.
2136pub struct AssetScope {
2137    modifier: Modifier,
2138    widget: WidgetKind,
2139    asset_id: u32,
2140    color: Option<Paint>,
2141    content_scale: Option<ContentScale>,
2142    done: bool,
2143}
2144
2145/// A registered asset drawn as a picture. Compose's `Image`.
2146pub fn Image(asset_id: u32) -> AssetScope {
2147    AssetScope {
2148        modifier: Modifier::new(),
2149        widget: WidgetKind::Image,
2150        asset_id,
2151        color: None,
2152        content_scale: None,
2153        done: false,
2154    }
2155}
2156
2157/// A registered icon, tinted. Compose's `Icon`.
2158pub fn Icon(asset_id: u32) -> AssetScope {
2159    AssetScope {
2160        modifier: Modifier::new(),
2161        widget: WidgetKind::Icon,
2162        asset_id,
2163        color: None,
2164        content_scale: None,
2165        done: false,
2166    }
2167}
2168
2169impl AssetScope {
2170    /// The tint. An icon only; an image ignores it, as the rsx widget has no such property.
2171    pub fn color(mut self, paint: Paint) -> Self {
2172        self.color = Some(paint);
2173        self
2174    }
2175
2176    /// How the picture is laid into its box. An image only; an icon takes its design
2177    /// system's optical size instead.
2178    ///
2179    /// Not calling it draws `ContentScale::Fit`, the whole picture in its own proportions.
2180    /// `ContentScale::FillBounds` stretches it to the box, as CSS `object-fit: fill` does.
2181    pub fn content_scale(mut self, scale: ContentScale) -> Self {
2182        self.content_scale = Some(scale);
2183        self
2184    }
2185
2186    fn compose(&mut self, content: impl FnOnce()) {
2187        let modifier = self.modifier;
2188        let (widget, asset_id, color) = (self.widget, self.asset_id, self.color);
2189        let content_scale = self.content_scale;
2190        let key = if widget == WidgetKind::Icon {
2191            widget_key("compose_rust::ui::Icon")
2192        } else {
2193            widget_key("compose_rust::ui::Image")
2194        };
2195        emit(
2196            key,
2197            widget,
2198            |attrs| {
2199                push_modifiers(&modifier, Mods::Standard, attrs);
2200                attrs.push(("asset", OwnedAttr::Int(i64::from(asset_id))));
2201                if widget == WidgetKind::Icon {
2202                    attrs.push(("color", paint_or_zero(color)));
2203                }
2204                // A picture that never chose is not sent a choice, so it reaches the
2205                // Renderer exactly as it did before there was one.
2206                if let (WidgetKind::Image, Some(scale)) = (widget, content_scale) {
2207                    attrs.push(("content_scale", OwnedAttr::Int(i64::from(u16::from(scale)))));
2208                }
2209            },
2210            Vec::new(),
2211            content,
2212        );
2213    }
2214}
2215
2216composed_on_drop!(AssetScope);
2217
2218/// A `Canvas` and its builder.
2219pub struct CanvasScope {
2220    modifier: Modifier,
2221    commands: DrawList,
2222    view_box: Option<(f32, f32)>,
2223    done: bool,
2224}
2225
2226/// Pixels the widget vocabulary cannot produce. Compose's `Canvas`, with the drawing as a
2227/// value rather than a callback: drawing code cannot cross into the Renderer, and an equal
2228/// list costs no record.
2229pub fn Canvas(commands: DrawList) -> CanvasScope {
2230    let view_box = commands.view_box();
2231    CanvasScope {
2232        modifier: Modifier::new(),
2233        commands,
2234        view_box,
2235        done: false,
2236    }
2237}
2238
2239impl CanvasScope {
2240    /// Scales the command list's coordinates from `(width, height)` to the `Canvas`'s
2241    /// measured width and height. Stroke widths stay in dp.
2242    pub fn view_box(mut self, width: f32, height: f32) -> Self {
2243        self.view_box = Some((width, height));
2244        self
2245    }
2246
2247    /// Scales the command list's coordinates as `0.0..=1.0` fractions of the `Canvas`'s
2248    /// measured width and height. Stroke widths stay in dp.
2249    pub fn fractional(self) -> Self {
2250        self.view_box(1.0, 1.0)
2251    }
2252
2253    fn compose(&mut self, content: impl FnOnce()) {
2254        let modifier = self.modifier;
2255        let commands = std::mem::take(&mut self.commands);
2256        let view_box = self.view_box;
2257        emit(
2258            widget_key("compose_rust::ui::Canvas"),
2259            WidgetKind::Canvas,
2260            |attrs| {
2261                push_modifiers(&modifier, Mods::Standard, attrs);
2262                attrs.push(("commands", OwnedAttr::Draw(commands)));
2263                if let Some((width, height)) = view_box {
2264                    attrs.push(("min", OwnedAttr::Float(f64::from(width))));
2265                    attrs.push(("max", OwnedAttr::Float(f64::from(height))));
2266                }
2267            },
2268            Vec::new(),
2269            content,
2270        );
2271    }
2272}
2273
2274composed_on_drop!(CanvasScope);
2275
2276// ----- pickers ----------------------------------------------------------------------------
2277
2278/// A `DatePicker` and its builder.
2279pub struct DatePickerScope {
2280    modifier: Modifier,
2281    value: i64,
2282    min: Option<i64>,
2283    max: Option<i64>,
2284    enabled: bool,
2285    on_change: Option<Box<dyn FnMut(i64)>>,
2286    done: bool,
2287}
2288
2289/// A date, as whole days since 1970-01-01. How it is picked is the design system's.
2290pub fn DatePicker(value: i64) -> DatePickerScope {
2291    DatePickerScope {
2292        modifier: Modifier::new(),
2293        value,
2294        min: None,
2295        max: None,
2296        enabled: true,
2297        on_change: None,
2298        done: false,
2299    }
2300}
2301
2302impl DatePickerScope {
2303    pub fn min(mut self, days: i64) -> Self {
2304        self.min = Some(days);
2305        self
2306    }
2307
2308    pub fn max(mut self, days: i64) -> Self {
2309        self.max = Some(days);
2310        self
2311    }
2312
2313    pub fn enabled(mut self, enabled: bool) -> Self {
2314        self.enabled = enabled;
2315        self
2316    }
2317
2318    pub fn on_change(mut self, handler: impl FnMut(i64) + 'static) -> Self {
2319        self.on_change = Some(std::boxed::Box::new(handler));
2320        self
2321    }
2322
2323    fn compose(&mut self, content: impl FnOnce()) {
2324        let modifier = self.modifier;
2325        let (value, min, max, enabled) = (self.value, self.min, self.max, self.enabled);
2326        let handler = match self.on_change.take() {
2327            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
2328                handler(value as i64)
2329            }))),
2330            None => value_handler(None),
2331        };
2332        emit(
2333            widget_key("compose_rust::ui::DatePicker"),
2334            WidgetKind::DatePicker,
2335            |attrs| {
2336                push_modifiers(&modifier, Mods::Standard, attrs);
2337                attrs.push(("value", OwnedAttr::Int(value)));
2338                attrs.push(("min", OwnedAttr::Int(min.unwrap_or(i64::MIN))));
2339                attrs.push(("max", OwnedAttr::Int(max.unwrap_or(i64::MAX))));
2340                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
2341            },
2342            vec![("onchange", handler)],
2343            content,
2344        );
2345    }
2346}
2347
2348composed_on_drop!(DatePickerScope);
2349
2350const MINUTES_IN_A_DAY: u32 = 24 * 60;
2351
2352/// A `TimePicker` and its builder.
2353pub struct TimePickerScope {
2354    modifier: Modifier,
2355    value: u32,
2356    min: Option<u32>,
2357    max: Option<u32>,
2358    enabled: bool,
2359    on_change: Option<Box<dyn FnMut(u32)>>,
2360    done: bool,
2361}
2362
2363/// A time of day, as minutes since midnight.
2364pub fn TimePicker(value: u32) -> TimePickerScope {
2365    TimePickerScope {
2366        modifier: Modifier::new(),
2367        value,
2368        min: None,
2369        max: None,
2370        enabled: true,
2371        on_change: None,
2372        done: false,
2373    }
2374}
2375
2376impl TimePickerScope {
2377    pub fn min(mut self, minutes: u32) -> Self {
2378        self.min = Some(minutes);
2379        self
2380    }
2381
2382    pub fn max(mut self, minutes: u32) -> Self {
2383        self.max = Some(minutes);
2384        self
2385    }
2386
2387    pub fn enabled(mut self, enabled: bool) -> Self {
2388        self.enabled = enabled;
2389        self
2390    }
2391
2392    pub fn on_change(mut self, handler: impl FnMut(u32) + 'static) -> Self {
2393        self.on_change = Some(std::boxed::Box::new(handler));
2394        self
2395    }
2396
2397    fn compose(&mut self, content: impl FnOnce()) {
2398        let modifier = self.modifier;
2399        let (value, min, max, enabled) = (self.value, self.min, self.max, self.enabled);
2400        let handler = match self.on_change.take() {
2401            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
2402                handler((value as i64).clamp(0, i64::from(MINUTES_IN_A_DAY - 1)) as u32)
2403            }))),
2404            None => value_handler(None),
2405        };
2406        emit(
2407            widget_key("compose_rust::ui::TimePicker"),
2408            WidgetKind::TimePicker,
2409            |attrs| {
2410                push_modifiers(&modifier, Mods::Standard, attrs);
2411                attrs.push(("value", OwnedAttr::Int(i64::from(value))));
2412                attrs.push(("min", OwnedAttr::Int(i64::from(min.unwrap_or(0)))));
2413                attrs.push((
2414                    "max",
2415                    OwnedAttr::Int(i64::from(max.unwrap_or(MINUTES_IN_A_DAY - 1))),
2416                ));
2417                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
2418            },
2419            vec![("onchange", handler)],
2420            content,
2421        );
2422    }
2423}
2424
2425composed_on_drop!(TimePickerScope);
2426
2427/// A `Dropdown` and its builder.
2428pub struct DropdownScope {
2429    modifier: Modifier,
2430    selected_index: usize,
2431    enabled: bool,
2432    closed: bool,
2433    bare: bool,
2434    on_change: Option<Box<dyn FnMut(usize)>>,
2435    done: bool,
2436}
2437
2438/// One choice out of a list; each child is one option.
2439pub fn Dropdown(selected_index: usize) -> DropdownScope {
2440    DropdownScope {
2441        modifier: Modifier::new(),
2442        selected_index,
2443        enabled: true,
2444        closed: false,
2445        bare: false,
2446        on_change: None,
2447        done: false,
2448    }
2449}
2450
2451/// A compact closed-field selector that always rests as a single line and opens its
2452/// options in a popup below the field on every design system.
2453pub fn Select(selected_index: usize) -> DropdownScope {
2454    Dropdown(selected_index).closed(true)
2455}
2456
2457impl DropdownScope {
2458    pub fn enabled(mut self, enabled: bool) -> Self {
2459        self.enabled = enabled;
2460        self
2461    }
2462
2463    /// Keeps the control as a closed single-line field that opens a popup menu on click,
2464    /// even on design systems whose default picker presentation is a wheel.
2465    pub fn closed(mut self, closed: bool) -> Self {
2466        self.closed = closed;
2467        self
2468    }
2469
2470    pub fn on_change(mut self, handler: impl FnMut(usize) + 'static) -> Self {
2471        self.on_change = Some(std::boxed::Box::new(handler));
2472        self
2473    }
2474
2475    /// Draws the closed field without the design system's frame: no fill, line, corner
2476    /// clip, inner padding, minimum height or focus line. For a select that sits inside a
2477    /// box which already draws its own frame. A `background` or `border` given on the node
2478    /// is still drawn.
2479    pub fn bare(mut self) -> Self {
2480        self.bare = true;
2481        self
2482    }
2483
2484    fn compose(&mut self, content: impl FnOnce()) {
2485        let modifier = self.modifier;
2486        let (selected_index, enabled, closed) = (self.selected_index, self.enabled, self.closed);
2487        let bare = self.bare;
2488        let handler = match self.on_change.take() {
2489            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
2490                handler((value as i64).max(0) as usize)
2491            }))),
2492            None => value_handler(None),
2493        };
2494        emit(
2495            widget_key("compose_rust::ui::Dropdown"),
2496            WidgetKind::Dropdown,
2497            |attrs| {
2498                push_modifiers(&modifier, Mods::Standard, attrs);
2499                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
2500                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
2501                if closed {
2502                    attrs.push(("collapsible", OwnedAttr::Bool(true)));
2503                }
2504                if bare {
2505                    attrs.push(("bare", OwnedAttr::Bool(true)));
2506                }
2507            },
2508            vec![("onchange", handler)],
2509            content,
2510        );
2511    }
2512}
2513
2514container!(DropdownScope);
2515
2516/// A `MenuItem` and its builder.
2517pub struct MenuItemScope {
2518    modifier: Modifier,
2519    text: String,
2520    icon: Option<IconRole>,
2521    selected: bool,
2522    enabled: bool,
2523    color: Option<Paint>,
2524    on_click: Option<Box<dyn FnMut()>>,
2525    done: bool,
2526}
2527
2528/// A single row in a `Menu` or action list: left-aligned label, optional leading icon,
2529/// and optional selected state.
2530pub fn MenuItem(text: impl Into<String>) -> MenuItemScope {
2531    MenuItemScope {
2532        modifier: Modifier::new(),
2533        text: text.into(),
2534        icon: None,
2535        selected: false,
2536        enabled: true,
2537        color: None,
2538        on_click: None,
2539        done: false,
2540    }
2541}
2542
2543impl MenuItemScope {
2544    /// Leading semantic icon for this menu row.
2545    pub fn icon(mut self, icon: IconRole) -> Self {
2546        self.icon = Some(icon);
2547        self
2548    }
2549
2550    /// Marks this row as the currently selected option.
2551    pub fn selected(mut self, selected: bool) -> Self {
2552        self.selected = selected;
2553        self
2554    }
2555
2556    pub fn enabled(mut self, enabled: bool) -> Self {
2557        self.enabled = enabled;
2558        self
2559    }
2560
2561    /// Overrides the row's label and icon colour (for example, a destructive action).
2562    pub fn color(mut self, paint: Paint) -> Self {
2563        self.color = Some(paint);
2564        self
2565    }
2566
2567    /// Conditional colour override.
2568    pub fn color_if(mut self, paint: Option<Paint>) -> Self {
2569        self.color = paint;
2570        self
2571    }
2572
2573    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
2574        self.on_click = Some(std::boxed::Box::new(handler));
2575        self
2576    }
2577
2578    fn compose(&mut self, content: impl FnOnce()) {
2579        let modifier = self.modifier;
2580        let text = std::mem::take(&mut self.text);
2581        let (icon, selected, enabled, color) = (self.icon, self.selected, self.enabled, self.color);
2582        let listeners = vec![("onclick", unit(self.on_click.take()))];
2583        emit(
2584            widget_key("compose_rust::ui::MenuItem"),
2585            WidgetKind::Button,
2586            |attrs| {
2587                push_modifiers(&modifier, Mods::Standard, attrs);
2588                attrs.push(("text", OwnedAttr::Text(text)));
2589                attrs.push(("icon", super::role(icon)));
2590                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
2591                attrs.push(("variant", role_or_zero(Some(ButtonVariant::Text))));
2592                attrs.push(("text_align", role_or_zero(Some(TextAlign::Start))));
2593                attrs.push(("checked", OwnedAttr::Bool(selected)));
2594                attrs.push(("color", super::paint(color)));
2595            },
2596            listeners,
2597            content,
2598        );
2599    }
2600}
2601
2602composed_on_drop!(MenuItemScope);
2603
2604// ----- surfaces that come and go ----------------------------------------------------------
2605
2606/// A `Dialog` or a `Sheet` and its builder: `open` seeds the Renderer's own open state,
2607/// and `on_dismiss` says once that the user asked to close it.
2608pub struct ModalScope {
2609    modifier: Modifier,
2610    widget: WidgetKind,
2611    open: bool,
2612    scrim: Option<Paint>,
2613    max_height_fraction: Option<f32>,
2614    on_dismiss: Option<Box<dyn FnMut()>>,
2615    done: bool,
2616}
2617
2618/// A modal. Compose's `Dialog`, kept in the tree and opened by `open`, so its enter and
2619/// exit belong to the Renderer.
2620pub fn Dialog(open: bool) -> ModalScope {
2621    ModalScope {
2622        modifier: Modifier::new(),
2623        widget: WidgetKind::Dialog,
2624        open,
2625        scrim: None,
2626        max_height_fraction: None,
2627        on_dismiss: None,
2628        done: false,
2629    }
2630}
2631
2632/// A surface that slides in from the edge the Renderer chooses. Compose's
2633/// `ModalBottomSheet`, without a promise about which edge.
2634pub fn Sheet(open: bool) -> ModalScope {
2635    ModalScope {
2636        modifier: Modifier::new(),
2637        widget: WidgetKind::Sheet,
2638        open,
2639        scrim: None,
2640        max_height_fraction: None,
2641        on_dismiss: None,
2642        done: false,
2643    }
2644}
2645
2646impl ModalScope {
2647    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
2648        self.on_dismiss = Some(std::boxed::Box::new(handler));
2649        self
2650    }
2651
2652    /// Sets the modal scrim colour, overriding the design system default.
2653    pub fn scrim(mut self, paint: impl Into<Paint>) -> Self {
2654        self.scrim = Some(paint.into());
2655        self
2656    }
2657
2658    /// The tallest the sheet grows, as a fraction of the window's height, above 0 and at
2659    /// most 1.
2660    ///
2661    /// Below it the sheet is as tall as its content; content taller than that stops the
2662    /// sheet at the fraction and scrolls inside it. A fraction above 1 is taken as 1, and
2663    /// one that is not above 0 is not sent, which leaves the height to the design system
2664    /// as it is without this call. A `Dialog` sizes itself to its content and does not
2665    /// take it.
2666    pub fn max_height_fraction(mut self, fraction: f32) -> Self {
2667        self.max_height_fraction = (fraction > 0.0).then(|| fraction.min(1.0));
2668        self
2669    }
2670
2671    fn compose(&mut self, content: impl FnOnce()) {
2672        let modifier = self.modifier;
2673        let (widget, open) = (self.widget, self.open);
2674        let max_height_fraction = self
2675            .max_height_fraction
2676            .filter(|_| widget == WidgetKind::Sheet);
2677        let key = if widget == WidgetKind::Dialog {
2678            widget_key("compose_rust::ui::Dialog")
2679        } else {
2680            widget_key("compose_rust::ui::Sheet")
2681        };
2682        let scrim = self.scrim;
2683        emit(
2684            key,
2685            widget,
2686            |attrs| {
2687                push_modifiers(&modifier, Mods::Standard, attrs);
2688                attrs.push(("open", OwnedAttr::Bool(open)));
2689                if let Some(scrim) = scrim {
2690                    attrs.push(("scrim", super::paint(Some(scrim))));
2691                }
2692                if let Some(fraction) = max_height_fraction {
2693                    attrs.push(("max_height_fraction", OwnedAttr::Float(f64::from(fraction))));
2694                }
2695            },
2696            vec![("ondismiss", unit(self.on_dismiss.take()))],
2697            content,
2698        );
2699    }
2700}
2701
2702container!(ModalScope);
2703
2704/// A `Menu` and its builder.
2705pub struct MenuScope<'a> {
2706    modifier: Modifier,
2707    expanded: bool,
2708    scrim: Option<Paint>,
2709    on_dismiss: Option<Box<dyn FnMut()>>,
2710    anchor: Option<Box<dyn FnOnce() + 'a>>,
2711    done: bool,
2712}
2713
2714/// A popup hanging off its anchor. Compose's `DropdownMenu`, with the anchor said rather
2715/// than positioned: the Renderer places the popup.
2716pub fn Menu<'a>(expanded: bool) -> MenuScope<'a> {
2717    MenuScope {
2718        modifier: Modifier::new(),
2719        expanded,
2720        scrim: None,
2721        on_dismiss: None,
2722        anchor: None,
2723        done: false,
2724    }
2725}
2726
2727impl<'a> MenuScope<'a> {
2728    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
2729        self.on_dismiss = Some(std::boxed::Box::new(handler));
2730        self
2731    }
2732
2733    /// Sets the popup scrim colour, overriding the design system default.
2734    pub fn scrim(mut self, paint: impl Into<Paint>) -> Self {
2735        self.scrim = Some(paint.into());
2736        self
2737    }
2738
2739    /// The widget the menu hangs off. It is the first child on the wire.
2740    pub fn anchor(mut self, anchor: impl FnOnce() + 'a) -> Self {
2741        self.anchor = Some(std::boxed::Box::new(anchor));
2742        self
2743    }
2744
2745    fn compose(&mut self, content: impl FnOnce()) {
2746        let modifier = self.modifier;
2747        let expanded = self.expanded;
2748        let anchor = self.anchor.take();
2749        let scrim = self.scrim;
2750        emit(
2751            widget_key("compose_rust::ui::Menu"),
2752            WidgetKind::Menu,
2753            |attrs| {
2754                push_modifiers(&modifier, Mods::Standard, attrs);
2755                attrs.push(("open", OwnedAttr::Bool(expanded)));
2756                if let Some(scrim) = scrim {
2757                    attrs.push(("scrim", super::paint(Some(scrim))));
2758                }
2759            },
2760            vec![("ondismiss", unit(self.on_dismiss.take()))],
2761            || {
2762                if let Some(anchor) = anchor {
2763                    let _anchor =
2764                        crate::runtime::group_guard(widget_key("compose_rust::ui::Menu::anchor"));
2765                    anchor();
2766                }
2767                let _entries =
2768                    crate::runtime::group_guard(widget_key("compose_rust::ui::Menu::entries"));
2769                content();
2770            },
2771        );
2772    }
2773}
2774
2775container!(MenuScope<'a>);
2776
2777/// A `Popover` and its builder.
2778pub struct PopoverScope<'a> {
2779    modifier: Modifier,
2780    open: bool,
2781    placement: Option<PopoverPlacement>,
2782    align: Option<PopoverAlign>,
2783    offset: Option<f32>,
2784    scrim: bool,
2785    scrim_color: Option<Paint>,
2786    on_dismiss: Option<Box<dyn FnMut()>>,
2787    anchor: Option<Box<dyn FnOnce() + 'a>>,
2788    done: bool,
2789}
2790
2791/// A floating box of any content attached to its anchor.
2792///
2793/// The anchor is always drawn. The content is drawn only while `open` is true, and `open`
2794/// is the application's: pressing outside the box or pressing Escape calls `on_dismiss`
2795/// once and changes nothing, so the handler is where the application closes it or keeps
2796/// it open to ask about unsaved work.
2797///
2798/// The Renderer places the box. It opens on the side `placement` names, lined up with the
2799/// edge `align` names, and it moves to the opposite side or slides along the edge when it
2800/// would otherwise leave the window.
2801///
2802/// `modifier` belongs to the floating box, not to the anchor: a shape, a border, a shadow
2803/// or a material given there replaces the design system's for this box.
2804pub fn Popover<'a>(open: bool) -> PopoverScope<'a> {
2805    PopoverScope {
2806        modifier: Modifier::new(),
2807        open,
2808        placement: None,
2809        align: None,
2810        offset: None,
2811        scrim: false,
2812        scrim_color: None,
2813        on_dismiss: None,
2814        anchor: None,
2815        done: false,
2816    }
2817}
2818
2819impl<'a> PopoverScope<'a> {
2820    /// Called once when the user presses outside the box or presses Escape.
2821    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
2822        self.on_dismiss = Some(std::boxed::Box::new(handler));
2823        self
2824    }
2825
2826    /// The side of the anchor the box opens on. `Bottom` when it is not said.
2827    pub fn placement(mut self, placement: PopoverPlacement) -> Self {
2828        self.placement = Some(placement);
2829        self
2830    }
2831
2832    /// The edge of the anchor the box lines up with. `Start` when it is not said.
2833    pub fn align(mut self, align: PopoverAlign) -> Self {
2834        self.align = Some(align);
2835        self
2836    }
2837
2838    /// The gap between the anchor and the box, in dp. The design system's gap when it is
2839    /// not said.
2840    pub fn offset(mut self, dp: f32) -> Self {
2841        self.offset = Some(dp);
2842        self
2843    }
2844
2845    /// Whether the window behind the box is dimmed while it is open. It is not, unless
2846    /// asked.
2847    pub fn scrim(mut self, scrim: bool) -> Self {
2848        self.scrim = scrim;
2849        self
2850    }
2851
2852    /// Dims the window behind the box with this colour instead of the design system's.
2853    pub fn scrim_color(mut self, paint: impl Into<Paint>) -> Self {
2854        self.scrim_color = Some(paint.into());
2855        self
2856    }
2857
2858    /// The widget the box is attached to. It is the first child on the wire.
2859    pub fn anchor(mut self, anchor: impl FnOnce() + 'a) -> Self {
2860        self.anchor = Some(std::boxed::Box::new(anchor));
2861        self
2862    }
2863
2864    fn compose(&mut self, content: impl FnOnce()) {
2865        let modifier = self.modifier;
2866        let open = self.open;
2867        let (placement, align, offset) = (self.placement, self.align, self.offset);
2868        let (scrim, scrim_color) = (self.scrim, self.scrim_color);
2869        let anchor = self.anchor.take();
2870        emit(
2871            widget_key("compose_rust::ui::Popover"),
2872            WidgetKind::Popover,
2873            |attrs| {
2874                push_modifiers(&modifier, Mods::Standard, attrs);
2875                attrs.push(("open", OwnedAttr::Bool(open)));
2876                if let Some(placement) = placement {
2877                    attrs.push(("placement", role_or_zero(Some(placement))));
2878                }
2879                if let Some(align) = align {
2880                    attrs.push(("align", role_or_zero(Some(align))));
2881                }
2882                if let Some(offset) = offset {
2883                    attrs.push(("offset_dp", OwnedAttr::Float(f64::from(offset))));
2884                }
2885                if let Some(color) = scrim_color {
2886                    attrs.push(("scrim", super::paint(Some(color))));
2887                } else if scrim {
2888                    attrs.push(("scrim", OwnedAttr::Bool(true)));
2889                }
2890            },
2891            vec![("ondismiss", unit(self.on_dismiss.take()))],
2892            || {
2893                if let Some(anchor) = anchor {
2894                    let _anchor = crate::runtime::group_guard(widget_key(
2895                        "compose_rust::ui::Popover::anchor",
2896                    ));
2897                    anchor();
2898                }
2899                let _content =
2900                    crate::runtime::group_guard(widget_key("compose_rust::ui::Popover::content"));
2901                content();
2902            },
2903        );
2904    }
2905}
2906
2907container!(PopoverScope<'a>);
2908
2909/// A `Tabs` and its builder.
2910pub struct TabsScope {
2911    modifier: Modifier,
2912    color: Option<Paint>,
2913    selected_index: usize,
2914    done: bool,
2915}
2916
2917/// A row of tabs; each child is one tab, and a tap is that child's own click. Compose's
2918/// `TabRow`.
2919pub fn Tabs(selected_index: usize) -> TabsScope {
2920    TabsScope {
2921        modifier: Modifier::new(),
2922        color: None,
2923        selected_index,
2924        done: false,
2925    }
2926}
2927
2928impl TabsScope {
2929    /// The mark that shows which tab is selected.
2930    pub fn color(mut self, paint: Paint) -> Self {
2931        self.color = Some(paint);
2932        self
2933    }
2934
2935    fn compose(&mut self, content: impl FnOnce()) {
2936        let modifier = self.modifier;
2937        let (color, selected_index) = (self.color, self.selected_index);
2938        emit(
2939            widget_key("compose_rust::ui::Tabs"),
2940            WidgetKind::Tabs,
2941            |attrs| {
2942                push_modifiers(&modifier, Mods::Standard, attrs);
2943                attrs.push(("color", super::paint(color)));
2944                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
2945            },
2946            Vec::new(),
2947            content,
2948        );
2949    }
2950}
2951
2952container!(TabsScope);
2953
2954/// A `TopAppBar` and its builder.
2955pub struct TopAppBarScope {
2956    modifier: Modifier,
2957    title: String,
2958    done: bool,
2959}
2960
2961/// The bar across the top of a screen; its children are its content. Compose's
2962/// `TopAppBar`, with the title a property so the Renderer can find it.
2963pub fn TopAppBar() -> TopAppBarScope {
2964    TopAppBarScope {
2965        modifier: Modifier::new(),
2966        title: String::new(),
2967        done: false,
2968    }
2969}
2970
2971impl TopAppBarScope {
2972    pub fn title(mut self, title: impl Into<String>) -> Self {
2973        self.title = title.into();
2974        self
2975    }
2976
2977    fn compose(&mut self, content: impl FnOnce()) {
2978        let modifier = self.modifier;
2979        let title = std::mem::take(&mut self.title);
2980        emit(
2981            widget_key("compose_rust::ui::TopAppBar"),
2982            WidgetKind::TopAppBar,
2983            |attrs| {
2984                push_modifiers(&modifier, Mods::Standard, attrs);
2985                attrs.push(("text", OwnedAttr::Text(title)));
2986            },
2987            Vec::new(),
2988            content,
2989        );
2990    }
2991}
2992
2993container!(TopAppBarScope);
2994
2995/// A `Tooltip` and its builder.
2996pub struct TooltipScope {
2997    modifier: Modifier,
2998    text: String,
2999    done: bool,
3000}
3001
3002/// An explanation attached to its child, which assistive technology reads too. Compose's
3003/// `TooltipBox`.
3004pub fn Tooltip(text: impl Into<String>) -> TooltipScope {
3005    TooltipScope {
3006        modifier: Modifier::new(),
3007        text: text.into(),
3008        done: false,
3009    }
3010}
3011
3012impl TooltipScope {
3013    fn compose(&mut self, content: impl FnOnce()) {
3014        let modifier = self.modifier;
3015        let text = std::mem::take(&mut self.text);
3016        emit(
3017            widget_key("compose_rust::ui::Tooltip"),
3018            WidgetKind::Tooltip,
3019            |attrs| {
3020                push_modifiers(&modifier, Mods::Standard, attrs);
3021                attrs.push(("text", OwnedAttr::Text(text)));
3022            },
3023            Vec::new(),
3024            content,
3025        );
3026    }
3027}
3028
3029container!(TooltipScope);
3030
3031// ----- frames and navigation --------------------------------------------------------------
3032
3033/// Composes one slot of a frame: a ScaffoldSlot node holding `content`, in a group of its
3034/// own so that a slot coming and going leaves the others where they were.
3035fn slot(role: SlotRole, key: u64, content: impl FnOnce()) {
3036    emit(
3037        key,
3038        WidgetKind::ScaffoldSlot,
3039        |attrs| attrs.push(("slot", OwnedAttr::Int(i64::from(u16::from(role))))),
3040        Vec::new(),
3041        content,
3042    );
3043}
3044
3045type Slot<'a> = Option<Box<dyn FnOnce() + 'a>>;
3046
3047/// A `Scaffold` and its builder.
3048pub struct ScaffoldScope<'a> {
3049    modifier: Modifier,
3050    top_bar: Slot<'a>,
3051    bottom_bar: Slot<'a>,
3052    floating_action: Slot<'a>,
3053    done: bool,
3054}
3055
3056/// The screen's frame: which slots it has filled, and the page. Compose's `Scaffold`.
3057/// What each slot becomes on this platform at this width is the Renderer's decision.
3058///
3059/// The Modifier carries the two things a frame takes: `background` and `material`.
3060pub fn Scaffold<'a>() -> ScaffoldScope<'a> {
3061    ScaffoldScope {
3062        modifier: Modifier::new(),
3063        top_bar: None,
3064        bottom_bar: None,
3065        floating_action: None,
3066        done: false,
3067    }
3068}
3069
3070impl<'a> ScaffoldScope<'a> {
3071    pub fn top_bar(mut self, bar: impl FnOnce() + 'a) -> Self {
3072        self.top_bar = Some(std::boxed::Box::new(bar));
3073        self
3074    }
3075
3076    pub fn bottom_bar(mut self, bar: impl FnOnce() + 'a) -> Self {
3077        self.bottom_bar = Some(std::boxed::Box::new(bar));
3078        self
3079    }
3080
3081    pub fn floating_action(mut self, action: impl FnOnce() + 'a) -> Self {
3082        self.floating_action = Some(std::boxed::Box::new(action));
3083        self
3084    }
3085
3086    fn compose(&mut self, content: impl FnOnce()) {
3087        let modifier = self.modifier;
3088        let (top_bar, bottom_bar, floating_action) = (
3089            self.top_bar.take(),
3090            self.bottom_bar.take(),
3091            self.floating_action.take(),
3092        );
3093        emit(
3094            widget_key("compose_rust::ui::Scaffold"),
3095            WidgetKind::Scaffold,
3096            |attrs| push_modifiers(&modifier, Mods::Scaffold, attrs),
3097            Vec::new(),
3098            || {
3099                if let Some(bar) = top_bar {
3100                    slot(
3101                        SlotRole::TopBar,
3102                        widget_key("compose_rust::ui::Scaffold::top_bar"),
3103                        bar,
3104                    );
3105                }
3106                if let Some(bar) = bottom_bar {
3107                    slot(
3108                        SlotRole::BottomBar,
3109                        widget_key("compose_rust::ui::Scaffold::bottom_bar"),
3110                        bar,
3111                    );
3112                }
3113                if let Some(action) = floating_action {
3114                    slot(
3115                        SlotRole::FloatingAction,
3116                        widget_key("compose_rust::ui::Scaffold::floating_action"),
3117                        action,
3118                    );
3119                }
3120                slot(
3121                    SlotRole::Content,
3122                    widget_key("compose_rust::ui::Scaffold::content"),
3123                    content,
3124                );
3125            },
3126        );
3127    }
3128}
3129
3130container!(ScaffoldScope<'a>);
3131
3132/// A `Navigation` and its builder.
3133pub struct NavigationScope<'a> {
3134    modifier: Modifier,
3135    selected_index: usize,
3136    compact: CompactNavigation,
3137    head: Slot<'a>,
3138    foot: Slot<'a>,
3139    done: bool,
3140}
3141
3142/// A set of destinations and the screen they lead to. The children that are
3143/// `NavigationItem`s are the destinations; whether they are a bar, a rail or a drawer is
3144/// the Renderer's choice. Compose's `NavigationSuiteScaffold`.
3145pub fn Navigation<'a>(selected_index: usize) -> NavigationScope<'a> {
3146    NavigationScope {
3147        modifier: Modifier::new(),
3148        selected_index,
3149        compact: CompactNavigation::Default,
3150        head: None,
3151        foot: None,
3152        done: false,
3153    }
3154}
3155
3156impl<'a> NavigationScope<'a> {
3157    /// What the destinations become in a compact window.
3158    ///
3159    /// Without it the design system answers, which is a bottom bar on most of them. An
3160    /// application whose own reference draws a compact window differently says so here:
3161    /// `Sidebar` puts the sidebar away behind a toggle on every design system, and `None`
3162    /// leaves the destinations off a compact window so the screen can reach them its own
3163    /// way. Medium and Expanded windows keep the design system's rail and drawer whatever
3164    /// this says, and the screen still never asks how wide the window is.
3165    pub fn compact(mut self, compact: CompactNavigation) -> Self {
3166        self.compact = compact;
3167        self
3168    }
3169
3170    /// What sits above the destinations in a strip.
3171    pub fn head(mut self, head: impl FnOnce() + 'a) -> Self {
3172        self.head = Some(std::boxed::Box::new(head));
3173        self
3174    }
3175
3176    /// What sits below them.
3177    pub fn foot(mut self, foot: impl FnOnce() + 'a) -> Self {
3178        self.foot = Some(std::boxed::Box::new(foot));
3179        self
3180    }
3181
3182    fn compose(&mut self, content: impl FnOnce()) {
3183        let modifier = self.modifier;
3184        let selected_index = self.selected_index;
3185        let compact = self.compact;
3186        let (head, foot) = (self.head.take(), self.foot.take());
3187        emit(
3188            widget_key("compose_rust::ui::Navigation"),
3189            WidgetKind::Navigation,
3190            |attrs| {
3191                push_modifiers(&modifier, Mods::Standard, attrs);
3192                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
3193                // No choice is the default, and the default is not sent: a navigation that
3194                // says nothing travels exactly as it did before there was a choice.
3195                if compact != CompactNavigation::Default {
3196                    attrs.push((
3197                        "compact_navigation",
3198                        OwnedAttr::Int(i64::from(u16::from(compact))),
3199                    ));
3200                }
3201            },
3202            Vec::new(),
3203            || {
3204                if let Some(head) = head {
3205                    slot(
3206                        SlotRole::TopBar,
3207                        widget_key("compose_rust::ui::Navigation::head"),
3208                        head,
3209                    );
3210                }
3211                if let Some(foot) = foot {
3212                    slot(
3213                        SlotRole::BottomBar,
3214                        widget_key("compose_rust::ui::Navigation::foot"),
3215                        foot,
3216                    );
3217                }
3218                let _destinations = crate::runtime::group_guard(widget_key(
3219                    "compose_rust::ui::Navigation::content",
3220                ));
3221                content();
3222            },
3223        );
3224    }
3225}
3226
3227container!(NavigationScope<'a>);
3228
3229/// A `NavigationItem` and its builder.
3230pub struct NavigationItemScope {
3231    modifier: Modifier,
3232    text: String,
3233    icon: Option<IconRole>,
3234    color: Option<Paint>,
3235    enabled: bool,
3236    section: Option<String>,
3237    on_click: Option<Box<dyn FnMut()>>,
3238    done: bool,
3239}
3240
3241/// One destination inside a `Navigation`. Compose's `NavigationSuiteItem`.
3242pub fn NavigationItem(text: impl Into<String>) -> NavigationItemScope {
3243    NavigationItemScope {
3244        modifier: Modifier::new(),
3245        text: text.into(),
3246        icon: None,
3247        color: None,
3248        enabled: true,
3249        section: None,
3250        on_click: None,
3251        done: false,
3252    }
3253}
3254
3255impl NavigationItemScope {
3256    pub fn icon(mut self, icon: IconRole) -> Self {
3257        self.icon = Some(icon);
3258        self
3259    }
3260
3261    pub fn color(mut self, paint: Paint) -> Self {
3262        self.color = Some(paint);
3263        self
3264    }
3265
3266    pub fn enabled(mut self, enabled: bool) -> Self {
3267        self.enabled = enabled;
3268        self
3269    }
3270
3271    /// The named group of the strip this destination belongs to.
3272    pub fn section(mut self, section: impl Into<String>) -> Self {
3273        self.section = Some(section.into());
3274        self
3275    }
3276
3277    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
3278        self.on_click = Some(std::boxed::Box::new(handler));
3279        self
3280    }
3281
3282    fn compose(&mut self, content: impl FnOnce()) {
3283        let modifier = self.modifier;
3284        let text = std::mem::take(&mut self.text);
3285        let section = self.section.take();
3286        let (icon, color, enabled) = (self.icon, self.color, self.enabled);
3287        emit(
3288            widget_key("compose_rust::ui::NavigationItem"),
3289            WidgetKind::NavigationItem,
3290            |attrs| {
3291                push_modifiers(&modifier, Mods::Standard, attrs);
3292                attrs.push(("text", OwnedAttr::Text(text)));
3293                attrs.push(("icon", super::role(icon)));
3294                attrs.push(("color", super::paint(color)));
3295                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
3296                attrs.push(("section", optional_text(section)));
3297            },
3298            vec![("onclick", unit(self.on_click.take()))],
3299            content,
3300        );
3301    }
3302}
3303
3304composed_on_drop!(NavigationItemScope);
3305
3306// ----- small things ---------------------------------------------------------------------
3307
3308/// A `FileDropTarget` and its builder.
3309pub struct FileDropTargetScope {
3310    modifier: Modifier,
3311    alignment: Option<Alignment>,
3312    on_files_entered: Option<Box<dyn FnMut()>>,
3313    on_files_dropped: Option<Box<dyn FnMut(FileDrop)>>,
3314    done: bool,
3315}
3316
3317/// A place files may be dropped. Desktop only in effect.
3318pub fn FileDropTarget() -> FileDropTargetScope {
3319    FileDropTargetScope {
3320        modifier: Modifier::new(),
3321        alignment: None,
3322        on_files_entered: None,
3323        on_files_dropped: None,
3324        done: false,
3325    }
3326}
3327
3328impl FileDropTargetScope {
3329    pub fn alignment(mut self, alignment: Alignment) -> Self {
3330        self.alignment = Some(alignment);
3331        self
3332    }
3333
3334    pub fn on_files_entered(mut self, handler: impl FnMut() + 'static) -> Self {
3335        self.on_files_entered = Some(std::boxed::Box::new(handler));
3336        self
3337    }
3338
3339    pub fn on_files_dropped(mut self, handler: impl FnMut(FileDrop) + 'static) -> Self {
3340        self.on_files_dropped = Some(std::boxed::Box::new(handler));
3341        self
3342    }
3343
3344    fn compose(&mut self, content: impl FnOnce()) {
3345        let modifier = self.modifier;
3346        let alignment = self.alignment;
3347        let dropped: EventCallback = match self.on_files_dropped.take() {
3348            Some(handler) => EventCallback::Files(Rc::new(RefCell::new(handler))),
3349            None => EventCallback::Files(Rc::new(RefCell::new(|_: FileDrop| {}))),
3350        };
3351        emit(
3352            widget_key("compose_rust::ui::FileDropTarget"),
3353            WidgetKind::FileDropTarget,
3354            |attrs| {
3355                push_modifiers(&modifier, Mods::Standard, attrs);
3356                attrs.push(("alignment", super::role(alignment)));
3357            },
3358            vec![
3359                ("onfilesentered", unit(self.on_files_entered.take())),
3360                ("onfilesdropped", dropped),
3361            ],
3362            content,
3363        );
3364    }
3365}
3366
3367container!(FileDropTargetScope);
3368
3369/// A `Chip` and its builder.
3370pub struct ChipScope {
3371    modifier: Modifier,
3372    text: String,
3373    icon: Option<IconRole>,
3374    selected: bool,
3375    enabled: bool,
3376    max_lines: Option<u32>,
3377    overflow: Option<TextOverflow>,
3378    on_click: Option<Box<dyn FnMut()>>,
3379    done: bool,
3380}
3381
3382/// A small token that is chosen or filters. Compose's `FilterChip`.
3383pub fn Chip(text: impl Into<String>) -> ChipScope {
3384    ChipScope {
3385        modifier: Modifier::new(),
3386        text: text.into(),
3387        icon: None,
3388        selected: false,
3389        enabled: true,
3390        max_lines: None,
3391        overflow: None,
3392        on_click: None,
3393        done: false,
3394    }
3395}
3396
3397impl ChipScope {
3398    pub fn icon(mut self, icon: IconRole) -> Self {
3399        self.icon = Some(icon);
3400        self
3401    }
3402
3403    pub fn selected(mut self, selected: bool) -> Self {
3404        self.selected = selected;
3405        self
3406    }
3407
3408    pub fn enabled(mut self, enabled: bool) -> Self {
3409        self.enabled = enabled;
3410        self
3411    }
3412
3413    /// How many lines the label may take. A label is one line unless this says more, so a
3414    /// chip label is one line unless this says more.
3415    pub fn max_lines(mut self, lines: u32) -> Self {
3416        self.max_lines = Some(lines);
3417        self
3418    }
3419
3420    /// What happens to a label longer than its lines.
3421    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
3422        self.overflow = Some(overflow);
3423        self
3424    }
3425
3426    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
3427        self.on_click = Some(std::boxed::Box::new(handler));
3428        self
3429    }
3430
3431    fn compose(&mut self, content: impl FnOnce()) {
3432        let modifier = self.modifier;
3433        let text = std::mem::take(&mut self.text);
3434        let (max_lines, overflow) = (self.max_lines, self.overflow);
3435        let (icon, selected, enabled) = (self.icon, self.selected, self.enabled);
3436        emit(
3437            widget_key("compose_rust::ui::Chip"),
3438            WidgetKind::Chip,
3439            |attrs| {
3440                push_modifiers(&modifier, Mods::Chip, attrs);
3441                attrs.push(("text", OwnedAttr::Text(text)));
3442                attrs.push(("icon", super::role(icon)));
3443                attrs.push(("checked", OwnedAttr::Bool(selected)));
3444                attrs.push(("enabled", OwnedAttr::Bool(enabled)));
3445                lines_and_overflow(max_lines, overflow, attrs);
3446            },
3447            vec![("onclick", unit(self.on_click.take()))],
3448            content,
3449        );
3450    }
3451}
3452
3453composed_on_drop!(ChipScope);
3454
3455/// A `FloatingAction` and its builder.
3456pub struct FloatingActionScope {
3457    modifier: Modifier,
3458    text: String,
3459    icon: Option<IconRole>,
3460    on_click: Option<Box<dyn FnMut()>>,
3461    done: bool,
3462}
3463
3464/// The one action a screen is about. Compose's `ExtendedFloatingActionButton`; where it
3465/// goes is the design system's answer.
3466pub fn FloatingAction(text: impl Into<String>) -> FloatingActionScope {
3467    FloatingActionScope {
3468        modifier: Modifier::new(),
3469        text: text.into(),
3470        icon: None,
3471        on_click: None,
3472        done: false,
3473    }
3474}
3475
3476impl FloatingActionScope {
3477    pub fn icon(mut self, icon: IconRole) -> Self {
3478        self.icon = Some(icon);
3479        self
3480    }
3481
3482    pub fn on_click(mut self, handler: impl FnMut() + 'static) -> Self {
3483        self.on_click = Some(std::boxed::Box::new(handler));
3484        self
3485    }
3486
3487    fn compose(&mut self, content: impl FnOnce()) {
3488        let modifier = self.modifier;
3489        let text = std::mem::take(&mut self.text);
3490        let icon = self.icon;
3491        emit(
3492            widget_key("compose_rust::ui::FloatingAction"),
3493            WidgetKind::FloatingAction,
3494            |attrs| {
3495                push_modifiers(&modifier, Mods::None, attrs);
3496                attrs.push(("text", OwnedAttr::Text(text)));
3497                attrs.push(("icon", super::role(icon)));
3498            },
3499            vec![("onclick", unit(self.on_click.take()))],
3500            content,
3501        );
3502    }
3503}
3504
3505composed_on_drop!(FloatingActionScope);
3506
3507/// A `Badge` and its builder.
3508pub struct BadgeScope {
3509    modifier: Modifier,
3510    value: Option<u32>,
3511    text: Option<String>,
3512    color: Option<ColorRole>,
3513    done: bool,
3514}
3515
3516/// A count, a word or a dot, on its child or on its own. Compose's `BadgedBox` and `Badge`.
3517pub fn Badge() -> BadgeScope {
3518    BadgeScope {
3519        modifier: Modifier::new(),
3520        value: None,
3521        text: None,
3522        color: None,
3523        done: false,
3524    }
3525}
3526
3527impl BadgeScope {
3528    /// How many.
3529    pub fn value(mut self, value: u32) -> Self {
3530        self.value = Some(value);
3531        self
3532    }
3533
3534    /// One short word, shown where a count would be.
3535    pub fn text(mut self, text: impl Into<String>) -> Self {
3536        self.text = Some(text.into());
3537        self
3538    }
3539
3540    /// The role the mark is filled with; the error role where it is not given.
3541    pub fn color(mut self, role: ColorRole) -> Self {
3542        self.color = Some(role);
3543        self
3544    }
3545
3546    fn compose(&mut self, content: impl FnOnce()) {
3547        let modifier = self.modifier;
3548        let text = self.text.take();
3549        let (value, color) = (self.value, self.color);
3550        emit(
3551            widget_key("compose_rust::ui::Badge"),
3552            WidgetKind::Badge,
3553            |attrs| {
3554                push_modifiers(&modifier, Mods::Standard, attrs);
3555                attrs.push((
3556                    "count",
3557                    value.map_or(OwnedAttr::None, |value| OwnedAttr::Int(i64::from(value))),
3558                ));
3559                attrs.push(("text", optional_text(text)));
3560                attrs.push(("color", super::paint(color.map(Paint::Role))));
3561            },
3562            Vec::new(),
3563            content,
3564        );
3565    }
3566}
3567
3568container!(BadgeScope);
3569
3570/// A `SplitPane` and its builder.
3571pub struct SplitPaneScope {
3572    modifier: Modifier,
3573    value: Option<f32>,
3574    min: Option<f32>,
3575    max: Option<f32>,
3576    collapsible: bool,
3577    collapse_below: Option<f32>,
3578    selected_index: usize,
3579    label: Option<String>,
3580    on_change: Option<Box<dyn FnMut(f32)>>,
3581    on_dismiss: Option<Box<dyn FnMut()>>,
3582    done: bool,
3583}
3584
3585/// A side pane and a body with a divider the user drags. Exactly two children, side pane
3586/// first. The drag is reported once, when it ends.
3587pub fn SplitPane() -> SplitPaneScope {
3588    SplitPaneScope {
3589        modifier: Modifier::new(),
3590        value: None,
3591        min: None,
3592        max: None,
3593        collapsible: false,
3594        collapse_below: None,
3595        selected_index: 0,
3596        label: None,
3597        on_change: None,
3598        on_dismiss: None,
3599        done: false,
3600    }
3601}
3602
3603impl SplitPaneScope {
3604    /// The side pane's width in dp, or 0 to open it folded.
3605    pub fn value(mut self, dp: f32) -> Self {
3606        self.value = Some(dp);
3607        self
3608    }
3609
3610    pub fn min(mut self, dp: f32) -> Self {
3611        self.min = Some(dp);
3612        self
3613    }
3614
3615    pub fn max(mut self, dp: f32) -> Self {
3616        self.max = Some(dp);
3617        self
3618    }
3619
3620    pub fn collapsible(mut self, collapsible: bool) -> Self {
3621        self.collapsible = collapsible;
3622        self
3623    }
3624
3625    /// The split pane's own width in dp below which it shows one pane at a time, the one
3626    /// `selected_index` names. At or above it the panes are never stacked, and the design
3627    /// system still decides between two columns and a side pane laid over the body.
3628    ///
3629    /// Without it the line is the size class's: stacked below 600 dp. 0 is the same as not
3630    /// calling this. The width is measured on the Renderer, so crossing the line sends the
3631    /// Host nothing. A negative or non-finite width is reported as a protocol error and
3632    /// the split pane is drawn as if it had none.
3633    pub fn collapse_below(mut self, dp: f32) -> Self {
3634        self.collapse_below = Some(dp);
3635        self
3636    }
3637
3638    /// Which pane shows when only one fits: 0 the side pane, 1 the body.
3639    pub fn selected_index(mut self, index: usize) -> Self {
3640        self.selected_index = index;
3641        self
3642    }
3643
3644    /// What a screen reader calls the divider.
3645    pub fn label(mut self, label: impl Into<String>) -> Self {
3646        self.label = Some(label.into());
3647        self
3648    }
3649
3650    pub fn on_change(mut self, handler: impl FnMut(f32) + 'static) -> Self {
3651        self.on_change = Some(std::boxed::Box::new(handler));
3652        self
3653    }
3654
3655    pub fn on_dismiss(mut self, handler: impl FnMut() + 'static) -> Self {
3656        self.on_dismiss = Some(std::boxed::Box::new(handler));
3657        self
3658    }
3659
3660    fn compose(&mut self, content: impl FnOnce()) {
3661        let modifier = self.modifier;
3662        let label = self.label.take();
3663        let (value, min, max, collapsible, collapse_below, selected_index) = (
3664            self.value,
3665            self.min,
3666            self.max,
3667            self.collapsible,
3668            self.collapse_below,
3669            self.selected_index,
3670        );
3671        let change = match self.on_change.take() {
3672            Some(mut handler) => EventCallback::Value(Rc::new(RefCell::new(move |value: f64| {
3673                handler(value as f32)
3674            }))),
3675            None => value_handler(None),
3676        };
3677        let as_float = |value: Option<f32>| {
3678            value.map_or(OwnedAttr::None, |value| OwnedAttr::Float(f64::from(value)))
3679        };
3680        emit(
3681            widget_key("compose_rust::ui::SplitPane"),
3682            WidgetKind::SplitPane,
3683            |attrs| {
3684                push_modifiers(&modifier, Mods::Standard, attrs);
3685                attrs.push(("value", as_float(value)));
3686                attrs.push(("min", as_float(min)));
3687                attrs.push(("max", as_float(max)));
3688                attrs.push(("collapsible", OwnedAttr::Bool(collapsible)));
3689                attrs.push(("collapse_below", as_float(collapse_below)));
3690                attrs.push(("selected_index", OwnedAttr::Int(selected_index as i64)));
3691                attrs.push(("text", optional_text(label)));
3692            },
3693            vec![
3694                ("onchange", change),
3695                ("ondismiss", unit(self.on_dismiss.take())),
3696            ],
3697            content,
3698        );
3699    }
3700}
3701
3702container!(SplitPaneScope);
3703
3704/// The worked extension widget: Material 3's determinate linear progress indicator.
3705pub struct LinearProgressIndicatorScope {
3706    modifier: Modifier,
3707    progress: f32,
3708    done: bool,
3709}
3710
3711/// The extension widget the schema carries as its example.
3712pub fn LinearProgressIndicator(progress: f32) -> LinearProgressIndicatorScope {
3713    LinearProgressIndicatorScope {
3714        modifier: Modifier::new(),
3715        progress,
3716        done: false,
3717    }
3718}
3719
3720impl LinearProgressIndicatorScope {
3721    fn compose(&mut self, content: impl FnOnce()) {
3722        let modifier = self.modifier;
3723        let progress = self.progress;
3724        emit(
3725            widget_key("compose_rust::ui::LinearProgressIndicator"),
3726            WidgetKind::LinearProgressIndicator,
3727            |attrs| {
3728                push_modifiers(&modifier, Mods::None, attrs);
3729                attrs.push(("progress", OwnedAttr::Float(f64::from(progress))));
3730            },
3731            Vec::new(),
3732            content,
3733        );
3734    }
3735}
3736
3737composed_on_drop!(LinearProgressIndicatorScope);
3738
3739// ----- windows --------------------------------------------------------------------
3740
3741/// A `Window` and its builder.
3742pub struct WindowScope {
3743    on_close_request: Option<Box<dyn FnMut()>>,
3744    title: Option<String>,
3745    size: Option<(f32, f32)>,
3746    min_size: Option<(f32, f32)>,
3747    resizable: Option<bool>,
3748    report_size: Option<bool>,
3749    chrome: Option<crate::schema::Chrome>,
3750    title_bar: Option<crate::schema::TitleBar>,
3751    icon: Option<u32>,
3752    position: Option<(f32, f32)>,
3753    restore_key: Option<String>,
3754    zoom_level: Option<i8>,
3755    drag_from_pointer: Option<crate::windows::DragFromPointer>,
3756    theme: Option<crate::schema::Theme>,
3757    on_focus_changed: Option<Box<dyn FnMut(bool)>>,
3758    on_moved: Option<Box<dyn FnMut(crate::windows::WindowFrame)>>,
3759    on_drag: Option<Box<dyn FnMut(crate::windows::WindowDragEvent)>>,
3760    on_unavailable: Option<Box<dyn FnMut()>>,
3761    on_closed: Option<Box<dyn FnMut()>>,
3762    done: bool,
3763}
3764
3765/// A platform window, a child of the root, whose children are the window's content. Compose
3766/// Desktop's `Window`. The application has to be launched with
3767/// [`LaunchBuilder::windows`](crate::LaunchBuilder::windows).
3768///
3769/// `on_close_request` is called when the user asks to close the window. It does not close
3770/// it: the window closes when the application stops composing this `Window`, which is
3771/// where a dialog that asks about unsaved work goes.
3772///
3773/// ```ignore
3774/// Window(move || open.set(false)).title("Notes").size(900.0, 600.0).content(|| {
3775///     Text("Hello");
3776/// });
3777/// ```
3778pub fn Window(on_close_request: impl FnMut() + 'static) -> WindowScope {
3779    WindowScope {
3780        on_close_request: Some(std::boxed::Box::new(on_close_request)),
3781        title: None,
3782        size: None,
3783        min_size: None,
3784        resizable: None,
3785        report_size: None,
3786        chrome: None,
3787        title_bar: None,
3788        icon: None,
3789        position: None,
3790        restore_key: None,
3791        zoom_level: None,
3792        drag_from_pointer: None,
3793        theme: None,
3794        on_focus_changed: None,
3795        on_moved: None,
3796        on_drag: None,
3797        on_unavailable: None,
3798        on_closed: None,
3799        done: false,
3800    }
3801}
3802
3803impl Drop for WindowScope {
3804    fn drop(&mut self) {
3805        // A panic is already unwinding the composition, and composing on the way out would
3806        // only risk a second one.
3807        if !self.done && !std::thread::panicking() {
3808            self.done = true;
3809            self.compose(|| {});
3810        }
3811    }
3812}
3813
3814/// A window handler as the callback the runtime delivers events to.
3815fn window_callback(handler: impl FnMut(crate::windows::WindowEvent) + 'static) -> EventCallback {
3816    EventCallback::Window(Rc::new(RefCell::new(handler)))
3817}
3818
3819impl WindowScope {
3820    /// What the window calls itself.
3821    pub fn title(mut self, title: impl Into<String>) -> Self {
3822        self.title = Some(title.into());
3823        self
3824    }
3825
3826    /// The size the window opens at, in dp. A later change resizes it.
3827    pub fn size(mut self, width: f32, height: f32) -> Self {
3828        self.size = Some((width, height));
3829        self
3830    }
3831
3832    /// The smallest size the user can make it, in dp.
3833    pub fn min_size(mut self, width: f32, height: f32) -> Self {
3834        self.min_size = Some((width, height));
3835        self
3836    }
3837
3838    pub fn resizable(mut self, resizable: bool) -> Self {
3839        self.resizable = Some(resizable);
3840        self
3841    }
3842
3843    /// Asks to be told every time the window's width or height, rounded to whole dp,
3844    /// changes. Without it the size is reported only when its class or the safe area
3845    /// changes. Turn it on only for a window that lays out by its exact size: each
3846    /// whole-dp change while the user drags an edge costs one call into the Host.
3847    pub fn report_size(mut self, report: bool) -> Self {
3848        self.report_size = Some(report);
3849        self
3850    }
3851
3852    /// Who draws the caption.
3853    pub fn chrome(mut self, chrome: crate::schema::Chrome) -> Self {
3854        self.chrome = Some(chrome);
3855        self
3856    }
3857
3858    /// Which of the two title bars the window has.
3859    pub fn title_bar(mut self, title_bar: crate::schema::TitleBar) -> Self {
3860        self.title_bar = Some(title_bar);
3861        self
3862    }
3863
3864    /// The window's icon, as the id of a registered asset. Without one it wears the
3865    /// application's.
3866    pub fn icon(mut self, asset_id: u32) -> Self {
3867        self.icon = Some(asset_id);
3868        self
3869    }
3870
3871    /// Where the window sits, in screen dp from the top left of the main screen. Changing
3872    /// it moves the window; the position the user chose is not given back. Without it the
3873    /// platform places the window.
3874    pub fn position(mut self, x: f32, y: f32) -> Self {
3875        self.position = Some((x, y));
3876        self
3877    }
3878
3879    /// The application's stable name for the window, which matches a window the system
3880    /// brings back to the node that describes it.
3881    pub fn restore_key(mut self, key: impl Into<String>) -> Self {
3882        self.restore_key = Some(key.into());
3883        self
3884    }
3885
3886    /// The window's own application zoom level, kept inside its range.
3887    pub fn zoom_level(mut self, level: i8) -> Self {
3888        self.zoom_level = Some(crate::zoom::clamp_level(level));
3889        self
3890    }
3891
3892    /// Opens the window under a pointer that is held down and follows it until it is let
3893    /// go. Read when the window opens and not again.
3894    pub fn drag_from_pointer(mut self, drag: crate::windows::DragFromPointer) -> Self {
3895        self.drag_from_pointer = Some(drag);
3896        self
3897    }
3898
3899    /// A theme for this window over the application's.
3900    pub fn theme(mut self, theme: crate::schema::Theme) -> Self {
3901        self.theme = Some(theme);
3902        self
3903    }
3904
3905    /// The window became the key window or stopped being it.
3906    pub fn on_focus_changed(mut self, handler: impl FnMut(bool) + 'static) -> Self {
3907        self.on_focus_changed = Some(std::boxed::Box::new(handler));
3908        self
3909    }
3910
3911    /// The window's outer frame moved or changed size.
3912    pub fn on_moved(mut self, handler: impl FnMut(crate::windows::WindowFrame) + 'static) -> Self {
3913        self.on_moved = Some(std::boxed::Box::new(handler));
3914        self
3915    }
3916
3917    /// The window is being dragged out of a tab, and how that ended.
3918    pub fn on_drag(
3919        mut self,
3920        handler: impl FnMut(crate::windows::WindowDragEvent) + 'static,
3921    ) -> Self {
3922        self.on_drag = Some(std::boxed::Box::new(handler));
3923        self
3924    }
3925
3926    /// The platform could not open this window. Tell the user, or put the tab back.
3927    pub fn on_unavailable(mut self, handler: impl FnMut() + 'static) -> Self {
3928        self.on_unavailable = Some(std::boxed::Box::new(handler));
3929        self
3930    }
3931
3932    /// The system closed the window without asking. Stop composing it.
3933    pub fn on_closed(mut self, handler: impl FnMut() + 'static) -> Self {
3934        self.on_closed = Some(std::boxed::Box::new(handler));
3935        self
3936    }
3937
3938    /// Composes this window with `content` as its children.
3939    pub fn content(mut self, content: impl FnOnce()) {
3940        self.done = true;
3941        self.compose(content);
3942    }
3943
3944    fn compose(&mut self, content: impl FnOnce()) {
3945        use crate::windows::WindowEvent;
3946        let pair = |value: (f32, f32)| {
3947            let mut bytes = Vec::with_capacity(8);
3948            bytes.extend_from_slice(&value.0.to_bits().to_le_bytes());
3949            bytes.extend_from_slice(&value.1.to_bits().to_le_bytes());
3950            OwnedAttr::Bytes(bytes)
3951        };
3952        // Only what was said is written. A property left out is not a property sent as
3953        // nothing, and one that was said before and is not now is taken away.
3954        let mut properties: Attrs = Vec::new();
3955        if let Some(title) = self.title.take() {
3956            properties.push(("window_title", OwnedAttr::Text(title)));
3957        }
3958        if let Some(size) = self.size {
3959            properties.push(("window_size", pair(size)));
3960        }
3961        if let Some(size) = self.min_size {
3962            properties.push(("window_min_size", pair(size)));
3963        }
3964        if let Some(resizable) = self.resizable {
3965            properties.push(("window_resizable", OwnedAttr::Bool(resizable)));
3966        }
3967        if let Some(report) = self.report_size {
3968            properties.push(("report_size", OwnedAttr::Bool(report)));
3969        }
3970        if let Some(chrome) = self.chrome {
3971            properties.push(("window_chrome", OwnedAttr::Int(i64::from(chrome as u16))));
3972        }
3973        if let Some(title_bar) = self.title_bar {
3974            properties.push((
3975                "window_title_bar",
3976                OwnedAttr::Int(i64::from(title_bar as u16)),
3977            ));
3978        }
3979        if let Some(icon) = self.icon {
3980            properties.push(("window_icon", OwnedAttr::Int(i64::from(icon))));
3981        }
3982        if let Some(position) = self.position {
3983            properties.push(("window_position", pair(position)));
3984        }
3985        if let Some(key) = self.restore_key.take() {
3986            properties.push(("window_restore_key", OwnedAttr::Text(key)));
3987        }
3988        if let Some(level) = self.zoom_level {
3989            properties.push(("window_zoom_level", OwnedAttr::Int(i64::from(level))));
3990        }
3991        if let Some(drag) = self.drag_from_pointer {
3992            properties.push(("drag_from_pointer", OwnedAttr::Bytes(drag.to_bytes())));
3993        }
3994
3995        let mut listeners: Vec<(&'static str, EventCallback)> = Vec::new();
3996        let mut close = self.on_close_request.take();
3997        listeners.push((
3998            "onwindowclose",
3999            window_callback(move |event| {
4000                if let (WindowEvent::CloseRequested, Some(handler)) = (event, close.as_mut()) {
4001                    handler();
4002                }
4003            }),
4004        ));
4005        if let Some(mut handler) = self.on_focus_changed.take() {
4006            listeners.push((
4007                "onwindowfocus",
4008                window_callback(move |event| {
4009                    if let WindowEvent::FocusChanged { focused } = event {
4010                        handler(focused);
4011                    }
4012                }),
4013            ));
4014        }
4015        if let Some(mut handler) = self.on_moved.take() {
4016            listeners.push((
4017                "onwindowmoved",
4018                window_callback(move |event| {
4019                    if let WindowEvent::Moved {
4020                        x,
4021                        y,
4022                        width,
4023                        height,
4024                    } = event
4025                    {
4026                        handler(crate::windows::WindowFrame {
4027                            x,
4028                            y,
4029                            width,
4030                            height,
4031                        });
4032                    }
4033                }),
4034            ));
4035        }
4036        if let Some(mut handler) = self.on_drag.take() {
4037            listeners.push((
4038                "onwindowdrag",
4039                window_callback(move |event| {
4040                    if let WindowEvent::Drag {
4041                        phase,
4042                        screen_x,
4043                        screen_y,
4044                        over_window,
4045                    } = event
4046                    {
4047                        handler(crate::windows::WindowDragEvent {
4048                            phase,
4049                            screen_x,
4050                            screen_y,
4051                            over_window,
4052                        });
4053                    }
4054                }),
4055            ));
4056        }
4057        if let Some(mut handler) = self.on_unavailable.take() {
4058            listeners.push((
4059                "onwindowunavailable",
4060                window_callback(move |event| {
4061                    if event == WindowEvent::Unavailable {
4062                        handler();
4063                    }
4064                }),
4065            ));
4066        }
4067        if let Some(mut handler) = self.on_closed.take() {
4068            listeners.push((
4069                "onwindowclosed",
4070                window_callback(move |event| {
4071                    if event == WindowEvent::Closed {
4072                        handler();
4073                    }
4074                }),
4075            ));
4076        }
4077
4078        let theme = self.theme.take();
4079        emit(
4080            widget_key("compose_rust::ui::Window"),
4081            WidgetKind::Window,
4082            |attrs| attrs.append(&mut properties),
4083            listeners,
4084            move || {
4085                // The group has its node by now, so the window is known.
4086                if let (Some(theme), Some(window)) = (theme, crate::windows::current_window()) {
4087                    window.set_theme(theme);
4088                }
4089                content();
4090            },
4091        );
4092    }
4093}