Skip to main content

compose_rust/ui/
mod.rs

1//! The widget vocabulary as composables: the same widgets, the same properties and the
2//! same Modifier slots the `rsx!` layer writes, with Compose's names and Compose's shape.
3//!
4//! ```ignore
5//! use compose_rust::runtime::*;
6//! use compose_rust::ui::*;
7//!
8//! #[Composable]
9//! fn Greeting(name: String) {
10//!     Column().modifier(Modifier.padding(16.0).fill_max_width()).content(|| {
11//!         Text(format!("Hello, {name}")).type_role(TypeRole::Title);
12//!         Button("Wave").on_click(|| show_message("Hello back"));
13//!     });
14//! }
15//! ```
16//!
17//! **A widget is written as a statement.** `Text("Hello")` creates a description and the
18//! statement's end hands it to the composition; the methods in between set what Compose
19//! would take as named arguments. A container's children are the closure given to
20//! `content`, which runs at once. Binding a widget to a variable delays it to the end of
21//! the variable's scope, so a widget is not bound.
22//!
23//! **Nothing here defines a widget.** Each composable writes exactly the attributes its
24//! `rsx!` counterpart writes, through the same translation into wire records, so a screen
25//! written either way is the same tree of the same nodes for the Renderer. A Modifier the
26//! `rsx!` counterpart does not accept is still written when it is set, because the wire
27//! carries every Modifier slot on every node.
28
29pub(crate) mod focus;
30mod lazy;
31mod widgets;
32
33pub use crate::boundary::LaunchBuilder;
34pub use crate::brush::{Brush, Stop, brush};
35pub use crate::clipboard::{Clipboard, use_clipboard};
36pub use crate::drawing::{DrawCommand, DrawList, DrawListBuilder};
37pub use crate::message::{Message, show_message};
38pub use crate::runtime::{application, launch};
39pub use crate::schema::SemanticsRole as Role;
40pub use crate::schema::{
41    Alignment, Arrangement, ButtonKind, ButtonVariant, Color, ColorRole, ColorScheme,
42    CompactNavigation, DesignSystem, HorizontalAlignment, IconRole, Key, MaterialRole,
43    MessageDuration, MotionRole, Paint, PopoverAlign, PopoverPlacement, SemanticsRole, ShapeRole,
44    SpaceRole, TextAlign, TextOverflow, Theme, TypeRole, VerticalAlignment, WindowHeightClass,
45    WindowSizeClass,
46};
47pub use crate::spans::TextSpans;
48pub use crate::theme::{ThemeHandle, current_theme};
49
50/// The application's theme, to read and to change while it runs. A change is one
51/// `SetTheme` record in the batch the call that made it produces.
52pub fn theme() -> ThemeHandle {
53    ThemeHandle::default()
54}
55pub use crate::window::{NodeSize, WindowSafeArea, WindowSize};
56pub use crate::{
57    Cursor, FileDrop, KeyEvent, PointerEvent, PointerKind, PointerKinds, RangeRequest, UserSelect,
58    asset, pointer_button,
59};
60pub use focus::{FocusHandle, use_focus_handle};
61pub use lazy::{LazyColumn, LazyGrid, LazyList, LazyListScope, LazyRow};
62pub use widgets::*;
63
64use crate::runtime::composer::{EventCallback, OwnedAttr};
65use crate::runtime::{group_guard, with_composer, with_composer_if_idle};
66use crate::schema::WidgetKind;
67
68/// The most shadow layers one node can carry through this API.
69pub const MAX_SHADOWS: usize = 4;
70
71/// One layer of a drop shadow, as CSS `box-shadow` writes it: an offset, a blur, a spread
72/// and a colour with its alpha. It follows the corners of the node that casts it.
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Shadow {
75    pub x: f32,
76    pub y: f32,
77    pub blur: f32,
78    pub spread: f32,
79    pub paint: Paint,
80}
81
82impl Shadow {
83    pub const fn new(x: f32, y: f32, blur: f32, spread: f32, paint: Paint) -> Self {
84        Self {
85            x,
86            y,
87            blur,
88            spread,
89            paint,
90        }
91    }
92}
93
94/// How a node presents itself to assistive technology. Each tri-state field is unset until
95/// it is given a value, and an unset one sends nothing. The flags are off until set.
96#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub struct SemanticsState {
98    pub checked: Option<bool>,
99    pub selected: Option<bool>,
100    pub expanded: Option<bool>,
101    pub disabled: Option<bool>,
102    pub focused: Option<bool>,
103    pub hidden: bool,
104    pub click_action: bool,
105    pub focus_action: bool,
106}
107
108impl SemanticsState {
109    const NONE: Self = Self {
110        checked: None,
111        selected: None,
112        expanded: None,
113        disabled: None,
114        focused: None,
115        hidden: false,
116        click_action: false,
117        focus_action: false,
118    };
119
120    pub const fn checked(checked: bool) -> Self {
121        Self {
122            checked: Some(checked),
123            ..Self::NONE
124        }
125    }
126
127    pub const fn selected(selected: bool) -> Self {
128        Self {
129            selected: Some(selected),
130            ..Self::NONE
131        }
132    }
133
134    pub const fn expanded(expanded: bool) -> Self {
135        Self {
136            expanded: Some(expanded),
137            ..Self::NONE
138        }
139    }
140
141    pub const fn disabled(disabled: bool) -> Self {
142        Self {
143            disabled: Some(disabled),
144            ..Self::NONE
145        }
146    }
147
148    pub const fn focused(focused: bool) -> Self {
149        Self {
150            focused: Some(focused),
151            ..Self::NONE
152        }
153    }
154
155    pub const fn hidden(hidden: bool) -> Self {
156        Self {
157            hidden,
158            ..Self::NONE
159        }
160    }
161
162    pub const fn click_action(exposed: bool) -> Self {
163        Self {
164            click_action: exposed,
165            ..Self::NONE
166        }
167    }
168
169    pub const fn focus_action(exposed: bool) -> Self {
170        Self {
171            focus_action: exposed,
172            ..Self::NONE
173        }
174    }
175
176    /// The state word the `Semantics` modifier carries.
177    pub(crate) fn bits(self) -> u32 {
178        use crate::schema::*;
179        let tri = |value: Option<bool>| value.map_or(0_u32, |on| if on { 2 } else { 1 });
180        (tri(self.checked) << SEMANTICS_CHECKED_SHIFT)
181            | (tri(self.selected) << SEMANTICS_SELECTED_SHIFT)
182            | (tri(self.expanded) << SEMANTICS_EXPANDED_SHIFT)
183            | (tri(self.disabled) << SEMANTICS_DISABLED_SHIFT)
184            | (tri(self.focused) << SEMANTICS_FOCUSED_SHIFT)
185            | if self.hidden { SEMANTICS_HIDDEN_BIT } else { 0 }
186            | if self.click_action {
187                SEMANTICS_CLICK_ACTION_BIT
188            } else {
189                0
190            }
191            | if self.focus_action {
192                SEMANTICS_FOCUS_ACTION_BIT
193            } else {
194                0
195            }
196    }
197}
198
199/// The most distinct accessible names a program can keep. Names are kept for the life of
200/// the program so a `Modifier` can stay `Copy`; the cap keeps a name built from data
201/// (an id, a counter) from growing that memory without end.
202pub(crate) const MAX_LABELS: usize = 4096;
203
204/// The distinct accessible names seen so far, each leaked once.
205struct LabelTable {
206    kept: std::sync::RwLock<std::collections::HashSet<&'static str>>,
207    warned: std::sync::atomic::AtomicBool,
208}
209
210impl LabelTable {
211    fn new() -> Self {
212        Self {
213            kept: Default::default(),
214            warned: Default::default(),
215        }
216    }
217
218    /// The kept copy of `label`. A name already kept is found under a shared lock with no
219    /// allocation. A new name is kept until the table holds `cap` names; past that it is
220    /// dropped (and reported in debug builds) so memory stays bounded.
221    fn intern(&self, label: &str, cap: usize) -> Option<&'static str> {
222        use std::sync::atomic::Ordering;
223
224        if label.is_empty() {
225            return None;
226        }
227        {
228            let kept = self.kept.read().unwrap_or_else(|p| p.into_inner());
229            if let Some(found) = kept.get(label) {
230                return Some(*found);
231            }
232        }
233        let mut kept = self.kept.write().unwrap_or_else(|p| p.into_inner());
234        if let Some(found) = kept.get(label) {
235            return Some(*found);
236        }
237        if kept.len() >= cap {
238            if cfg!(debug_assertions) && !self.warned.swap(true, Ordering::Relaxed) {
239                eprintln!(
240                    "compose-rust: more than {cap} distinct accessible names; \
241                     names past the limit are left off. Use a fixed set of names."
242                );
243            }
244            return None;
245        }
246        let leaked: &'static str = Box::leak(label.to_owned().into_boxed_str());
247        kept.insert(leaked);
248        Some(leaked)
249    }
250
251    #[cfg(test)]
252    fn len(&self) -> usize {
253        self.kept.read().unwrap_or_else(|p| p.into_inner()).len()
254    }
255}
256
257fn keep_label(label: &str) -> Option<&'static str> {
258    use std::cell::Cell;
259    use std::sync::OnceLock;
260
261    static LABELS: OnceLock<LabelTable> = OnceLock::new();
262    thread_local! {
263        // The name this thread kept last. A name written on every frame is found here
264        // without taking any lock.
265        static LAST: Cell<Option<&'static str>> = const { Cell::new(None) };
266    }
267    if let Some(last) = LAST.with(Cell::get) {
268        if last == label {
269            return Some(last);
270        }
271    }
272    let kept = LABELS
273        .get_or_init(LabelTable::new)
274        .intern(label, MAX_LABELS);
275    if kept.is_some() {
276        LAST.with(|last| last.set(kept));
277    }
278    kept
279}
280
281/// What sits around a widget: its size, its surface, its outline, how far its content
282/// is inset, and what the Renderer reports about it. Compose's `Modifier`.
283///
284/// Written the way Compose writes it, from the empty value `Modifier`:
285///
286/// ```ignore
287/// Card().modifier(Modifier.fill_max_width().padding(16.0).background(Paint::Role(ColorRole::Surface)))
288/// ```
289///
290/// The order of the calls does not matter. Each kind of Modifier has a fixed slot in the
291/// node's chain, outside in: size, shape, fill, outline, lift, press, inset. That is what
292/// the `rsx!` attributes do too, so the same Modifier draws the same either way.
293#[derive(Clone, Copy, Debug, Default, PartialEq)]
294pub struct Modifier {
295    observe_size: Option<i64>,
296    observe_size_every_dp: bool,
297    motion: Option<MotionRole>,
298    material: Option<MaterialRole>,
299    weight: Option<f32>,
300    width: Option<f32>,
301    height: Option<f32>,
302    padding: Option<f32>,
303    padding_role: Option<SpaceRole>,
304    background: Option<Paint>,
305    shape_role: Option<ShapeRole>,
306    corner_radius: Option<f32>,
307    corner_radii: Option<[f32; 4]>,
308    corner_curve: Option<f32>,
309    border_width: Option<f32>,
310    border_color: Option<Paint>,
311    elevation: Option<f32>,
312    alpha: Option<f32>,
313    press_feedback: Option<(f32, f32)>,
314    semantics_role: Option<SemanticsRole>,
315    semantics_label: Option<&'static str>,
316    semantics_state: SemanticsState,
317    transform: Option<[f32; 8]>,
318    animated_alpha: Option<crate::Animated<f32>>,
319    animated_background: Option<crate::Animated<Paint>>,
320    animated_offset: Option<crate::Animated<(f32, f32)>>,
321    cursor: Option<crate::Cursor>,
322    user_select: Option<crate::UserSelect>,
323    clip: bool,
324    window_drag: Option<bool>,
325    offset: Option<(f32, f32)>,
326    shadows: [Option<Shadow>; MAX_SHADOWS],
327    fill_max_width: bool,
328    match_parent_size: bool,
329    fill_max_height: bool,
330}
331
332/// The empty Modifier, to start a chain from: `Modifier.padding(8.0)`.
333#[allow(non_upper_case_globals)]
334pub const Modifier: Modifier = Modifier::new();
335
336impl Modifier {
337    /// The empty Modifier. The same as the value `Modifier`.
338    pub const fn new() -> Self {
339        Self {
340            observe_size: None,
341            observe_size_every_dp: false,
342            motion: None,
343            material: None,
344            weight: None,
345            width: None,
346            height: None,
347            padding: None,
348            padding_role: None,
349            background: None,
350            shape_role: None,
351            corner_radius: None,
352            corner_radii: None,
353            corner_curve: None,
354            border_width: None,
355            border_color: None,
356            elevation: None,
357            alpha: None,
358            press_feedback: None,
359            semantics_role: None,
360            semantics_label: None,
361            semantics_state: SemanticsState::NONE,
362            transform: None,
363            animated_alpha: None,
364            animated_background: None,
365            animated_offset: None,
366            cursor: None,
367            user_select: None,
368            clip: false,
369            window_drag: None,
370            offset: None,
371            shadows: [None; MAX_SHADOWS],
372            fill_max_width: false,
373            match_parent_size: false,
374            fill_max_height: false,
375        }
376    }
377
378    /// A share of the space left in a `Row` or `Column`.
379    pub const fn weight(mut self, weight: f32) -> Self {
380        self.weight = Some(weight);
381        self
382    }
383
384    pub const fn width(mut self, dp: f32) -> Self {
385        self.width = Some(dp);
386        self
387    }
388
389    pub const fn height(mut self, dp: f32) -> Self {
390        self.height = Some(dp);
391        self
392    }
393
394    /// Both at once.
395    pub const fn size(self, width: f32, height: f32) -> Self {
396        self.width(width).height(height)
397    }
398
399    /// A width that may be unset, for a screen whose measure depends on the window.
400    pub const fn width_if(mut self, dp: Option<f32>) -> Self {
401        self.width = dp;
402        self
403    }
404
405    /// Matches the bounds of a Box parent without affecting its size.
406    pub const fn match_parent_size(mut self) -> Self {
407        self.match_parent_size = true;
408        self
409    }
410
411    pub const fn fill_max_width(mut self) -> Self {
412        self.fill_max_width = true;
413        self
414    }
415
416    pub const fn fill_max_height(mut self) -> Self {
417        self.fill_max_height = true;
418        self
419    }
420
421    pub const fn fill_max_size(self) -> Self {
422        self.fill_max_width().fill_max_height()
423    }
424
425    /// Fills the width only when `fill` is true.
426    pub const fn fill_max_width_if(mut self, fill: bool) -> Self {
427        self.fill_max_width = fill;
428        self
429    }
430
431    /// The same inset on every side, in dp.
432    pub const fn padding(mut self, dp: f32) -> Self {
433        self.padding = Some(dp);
434        self
435    }
436
437    /// An inset from the design system's spacing ladder. Wins over `padding`.
438    pub const fn padding_role(mut self, role: SpaceRole) -> Self {
439        self.padding_role = Some(role);
440        self
441    }
442
443    /// The same, when the inset may be unset.
444    pub const fn padding_role_if(mut self, role: Option<SpaceRole>) -> Self {
445        self.padding_role = role;
446        self
447    }
448
449    pub const fn background(mut self, paint: Paint) -> Self {
450        self.background = Some(paint);
451        self
452    }
453
454    /// The same, when there may be no background.
455    pub const fn background_if(mut self, paint: Option<Paint>) -> Self {
456        self.background = paint;
457        self
458    }
459
460    /// A shape from the design system's ladder.
461    pub const fn shape_role(mut self, role: ShapeRole) -> Self {
462        self.shape_role = Some(role);
463        self
464    }
465
466    /// A literal corner, in dp. Wins over `shape_role`.
467    pub const fn corner_radius(mut self, dp: f32) -> Self {
468        self.corner_radius = Some(dp);
469        self.corner_radii = None;
470        self
471    }
472
473    /// Individual corner radii `[top_start, top_end, bottom_end, bottom_start]`, in dp.
474    /// Wins over `shape_role` and `corner_radius`.
475    pub const fn corner_radii(mut self, radii: [f32; 4]) -> Self {
476        self.corner_radii = Some(radii);
477        self.corner_radius = None;
478        self
479    }
480
481    /// Clips the node and its children to its shape, or to its rectangle when it has none.
482    pub const fn clip(mut self, clip: bool) -> Self {
483        self.clip = clip;
484        self
485    }
486
487    /// Includes or excludes this node from window dragging.
488    pub const fn window_drag(mut self, enabled: bool) -> Self {
489        self.window_drag = Some(enabled);
490        self
491    }
492
493    pub const fn border(mut self, width: f32, paint: Paint) -> Self {
494        self.border_width = Some(width);
495        self.border_color = Some(paint);
496        self
497    }
498
499    /// A resting height above the default, in dp. How it is drawn is the design system's.
500    pub const fn elevation(mut self, dp: f32) -> Self {
501        self.elevation = Some(dp);
502        self
503    }
504
505    /// Asks the Renderer to report this node's size under the token from
506    /// [`crate::runtime::remember_node_size`].
507    ///
508    /// A report goes out on the first measurement and then whenever the width or height
509    /// class changes. A resize inside one class says nothing; use
510    /// [`Modifier::observe_size_every_dp`] for that.
511    pub const fn observe_size(mut self, token: i64) -> Self {
512        self.observe_size = Some(token);
513        self.observe_size_every_dp = false;
514        self
515    }
516
517    /// Like [`Modifier::observe_size`], and also reports whenever the width or height,
518    /// rounded to the nearest whole dp, changes inside one class. Several changes in one
519    /// frame are one report carrying the last size.
520    pub const fn observe_size_every_dp(mut self, token: i64) -> Self {
521        self.observe_size = Some(token);
522        self.observe_size_every_dp = true;
523        self
524    }
525
526    /// How important this node's changes are. The curve is the design system's.
527    pub const fn motion(mut self, role: MotionRole) -> Self {
528        self.motion = Some(role);
529        self
530    }
531
532    /// What this node's surface is made of. The effect is the design system's.
533    pub const fn material(mut self, role: MaterialRole) -> Self {
534        self.material = Some(role);
535        self
536    }
537
538    /// Moves the node from where it was laid out, in dp, without moving anything else.
539    pub const fn offset(mut self, x: f32, y: f32) -> Self {
540        self.offset = Some((x, y));
541        self
542    }
543
544    /// Adds a shadow layer. Layers are drawn in the order they are added, the first at
545    /// the bottom. A node takes at most [`MAX_SHADOWS`]; a layer past that is ignored.
546    pub const fn shadow(mut self, shadow: Shadow) -> Self {
547        let mut index = 0;
548        while index < MAX_SHADOWS {
549            if self.shadows[index].is_none() {
550                self.shadows[index] = Some(shadow);
551                break;
552            }
553            index += 1;
554        }
555        self
556    }
557
558    /// Adds several shadow layers, in order.
559    pub fn shadows(mut self, shadows: impl IntoIterator<Item = Shadow>) -> Self {
560        for shadow in shadows {
561            self = self.shadow(shadow);
562        }
563        self
564    }
565
566    /// An opacity for the node and everything in it, faded as one group.
567    pub const fn alpha(mut self, alpha: f32) -> Self {
568        self.alpha = Some(alpha);
569        self
570    }
571
572    /// How the node's rounded corners curve, from 0 (a circular arc, the default) to 1.
573    /// Above 0 the curve eases in from each side with continuously changing curvature, as
574    /// Figma's corner smoothing does; 0.6 is the look of iOS. The radius stays whatever
575    /// `corner_radius`, `corner_radii` or `shape_role` made it, and the background, border,
576    /// shadow and clip all follow the same curve. A value outside 0..=1 is taken as the
577    /// nearer end, and NaN as 0.
578    pub fn corner_curve(mut self, smoothing: f32) -> Self {
579        self.corner_curve = Some(crate::schema::corner_smoothing(smoothing));
580        self
581    }
582
583    /// Sets the pressed scale and tint opacity.
584    pub const fn press_feedback(mut self, scale: f32, tint_alpha: f32) -> Self {
585        self.press_feedback = Some((scale, tint_alpha));
586        self
587    }
588
589    /// The name assistive technology reads for this node. On a widget with a visible label
590    /// it replaces that label; an empty name is the same as none.
591    ///
592    /// Names are kept for the life of the program, and at most 4096 distinct names are
593    /// kept. A name past that limit is left off (debug builds print a warning once), so
594    /// use a fixed set of names rather than one built from changing data.
595    pub fn label(mut self, label: &str) -> Self {
596        self.semantics_label = keep_label(label);
597        self
598    }
599
600    /// What this node is to assistive technology, for a node that is not a widget with
601    /// a role of its own: a `Box` that is pressed is a button.
602    pub const fn role(mut self, role: Role) -> Self {
603        self.semantics_role = Some(role);
604        self
605    }
606
607    /// Whether the node is checked, as a toggle announces it.
608    pub const fn checked(mut self, checked: bool) -> Self {
609        self.semantics_state.checked = Some(checked);
610        self
611    }
612
613    /// Whether the node is the selected one of its group.
614    pub const fn selected(mut self, selected: bool) -> Self {
615        self.semantics_state.selected = Some(selected);
616        self
617    }
618
619    /// Whether the node is expanded, as a disclosure or a tree item announces it.
620    pub const fn expanded(mut self, expanded: bool) -> Self {
621        self.semantics_state.expanded = Some(expanded);
622        self
623    }
624
625    /// Whether the node is disabled for assistive technology.
626    pub const fn disabled(mut self, disabled: bool) -> Self {
627        self.semantics_state.disabled = Some(disabled);
628        self
629    }
630
631    /// Whether the node holds the focus, as assistive technology reads it.
632    pub const fn focused(mut self, focused: bool) -> Self {
633        self.semantics_state.focused = Some(focused);
634        self
635    }
636
637    /// Leaves the node out of the accessibility tree when true.
638    pub const fn hidden(mut self, hidden: bool) -> Self {
639        self.semantics_state.hidden = hidden;
640        self
641    }
642
643    /// Exposes a click action on the node, which runs the node's click handler.
644    pub const fn click_action(mut self, exposed: bool) -> Self {
645        self.semantics_state.click_action = exposed;
646        self
647    }
648
649    /// Exposes a focus action on the node.
650    pub const fn focus_action(mut self, exposed: bool) -> Self {
651        self.semantics_state.focus_action = exposed;
652        self
653    }
654
655    /// The role, the name and the state in one call.
656    pub fn semantics(self, role: Role, label: &str, state: SemanticsState) -> Self {
657        let mut named = self.role(role).label(label);
658        named.semantics_state = state;
659        named
660    }
661
662    /// Compose's `graphicsLayer`: a rotation, a scale and a translation about an origin,
663    /// drawn without moving the layout, and an opacity for the whole group. A pointer is
664    /// tested against the shape that is drawn.
665    pub fn graphics_layer(mut self, layer: crate::GraphicsLayer) -> Self {
666        if let crate::schema::Modifier::Transform {
667            a,
668            b,
669            c,
670            d,
671            e,
672            f,
673            origin_x,
674            origin_y,
675        } = layer.transform()
676        {
677            self.transform = Some([a, b, c, d, e, f, origin_x, origin_y]);
678        }
679        self.alpha = Some(layer.alpha);
680        self
681    }
682
683    /// An opacity the Renderer animates to, from [`crate::animate_float_as_state`]. It
684    /// takes the place of `alpha`.
685    pub const fn animated_alpha(mut self, value: crate::Animated<f32>) -> Self {
686        self.animated_alpha = Some(value);
687        self
688    }
689
690    /// A background the Renderer animates to, from [`crate::animate_color_as_state`]. It
691    /// takes the place of `background`.
692    pub const fn animated_background(mut self, value: crate::Animated<Paint>) -> Self {
693        self.animated_background = Some(value);
694        self
695    }
696
697    /// A translation the Renderer animates to, from [`crate::animate_offset_as_state`].
698    /// It takes the place of the transform a `graphics_layer` would draw.
699    pub const fn animated_offset(mut self, value: crate::Animated<(f32, f32)>) -> Self {
700        self.animated_offset = Some(value);
701        self
702    }
703
704    /// The cursor the platform shows over the node. CSS `cursor`: the deepest node under
705    /// the pointer that names one other than `Cursor::Auto` wins, and while a node holds a
706    /// pointer capture its cursor stays.
707    pub const fn cursor(mut self, cursor: crate::Cursor) -> Self {
708        self.cursor = Some(cursor);
709        self
710    }
711
712    /// Whether the text under the node can be selected. CSS `user-select`; `Auto` follows
713    /// the parent.
714    pub const fn user_select(mut self, user_select: crate::UserSelect) -> Self {
715        self.user_select = Some(user_select);
716        self
717    }
718}
719
720/// Which Modifier attributes a widget's `rsx!` counterpart always writes. The others are
721/// written only when they are set.
722#[derive(Clone, Copy, PartialEq, Eq)]
723pub(crate) enum Mods {
724    /// The containers that take every Modifier: Column, Row, Box, Card, Surface.
725    Container,
726    /// The thirteen every other widget takes.
727    Standard,
728    /// A chip's six.
729    Chip,
730    /// A Scaffold's background and material.
731    Scaffold,
732    /// None.
733    None,
734}
735
736pub(crate) type Attrs = Vec<(&'static str, OwnedAttr)>;
737
738fn float(value: Option<f32>) -> OwnedAttr {
739    value.map_or(OwnedAttr::None, |value| OwnedAttr::Float(f64::from(value)))
740}
741
742fn role(value: Option<impl Into<u16>>) -> OwnedAttr {
743    value.map_or(OwnedAttr::None, |value| {
744        OwnedAttr::Int(i64::from(value.into()))
745    })
746}
747
748fn paint(value: Option<Paint>) -> OwnedAttr {
749    value.map_or(OwnedAttr::None, |paint| {
750        OwnedAttr::Int(paint.to_bits() as i64)
751    })
752}
753
754/// A role that was not set is tag 0, which means "not sent".
755pub(crate) fn role_or_zero(value: Option<impl Into<u16>>) -> OwnedAttr {
756    OwnedAttr::Int(value.map_or(0, |value| i64::from(value.into())))
757}
758
759/// A dp value that was not set is 0.
760pub(crate) fn dp_or_zero(value: Option<f32>) -> OwnedAttr {
761    OwnedAttr::Float(f64::from(value.unwrap_or(0.0)))
762}
763
764/// A paint that was not set is 0.
765pub(crate) fn paint_or_zero(value: Option<Paint>) -> OwnedAttr {
766    OwnedAttr::Int(value.map_or(0, |paint| paint.to_bits() as i64))
767}
768
769pub(crate) fn optional_text(value: Option<String>) -> OwnedAttr {
770    value.map_or(OwnedAttr::None, OwnedAttr::Text)
771}
772
773/// The role and state of a Modifier as the one integer the `semantics` attribute carries,
774/// or none when neither was given.
775fn semantics_bits(modifier: &Modifier) -> Option<i64> {
776    let state = modifier.semantics_state;
777    if modifier.semantics_role.is_none() && state == SemanticsState::NONE {
778        return None;
779    }
780    let role = i64::from(u16::from(
781        modifier.semantics_role.unwrap_or(SemanticsRole::None),
782    ));
783    Some(role | (i64::from(state.bits()) << 16))
784}
785
786/// Writes the Modifier attributes, in the order the `rsx!` widgets write them.
787pub(crate) fn push_modifiers(modifier: &Modifier, mods: Mods, out: &mut Attrs) {
788    let container = mods == Mods::Container;
789    let standard = container || mods == Mods::Standard;
790    let chip = standard || mods == Mods::Chip;
791    let scaffold = mods == Mods::Scaffold;
792    let mut push = |always: bool, name: &'static str, value: OwnedAttr| {
793        let set = !matches!(value, OwnedAttr::None | OwnedAttr::Bool(false));
794        if always || set {
795            out.push((name, value));
796        }
797    };
798    // One slot, two names: whichever is not in use goes out unset, which clears nothing
799    // because only the name that wrote a slot may clear it.
800    let (plain, every_dp) = match modifier.observe_size {
801        Some(token) if modifier.observe_size_every_dp => (OwnedAttr::None, OwnedAttr::Int(token)),
802        Some(token) => (OwnedAttr::Int(token), OwnedAttr::None),
803        None => (OwnedAttr::None, OwnedAttr::None),
804    };
805    push(container, "observe_size", plain);
806    push(container, "observe_size_every_dp", every_dp);
807    push(container, "motion", role(modifier.motion));
808    push(container || scaffold, "material", role(modifier.material));
809    push(chip, "weight", float(modifier.weight));
810    push(chip, "width", float(modifier.width));
811    push(chip, "height", float(modifier.height));
812    push(chip, "padding", float(modifier.padding));
813    push(chip, "padding_role", role(modifier.padding_role));
814    // An animated background owns the background's slot while it is set.
815    push(
816        standard || scaffold,
817        "background",
818        paint(if modifier.animated_background.is_some() {
819            None
820        } else {
821            modifier.background
822        }),
823    );
824    push(standard, "shape_role", role(modifier.shape_role));
825    push(standard, "corner_radius", float(modifier.corner_radius));
826    push(
827        false,
828        "corner_radii",
829        modifier.corner_radii.map_or(OwnedAttr::None, |values| {
830            OwnedAttr::Bytes(
831                values
832                    .iter()
833                    .flat_map(|value| value.to_le_bytes())
834                    .collect(),
835            )
836        }),
837    );
838    push(false, "corner_curve", float(modifier.corner_curve));
839    push(false, "clip", OwnedAttr::Bool(modifier.clip));
840    push(
841        true,
842        "window_drag",
843        modifier
844            .window_drag
845            .map_or(OwnedAttr::None, OwnedAttr::Bool),
846    );
847    push(standard, "border_width", float(modifier.border_width));
848    push(standard, "border_color", paint(modifier.border_color));
849    push(standard, "elevation", float(modifier.elevation));
850    push(
851        false,
852        "match_parent_size",
853        OwnedAttr::Bool(modifier.match_parent_size),
854    );
855    push(
856        chip,
857        "fill_max_width",
858        OwnedAttr::Bool(modifier.fill_max_width),
859    );
860    push(
861        standard,
862        "fill_max_height",
863        OwnedAttr::Bool(modifier.fill_max_height),
864    );
865    // The layer and the values the Renderer animates, written only when they are set. An
866    // animated value owns its slot while it is set, as an animated background does above.
867    push(
868        false,
869        "press_feedback",
870        modifier
871            .press_feedback
872            .map_or(OwnedAttr::None, |(scale, alpha)| {
873                OwnedAttr::Bytes([scale.to_le_bytes(), alpha.to_le_bytes()].concat())
874            }),
875    );
876    // The role and state travel as one integer, the role in the low 16 bits; the name is a
877    // property of the node because a string does not fit in a modifier's words.
878    push(
879        false,
880        "semantics",
881        semantics_bits(modifier).map_or(OwnedAttr::None, OwnedAttr::Int),
882    );
883    push(
884        false,
885        "accessible_name",
886        modifier
887            .semantics_label
888            .map_or(OwnedAttr::None, OwnedAttr::Label),
889    );
890    push(
891        false,
892        "alpha",
893        float(if modifier.animated_alpha.is_some() {
894            None
895        } else {
896            modifier.alpha
897        }),
898    );
899    push(
900        false,
901        "transform",
902        match (modifier.animated_offset, modifier.transform) {
903            (None, Some(values)) => OwnedAttr::Bytes(
904                values
905                    .iter()
906                    .flat_map(|value| value.to_le_bytes())
907                    .collect(),
908            ),
909            _ => OwnedAttr::None,
910        },
911    );
912    // Written only when set, and `Auto` is the same as not set.
913    push(
914        false,
915        "cursor",
916        match modifier.cursor {
917            Some(crate::Cursor::Auto) | None => OwnedAttr::None,
918            Some(cursor) => OwnedAttr::Int(i64::from(u16::from(cursor))),
919        },
920    );
921    push(
922        false,
923        "user_select",
924        match modifier.user_select {
925            Some(crate::UserSelect::Auto) | None => OwnedAttr::None,
926            Some(select) => OwnedAttr::Int(i64::from(u16::from(select))),
927        },
928    );
929    // The layers and the displacement, written only when they are set.
930    push(
931        false,
932        "offset",
933        modifier.offset.map_or(OwnedAttr::None, |(x, y)| {
934            let mut bytes = Vec::with_capacity(8);
935            bytes.extend_from_slice(&x.to_le_bytes());
936            bytes.extend_from_slice(&y.to_le_bytes());
937            OwnedAttr::Bytes(bytes)
938        }),
939    );
940    push(
941        false,
942        "shadows",
943        if modifier.shadows[0].is_none() {
944            OwnedAttr::None
945        } else {
946            let mut bytes = Vec::new();
947            for shadow in modifier.shadows.iter().flatten() {
948                for value in [shadow.x, shadow.y, shadow.blur, shadow.spread] {
949                    bytes.extend_from_slice(&value.to_le_bytes());
950                }
951                bytes.extend_from_slice(&shadow.paint.to_bits().to_le_bytes());
952            }
953            OwnedAttr::Bytes(bytes)
954        },
955    );
956    push(
957        false,
958        "animated_alpha",
959        modifier.animated_alpha.map_or(OwnedAttr::None, |value| {
960            OwnedAttr::Bytes(crate::animation::animated_bytes(
961                &[value.target.to_bits()],
962                &value.spec,
963            ))
964        }),
965    );
966    push(
967        false,
968        "animated_background",
969        modifier
970            .animated_background
971            .map_or(OwnedAttr::None, |value| {
972                let bits = value.target.to_bits();
973                OwnedAttr::Bytes(crate::animation::animated_bytes(
974                    &[bits as u32, (bits >> 32) as u32],
975                    &value.spec,
976                ))
977            }),
978    );
979    push(
980        false,
981        "animated_offset",
982        modifier.animated_offset.map_or(OwnedAttr::None, |value| {
983            OwnedAttr::Bytes(crate::animation::animated_bytes(
984                &[value.target.0.to_bits(), value.target.1.to_bits()],
985                &value.spec,
986            ))
987        }),
988    );
989}
990
991/// Composes one widget node: its group, its attributes and listeners, and its children.
992///
993/// `fill` writes the attributes and runs while the composer is held, so it only moves
994/// values out of the builder. `content` runs afterwards, with the node as the parent of
995/// whatever it composes.
996pub(crate) fn emit(
997    key: u64,
998    widget: WidgetKind,
999    fill: impl FnOnce(&mut Attrs),
1000    listeners: Vec<(&'static str, EventCallback)>,
1001    content: impl FnOnce(),
1002) {
1003    let _group = group_guard(key);
1004    emit_node(widget, fill, listeners, content);
1005}
1006
1007/// The same, inside a group the caller has already opened, for a widget that remembers
1008/// something of its own before its node.
1009pub(crate) fn emit_node(
1010    widget: WidgetKind,
1011    fill: impl FnOnce(&mut Attrs),
1012    listeners: Vec<(&'static str, EventCallback)>,
1013    content: impl FnOnce(),
1014) {
1015    let node = with_composer_if_idle(|composer| {
1016        if !composer.composing() {
1017            return None;
1018        }
1019        let mut attrs = composer.take_attrs();
1020        fill(&mut attrs);
1021        Some(composer.update_node(widget, attrs, listeners))
1022    })
1023    .flatten();
1024    let Some(node) = node else {
1025        return;
1026    };
1027    with_composer(|composer| composer.push_node_context(node));
1028    let _context = NodeContextGuard;
1029    content();
1030}
1031
1032/// Closes a widget's node context on every way out of its content, an unwind included.
1033struct NodeContextGuard;
1034
1035impl Drop for NodeContextGuard {
1036    fn drop(&mut self) {
1037        with_composer_if_idle(|composer| composer.pop_node_context());
1038    }
1039}
1040
1041/// A call site key for a widget, from its name.
1042pub(crate) const fn widget_key(name: &str) -> u64 {
1043    crate::runtime::composer::call_site(name, 0)
1044}
1045
1046#[cfg(test)]
1047mod fr19_9_tests {
1048    use super::*;
1049
1050    #[test]
1051    fn fr19_9_window_drag_builder_preserves_false() {
1052        for enabled in [false, true] {
1053            let mut attrs = Vec::new();
1054            push_modifiers(
1055                &Modifier::new().window_drag(enabled),
1056                Mods::Standard,
1057                &mut attrs,
1058            );
1059            assert!(attrs.iter().any(|(name, value)| *name == "window_drag"
1060                && matches!(value, OwnedAttr::Bool(value) if *value == enabled)));
1061        }
1062    }
1063}
1064
1065#[cfg(test)]
1066mod fr68_label_tests {
1067    use super::*;
1068
1069    #[test]
1070    fn fr68_repeated_labels_do_not_grow_the_table() {
1071        let table = LabelTable::new();
1072        let first = table.intern("Refresh", MAX_LABELS).unwrap();
1073        for _ in 0..10_000 {
1074            let again = table.intern("Refresh", MAX_LABELS).unwrap();
1075            assert!(std::ptr::eq(first, again));
1076        }
1077        assert_eq!(table.len(), 1);
1078    }
1079
1080    #[test]
1081    fn fr68_labels_past_the_cap_are_dropped() {
1082        let table = LabelTable::new();
1083        for n in 0..MAX_LABELS {
1084            assert!(table.intern(&format!("label {n}"), MAX_LABELS).is_some());
1085        }
1086        assert_eq!(table.len(), MAX_LABELS);
1087        assert_eq!(table.intern("one too many", MAX_LABELS), None);
1088        assert_eq!(table.len(), MAX_LABELS);
1089        // A name already kept is still found.
1090        assert!(table.intern("label 0", MAX_LABELS).is_some());
1091    }
1092}