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