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;
113
114use std::cell::RefCell;
115use std::collections::{HashMap, HashSet};
116use std::rc::Rc;
117use std::sync::atomic::{AtomicU64, Ordering};
118
119use repose_core::*;
120use taffy::style::FlexDirection;
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 pressed: HashSet<u64>,
138}
139
140pub fn Box(modifier: Modifier) -> View {
141    View::new(0, ViewKind::Box).modifier(modifier)
142}
143
144pub fn Row(modifier: Modifier) -> View {
145    View::new(0, ViewKind::Row).modifier(modifier)
146}
147
148pub fn Column(modifier: Modifier) -> View {
149    View::new(0, ViewKind::Column).modifier(modifier)
150}
151
152/// A horizontally-oriented flow layout that wraps children to new rows when
153/// they exceed the available width. Equivalent to `Row` with `flex_wrap(Wrap)`.
154pub fn FlowRow(modifier: Modifier) -> View {
155    Row(modifier.flex_wrap(FlexWrap::Wrap))
156}
157
158/// Flipped container (identical to `Column`).
159/// Deprecated: use `Column` directly.
160#[deprecated = "Use Column instead (identical behavior)"]
161pub fn Stack(modifier: Modifier) -> View {
162    Column(modifier)
163}
164
165/// A vertically-oriented flow layout that wraps children to new columns when
166/// they exceed the available height. Equivalent to `Column` with `flex_wrap(Wrap)`.
167pub fn FlowColumn(modifier: Modifier) -> View {
168    Column(modifier.flex_wrap(FlexWrap::Wrap))
169}
170
171/// Align self-center shorthand.
172pub fn Center(modifier: Modifier) -> View {
173    Box(modifier.align_self(AlignSelf::CENTER))
174}
175
176pub fn ZStack(modifier: Modifier) -> View {
177    View::new(0, ViewKind::ZStack).modifier(modifier)
178}
179
180pub fn OverlayHost(modifier: Modifier) -> View {
181    View::new(0, ViewKind::OverlayHost).modifier(modifier)
182}
183
184#[deprecated = "Use Modifier::vertical_scroll instead"]
185pub fn Scroll(modifier: Modifier) -> View {
186    View::new(0, ViewKind::Box).modifier(modifier.vertical_scroll(ScrollAxisBinding {
187        show_scrollbar: true,
188        ..Default::default()
189    }))
190}
191
192pub fn Text(text: impl Into<String>) -> View {
193    View::new(
194        0,
195        ViewKind::Text {
196            text: text.into(),
197            color: locals::content_color(),
198            font_size: 16.0, // dp (converted to px in layout/paint)
199            soft_wrap: true,
200            max_lines: None,
201            overflow: TextOverflow::Visible,
202            font_family: Some("sans-serif"),
203            annotations: None,
204            text_align: TextAlign::Start,
205            font_weight: FontWeight::NORMAL,
206            font_style: FontStyle::Normal,
207            text_decoration: TextDecoration::default(),
208            letter_spacing: 0.0,
209            line_height: 0.0,
210            url: None,
211            font_variation_settings: None,
212        },
213    )
214}
215
216/// Create a text view with rich text spans (AnnotatedString).
217///
218/// Each span can override color and font_size for a range of text.
219pub fn AnnotatedText(annotated: AnnotatedString) -> View {
220    let annotations: Option<std::sync::Arc<[TextSpan]>> = if annotated.spans.is_empty() {
221        None
222    } else {
223        Some(annotated.spans.clone())
224    };
225    View::new(
226        0,
227        ViewKind::Text {
228            text: annotated.text,
229            color: locals::content_color(),
230            font_size: 16.0,
231            soft_wrap: true,
232            max_lines: None,
233            overflow: TextOverflow::Visible,
234            font_family: Some("sans-serif"),
235            annotations,
236            text_align: TextAlign::Start,
237            font_weight: FontWeight::NORMAL,
238            font_style: FontStyle::Normal,
239            text_decoration: TextDecoration::default(),
240            letter_spacing: 0.0,
241            line_height: 0.0,
242            url: None,
243            font_variation_settings: None,
244        },
245    )
246}
247
248pub fn Spacer() -> View {
249    Box(Modifier::new().flex_grow(1.0))
250}
251
252pub fn Space(modifier: Modifier) -> View {
253    Box(modifier)
254}
255
256pub fn Grid(
257    columns: usize,
258    modifier: Modifier,
259    children: Vec<View>,
260    row_gap: f32,
261    column_gap: f32,
262) -> View {
263    Column(modifier.grid(columns, row_gap, column_gap)).with_children(children)
264}
265
266pub fn Expander(modifier: Modifier, expanded: bool, on_toggle: impl Fn() + 'static) -> View {
267    View::new(
268        0,
269        ViewKind::Expander {
270            expanded,
271            on_toggle: Some(Rc::new(on_toggle)),
272        },
273    )
274    .modifier(modifier)
275}
276
277/// A single row in a tree view.
278///
279/// Renders with indentation based on `depth`, an expand/collapse arrow if
280/// `has_children` is true, and a highlight background if `is_selected`.
281/// The first child is the row's label/content.
282pub fn TreeRow(
283    modifier: Modifier,
284    depth: usize,
285    has_children: bool,
286    is_expanded: bool,
287    is_selected: bool,
288    on_toggle: impl Fn() + 'static,
289    on_select: impl Fn() + 'static,
290) -> View {
291    View::new(
292        0,
293        ViewKind::TreeRow {
294            depth,
295            has_children,
296            is_expanded,
297            is_selected,
298            on_toggle: Some(Rc::new(on_toggle)),
299            on_select: Some(Rc::new(on_select)),
300        },
301    )
302    .modifier(modifier)
303}
304
305static DRAGVALUE_COUNTER: AtomicU64 = AtomicU64::new(0);
306
307/// A drag-to-change numeric value field (like egui's `DragValue`).
308///
309/// Click and drag left/right to change the value. Displays the current value
310/// as centered text in a bordered box.
311pub fn DragValue(
312    value: f32,
313    range: (f32, f32),
314    speed: f32,
315    on_change: impl Fn(f32) + 'static,
316) -> View {
317    let id = DRAGVALUE_COUNTER.fetch_add(1, Ordering::Relaxed);
318    let drag_start_x = remember_mutable_with_key(format!("dv_dsx_{}", id), || 0.0f32);
319    let drag_start_val = remember_mutable_with_key(format!("dv_dsv_{}", id), || 0.0f32);
320    let is_dragging = remember_mutable_with_key(format!("dv_drg_{}", id), || false);
321
322    let oc = Rc::new(on_change);
323    let min = range.0;
324    let max = range.1;
325    let cur = value;
326
327    let th = locals::theme();
328
329    Box(Modifier::new()
330        .min_width(48.0)
331        .height(28.0)
332        .background(th.surface_container)
333        .border(1.0, th.outline, 4.0)
334        .clip_rounded(4.0)
335        .padding_values(PaddingValues {
336            left: 4.0,
337            right: 4.0,
338            top: 0.0,
339            bottom: 0.0,
340        })
341        .on_pointer_down({
342            let dsx = drag_start_x.clone();
343            let dsv = drag_start_val.clone();
344            let drg = is_dragging.clone();
345            move |pe: PointerEvent| {
346                drg.set(true);
347                dsx.set(pe.position.x);
348                dsv.set(cur);
349            }
350        })
351        .on_pointer_move({
352            let dsx = drag_start_x.clone();
353            let dsv = drag_start_val.clone();
354            let drg = is_dragging.clone();
355            let oc = oc.clone();
356            move |pe: PointerEvent| {
357                if !drg.with(|v| *v) {
358                    return;
359                }
360                let start_x = dsx.with(|v| *v);
361                let start_val = dsv.with(|v| *v);
362                let new_val = (start_val + (pe.position.x - start_x) * speed).clamp(min, max);
363                (oc)(new_val);
364            }
365        })
366        .on_pointer_up({
367            let drg = is_dragging.clone();
368            move |_pe: PointerEvent| {
369                drg.set(false);
370            }
371        })
372        .cursor(CursorIcon::EwResize))
373    .child(
374        Text(format_value(value))
375            .size(13.0)
376            .color(th.on_surface)
377            .single_line()
378            .overflow_ellipsize(),
379    )
380}
381
382fn format_value(v: f32) -> String {
383    if (v - v.round()).abs() < 1e-6 {
384        format!("{}", v.round() as i64)
385    } else if (v * 10.0 - (v * 10.0).round()).abs() < 1e-6 {
386        format!("{:.1}", v)
387    } else {
388        format!("{:.2}", v)
389    }
390}
391
392pub fn Image(modifier: Modifier, handle: ImageHandle) -> View {
393    View::new(
394        0,
395        ViewKind::Image {
396            handle,
397            tint: Color::WHITE,
398            fit: ImageFit::Contain,
399        },
400    )
401    .modifier(modifier)
402}
403
404pub trait ImageExt {
405    fn image_tint(self, c: Color) -> View;
406    fn image_fit(self, fit: ImageFit) -> View;
407}
408impl ImageExt for View {
409    fn image_tint(mut self, c: Color) -> View {
410        if let ViewKind::Image { tint, .. } = &mut self.kind {
411            *tint = c;
412        }
413        self
414    }
415    fn image_fit(mut self, fit: ImageFit) -> View {
416        if let ViewKind::Image { fit: f, .. } = &mut self.kind {
417            *f = fit;
418        }
419        self
420    }
421}
422
423fn flex_dir_for(kind: &ViewKind, modifier: &Modifier) -> Option<FlexDirection> {
424    if let Some(ref scroll) = modifier.scroll {
425        return Some(match scroll.axis() {
426            ScrollAxis::Vertical => FlexDirection::Column,
427            ScrollAxis::Horizontal => FlexDirection::Row,
428            ScrollAxis::Both => FlexDirection::Column,
429        });
430    }
431    match kind {
432        ViewKind::Row => {
433            if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
434                Some(FlexDirection::RowReverse)
435            } else {
436                Some(FlexDirection::Row)
437            }
438        }
439        ViewKind::Column => Some(FlexDirection::Column),
440        _ => None,
441    }
442}
443
444/// Extension trait for child building
445pub trait ViewExt: Sized {
446    fn child(self, children: impl IntoChildren) -> Self;
447}
448
449impl ViewExt for View {
450    fn child(mut self, children: impl IntoChildren) -> Self {
451        self.children.extend(children.into_children());
452        self
453    }
454}
455
456pub trait IntoChildren {
457    fn into_children(self) -> Vec<View>;
458}
459
460impl IntoChildren for View {
461    fn into_children(self) -> Vec<View> {
462        vec![self]
463    }
464}
465
466impl IntoChildren for Vec<View> {
467    fn into_children(self) -> Vec<View> {
468        self
469    }
470}
471
472impl<const N: usize> IntoChildren for [View; N] {
473    fn into_children(self) -> Vec<View> {
474        self.into()
475    }
476}
477
478// Tuple implementations
479macro_rules! impl_into_children_tuple {
480    ($($idx:tt $t:ident),+) => {
481        impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
482            fn into_children(self) -> Vec<View> {
483                let mut v = Vec::new();
484                $(v.extend(self.$idx.into_children());)+
485                v
486            }
487        }
488    };
489}
490
491impl_into_children_tuple!(0 A);
492impl_into_children_tuple!(0 A, 1 B);
493impl_into_children_tuple!(0 A, 1 B, 2 C);
494impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
495impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
496impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
497impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
498impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
499
500/// Reconcile `root` into the thread-local `LayoutEngine` and run incremental
501/// layout + paint for this frame.
502pub fn layout_and_paint(
503    root: &View,
504    size_px_u32: (u32, u32),
505    textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
506    interactions: &Interactions,
507    focused: Option<u64>,
508) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
509    LAYOUT_ENGINE.with(|engine| {
510        engine
511            .borrow_mut()
512            .layout_frame(root, size_px_u32, textfield_states, interactions, focused)
513    })
514}
515
516/// Return the [`LayoutStats`] from the most recent `layout_and_paint` call on
517/// this thread. Used by the inspector / HUD to report real layout+paint timing
518/// and cache counters instead of a hardcoded estimate.
519pub fn last_layout_stats() -> layout::LayoutStats {
520    LAYOUT_ENGINE.with(|engine| engine.borrow().stats.clone())
521}
522
523pub use layout::LayoutStats;
524
525/// Method styling
526pub trait TextStyle {
527    fn color(self, c: Color) -> View;
528    fn size(self, px: f32) -> View;
529    fn max_lines(self, n: usize) -> View;
530    fn single_line(self) -> View;
531    fn overflow_ellipsize(self) -> View;
532    fn overflow_clip(self) -> View;
533    fn overflow_visible(self) -> View;
534    fn font_family(self, family: &'static str) -> View;
535    fn text_align(self, align: TextAlign) -> View;
536    fn font_weight(self, weight: FontWeight) -> View;
537    fn font_style(self, style: FontStyle) -> View;
538    fn text_decoration(self, decoration: TextDecoration) -> View;
539    fn letter_spacing(self, spacing: f32) -> View;
540    fn line_height(self, height: f32) -> View;
541    fn url(self, url: impl Into<std::sync::Arc<str>>) -> View;
542    fn font_variation_settings(self, settings: &str) -> View;
543}
544impl TextStyle for View {
545    fn color(mut self, c: Color) -> View {
546        if let ViewKind::Text {
547            color: text_color, ..
548        } = &mut self.kind
549        {
550            *text_color = c;
551        }
552        self
553    }
554    fn size(mut self, dp_font: f32) -> View {
555        if let ViewKind::Text {
556            font_size: text_size_dp,
557            ..
558        } = &mut self.kind
559        {
560            *text_size_dp = dp_font;
561        }
562        self
563    }
564    fn max_lines(mut self, n: usize) -> View {
565        if let ViewKind::Text {
566            max_lines,
567            soft_wrap,
568            ..
569        } = &mut self.kind
570        {
571            *max_lines = Some(n);
572            *soft_wrap = true;
573        }
574        self
575    }
576    fn single_line(mut self) -> View {
577        if let ViewKind::Text {
578            soft_wrap,
579            max_lines,
580            ..
581        } = &mut self.kind
582        {
583            *soft_wrap = false;
584            *max_lines = Some(1);
585        }
586        self
587    }
588    fn overflow_ellipsize(mut self) -> View {
589        if let ViewKind::Text { overflow, .. } = &mut self.kind {
590            *overflow = TextOverflow::Ellipsis;
591        }
592        self
593    }
594    fn overflow_clip(mut self) -> View {
595        if let ViewKind::Text { overflow, .. } = &mut self.kind {
596            *overflow = TextOverflow::Clip;
597        }
598        self
599    }
600    fn overflow_visible(mut self) -> View {
601        if let ViewKind::Text { overflow, .. } = &mut self.kind {
602            *overflow = TextOverflow::Visible;
603        }
604        self
605    }
606    fn font_family(mut self, family: &'static str) -> View {
607        if let ViewKind::Text {
608            font_family: ff, ..
609        } = &mut self.kind
610        {
611            *ff = Some(family);
612        }
613        self
614    }
615    fn text_align(mut self, align: TextAlign) -> View {
616        if let ViewKind::Text { text_align, .. } = &mut self.kind {
617            *text_align = align;
618        }
619        self
620    }
621    fn font_weight(mut self, weight: FontWeight) -> View {
622        if let ViewKind::Text { font_weight, .. } = &mut self.kind {
623            *font_weight = weight;
624        }
625        self
626    }
627    fn font_style(mut self, style: FontStyle) -> View {
628        if let ViewKind::Text { font_style, .. } = &mut self.kind {
629            *font_style = style;
630        }
631        self
632    }
633    fn text_decoration(mut self, decoration: TextDecoration) -> View {
634        if let ViewKind::Text {
635            text_decoration, ..
636        } = &mut self.kind
637        {
638            *text_decoration = decoration;
639        }
640        self
641    }
642    fn letter_spacing(mut self, spacing: f32) -> View {
643        if let ViewKind::Text { letter_spacing, .. } = &mut self.kind {
644            *letter_spacing = spacing;
645        }
646        self
647    }
648    fn line_height(mut self, height: f32) -> View {
649        if let ViewKind::Text { line_height, .. } = &mut self.kind {
650            *line_height = height;
651        }
652        self
653    }
654    fn url(mut self, url: impl Into<std::sync::Arc<str>>) -> View {
655        if let ViewKind::Text {
656            url: u,
657            text_decoration,
658            ..
659        } = &mut self.kind
660        {
661            *u = Some(url.into());
662            if !text_decoration.underline && !text_decoration.strikethrough {
663                *text_decoration = TextDecoration::UNDERLINE;
664            }
665        }
666        self
667    }
668    fn font_variation_settings(mut self, settings: &str) -> View {
669        if let ViewKind::Text {
670            font_variation_settings,
671            ..
672        } = &mut self.kind
673        {
674            *font_variation_settings = Some(settings.into());
675        }
676        self
677    }
678}