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.
7//! - `Modifier`: layout, styling, and interaction hints attached to a `View`.
8//! - Layout + paint: a separate pass (`layout_and_paint`) that turns the
9//!   `View` tree into a `Scene` + hit regions using the Taffy layout engine.
10//!
11//! ## Views
12//!
13//! A `View` is a lightweight value that describes *what* to show, not *how* it is
14//! rendered. It is cheap to create and you are expected to rebuild the entire
15//! view tree on each frame:
16//!
17//! ```rust,ignore
18//! use repose_core::*;
19//! use repose_ui::*;
20//!
21//! fn Counter(count: i32, on_inc: impl Fn() + 'static) -> View {
22//!     Column(Modifier::new().padding(16.0)).child((
23//!         Text(format!("Count = {count}")),
24//!         Button("Increment".into_children(), on_inc),
25//!     ))
26//! }
27//! ```
28//!
29//! Internally, a `View` has:
30//!
31//! - `id: ViewId` - assigned during composition/layout.
32//! - `kind: ViewKind` - which widget it is (Text, Button, ScrollV, etc.).
33//! - `modifier: Modifier` - layout/styling/interaction metadata.
34//! - `children: Vec<View>` - owned child views.
35//!
36//! Views are *pure data*: they do not hold state or references into platform
37//! APIs. State lives in signals / `remember_*` and platform integration happens
38//! in the runner (`repose-platform`).
39//!
40//! ## Modifiers
41//!
42//! `Modifier` describes *how* a view participates in layout and hit‑testing:
43//!
44//! - Size hints: `size`, `width`, `height`, `min_size`, `max_size`,
45//!   `fill_max_size`, `fill_max_width`, `fill_max_height`.
46//! - Box model: `padding`, `padding_values`.
47//! - Visuals: `background`, `background_brush`, `border`, `clip_rounded`, `alpha`, `transform`.
48//! - Flex / grid: `flex_grow`, `flex_shrink`, `flex_basis`, `align_self`,
49//!   `justify_content`, `align_items`, `grid`, `grid_span`.
50//! - Positioning: `absolute()`, `offset(..)` for overlay / Stack / FABs.
51//! - Interaction: `clickable()`, pointer callbacks, `on_scroll`, `semantics`.
52//! - Custom paint: `painter` (used by `repose-canvas`).
53//!
54//! Example:
55//!
56//! ```rust
57//! use repose_core::*;
58//! use repose_ui::*;
59//!
60//! fn CardExample() -> View {
61//!     Box(
62//!         Modifier::new()
63//!             .padding(16.0)
64//!             .background(theme().surface)
65//!             .border(1.0, theme().outline, 8.0)
66//!             .clip_rounded(8.0),
67//!     )
68//!     .child(Text("Hello, Repose!"))
69//! }
70//! ```
71//!
72//! Modifiers are merged into a Taffy `Style` inside `layout_and_paint`. Most
73//! values are specified in density‑independent pixels (dp) and converted to
74//! physical pixels (`px`) using the current `Density` local.
75//!
76//! ## Layout
77//!
78//! Layout is a pure function:
79//!
80//! ```rust,ignore
81//! pub fn layout_and_paint(
82//!     root: &View,
83//!     size_px: (u32, u32),
84//!     textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
85//!     interactions: &Interactions,
86//!     focused: Option<u64>,
87//! ) -> (Scene, Vec<HitRegion>, Vec<SemNode>);
88//! ```
89//!
90//! It:
91//!
92//! 1. Clones the root `View` and assigns stable `ViewId`s.
93//! 2. Builds a parallel Taffy tree and computes layout for the given window size.
94//! 3. Walks the tree to:
95//!    - Emit `SceneNode`s for visuals (rects, text, images, scrollbars, etc.).
96//!    - Build `HitRegion`s for input routing (clicks, pointer events, scroll).
97//!    - Build `SemNode`s for accessibility / semantics.
98//!
99//! `Row`, `Column`, `Stack`, `Grid`, `ScrollV` and `ScrollXY` are all special
100//! `ViewKind`s that map into Taffy styles and additional paint/hit logic.
101//!
102//! Because layout + paint are separate from the platform runner, you can reuse
103//! the same UI code on desktop, Android, and other platforms.
104
105pub mod adaptive;
106pub mod anim;
107pub mod anim_ext;
108pub mod color_picker;
109pub mod gestures;
110pub mod layout;
111pub use layout::IntrinsicSizeMode;
112pub mod lazy;
113pub mod selection;
114pub mod subcompose;
115pub use lazy::{
116    LazyColumn, LazyColumnState, LazyGridState, LazyRow, LazyRowState, LazyVerticalGrid,
117    LazyVerticalStaggeredGrid, LazyVerticalStaggeredGridState, SimpleList,
118};
119pub use subcompose::{
120    BoxWithConstraints, SubcomposeLayout, box_with_constraints_with_key, subcompose_hash_key,
121    subcompose_layout_with_slots, subcompose_with_key, subcompose_with_key_slots,
122};
123pub mod overlay;
124pub mod pager;
125pub mod scroll;
126pub mod windowing;
127
128use std::cell::RefCell;
129use std::collections::{HashMap, HashSet};
130use std::rc::Rc;
131use std::sync::atomic::{AtomicU64, Ordering};
132
133use repose_core::*;
134use taffy::style::FlexDirection;
135
136pub mod textfield;
137use repose_core::locals;
138pub use selection::SelectableText;
139pub use textfield::{
140    KeyboardOptions, TextArea, TextAreaEx, TextField, TextFieldEx, TextFieldState,
141};
142
143thread_local! {
144    static LAYOUT_ENGINE: RefCell<layout::LayoutEngine> =
145        RefCell::new(layout::LayoutEngine::new());
146}
147
148#[derive(Default)]
149pub struct Interactions {
150    pub hover: Option<u64>,
151    pub pressed: HashSet<u64>,
152}
153
154pub fn Box(modifier: Modifier) -> View {
155    View::new(0, ViewKind::Box).modifier(modifier)
156}
157
158pub fn Row(modifier: Modifier) -> View {
159    View::new(0, ViewKind::Row).modifier(modifier)
160}
161
162pub fn Column(modifier: Modifier) -> View {
163    View::new(0, ViewKind::Column).modifier(modifier)
164}
165
166/// A horizontally-oriented flow layout that wraps children to new rows when
167/// they exceed the available width. Equivalent to `Row` with `flex_wrap(Wrap)`.
168pub fn FlowRow(modifier: Modifier) -> View {
169    Row(modifier.flex_wrap(FlexWrap::Wrap))
170}
171
172/// A vertically-oriented flow layout that wraps children to new columns when
173/// they exceed the available height. Equivalent to `Column` with `flex_wrap(Wrap)`.
174pub fn FlowColumn(modifier: Modifier) -> View {
175    Column(modifier.flex_wrap(FlexWrap::Wrap))
176}
177
178/// Align self-center shorthand.
179pub fn Center(modifier: Modifier) -> View {
180    Box(modifier.align_self(AlignSelf::Center))
181}
182
183pub fn Stack(modifier: Modifier) -> View {
184    View::new(0, ViewKind::Stack).modifier(modifier)
185}
186
187pub fn ZStack(modifier: Modifier) -> View {
188    View::new(0, ViewKind::ZStack).modifier(modifier)
189}
190
191pub fn OverlayHost(modifier: Modifier) -> View {
192    View::new(0, ViewKind::OverlayHost).modifier(modifier)
193}
194
195#[deprecated = "Use ScollArea instead"]
196pub fn Scroll(modifier: Modifier) -> View {
197    View::new(
198        0,
199        ViewKind::ScrollV {
200            on_scroll: None,
201            set_viewport_height: None,
202            set_content_height: None,
203            get_scroll_offset: None,
204            set_scroll_offset: None,
205            show_scrollbar: true,
206            tick_scroll: None,
207        },
208    )
209    .modifier(modifier)
210}
211
212pub fn Text(text: impl Into<String>) -> View {
213    View::new(
214        0,
215        ViewKind::Text {
216            text: text.into(),
217            color: locals::content_color(),
218            font_size: 16.0, // dp (converted to px in layout/paint)
219            soft_wrap: true,
220            max_lines: None,
221            overflow: TextOverflow::Visible,
222            font_family: None,
223            annotations: None,
224        },
225    )
226}
227
228/// Create a text view with rich text spans (AnnotatedString).
229///
230/// Each span can override color and font_size for a range of text.
231pub fn AnnotatedText(annotated: AnnotatedString) -> View {
232    let annotations: Option<std::sync::Arc<[TextSpan]>> = if annotated.spans.is_empty() {
233        None
234    } else {
235        Some(annotated.spans.clone())
236    };
237    View::new(
238        0,
239        ViewKind::Text {
240            text: annotated.text,
241            color: locals::content_color(),
242            font_size: 16.0,
243            soft_wrap: true,
244            max_lines: None,
245            overflow: TextOverflow::Visible,
246            font_family: None,
247            annotations,
248        },
249    )
250}
251
252pub fn Spacer() -> View {
253    Box(Modifier::new().flex_grow(1.0))
254}
255
256pub fn Space(modifier: Modifier) -> View {
257    Box(modifier)
258}
259
260pub fn Grid(
261    columns: usize,
262    modifier: Modifier,
263    children: Vec<View>,
264    row_gap: f32,
265    column_gap: f32,
266) -> View {
267    Column(modifier.grid(columns, row_gap, column_gap)).with_children(children)
268}
269
270pub fn Expander(modifier: Modifier, expanded: bool, on_toggle: impl Fn() + 'static) -> View {
271    View::new(
272        0,
273        ViewKind::Expander {
274            expanded,
275            on_toggle: Some(Rc::new(on_toggle)),
276        },
277    )
278    .modifier(modifier)
279}
280
281/// A single row in a tree view.
282///
283/// Renders with indentation based on `depth`, an expand/collapse arrow if
284/// `has_children` is true, and a highlight background if `is_selected`.
285/// The first child is the row's label/content.
286pub fn TreeRow(
287    modifier: Modifier,
288    depth: usize,
289    has_children: bool,
290    is_expanded: bool,
291    is_selected: bool,
292    on_toggle: impl Fn() + 'static,
293    on_select: impl Fn() + 'static,
294) -> View {
295    View::new(
296        0,
297        ViewKind::TreeRow {
298            depth,
299            has_children,
300            is_expanded,
301            is_selected,
302            on_toggle: Some(Rc::new(on_toggle)),
303            on_select: Some(Rc::new(on_select)),
304        },
305    )
306    .modifier(modifier)
307}
308
309static DRAGVALUE_COUNTER: AtomicU64 = AtomicU64::new(0);
310
311/// A drag-to-change numeric value field (like egui's `DragValue`).
312///
313/// Click and drag left/right to change the value. Displays the current value
314/// as centered text in a bordered box.
315pub fn DragValue(
316    value: f32,
317    range: (f32, f32),
318    speed: f32,
319    on_change: impl Fn(f32) + 'static,
320) -> View {
321    let id = DRAGVALUE_COUNTER.fetch_add(1, Ordering::Relaxed);
322    let drag_start_x = remember_state_with_key(format!("dv_dsx_{}", id), || 0.0f32);
323    let drag_start_val = remember_state_with_key(format!("dv_dsv_{}", id), || 0.0f32);
324    let is_dragging = remember_state_with_key(format!("dv_drg_{}", id), || false);
325
326    let oc = Rc::new(on_change);
327    let min = range.0;
328    let max = range.1;
329    let cur = value;
330
331    let th = locals::theme();
332
333    Box(Modifier::new()
334        .min_width(48.0)
335        .height(28.0)
336        .background(th.surface_container)
337        .border(1.0, th.outline, 4.0)
338        .clip_rounded(4.0)
339        .padding_values(PaddingValues {
340            left: 4.0,
341            right: 4.0,
342            top: 0.0,
343            bottom: 0.0,
344        })
345        .on_pointer_down({
346            let dsx = drag_start_x.clone();
347            let dsv = drag_start_val.clone();
348            let drg = is_dragging.clone();
349            move |pe: PointerEvent| {
350                *drg.borrow_mut() = true;
351                *dsx.borrow_mut() = pe.position.x;
352                *dsv.borrow_mut() = cur;
353            }
354        })
355        .on_pointer_move({
356            let dsx = drag_start_x.clone();
357            let dsv = drag_start_val.clone();
358            let drg = is_dragging.clone();
359            let oc = oc.clone();
360            move |pe: PointerEvent| {
361                if !*drg.borrow() {
362                    return;
363                }
364                let dx = pe.position.x - *dsx.borrow();
365                let new_val = (*dsv.borrow() + dx * 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.borrow_mut() = 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
426fn flex_dir_for(kind: &ViewKind) -> Option<FlexDirection> {
427    match kind {
428        ViewKind::Row => {
429            if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
430                Some(FlexDirection::RowReverse)
431            } else {
432                Some(FlexDirection::Row)
433            }
434        }
435        ViewKind::Column | ViewKind::ScrollV { .. } => Some(FlexDirection::Column),
436        _ => None,
437    }
438}
439
440/// Extension trait for child building
441pub trait ViewExt: Sized {
442    fn child(self, children: impl IntoChildren) -> Self;
443}
444
445impl ViewExt for View {
446    fn child(self, children: impl IntoChildren) -> Self {
447        self.with_children(children.into_children())
448    }
449}
450
451pub trait IntoChildren {
452    fn into_children(self) -> Vec<View>;
453}
454
455impl IntoChildren for View {
456    fn into_children(self) -> Vec<View> {
457        vec![self]
458    }
459}
460
461impl IntoChildren for Vec<View> {
462    fn into_children(self) -> Vec<View> {
463        self
464    }
465}
466
467impl<const N: usize> IntoChildren for [View; N] {
468    fn into_children(self) -> Vec<View> {
469        self.into()
470    }
471}
472
473// Tuple implementations
474macro_rules! impl_into_children_tuple {
475    ($($idx:tt $t:ident),+) => {
476        impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
477            fn into_children(self) -> Vec<View> {
478                let mut v = Vec::new();
479                $(v.extend(self.$idx.into_children());)+
480                v
481            }
482        }
483    };
484}
485
486impl_into_children_tuple!(0 A);
487impl_into_children_tuple!(0 A, 1 B);
488impl_into_children_tuple!(0 A, 1 B, 2 C);
489impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
490impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
491impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
492impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
493impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
494
495/// Layout and paint with TextField state injection (Taffy 0.9 API)
496pub fn layout_and_paint(
497    root: &View,
498    size_px_u32: (u32, u32),
499    textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
500    interactions: &Interactions,
501    focused: Option<u64>,
502) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
503    LAYOUT_ENGINE.with(|engine| {
504        engine
505            .borrow_mut()
506            .layout_frame(root, size_px_u32, textfield_states, interactions, focused)
507    })
508}
509
510/// Method styling
511pub trait TextStyle {
512    fn color(self, c: Color) -> View;
513    fn size(self, px: f32) -> View;
514    fn max_lines(self, n: usize) -> View;
515    fn single_line(self) -> View;
516    fn overflow_ellipsize(self) -> View;
517    fn overflow_clip(self) -> View;
518    fn overflow_visible(self) -> View;
519    fn font_family(self, family: &'static str) -> View;
520}
521impl TextStyle for View {
522    fn color(mut self, c: Color) -> View {
523        if let ViewKind::Text {
524            color: text_color, ..
525        } = &mut self.kind
526        {
527            *text_color = c;
528        }
529        self
530    }
531    fn size(mut self, dp_font: f32) -> View {
532        if let ViewKind::Text {
533            font_size: text_size_dp,
534            ..
535        } = &mut self.kind
536        {
537            *text_size_dp = dp_font;
538        }
539        self
540    }
541    fn max_lines(mut self, n: usize) -> View {
542        if let ViewKind::Text {
543            max_lines,
544            soft_wrap,
545            ..
546        } = &mut self.kind
547        {
548            *max_lines = Some(n);
549            *soft_wrap = true;
550        }
551        self
552    }
553    fn single_line(mut self) -> View {
554        if let ViewKind::Text {
555            soft_wrap,
556            max_lines,
557            ..
558        } = &mut self.kind
559        {
560            *soft_wrap = false;
561            *max_lines = Some(1);
562        }
563        self
564    }
565    fn overflow_ellipsize(mut self) -> View {
566        if let ViewKind::Text { overflow, .. } = &mut self.kind {
567            *overflow = TextOverflow::Ellipsis;
568        }
569        self
570    }
571    fn overflow_clip(mut self) -> View {
572        if let ViewKind::Text { overflow, .. } = &mut self.kind {
573            *overflow = TextOverflow::Clip;
574        }
575        self
576    }
577    fn overflow_visible(mut self) -> View {
578        if let ViewKind::Text { overflow, .. } = &mut self.kind {
579            *overflow = TextOverflow::Visible;
580        }
581        self
582    }
583    fn font_family(mut self, family: &'static str) -> View {
584        if let ViewKind::Text {
585            font_family: ff, ..
586        } = &mut self.kind
587        {
588            *ff = Some(family);
589        }
590        self
591    }
592}