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