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
29mod lazy;
30mod widgets;
31
32pub use crate::boundary::LaunchBuilder;
33pub use crate::brush::{Brush, Stop, brush};
34pub use crate::drawing::{DrawCommand, DrawList, DrawListBuilder};
35pub use crate::message::{Message, show_message};
36pub use crate::runtime::{application, launch};
37pub use crate::schema::{
38    Alignment, Arrangement, ButtonKind, ButtonVariant, Color, ColorRole, ColorScheme, DesignSystem,
39    IconRole, Key, MaterialRole, MessageDuration, MotionRole, Paint, ShapeRole, SpaceRole,
40    TextAlign, TextOverflow, Theme, TypeRole, WindowHeightClass, WindowSizeClass,
41};
42pub use crate::spans::TextSpans;
43pub use crate::theme::{ThemeHandle, current_theme};
44
45/// The application's theme, to read and to change while it runs. A change is one
46/// `SetTheme` record in the batch the call that made it produces.
47pub fn theme() -> ThemeHandle {
48    ThemeHandle::default()
49}
50pub use crate::window::{NodeSize, WindowSize};
51pub use crate::{
52    Cursor, FileDrop, KeyEvent, PointerEvent, PointerKind, PointerKinds, RangeRequest, UserSelect,
53    asset, pointer_button,
54};
55pub use lazy::{LazyColumn, LazyGrid, LazyList, LazyListScope, LazyRow};
56pub use widgets::*;
57
58use crate::runtime::composer::{EventCallback, OwnedAttr};
59use crate::runtime::{group_guard, with_composer, with_composer_if_idle};
60use crate::schema::WidgetKind;
61
62/// What sits around a widget: its size, its surface, its outline, how far its content
63/// is inset, and what the Renderer reports about it. Compose's `Modifier`.
64///
65/// Written the way Compose writes it, from the empty value `Modifier`:
66///
67/// ```ignore
68/// Card().modifier(Modifier.fill_max_width().padding(16.0).background(Paint::Role(ColorRole::Surface)))
69/// ```
70///
71/// The order of the calls does not matter. Each kind of Modifier has a fixed slot in the
72/// node's chain, outside in: size, shape, fill, outline, lift, press, inset. That is what
73/// the `rsx!` attributes do too, so the same Modifier draws the same either way.
74#[derive(Clone, Copy, Debug, Default, PartialEq)]
75pub struct Modifier {
76    observe_size: Option<i64>,
77    motion: Option<MotionRole>,
78    material: Option<MaterialRole>,
79    weight: Option<f32>,
80    width: Option<f32>,
81    height: Option<f32>,
82    padding: Option<f32>,
83    padding_role: Option<SpaceRole>,
84    background: Option<Paint>,
85    shape_role: Option<ShapeRole>,
86    corner_radius: Option<f32>,
87    border_width: Option<f32>,
88    border_color: Option<Paint>,
89    elevation: Option<f32>,
90    alpha: Option<f32>,
91    transform: Option<[f32; 8]>,
92    animated_alpha: Option<crate::Animated<f32>>,
93    animated_background: Option<crate::Animated<Paint>>,
94    animated_offset: Option<crate::Animated<(f32, f32)>>,
95    cursor: Option<crate::Cursor>,
96    user_select: Option<crate::UserSelect>,
97    fill_max_width: bool,
98    fill_max_height: bool,
99}
100
101/// The empty Modifier, to start a chain from: `Modifier.padding(8.0)`.
102#[allow(non_upper_case_globals)]
103pub const Modifier: Modifier = Modifier::new();
104
105impl Modifier {
106    /// The empty Modifier. The same as the value `Modifier`.
107    pub const fn new() -> Self {
108        Self {
109            observe_size: None,
110            motion: None,
111            material: None,
112            weight: None,
113            width: None,
114            height: None,
115            padding: None,
116            padding_role: None,
117            background: None,
118            shape_role: None,
119            corner_radius: None,
120            border_width: None,
121            border_color: None,
122            elevation: None,
123            alpha: None,
124            transform: None,
125            animated_alpha: None,
126            animated_background: None,
127            animated_offset: None,
128            cursor: None,
129            user_select: None,
130            fill_max_width: false,
131            fill_max_height: false,
132        }
133    }
134
135    /// A share of the space left in a `Row` or `Column`.
136    pub const fn weight(mut self, weight: f32) -> Self {
137        self.weight = Some(weight);
138        self
139    }
140
141    pub const fn width(mut self, dp: f32) -> Self {
142        self.width = Some(dp);
143        self
144    }
145
146    pub const fn height(mut self, dp: f32) -> Self {
147        self.height = Some(dp);
148        self
149    }
150
151    /// Both at once.
152    pub const fn size(self, width: f32, height: f32) -> Self {
153        self.width(width).height(height)
154    }
155
156    /// A width that may be unset, for a screen whose measure depends on the window.
157    pub const fn width_if(mut self, dp: Option<f32>) -> Self {
158        self.width = dp;
159        self
160    }
161
162    pub const fn fill_max_width(mut self) -> Self {
163        self.fill_max_width = true;
164        self
165    }
166
167    pub const fn fill_max_height(mut self) -> Self {
168        self.fill_max_height = true;
169        self
170    }
171
172    pub const fn fill_max_size(self) -> Self {
173        self.fill_max_width().fill_max_height()
174    }
175
176    /// Fills the width only when `fill` is true.
177    pub const fn fill_max_width_if(mut self, fill: bool) -> Self {
178        self.fill_max_width = fill;
179        self
180    }
181
182    /// The same inset on every side, in dp.
183    pub const fn padding(mut self, dp: f32) -> Self {
184        self.padding = Some(dp);
185        self
186    }
187
188    /// An inset from the design system's spacing ladder. Wins over `padding`.
189    pub const fn padding_role(mut self, role: SpaceRole) -> Self {
190        self.padding_role = Some(role);
191        self
192    }
193
194    /// The same, when the inset may be unset.
195    pub const fn padding_role_if(mut self, role: Option<SpaceRole>) -> Self {
196        self.padding_role = role;
197        self
198    }
199
200    pub const fn background(mut self, paint: Paint) -> Self {
201        self.background = Some(paint);
202        self
203    }
204
205    /// The same, when there may be no background.
206    pub const fn background_if(mut self, paint: Option<Paint>) -> Self {
207        self.background = paint;
208        self
209    }
210
211    /// A shape from the design system's ladder.
212    pub const fn shape_role(mut self, role: ShapeRole) -> Self {
213        self.shape_role = Some(role);
214        self
215    }
216
217    /// A literal corner, in dp. Wins over `shape_role`.
218    pub const fn corner_radius(mut self, dp: f32) -> Self {
219        self.corner_radius = Some(dp);
220        self
221    }
222
223    pub const fn border(mut self, width: f32, paint: Paint) -> Self {
224        self.border_width = Some(width);
225        self.border_color = Some(paint);
226        self
227    }
228
229    /// A resting height above the default, in dp. How it is drawn is the design system's.
230    pub const fn elevation(mut self, dp: f32) -> Self {
231        self.elevation = Some(dp);
232        self
233    }
234
235    /// Asks the Renderer to report this node's size under the token from
236    /// [`crate::runtime::remember_node_size`].
237    pub const fn observe_size(mut self, token: i64) -> Self {
238        self.observe_size = Some(token);
239        self
240    }
241
242    /// How important this node's changes are. The curve is the design system's.
243    pub const fn motion(mut self, role: MotionRole) -> Self {
244        self.motion = Some(role);
245        self
246    }
247
248    /// What this node's surface is made of. The effect is the design system's.
249    pub const fn material(mut self, role: MaterialRole) -> Self {
250        self.material = Some(role);
251        self
252    }
253
254    /// An opacity for the node and everything in it, faded as one group.
255    pub const fn alpha(mut self, alpha: f32) -> Self {
256        self.alpha = Some(alpha);
257        self
258    }
259
260    /// Compose's `graphicsLayer`: a rotation, a scale and a translation about an origin,
261    /// drawn without moving the layout, and an opacity for the whole group. A pointer is
262    /// tested against the shape that is drawn.
263    pub fn graphics_layer(mut self, layer: crate::GraphicsLayer) -> Self {
264        if let crate::schema::Modifier::Transform {
265            a,
266            b,
267            c,
268            d,
269            e,
270            f,
271            origin_x,
272            origin_y,
273        } = layer.transform()
274        {
275            self.transform = Some([a, b, c, d, e, f, origin_x, origin_y]);
276        }
277        self.alpha = Some(layer.alpha);
278        self
279    }
280
281    /// An opacity the Renderer animates to, from [`crate::animate_float_as_state`]. It
282    /// takes the place of `alpha`.
283    pub const fn animated_alpha(mut self, value: crate::Animated<f32>) -> Self {
284        self.animated_alpha = Some(value);
285        self
286    }
287
288    /// A background the Renderer animates to, from [`crate::animate_color_as_state`]. It
289    /// takes the place of `background`.
290    pub const fn animated_background(mut self, value: crate::Animated<Paint>) -> Self {
291        self.animated_background = Some(value);
292        self
293    }
294
295    /// A translation the Renderer animates to, from [`crate::animate_offset_as_state`].
296    /// It takes the place of the transform a `graphics_layer` would draw.
297    pub const fn animated_offset(mut self, value: crate::Animated<(f32, f32)>) -> Self {
298        self.animated_offset = Some(value);
299        self
300    }
301
302    /// The cursor the platform shows over the node. CSS `cursor`: the deepest node under
303    /// the pointer that names one other than `Cursor::Auto` wins, and while a node holds a
304    /// pointer capture its cursor stays.
305    pub const fn cursor(mut self, cursor: crate::Cursor) -> Self {
306        self.cursor = Some(cursor);
307        self
308    }
309
310    /// Whether the text under the node can be selected. CSS `user-select`; `Auto` follows
311    /// the parent.
312    pub const fn user_select(mut self, user_select: crate::UserSelect) -> Self {
313        self.user_select = Some(user_select);
314        self
315    }
316}
317
318/// Which Modifier attributes a widget's `rsx!` counterpart always writes. The others are
319/// written only when they are set.
320#[derive(Clone, Copy, PartialEq, Eq)]
321pub(crate) enum Mods {
322    /// The containers that take every Modifier: Column, Row, Box, Card, Surface.
323    Container,
324    /// The thirteen every other widget takes.
325    Standard,
326    /// A chip's six.
327    Chip,
328    /// A Scaffold's background and material.
329    Scaffold,
330    /// None.
331    None,
332}
333
334pub(crate) type Attrs = Vec<(&'static str, OwnedAttr)>;
335
336fn float(value: Option<f32>) -> OwnedAttr {
337    value.map_or(OwnedAttr::None, |value| OwnedAttr::Float(f64::from(value)))
338}
339
340fn role(value: Option<impl Into<u16>>) -> OwnedAttr {
341    value.map_or(OwnedAttr::None, |value| {
342        OwnedAttr::Int(i64::from(value.into()))
343    })
344}
345
346fn paint(value: Option<Paint>) -> OwnedAttr {
347    value.map_or(OwnedAttr::None, |paint| {
348        OwnedAttr::Int(paint.to_bits() as i64)
349    })
350}
351
352/// A role that was not set is tag 0, which means "not sent".
353pub(crate) fn role_or_zero(value: Option<impl Into<u16>>) -> OwnedAttr {
354    OwnedAttr::Int(value.map_or(0, |value| i64::from(value.into())))
355}
356
357/// A dp value that was not set is 0.
358pub(crate) fn dp_or_zero(value: Option<f32>) -> OwnedAttr {
359    OwnedAttr::Float(f64::from(value.unwrap_or(0.0)))
360}
361
362/// A paint that was not set is 0.
363pub(crate) fn paint_or_zero(value: Option<Paint>) -> OwnedAttr {
364    OwnedAttr::Int(value.map_or(0, |paint| paint.to_bits() as i64))
365}
366
367pub(crate) fn optional_text(value: Option<String>) -> OwnedAttr {
368    value.map_or(OwnedAttr::None, OwnedAttr::Text)
369}
370
371/// Writes the Modifier attributes, in the order the `rsx!` widgets write them.
372pub(crate) fn push_modifiers(modifier: &Modifier, mods: Mods, out: &mut Attrs) {
373    let container = mods == Mods::Container;
374    let standard = container || mods == Mods::Standard;
375    let chip = standard || mods == Mods::Chip;
376    let scaffold = mods == Mods::Scaffold;
377    let mut push = |always: bool, name: &'static str, value: OwnedAttr| {
378        let set = !matches!(value, OwnedAttr::None | OwnedAttr::Bool(false));
379        if always || set {
380            out.push((name, value));
381        }
382    };
383    push(
384        container,
385        "observe_size",
386        modifier
387            .observe_size
388            .map_or(OwnedAttr::None, OwnedAttr::Int),
389    );
390    push(container, "motion", role(modifier.motion));
391    push(container || scaffold, "material", role(modifier.material));
392    push(chip, "weight", float(modifier.weight));
393    push(chip, "width", float(modifier.width));
394    push(chip, "height", float(modifier.height));
395    push(chip, "padding", float(modifier.padding));
396    push(chip, "padding_role", role(modifier.padding_role));
397    // An animated background owns the background's slot while it is set.
398    push(
399        standard || scaffold,
400        "background",
401        paint(if modifier.animated_background.is_some() {
402            None
403        } else {
404            modifier.background
405        }),
406    );
407    push(standard, "shape_role", role(modifier.shape_role));
408    push(standard, "corner_radius", float(modifier.corner_radius));
409    push(standard, "border_width", float(modifier.border_width));
410    push(standard, "border_color", paint(modifier.border_color));
411    push(standard, "elevation", float(modifier.elevation));
412    push(
413        chip,
414        "fill_max_width",
415        OwnedAttr::Bool(modifier.fill_max_width),
416    );
417    push(
418        standard,
419        "fill_max_height",
420        OwnedAttr::Bool(modifier.fill_max_height),
421    );
422    // The layer and the values the Renderer animates, written only when they are set. An
423    // animated value owns its slot while it is set, as an animated background does above.
424    push(
425        false,
426        "alpha",
427        float(if modifier.animated_alpha.is_some() {
428            None
429        } else {
430            modifier.alpha
431        }),
432    );
433    push(
434        false,
435        "transform",
436        match (modifier.animated_offset, modifier.transform) {
437            (None, Some(values)) => OwnedAttr::Bytes(
438                values
439                    .iter()
440                    .flat_map(|value| value.to_le_bytes())
441                    .collect(),
442            ),
443            _ => OwnedAttr::None,
444        },
445    );
446    // Written only when set, and `Auto` is the same as not set.
447    push(
448        false,
449        "cursor",
450        match modifier.cursor {
451            Some(crate::Cursor::Auto) | None => OwnedAttr::None,
452            Some(cursor) => OwnedAttr::Int(i64::from(u16::from(cursor))),
453        },
454    );
455    push(
456        false,
457        "user_select",
458        match modifier.user_select {
459            Some(crate::UserSelect::Auto) | None => OwnedAttr::None,
460            Some(select) => OwnedAttr::Int(i64::from(u16::from(select))),
461        },
462    );
463    push(
464        false,
465        "animated_alpha",
466        modifier.animated_alpha.map_or(OwnedAttr::None, |value| {
467            OwnedAttr::Bytes(crate::animation::animated_bytes(
468                &[value.target.to_bits()],
469                &value.spec,
470            ))
471        }),
472    );
473    push(
474        false,
475        "animated_background",
476        modifier
477            .animated_background
478            .map_or(OwnedAttr::None, |value| {
479                let bits = value.target.to_bits();
480                OwnedAttr::Bytes(crate::animation::animated_bytes(
481                    &[bits as u32, (bits >> 32) as u32],
482                    &value.spec,
483                ))
484            }),
485    );
486    push(
487        false,
488        "animated_offset",
489        modifier.animated_offset.map_or(OwnedAttr::None, |value| {
490            OwnedAttr::Bytes(crate::animation::animated_bytes(
491                &[value.target.0.to_bits(), value.target.1.to_bits()],
492                &value.spec,
493            ))
494        }),
495    );
496}
497
498/// Composes one widget node: its group, its attributes and listeners, and its children.
499///
500/// `fill` writes the attributes and runs while the composer is held, so it only moves
501/// values out of the builder. `content` runs afterwards, with the node as the parent of
502/// whatever it composes.
503pub(crate) fn emit(
504    key: u64,
505    widget: WidgetKind,
506    fill: impl FnOnce(&mut Attrs),
507    listeners: Vec<(&'static str, EventCallback)>,
508    content: impl FnOnce(),
509) {
510    let _group = group_guard(key);
511    emit_node(widget, fill, listeners, content);
512}
513
514/// The same, inside a group the caller has already opened, for a widget that remembers
515/// something of its own before its node.
516pub(crate) fn emit_node(
517    widget: WidgetKind,
518    fill: impl FnOnce(&mut Attrs),
519    listeners: Vec<(&'static str, EventCallback)>,
520    content: impl FnOnce(),
521) {
522    let node = with_composer_if_idle(|composer| {
523        if !composer.composing() {
524            return None;
525        }
526        let mut attrs = composer.take_attrs();
527        fill(&mut attrs);
528        Some(composer.update_node(widget, attrs, listeners))
529    })
530    .flatten();
531    let Some(node) = node else {
532        return;
533    };
534    with_composer(|composer| composer.push_node_context(node));
535    let _context = NodeContextGuard;
536    content();
537}
538
539/// Closes a widget's node context on every way out of its content, an unwind included.
540struct NodeContextGuard;
541
542impl Drop for NodeContextGuard {
543    fn drop(&mut self) {
544        with_composer_if_idle(|composer| composer.pop_node_context());
545    }
546}
547
548/// A call site key for a widget, from its name.
549pub(crate) const fn widget_key(name: &str) -> u64 {
550    crate::runtime::composer::call_site(name, 0)
551}