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/// The most shadow layers one node can carry through this API.
63pub const MAX_SHADOWS: usize = 4;
64
65/// One layer of a drop shadow, as CSS `box-shadow` writes it: an offset, a blur, a spread
66/// and a colour with its alpha. It follows the corners of the node that casts it.
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct Shadow {
69    pub x: f32,
70    pub y: f32,
71    pub blur: f32,
72    pub spread: f32,
73    pub paint: Paint,
74}
75
76impl Shadow {
77    pub const fn new(x: f32, y: f32, blur: f32, spread: f32, paint: Paint) -> Self {
78        Self {
79            x,
80            y,
81            blur,
82            spread,
83            paint,
84        }
85    }
86}
87
88/// What sits around a widget: its size, its surface, its outline, how far its content
89/// is inset, and what the Renderer reports about it. Compose's `Modifier`.
90///
91/// Written the way Compose writes it, from the empty value `Modifier`:
92///
93/// ```ignore
94/// Card().modifier(Modifier.fill_max_width().padding(16.0).background(Paint::Role(ColorRole::Surface)))
95/// ```
96///
97/// The order of the calls does not matter. Each kind of Modifier has a fixed slot in the
98/// node's chain, outside in: size, shape, fill, outline, lift, press, inset. That is what
99/// the `rsx!` attributes do too, so the same Modifier draws the same either way.
100#[derive(Clone, Copy, Debug, Default, PartialEq)]
101pub struct Modifier {
102    observe_size: Option<i64>,
103    motion: Option<MotionRole>,
104    material: Option<MaterialRole>,
105    weight: Option<f32>,
106    width: Option<f32>,
107    height: Option<f32>,
108    padding: Option<f32>,
109    padding_role: Option<SpaceRole>,
110    background: Option<Paint>,
111    shape_role: Option<ShapeRole>,
112    corner_radius: Option<f32>,
113    corner_radii: Option<[f32; 4]>,
114    border_width: Option<f32>,
115    border_color: Option<Paint>,
116    elevation: Option<f32>,
117    alpha: Option<f32>,
118    transform: Option<[f32; 8]>,
119    animated_alpha: Option<crate::Animated<f32>>,
120    animated_background: Option<crate::Animated<Paint>>,
121    animated_offset: Option<crate::Animated<(f32, f32)>>,
122    cursor: Option<crate::Cursor>,
123    user_select: Option<crate::UserSelect>,
124    clip: bool,
125    offset: Option<(f32, f32)>,
126    shadows: [Option<Shadow>; MAX_SHADOWS],
127    fill_max_width: bool,
128    fill_max_height: bool,
129}
130
131/// The empty Modifier, to start a chain from: `Modifier.padding(8.0)`.
132#[allow(non_upper_case_globals)]
133pub const Modifier: Modifier = Modifier::new();
134
135impl Modifier {
136    /// The empty Modifier. The same as the value `Modifier`.
137    pub const fn new() -> Self {
138        Self {
139            observe_size: None,
140            motion: None,
141            material: None,
142            weight: None,
143            width: None,
144            height: None,
145            padding: None,
146            padding_role: None,
147            background: None,
148            shape_role: None,
149            corner_radius: None,
150            corner_radii: None,
151            border_width: None,
152            border_color: None,
153            elevation: None,
154            alpha: None,
155            transform: None,
156            animated_alpha: None,
157            animated_background: None,
158            animated_offset: None,
159            cursor: None,
160            user_select: None,
161            clip: false,
162            offset: None,
163            shadows: [None; MAX_SHADOWS],
164            fill_max_width: false,
165            fill_max_height: false,
166        }
167    }
168
169    /// A share of the space left in a `Row` or `Column`.
170    pub const fn weight(mut self, weight: f32) -> Self {
171        self.weight = Some(weight);
172        self
173    }
174
175    pub const fn width(mut self, dp: f32) -> Self {
176        self.width = Some(dp);
177        self
178    }
179
180    pub const fn height(mut self, dp: f32) -> Self {
181        self.height = Some(dp);
182        self
183    }
184
185    /// Both at once.
186    pub const fn size(self, width: f32, height: f32) -> Self {
187        self.width(width).height(height)
188    }
189
190    /// A width that may be unset, for a screen whose measure depends on the window.
191    pub const fn width_if(mut self, dp: Option<f32>) -> Self {
192        self.width = dp;
193        self
194    }
195
196    pub const fn fill_max_width(mut self) -> Self {
197        self.fill_max_width = true;
198        self
199    }
200
201    pub const fn fill_max_height(mut self) -> Self {
202        self.fill_max_height = true;
203        self
204    }
205
206    pub const fn fill_max_size(self) -> Self {
207        self.fill_max_width().fill_max_height()
208    }
209
210    /// Fills the width only when `fill` is true.
211    pub const fn fill_max_width_if(mut self, fill: bool) -> Self {
212        self.fill_max_width = fill;
213        self
214    }
215
216    /// The same inset on every side, in dp.
217    pub const fn padding(mut self, dp: f32) -> Self {
218        self.padding = Some(dp);
219        self
220    }
221
222    /// An inset from the design system's spacing ladder. Wins over `padding`.
223    pub const fn padding_role(mut self, role: SpaceRole) -> Self {
224        self.padding_role = Some(role);
225        self
226    }
227
228    /// The same, when the inset may be unset.
229    pub const fn padding_role_if(mut self, role: Option<SpaceRole>) -> Self {
230        self.padding_role = role;
231        self
232    }
233
234    pub const fn background(mut self, paint: Paint) -> Self {
235        self.background = Some(paint);
236        self
237    }
238
239    /// The same, when there may be no background.
240    pub const fn background_if(mut self, paint: Option<Paint>) -> Self {
241        self.background = paint;
242        self
243    }
244
245    /// A shape from the design system's ladder.
246    pub const fn shape_role(mut self, role: ShapeRole) -> Self {
247        self.shape_role = Some(role);
248        self
249    }
250
251    /// A literal corner, in dp. Wins over `shape_role`.
252    pub const fn corner_radius(mut self, dp: f32) -> Self {
253        self.corner_radius = Some(dp);
254        self.corner_radii = None;
255        self
256    }
257
258    /// Individual corner radii `[top_start, top_end, bottom_end, bottom_start]`, in dp.
259    /// Wins over `shape_role` and `corner_radius`.
260    pub const fn corner_radii(mut self, radii: [f32; 4]) -> Self {
261        self.corner_radii = Some(radii);
262        self.corner_radius = None;
263        self
264    }
265
266    /// Clips the node and its children to its shape, or to its rectangle when it has none.
267    pub const fn clip(mut self, clip: bool) -> Self {
268        self.clip = clip;
269        self
270    }
271
272    pub const fn border(mut self, width: f32, paint: Paint) -> Self {
273        self.border_width = Some(width);
274        self.border_color = Some(paint);
275        self
276    }
277
278    /// A resting height above the default, in dp. How it is drawn is the design system's.
279    pub const fn elevation(mut self, dp: f32) -> Self {
280        self.elevation = Some(dp);
281        self
282    }
283
284    /// Asks the Renderer to report this node's size under the token from
285    /// [`crate::runtime::remember_node_size`].
286    pub const fn observe_size(mut self, token: i64) -> Self {
287        self.observe_size = Some(token);
288        self
289    }
290
291    /// How important this node's changes are. The curve is the design system's.
292    pub const fn motion(mut self, role: MotionRole) -> Self {
293        self.motion = Some(role);
294        self
295    }
296
297    /// What this node's surface is made of. The effect is the design system's.
298    pub const fn material(mut self, role: MaterialRole) -> Self {
299        self.material = Some(role);
300        self
301    }
302
303    /// Moves the node from where it was laid out, in dp, without moving anything else.
304    pub const fn offset(mut self, x: f32, y: f32) -> Self {
305        self.offset = Some((x, y));
306        self
307    }
308
309    /// Adds a shadow layer. Layers are drawn in the order they are added, the first at
310    /// the bottom. A node takes at most [`MAX_SHADOWS`]; a layer past that is ignored.
311    pub const fn shadow(mut self, shadow: Shadow) -> Self {
312        let mut index = 0;
313        while index < MAX_SHADOWS {
314            if self.shadows[index].is_none() {
315                self.shadows[index] = Some(shadow);
316                break;
317            }
318            index += 1;
319        }
320        self
321    }
322
323    /// Adds several shadow layers, in order.
324    pub fn shadows(mut self, shadows: impl IntoIterator<Item = Shadow>) -> Self {
325        for shadow in shadows {
326            self = self.shadow(shadow);
327        }
328        self
329    }
330
331    /// An opacity for the node and everything in it, faded as one group.
332    pub const fn alpha(mut self, alpha: f32) -> Self {
333        self.alpha = Some(alpha);
334        self
335    }
336
337    /// Compose's `graphicsLayer`: a rotation, a scale and a translation about an origin,
338    /// drawn without moving the layout, and an opacity for the whole group. A pointer is
339    /// tested against the shape that is drawn.
340    pub fn graphics_layer(mut self, layer: crate::GraphicsLayer) -> Self {
341        if let crate::schema::Modifier::Transform {
342            a,
343            b,
344            c,
345            d,
346            e,
347            f,
348            origin_x,
349            origin_y,
350        } = layer.transform()
351        {
352            self.transform = Some([a, b, c, d, e, f, origin_x, origin_y]);
353        }
354        self.alpha = Some(layer.alpha);
355        self
356    }
357
358    /// An opacity the Renderer animates to, from [`crate::animate_float_as_state`]. It
359    /// takes the place of `alpha`.
360    pub const fn animated_alpha(mut self, value: crate::Animated<f32>) -> Self {
361        self.animated_alpha = Some(value);
362        self
363    }
364
365    /// A background the Renderer animates to, from [`crate::animate_color_as_state`]. It
366    /// takes the place of `background`.
367    pub const fn animated_background(mut self, value: crate::Animated<Paint>) -> Self {
368        self.animated_background = Some(value);
369        self
370    }
371
372    /// A translation the Renderer animates to, from [`crate::animate_offset_as_state`].
373    /// It takes the place of the transform a `graphics_layer` would draw.
374    pub const fn animated_offset(mut self, value: crate::Animated<(f32, f32)>) -> Self {
375        self.animated_offset = Some(value);
376        self
377    }
378
379    /// The cursor the platform shows over the node. CSS `cursor`: the deepest node under
380    /// the pointer that names one other than `Cursor::Auto` wins, and while a node holds a
381    /// pointer capture its cursor stays.
382    pub const fn cursor(mut self, cursor: crate::Cursor) -> Self {
383        self.cursor = Some(cursor);
384        self
385    }
386
387    /// Whether the text under the node can be selected. CSS `user-select`; `Auto` follows
388    /// the parent.
389    pub const fn user_select(mut self, user_select: crate::UserSelect) -> Self {
390        self.user_select = Some(user_select);
391        self
392    }
393}
394
395/// Which Modifier attributes a widget's `rsx!` counterpart always writes. The others are
396/// written only when they are set.
397#[derive(Clone, Copy, PartialEq, Eq)]
398pub(crate) enum Mods {
399    /// The containers that take every Modifier: Column, Row, Box, Card, Surface.
400    Container,
401    /// The thirteen every other widget takes.
402    Standard,
403    /// A chip's six.
404    Chip,
405    /// A Scaffold's background and material.
406    Scaffold,
407    /// None.
408    None,
409}
410
411pub(crate) type Attrs = Vec<(&'static str, OwnedAttr)>;
412
413fn float(value: Option<f32>) -> OwnedAttr {
414    value.map_or(OwnedAttr::None, |value| OwnedAttr::Float(f64::from(value)))
415}
416
417fn role(value: Option<impl Into<u16>>) -> OwnedAttr {
418    value.map_or(OwnedAttr::None, |value| {
419        OwnedAttr::Int(i64::from(value.into()))
420    })
421}
422
423fn paint(value: Option<Paint>) -> OwnedAttr {
424    value.map_or(OwnedAttr::None, |paint| {
425        OwnedAttr::Int(paint.to_bits() as i64)
426    })
427}
428
429/// A role that was not set is tag 0, which means "not sent".
430pub(crate) fn role_or_zero(value: Option<impl Into<u16>>) -> OwnedAttr {
431    OwnedAttr::Int(value.map_or(0, |value| i64::from(value.into())))
432}
433
434/// A dp value that was not set is 0.
435pub(crate) fn dp_or_zero(value: Option<f32>) -> OwnedAttr {
436    OwnedAttr::Float(f64::from(value.unwrap_or(0.0)))
437}
438
439/// A paint that was not set is 0.
440pub(crate) fn paint_or_zero(value: Option<Paint>) -> OwnedAttr {
441    OwnedAttr::Int(value.map_or(0, |paint| paint.to_bits() as i64))
442}
443
444pub(crate) fn optional_text(value: Option<String>) -> OwnedAttr {
445    value.map_or(OwnedAttr::None, OwnedAttr::Text)
446}
447
448/// Writes the Modifier attributes, in the order the `rsx!` widgets write them.
449pub(crate) fn push_modifiers(modifier: &Modifier, mods: Mods, out: &mut Attrs) {
450    let container = mods == Mods::Container;
451    let standard = container || mods == Mods::Standard;
452    let chip = standard || mods == Mods::Chip;
453    let scaffold = mods == Mods::Scaffold;
454    let mut push = |always: bool, name: &'static str, value: OwnedAttr| {
455        let set = !matches!(value, OwnedAttr::None | OwnedAttr::Bool(false));
456        if always || set {
457            out.push((name, value));
458        }
459    };
460    push(
461        container,
462        "observe_size",
463        modifier
464            .observe_size
465            .map_or(OwnedAttr::None, OwnedAttr::Int),
466    );
467    push(container, "motion", role(modifier.motion));
468    push(container || scaffold, "material", role(modifier.material));
469    push(chip, "weight", float(modifier.weight));
470    push(chip, "width", float(modifier.width));
471    push(chip, "height", float(modifier.height));
472    push(chip, "padding", float(modifier.padding));
473    push(chip, "padding_role", role(modifier.padding_role));
474    // An animated background owns the background's slot while it is set.
475    push(
476        standard || scaffold,
477        "background",
478        paint(if modifier.animated_background.is_some() {
479            None
480        } else {
481            modifier.background
482        }),
483    );
484    push(standard, "shape_role", role(modifier.shape_role));
485    push(standard, "corner_radius", float(modifier.corner_radius));
486    push(
487        false,
488        "corner_radii",
489        modifier.corner_radii.map_or(OwnedAttr::None, |values| {
490            OwnedAttr::Bytes(
491                values
492                    .iter()
493                    .flat_map(|value| value.to_le_bytes())
494                    .collect(),
495            )
496        }),
497    );
498    push(false, "clip", OwnedAttr::Bool(modifier.clip));
499    push(standard, "border_width", float(modifier.border_width));
500    push(standard, "border_color", paint(modifier.border_color));
501    push(standard, "elevation", float(modifier.elevation));
502    push(
503        chip,
504        "fill_max_width",
505        OwnedAttr::Bool(modifier.fill_max_width),
506    );
507    push(
508        standard,
509        "fill_max_height",
510        OwnedAttr::Bool(modifier.fill_max_height),
511    );
512    // The layer and the values the Renderer animates, written only when they are set. An
513    // animated value owns its slot while it is set, as an animated background does above.
514    push(
515        false,
516        "alpha",
517        float(if modifier.animated_alpha.is_some() {
518            None
519        } else {
520            modifier.alpha
521        }),
522    );
523    push(
524        false,
525        "transform",
526        match (modifier.animated_offset, modifier.transform) {
527            (None, Some(values)) => OwnedAttr::Bytes(
528                values
529                    .iter()
530                    .flat_map(|value| value.to_le_bytes())
531                    .collect(),
532            ),
533            _ => OwnedAttr::None,
534        },
535    );
536    // Written only when set, and `Auto` is the same as not set.
537    push(
538        false,
539        "cursor",
540        match modifier.cursor {
541            Some(crate::Cursor::Auto) | None => OwnedAttr::None,
542            Some(cursor) => OwnedAttr::Int(i64::from(u16::from(cursor))),
543        },
544    );
545    push(
546        false,
547        "user_select",
548        match modifier.user_select {
549            Some(crate::UserSelect::Auto) | None => OwnedAttr::None,
550            Some(select) => OwnedAttr::Int(i64::from(u16::from(select))),
551        },
552    );
553    // The layers and the displacement, written only when they are set.
554    push(
555        false,
556        "offset",
557        modifier.offset.map_or(OwnedAttr::None, |(x, y)| {
558            let mut bytes = Vec::with_capacity(8);
559            bytes.extend_from_slice(&x.to_le_bytes());
560            bytes.extend_from_slice(&y.to_le_bytes());
561            OwnedAttr::Bytes(bytes)
562        }),
563    );
564    push(
565        false,
566        "shadows",
567        if modifier.shadows[0].is_none() {
568            OwnedAttr::None
569        } else {
570            let mut bytes = Vec::new();
571            for shadow in modifier.shadows.iter().flatten() {
572                for value in [shadow.x, shadow.y, shadow.blur, shadow.spread] {
573                    bytes.extend_from_slice(&value.to_le_bytes());
574                }
575                bytes.extend_from_slice(&shadow.paint.to_bits().to_le_bytes());
576            }
577            OwnedAttr::Bytes(bytes)
578        },
579    );
580    push(
581        false,
582        "animated_alpha",
583        modifier.animated_alpha.map_or(OwnedAttr::None, |value| {
584            OwnedAttr::Bytes(crate::animation::animated_bytes(
585                &[value.target.to_bits()],
586                &value.spec,
587            ))
588        }),
589    );
590    push(
591        false,
592        "animated_background",
593        modifier
594            .animated_background
595            .map_or(OwnedAttr::None, |value| {
596                let bits = value.target.to_bits();
597                OwnedAttr::Bytes(crate::animation::animated_bytes(
598                    &[bits as u32, (bits >> 32) as u32],
599                    &value.spec,
600                ))
601            }),
602    );
603    push(
604        false,
605        "animated_offset",
606        modifier.animated_offset.map_or(OwnedAttr::None, |value| {
607            OwnedAttr::Bytes(crate::animation::animated_bytes(
608                &[value.target.0.to_bits(), value.target.1.to_bits()],
609                &value.spec,
610            ))
611        }),
612    );
613}
614
615/// Composes one widget node: its group, its attributes and listeners, and its children.
616///
617/// `fill` writes the attributes and runs while the composer is held, so it only moves
618/// values out of the builder. `content` runs afterwards, with the node as the parent of
619/// whatever it composes.
620pub(crate) fn emit(
621    key: u64,
622    widget: WidgetKind,
623    fill: impl FnOnce(&mut Attrs),
624    listeners: Vec<(&'static str, EventCallback)>,
625    content: impl FnOnce(),
626) {
627    let _group = group_guard(key);
628    emit_node(widget, fill, listeners, content);
629}
630
631/// The same, inside a group the caller has already opened, for a widget that remembers
632/// something of its own before its node.
633pub(crate) fn emit_node(
634    widget: WidgetKind,
635    fill: impl FnOnce(&mut Attrs),
636    listeners: Vec<(&'static str, EventCallback)>,
637    content: impl FnOnce(),
638) {
639    let node = with_composer_if_idle(|composer| {
640        if !composer.composing() {
641            return None;
642        }
643        let mut attrs = composer.take_attrs();
644        fill(&mut attrs);
645        Some(composer.update_node(widget, attrs, listeners))
646    })
647    .flatten();
648    let Some(node) = node else {
649        return;
650    };
651    with_composer(|composer| composer.push_node_context(node));
652    let _context = NodeContextGuard;
653    content();
654}
655
656/// Closes a widget's node context on every way out of its content, an unwind included.
657struct NodeContextGuard;
658
659impl Drop for NodeContextGuard {
660    fn drop(&mut self) {
661        with_composer_if_idle(|composer| composer.pop_node_context());
662    }
663}
664
665/// A call site key for a widget, from its name.
666pub(crate) const fn widget_key(name: &str) -> u64 {
667    crate::runtime::composer::call_site(name, 0)
668}