Skip to main content

repose_ui/
lib.rs

1#![allow(non_snake_case)]
2//! # Views, Modifiers, and Layout
3//!
4//! Repose UI is built around three core ideas:
5//!
6//! - `View`: an immutable description of a UI node (cheap to rebuild every frame).
7//! - `Modifier`: layout, styling, and interaction hints attached to a `View`.
8//! - Incremental layout + paint via a persistent engine:
9//!   composition produces a new `View` tree each frame; `LayoutEngine`
10//!   reconciles it into a persistent `ViewTree` (`repose-tree`) and runs
11//!   incremental Taffy layout + paint (with scopes, dirty sets, and paint caches).
12//!
13//! ## Views
14//!
15//! A `View` is a lightweight value that describes *what* to show, not *how* it is
16//! rendered. It is cheap to create; you rebuild the description each frame
17//! (Compose-style). Identity and layout state live in the persistent tree, not
18//! in the `View` values themselves.
19//!
20//! ```rust,ignore
21//! use repose_core::*;
22//! use repose_ui::*;
23//!
24//! fn Counter(count: i32, on_inc: impl Fn() + 'static) -> View {
25//!     Column(Modifier::new().padding(16.0)).child((
26//!         Text(format!("Count = {count}")),
27//!         Button("Increment".into_children(), on_inc),
28//!     ))
29//! }
30//! ```
31//!
32//! Internally, a `View` has:
33//!
34//! - `id: ViewId` - assigned during composition / layout.
35//! - `kind: ViewKind` - which widget it is (Text, Button, etc.).
36//! - `modifier: Modifier` - layout/styling/interaction metadata.
37//! - `children: Vec<View>` - owned child views.
38//!
39//! Views are *pure data*: they do not hold state or platform handles.
40//! State lives in signals / `remember_*`; platform integration is in
41//! `repose-platform` / `repose-app`.
42//!
43//! ## Modifiers
44//!
45//! `Modifier` describes *how* a view participates in layout and hit-testing:
46//!
47//! - Size: `size`, `width`, `height`, `min_*`, `max_*`, `fill_max_*`
48//! - Box model: `padding`, `padding_values`, margins
49//! - Visuals: `background`, `border`, `clip_rounded`, `alpha`, `transform`, layers
50//! - Flex / grid: `flex_*`, `align_*`, `justify_*`, `grid`, `grid_span`
51//! - Positioning: `absolute()`, `offset(..)`
52//! - Scroll: `vertical_scroll` / `horizontal_scroll` / `scrollable`, `nested_scroll_connection`
53//! - Interaction: `clickable()`, pointer callbacks, `semantics`
54//! - Custom paint: `painter` (used by `repose-canvas`)
55//! - Incremental helpers: `key`, `repaint_boundary`, `scope!` (core)
56//!
57//! Modifiers are mapped to Taffy `Style` inside `LayoutEngine`. Values are in
58//! density-independent pixels (dp) and converted to physical px via `Density`.
59//!
60//! ## Layout + paint
61//!
62//! Public entry:
63//!
64//! ```rust,ignore
65//! pub fn layout_and_paint(
66//!     root: &View,
67//!     size_px: (u32, u32),
68//!     textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
69//!     interactions: &Interactions,
70//!     focused: Option<u64>,
71//! ) -> (Scene, Vec<HitRegion>, Vec<SemNode>);
72//! ```
73//!
74//! This is a thin thread-local wrapper around `LayoutEngine::layout_frame`, which:
75//!
76//! 1. Reconciles `root` into the persistent `ViewTree` (stable `NodeId`s, content +
77//!    subtree hashes, dirty set, generation GC).
78//! 2. Syncs dual Taffy trees (root + per-`scope!` `ScopeLayoutTree`s).
79//! 3. Computes layout (measure callbacks, constraint equality skip for scopes).
80//! 4. Walks the tree to emit `SceneNode`s, `HitRegion`s, and `SemNode`s, with
81//!    paint-cache hits on `repaint_boundary` / scopes, culling, nested scroll, etc.
82//!
83//! Prefer `scope!`, stable keys, and `repaint_boundary` on expensive subtrees so
84//! the incremental engine can skip work.
85
86pub mod adaptive;
87pub mod anim;
88pub mod anim_ext;
89pub mod color_picker;
90pub mod gestures;
91pub mod layout;
92pub use layout::IntrinsicSizeMode;
93pub mod lazy;
94pub mod selection;
95pub mod subcompose;
96pub use lazy::{
97    LazyColumn, LazyHorizontalGrid, LazyRow, LazyVerticalGrid, LazyVerticalStaggeredGrid,
98    SimpleList,
99};
100pub mod lazy_states;
101pub use lazy_states::{
102    ItemHeight, LazyColumnConfig, LazyColumnState, LazyGridConfig, LazyGridState, LazyRowConfig,
103    LazyRowState, LazyVerticalStaggeredGridConfig, LazyVerticalStaggeredGridState,
104};
105pub use subcompose::{
106    BoxWithConstraints, SubcomposeLayout, box_with_constraints_with_key, subcompose_hash_key,
107    subcompose_layout_with_slots, subcompose_with_key, subcompose_with_key_slots,
108};
109pub mod overlay;
110pub mod pager;
111pub mod scroll;
112pub mod windowing;
113pub mod window_v2;
114
115use std::cell::RefCell;
116use std::collections::{HashMap, HashSet};
117use std::rc::Rc;
118use std::sync::atomic::{AtomicU64, Ordering};
119
120use repose_core::*;
121
122pub mod textfield;
123use repose_core::locals;
124pub use selection::{SelectableText, SelectableTextExt};
125pub use textfield::{
126    BasicSecureTextField, BasicTextField, KeyboardOptions, TextFieldConfig, TextFieldState,
127};
128
129thread_local! {
130    static LAYOUT_ENGINE: RefCell<layout::LayoutEngine> =
131        RefCell::new(layout::LayoutEngine::new());
132}
133
134#[derive(Default)]
135pub struct Interactions {
136    pub hover: Option<u64>,
137    pub hover_ancestors: std::collections::HashSet<u64>,
138    pub pressed: HashSet<u64>,
139}
140
141pub fn Box(modifier: Modifier) -> View {
142    View::new(0, ViewKind::Box).modifier(modifier)
143}
144
145pub fn Row(modifier: Modifier) -> View {
146    View::new(0, ViewKind::Row).modifier(modifier)
147}
148
149pub fn Column(modifier: Modifier) -> View {
150    View::new(0, ViewKind::Column).modifier(modifier)
151}
152
153/// A horizontally-oriented flow layout that wraps children to new rows when
154/// they exceed the available width. Equivalent to `Row` with `flex_wrap(Wrap)`.
155pub fn FlowRow(modifier: Modifier) -> View {
156    Row(modifier.flex_wrap(FlexWrap::Wrap))
157}
158
159/// Flipped container (identical to `Column`).
160/// Deprecated: use `Column` directly.
161#[deprecated = "Use Column instead (identical behavior)"]
162pub fn Stack(modifier: Modifier) -> View {
163    Column(modifier)
164}
165
166/// A vertically-oriented flow layout that wraps children to new columns when
167/// they exceed the available height. Equivalent to `Column` with `flex_wrap(Wrap)`.
168pub fn FlowColumn(modifier: Modifier) -> View {
169    Column(modifier.flex_wrap(FlexWrap::Wrap))
170}
171
172/// Centers children both axes inside this Box.
173/// (Compose `Box(contentAlignment = Alignment.Center)`.)
174pub fn Center(modifier: Modifier) -> View {
175    Box(modifier.content_alignment(Alignment::Center))
176}
177
178pub fn ZStack(modifier: Modifier) -> View {
179    View::new(0, ViewKind::ZStack).modifier(modifier)
180}
181
182pub fn OverlayHost(modifier: Modifier) -> View {
183    View::new(0, ViewKind::OverlayHost).modifier(modifier)
184}
185
186#[deprecated = "Use Modifier::vertical_scroll instead"]
187pub fn Scroll(modifier: Modifier) -> View {
188    View::new(0, ViewKind::Box).modifier(modifier.vertical_scroll(ScrollAxisBinding {
189        show_scrollbar: true,
190        ..Default::default()
191    }))
192}
193
194pub fn Text(text: impl Into<String>) -> View {
195    View::new(
196        0,
197        ViewKind::Text {
198            text: text.into(),
199            color: locals::content_color(),
200            font_size: locals::text_size().unwrap_or(16.0), // dp (converted to px in layout/paint)
201            soft_wrap: true,
202            max_lines: None,
203            overflow: TextOverflow::Clip,
204            font_family: Some("sans-serif"),
205            annotations: None,
206            text_align: TextAlign::Start,
207            font_weight: FontWeight::NORMAL,
208            font_style: FontStyle::Normal,
209            text_decoration: TextDecoration::default(),
210            letter_spacing: 0.0,
211            line_height: 0.0,
212            url: None,
213            font_variation_settings: None,
214        },
215    )
216}
217
218/// Create a text view with rich text spans (AnnotatedString).
219///
220/// Each span can override color and font_size for a range of text.
221pub fn AnnotatedText(annotated: AnnotatedString) -> View {
222    let annotations: Option<std::sync::Arc<[TextSpan]>> = if annotated.spans.is_empty() {
223        None
224    } else {
225        Some(annotated.spans.clone())
226    };
227    View::new(
228        0,
229        ViewKind::Text {
230            text: annotated.text,
231            color: locals::content_color(),
232            font_size: locals::text_size().unwrap_or(16.0),
233            soft_wrap: true,
234            max_lines: None,
235            overflow: TextOverflow::Clip,
236            font_family: Some("sans-serif"),
237            annotations,
238            text_align: TextAlign::Start,
239            font_weight: FontWeight::NORMAL,
240            font_style: FontStyle::Normal,
241            text_decoration: TextDecoration::default(),
242            letter_spacing: 0.0,
243            line_height: 0.0,
244            url: None,
245            font_variation_settings: None,
246        },
247    )
248}
249
250pub fn Spacer() -> View {
251    Box(Modifier::new().flex_grow(1.0))
252}
253
254pub fn Space(modifier: Modifier) -> View {
255    Box(modifier)
256}
257
258pub fn Grid(
259    columns: usize,
260    modifier: Modifier,
261    children: Vec<View>,
262    row_gap: f32,
263    column_gap: f32,
264) -> View {
265    Column(modifier.grid(columns, row_gap, column_gap)).with_children(children)
266}
267
268pub fn Expander(modifier: Modifier, expanded: bool, on_toggle: impl Fn() + 'static) -> View {
269    View::new(
270        0,
271        ViewKind::Expander {
272            expanded,
273            on_toggle: Some(Rc::new(on_toggle)),
274        },
275    )
276    .modifier(modifier)
277}
278
279/// A single row in a tree view.
280///
281/// Renders with indentation based on `depth`, an expand/collapse arrow if
282/// `has_children` is true, and a highlight background if `is_selected`.
283/// The first child is the row's label/content.
284pub fn TreeRow(
285    modifier: Modifier,
286    depth: usize,
287    has_children: bool,
288    is_expanded: bool,
289    is_selected: bool,
290    on_toggle: impl Fn() + 'static,
291    on_select: impl Fn() + 'static,
292) -> View {
293    View::new(
294        0,
295        ViewKind::TreeRow {
296            depth,
297            has_children,
298            is_expanded,
299            is_selected,
300            on_toggle: Some(Rc::new(on_toggle)),
301            on_select: Some(Rc::new(on_select)),
302        },
303    )
304    .modifier(modifier)
305}
306
307static DRAGVALUE_COUNTER: AtomicU64 = AtomicU64::new(0);
308
309/// A drag-to-change numeric value field (like egui's `DragValue`).
310///
311/// Click and drag left/right to change the value. Displays the current value
312/// as centered text in a bordered box.
313pub fn DragValue(
314    value: f32,
315    range: (f32, f32),
316    speed: f32,
317    on_change: impl Fn(f32) + 'static,
318) -> View {
319    let id = DRAGVALUE_COUNTER.fetch_add(1, Ordering::Relaxed);
320    let drag_start_x = remember_mutable_with_key(format!("dv_dsx_{}", id), || 0.0f32);
321    let drag_start_val = remember_mutable_with_key(format!("dv_dsv_{}", id), || 0.0f32);
322    let is_dragging = remember_mutable_with_key(format!("dv_drg_{}", id), || false);
323
324    let oc = Rc::new(on_change);
325    let min = range.0;
326    let max = range.1;
327    let cur = value;
328
329    let th = locals::theme();
330
331    Box(Modifier::new()
332        .min_width(48.0)
333        .height(28.0)
334        .background(th.surface_container)
335        .border(1.0, th.outline, 4.0)
336        .clip_rounded(4.0)
337        .padding_values(PaddingValues {
338            left: 4.0,
339            right: 4.0,
340            top: 0.0,
341            bottom: 0.0,
342        })
343        .on_pointer_down({
344            let dsx = drag_start_x.clone();
345            let dsv = drag_start_val.clone();
346            let drg = is_dragging.clone();
347            move |pe: PointerEvent| {
348                drg.set(true);
349                dsx.set(pe.position_in_window().x);
350                dsv.set(cur);
351            }
352        })
353        .on_pointer_move({
354            let dsx = drag_start_x.clone();
355            let dsv = drag_start_val.clone();
356            let drg = is_dragging.clone();
357            let oc = oc.clone();
358            move |pe: PointerEvent| {
359                if !drg.with(|v| *v) {
360                    return;
361                }
362                let start_x = dsx.with(|v| *v);
363                let start_val = dsv.with(|v| *v);
364                let new_val =
365                    (start_val + (pe.position_in_window().x - start_x) * speed).clamp(min, max);
366                (oc)(new_val);
367            }
368        })
369        .on_pointer_up({
370            let drg = is_dragging.clone();
371            move |_pe: PointerEvent| {
372                drg.set(false);
373            }
374        })
375        .cursor(CursorIcon::EwResize))
376    .child(
377        Text(format_value(value))
378            .size(13.0)
379            .color(th.on_surface)
380            .single_line()
381            .overflow_ellipsize(),
382    )
383}
384
385fn format_value(v: f32) -> String {
386    if (v - v.round()).abs() < 1e-6 {
387        format!("{}", v.round() as i64)
388    } else if (v * 10.0 - (v * 10.0).round()).abs() < 1e-6 {
389        format!("{:.1}", v)
390    } else {
391        format!("{:.2}", v)
392    }
393}
394
395pub fn Image(modifier: Modifier, handle: ImageHandle) -> View {
396    View::new(
397        0,
398        ViewKind::Image {
399            handle,
400            tint: Color::WHITE,
401            fit: ImageFit::Contain,
402        },
403    )
404    .modifier(modifier)
405}
406
407pub trait ImageExt {
408    fn image_tint(self, c: Color) -> View;
409    fn image_fit(self, fit: ImageFit) -> View;
410}
411impl ImageExt for View {
412    fn image_tint(mut self, c: Color) -> View {
413        if let ViewKind::Image { tint, .. } = &mut self.kind {
414            *tint = c;
415        }
416        self
417    }
418    fn image_fit(mut self, fit: ImageFit) -> View {
419        if let ViewKind::Image { fit: f, .. } = &mut self.kind {
420            *f = fit;
421        }
422        self
423    }
424}
425
426/// Extension trait for child building
427pub trait ViewExt: Sized {
428    fn child(self, children: impl IntoChildren) -> Self;
429}
430
431impl ViewExt for View {
432    fn child(mut self, children: impl IntoChildren) -> Self {
433        self.children.extend(children.into_children());
434        self
435    }
436}
437
438pub trait IntoChildren {
439    fn into_children(self) -> Vec<View>;
440}
441
442impl IntoChildren for View {
443    fn into_children(self) -> Vec<View> {
444        vec![self]
445    }
446}
447
448impl IntoChildren for Vec<View> {
449    fn into_children(self) -> Vec<View> {
450        self
451    }
452}
453
454impl<const N: usize> IntoChildren for [View; N] {
455    fn into_children(self) -> Vec<View> {
456        self.into()
457    }
458}
459
460// Tuple implementations
461macro_rules! impl_into_children_tuple {
462    ($($idx:tt $t:ident),+) => {
463        impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
464            fn into_children(self) -> Vec<View> {
465                let mut v = Vec::new();
466                $(v.extend(self.$idx.into_children());)+
467                v
468            }
469        }
470    };
471}
472
473impl_into_children_tuple!(0 A);
474impl_into_children_tuple!(0 A, 1 B);
475impl_into_children_tuple!(0 A, 1 B, 2 C);
476impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
477impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
478impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
479impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
480impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
481
482/// Reconcile `root` into the thread-local `LayoutEngine` and run incremental
483/// layout + paint for this frame.
484pub fn layout_and_paint(
485    root: &View,
486    size_px_u32: (u32, u32),
487    textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
488    interactions: &Interactions,
489    focused: Option<u64>,
490) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
491    LAYOUT_ENGINE.with(|engine| {
492        engine
493            .borrow_mut()
494            .layout_frame(root, size_px_u32, textfield_states, interactions, focused)
495    })
496}
497
498/// Return the [`LayoutStats`] from the most recent `layout_and_paint` call on
499/// this thread. Used by the inspector / HUD to report real layout+paint timing
500/// and cache counters instead of a hardcoded estimate.
501pub fn last_layout_stats() -> layout::LayoutStats {
502    LAYOUT_ENGINE.with(|engine| engine.borrow().stats.clone())
503}
504
505pub use layout::LayoutStats;
506
507/// Method styling
508pub trait TextStyle {
509    fn color(self, c: Color) -> View;
510    fn size(self, px: f32) -> View;
511    fn max_lines(self, n: usize) -> View;
512    fn single_line(self) -> View;
513    fn overflow_ellipsize(self) -> View;
514    fn overflow_clip(self) -> View;
515    fn overflow_visible(self) -> View;
516    fn font_family(self, family: &'static str) -> View;
517    fn text_align(self, align: TextAlign) -> View;
518    fn font_weight(self, weight: FontWeight) -> View;
519    fn font_style(self, style: FontStyle) -> View;
520    fn text_decoration(self, decoration: TextDecoration) -> View;
521    fn letter_spacing(self, spacing: f32) -> View;
522    fn line_height(self, height: f32) -> View;
523    fn url(self, url: impl Into<std::sync::Arc<str>>) -> View;
524    fn font_variation_settings(self, settings: &str) -> View;
525}
526impl TextStyle for View {
527    fn color(mut self, c: Color) -> View {
528        if let ViewKind::Text {
529            color: text_color, ..
530        } = &mut self.kind
531        {
532            *text_color = c;
533        }
534        self
535    }
536    fn size(mut self, dp_font: f32) -> View {
537        if let ViewKind::Text {
538            font_size: text_size_dp,
539            ..
540        } = &mut self.kind
541        {
542            *text_size_dp = dp_font;
543        }
544        self
545    }
546    fn max_lines(mut self, n: usize) -> View {
547        if let ViewKind::Text {
548            max_lines,
549            soft_wrap,
550            ..
551        } = &mut self.kind
552        {
553            *max_lines = Some(n);
554            *soft_wrap = true;
555        }
556        self
557    }
558    fn single_line(mut self) -> View {
559        if let ViewKind::Text {
560            soft_wrap,
561            max_lines,
562            ..
563        } = &mut self.kind
564        {
565            *soft_wrap = false;
566            *max_lines = Some(1);
567        }
568        self
569    }
570    fn overflow_ellipsize(mut self) -> View {
571        if let ViewKind::Text { overflow, .. } = &mut self.kind {
572            *overflow = TextOverflow::Ellipsis;
573        }
574        self
575    }
576    fn overflow_clip(mut self) -> View {
577        if let ViewKind::Text { overflow, .. } = &mut self.kind {
578            *overflow = TextOverflow::Clip;
579        }
580        self
581    }
582    fn overflow_visible(mut self) -> View {
583        if let ViewKind::Text { overflow, .. } = &mut self.kind {
584            *overflow = TextOverflow::Visible;
585        }
586        self
587    }
588    fn font_family(mut self, family: &'static str) -> View {
589        if let ViewKind::Text {
590            font_family: ff, ..
591        } = &mut self.kind
592        {
593            *ff = Some(family);
594        }
595        self
596    }
597    fn text_align(mut self, align: TextAlign) -> View {
598        if let ViewKind::Text { text_align, .. } = &mut self.kind {
599            *text_align = align;
600        }
601        self
602    }
603    fn font_weight(mut self, weight: FontWeight) -> View {
604        if let ViewKind::Text { font_weight, .. } = &mut self.kind {
605            *font_weight = weight;
606        }
607        self
608    }
609    fn font_style(mut self, style: FontStyle) -> View {
610        if let ViewKind::Text { font_style, .. } = &mut self.kind {
611            *font_style = style;
612        }
613        self
614    }
615    fn text_decoration(mut self, decoration: TextDecoration) -> View {
616        if let ViewKind::Text {
617            text_decoration, ..
618        } = &mut self.kind
619        {
620            *text_decoration = decoration;
621        }
622        self
623    }
624    fn letter_spacing(mut self, spacing: f32) -> View {
625        if let ViewKind::Text { letter_spacing, .. } = &mut self.kind {
626            *letter_spacing = spacing;
627        }
628        self
629    }
630    fn line_height(mut self, height: f32) -> View {
631        if let ViewKind::Text { line_height, .. } = &mut self.kind {
632            *line_height = height;
633        }
634        self
635    }
636    fn url(mut self, url: impl Into<std::sync::Arc<str>>) -> View {
637        if let ViewKind::Text {
638            url: u,
639            text_decoration,
640            ..
641        } = &mut self.kind
642        {
643            *u = Some(url.into());
644            if !text_decoration.underline && !text_decoration.strikethrough {
645                *text_decoration = TextDecoration::UNDERLINE;
646            }
647        }
648        self
649    }
650    fn font_variation_settings(mut self, settings: &str) -> View {
651        if let ViewKind::Text {
652            font_variation_settings,
653            ..
654        } = &mut self.kind
655        {
656            *font_variation_settings = Some(settings.into());
657        }
658        self
659    }
660}