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
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//!     Surface(
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//!         Text("Hello, Repose!"),
68//!     )
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
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 gestures;
109pub mod layout;
110pub use layout::IntrinsicSizeMode;
111pub mod lazy;
112pub mod selection;
113pub mod subcompose;
114pub use lazy::{LazyColumn, LazyColumnState, LazyGridState, LazyRow, LazyRowState, LazyVerticalGrid, SimpleList};
115pub use subcompose::{
116    BoxWithConstraints, SubcomposeLayout, box_with_constraints_with_key, subcompose_hash_key,
117    subcompose_layout_with_slots, subcompose_with_key, subcompose_with_key_slots,
118};
119pub mod navigation;
120pub mod overlay;
121pub mod pager;
122pub mod scroll;
123pub mod windowing;
124
125use rustc_hash::{FxHashMap, FxHashSet};
126use std::collections::{HashMap, HashSet};
127use std::rc::Rc;
128use std::{cell::RefCell, cmp::Ordering};
129
130use repose_core::*;
131use taffy::style::FlexDirection;
132use taffy::{Overflow, Point};
133
134pub mod textfield;
135use crate::textfield::{TF_FONT_DP, TF_PADDING_X_DP, byte_to_char_index, measure_text};
136use repose_core::locals;
137pub use selection::SelectableText;
138pub use textfield::{TextArea, TextField, TextFieldState};
139
140thread_local! {
141    static LAYOUT_ENGINE: RefCell<layout::LayoutEngine> =
142        RefCell::new(layout::LayoutEngine::new());
143}
144
145#[derive(Default)]
146pub struct Interactions {
147    pub hover: Option<u64>,
148    pub pressed: HashSet<u64>,
149}
150
151pub fn Surface(modifier: Modifier, child: View) -> View {
152    let mut v = View::new(0, ViewKind::Surface).modifier(modifier);
153    v.children = vec![child];
154    v
155}
156
157pub fn Box(modifier: Modifier) -> View {
158    View::new(0, ViewKind::Box).modifier(modifier)
159}
160
161pub fn Row(modifier: Modifier) -> View {
162    View::new(0, ViewKind::Row).modifier(modifier)
163}
164
165pub fn Column(modifier: Modifier) -> View {
166    View::new(0, ViewKind::Column).modifier(modifier)
167}
168
169/// A horizontally-oriented flow layout that wraps children to new rows when
170/// they exceed the available width. Equivalent to `Row` with `flex_wrap(Wrap)`.
171pub fn FlowRow(modifier: Modifier) -> View {
172    Row(modifier.flex_wrap(FlexWrap::Wrap))
173}
174
175/// A vertically-oriented flow layout that wraps children to new columns when
176/// they exceed the available height. Equivalent to `Column` with `flex_wrap(Wrap)`.
177pub fn FlowColumn(modifier: Modifier) -> View {
178    Column(modifier.flex_wrap(FlexWrap::Wrap))
179}
180
181/// Align self-center shorthand.
182pub fn Center(modifier: Modifier) -> View {
183    Box(modifier.align_self(AlignSelf::Center))
184}
185
186pub fn Stack(modifier: Modifier) -> View {
187    View::new(0, ViewKind::Stack).modifier(modifier)
188}
189
190pub fn ZStack(modifier: Modifier) -> View {
191    View::new(0, ViewKind::ZStack).modifier(modifier)
192}
193
194pub fn OverlayHost(modifier: Modifier) -> View {
195    View::new(0, ViewKind::OverlayHost).modifier(modifier)
196}
197
198#[deprecated = "Use ScollArea instead"]
199pub fn Scroll(modifier: Modifier) -> View {
200    View::new(
201        0,
202        ViewKind::ScrollV {
203            on_scroll: None,
204            set_viewport_height: None,
205            set_content_height: None,
206            get_scroll_offset: None,
207            set_scroll_offset: None,
208            show_scrollbar: true,
209        },
210    )
211    .modifier(modifier)
212}
213
214pub fn Text(text: impl Into<String>) -> View {
215    View::new(
216        0,
217        ViewKind::Text {
218            text: text.into(),
219            color: locals::content_color(),
220            font_size: 16.0, // dp (converted to px in layout/paint)
221            soft_wrap: true,
222            max_lines: None,
223            overflow: TextOverflow::Visible,
224            font_family: None,
225            annotations: None,
226        },
227    )
228}
229
230/// Create a text view with rich text spans (AnnotatedString).
231///
232/// Each span can override color and font_size for a range of text.
233pub fn AnnotatedText(annotated: AnnotatedString) -> View {
234    let annotations: Option<std::sync::Arc<[TextSpan]>> = if annotated.spans.is_empty() {
235        None
236    } else {
237        Some(annotated.spans.clone())
238    };
239    View::new(
240        0,
241        ViewKind::Text {
242            text: annotated.text,
243            color: locals::content_color(),
244            font_size: 16.0,
245            soft_wrap: true,
246            max_lines: None,
247            overflow: TextOverflow::Visible,
248            font_family: None,
249            annotations,
250        },
251    )
252}
253
254pub fn Spacer() -> View {
255    Box(Modifier::new().flex_grow(1.0))
256}
257
258pub fn Space(modifier: Modifier) -> View {
259    Box(modifier)
260}
261
262pub fn Grid(
263    columns: usize,
264    modifier: Modifier,
265    children: Vec<View>,
266    row_gap: f32,
267    column_gap: f32,
268) -> View {
269    Column(modifier.grid(columns, row_gap, column_gap)).with_children(children)
270}
271
272pub fn Button(content: impl IntoChildren, on_click: impl Fn() + 'static) -> View {
273    View::new(
274        0,
275        ViewKind::Button {
276            on_click: Some(Rc::new(on_click)),
277        },
278    )
279    .with_children(content.into_children())
280    .semantics(Semantics {
281        role: Role::Button,
282        label: None, // optional: we could derive from first Text child later
283        focused: false,
284        enabled: true,
285    })
286}
287
288pub fn Slider(
289    value: f32,
290    range: (f32, f32),
291    step: Option<f32>,
292    on_change: impl Fn(f32) + 'static,
293) -> View {
294    View::new(
295        0,
296        ViewKind::Slider {
297            value,
298            min: range.0,
299            max: range.1,
300            step,
301            on_change: Some(Rc::new(on_change)),
302        },
303    )
304    .semantics(Semantics {
305        role: Role::Slider,
306        label: None,
307        focused: false,
308        enabled: true,
309    })
310}
311
312pub fn RangeSlider(
313    start: f32,
314    end: f32,
315    range: (f32, f32),
316    step: Option<f32>,
317    on_change: impl Fn(f32, f32) + 'static,
318) -> View {
319    View::new(
320        0,
321        ViewKind::RangeSlider {
322            start,
323            end,
324            min: range.0,
325            max: range.1,
326            step,
327            on_change: Some(Rc::new(on_change)),
328        },
329    )
330    .semantics(Semantics {
331        role: Role::Slider,
332        label: None,
333        focused: false,
334        enabled: true,
335    })
336}
337
338pub fn LinearProgress(value: Option<f32>) -> View {
339    View::new(
340        0,
341        ViewKind::ProgressBar {
342            value: value.unwrap_or(0.0),
343            min: 0.0,
344            max: 1.0,
345            circular: false,
346        },
347    )
348    .semantics(Semantics {
349        role: Role::ProgressBar,
350        label: None,
351        focused: false,
352        enabled: true,
353    })
354}
355
356pub fn ProgressBar(value: f32, range: (f32, f32)) -> View {
357    View::new(
358        0,
359        ViewKind::ProgressBar {
360            value,
361            min: range.0,
362            max: range.1,
363            circular: false,
364        },
365    )
366    .semantics(Semantics {
367        role: Role::ProgressBar,
368        label: None,
369        focused: false,
370        enabled: true,
371    })
372}
373
374/// A circular progress indicator (spinner).
375///
376/// If `value` is `None`, renders an indeterminate spinner (filled circle).
377/// If `value` is `Some(f)`, renders a determinate circular progress with
378/// the value clamped to `0.0..=1.0`.
379pub fn CircularProgress(value: Option<f32>) -> View {
380    View::new(
381        0,
382        ViewKind::ProgressBar {
383            value: value.unwrap_or(0.0),
384            min: 0.0,
385            max: 1.0,
386            circular: true,
387        },
388    )
389    .modifier(Modifier::new().width(48.0).height(48.0))
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) -> Option<FlexDirection> {
424    match kind {
425        ViewKind::Row => {
426            if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
427                Some(FlexDirection::RowReverse)
428            } else {
429                Some(FlexDirection::Row)
430            }
431        }
432        ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => {
433            Some(FlexDirection::Column)
434        }
435        _ => None,
436    }
437}
438
439/// Extension trait for child building
440pub trait ViewExt: Sized {
441    fn child(self, children: impl IntoChildren) -> Self;
442}
443
444impl ViewExt for View {
445    fn child(self, children: impl IntoChildren) -> Self {
446        self.with_children(children.into_children())
447    }
448}
449
450pub trait IntoChildren {
451    fn into_children(self) -> Vec<View>;
452}
453
454impl IntoChildren for View {
455    fn into_children(self) -> Vec<View> {
456        vec![self]
457    }
458}
459
460impl IntoChildren for Vec<View> {
461    fn into_children(self) -> Vec<View> {
462        self
463    }
464}
465
466impl<const N: usize> IntoChildren for [View; N] {
467    fn into_children(self) -> Vec<View> {
468        self.into()
469    }
470}
471
472// Tuple implementations
473macro_rules! impl_into_children_tuple {
474    ($($idx:tt $t:ident),+) => {
475        impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
476            fn into_children(self) -> Vec<View> {
477                let mut v = Vec::new();
478                $(v.extend(self.$idx.into_children());)+
479                v
480            }
481        }
482    };
483}
484
485impl_into_children_tuple!(0 A);
486impl_into_children_tuple!(0 A, 1 B);
487impl_into_children_tuple!(0 A, 1 B, 2 C);
488impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
489impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
490impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
491impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
492impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
493
494/// Layout and paint with TextField state injection (Taffy 0.9 API)
495pub fn layout_and_paint(
496    root: &View,
497    size_px_u32: (u32, u32),
498    textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
499    interactions: &Interactions,
500    focused: Option<u64>,
501) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
502    LAYOUT_ENGINE.with(|engine| {
503        engine
504            .borrow_mut()
505            .layout_frame(root, size_px_u32, textfield_states, interactions, focused)
506    })
507}
508
509/// Method styling
510pub trait TextStyle {
511    fn color(self, c: Color) -> View;
512    fn size(self, px: f32) -> View;
513    fn max_lines(self, n: usize) -> View;
514    fn single_line(self) -> View;
515    fn overflow_ellipsize(self) -> View;
516    fn overflow_clip(self) -> View;
517    fn overflow_visible(self) -> View;
518    fn font_family(self, family: &'static str) -> View;
519}
520impl TextStyle for View {
521    fn color(mut self, c: Color) -> View {
522        if let ViewKind::Text {
523            color: text_color, ..
524        } = &mut self.kind
525        {
526            *text_color = c;
527        }
528        self
529    }
530    fn size(mut self, dp_font: f32) -> View {
531        if let ViewKind::Text {
532            font_size: text_size_dp,
533            ..
534        } = &mut self.kind
535        {
536            *text_size_dp = dp_font;
537        }
538        self
539    }
540    fn max_lines(mut self, n: usize) -> View {
541        if let ViewKind::Text {
542            max_lines,
543            soft_wrap,
544            ..
545        } = &mut self.kind
546        {
547            *max_lines = Some(n);
548            *soft_wrap = true;
549        }
550        self
551    }
552    fn single_line(mut self) -> View {
553        if let ViewKind::Text {
554            soft_wrap,
555            max_lines,
556            ..
557        } = &mut self.kind
558        {
559            *soft_wrap = false;
560            *max_lines = Some(1);
561        }
562        self
563    }
564    fn overflow_ellipsize(mut self) -> View {
565        if let ViewKind::Text { overflow, .. } = &mut self.kind {
566            *overflow = TextOverflow::Ellipsis;
567        }
568        self
569    }
570    fn overflow_clip(mut self) -> View {
571        if let ViewKind::Text { overflow, .. } = &mut self.kind {
572            *overflow = TextOverflow::Clip;
573        }
574        self
575    }
576    fn overflow_visible(mut self) -> View {
577        if let ViewKind::Text { overflow, .. } = &mut self.kind {
578            *overflow = TextOverflow::Visible;
579        }
580        self
581    }
582    fn font_family(mut self, family: &'static str) -> View {
583        if let ViewKind::Text {
584            font_family: ff, ..
585        } = &mut self.kind
586        {
587            *ff = Some(family);
588        }
589        self
590    }
591}