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 anim;
106pub mod anim_ext;
107pub mod gestures;
108pub mod layout;
109pub mod lazy;
110pub mod navigation;
111pub mod overlay;
112pub mod scroll;
113
114use rustc_hash::{FxHashMap, FxHashSet};
115use std::collections::{HashMap, HashSet};
116use std::rc::Rc;
117use std::{cell::RefCell, cmp::Ordering};
118
119use repose_core::*;
120use taffy::style::FlexDirection;
121use taffy::{Overflow, Point};
122
123pub mod textfield;
124use crate::textfield::{TF_FONT_DP, TF_PADDING_X_DP, byte_to_char_index, measure_text};
125use repose_core::locals;
126pub use textfield::{TextArea, TextField, TextFieldState};
127
128thread_local! {
129    static LAYOUT_ENGINE: RefCell<layout::LayoutEngine> =
130        RefCell::new(layout::LayoutEngine::new());
131}
132
133#[derive(Default)]
134pub struct Interactions {
135    pub hover: Option<u64>,
136    pub pressed: HashSet<u64>,
137}
138
139pub fn Surface(modifier: Modifier, child: View) -> View {
140    let mut v = View::new(0, ViewKind::Surface).modifier(modifier);
141    v.children = vec![child];
142    v
143}
144
145pub fn Box(modifier: Modifier) -> View {
146    View::new(0, ViewKind::Box).modifier(modifier)
147}
148
149pub fn Row(modifier: Modifier) -> View {
150    View::new(0, ViewKind::Row).modifier(modifier)
151}
152
153pub fn Column(modifier: Modifier) -> View {
154    View::new(0, ViewKind::Column).modifier(modifier)
155}
156
157pub fn Stack(modifier: Modifier) -> View {
158    View::new(0, ViewKind::Stack).modifier(modifier)
159}
160
161pub fn OverlayHost(modifier: Modifier) -> View {
162    View::new(0, ViewKind::OverlayHost).modifier(modifier)
163}
164
165#[deprecated = "Use ScollArea instead"]
166pub fn Scroll(modifier: Modifier) -> View {
167    View::new(
168        0,
169        ViewKind::ScrollV {
170            on_scroll: None,
171            set_viewport_height: None,
172            set_content_height: None,
173            get_scroll_offset: None,
174            set_scroll_offset: None,
175        },
176    )
177    .modifier(modifier)
178}
179
180pub fn Text(text: impl Into<String>) -> View {
181    View::new(
182        0,
183        ViewKind::Text {
184            text: text.into(),
185            color: locals::theme().on_surface,
186            font_size: 16.0, // dp (converted to px in layout/paint)
187            soft_wrap: true,
188            max_lines: None,
189            overflow: TextOverflow::Visible,
190        },
191    )
192}
193
194pub fn Spacer() -> View {
195    Box(Modifier::new().flex_grow(1.0))
196}
197
198pub fn Space(modifier: Modifier) -> View {
199    Box(modifier)
200}
201
202pub fn Grid(
203    columns: usize,
204    modifier: Modifier,
205    children: Vec<View>,
206    row_gap: f32,
207    column_gap: f32,
208) -> View {
209    Column(modifier.grid(columns, row_gap, column_gap)).with_children(children)
210}
211
212pub fn Button(content: impl IntoChildren, on_click: impl Fn() + 'static) -> View {
213    View::new(
214        0,
215        ViewKind::Button {
216            on_click: Some(Rc::new(on_click)),
217        },
218    )
219    .with_children(content.into_children())
220    .semantics(Semantics {
221        role: Role::Button,
222        label: None, // optional: we could derive from first Text child later
223        focused: false,
224        enabled: true,
225    })
226}
227
228pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
229    View::new(
230        0,
231        ViewKind::Checkbox {
232            checked,
233            on_change: Some(Rc::new(on_change)),
234        },
235    )
236    .semantics(Semantics {
237        role: Role::Checkbox,
238        label: None,
239        focused: false,
240        enabled: true,
241    })
242}
243
244pub fn RadioButton(selected: bool, on_select: impl Fn() + 'static) -> View {
245    View::new(
246        0,
247        ViewKind::RadioButton {
248            selected,
249            on_select: Some(Rc::new(on_select)),
250        },
251    )
252    .semantics(Semantics {
253        role: Role::RadioButton,
254        label: None,
255        focused: false,
256        enabled: true,
257    })
258}
259
260pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
261    View::new(
262        0,
263        ViewKind::Switch {
264            checked,
265            on_change: Some(Rc::new(on_change)),
266        },
267    )
268    .semantics(Semantics {
269        role: Role::Switch,
270        label: None,
271        focused: false,
272        enabled: true,
273    })
274}
275pub fn Slider(
276    value: f32,
277    range: (f32, f32),
278    step: Option<f32>,
279    on_change: impl Fn(f32) + 'static,
280) -> View {
281    View::new(
282        0,
283        ViewKind::Slider {
284            value,
285            min: range.0,
286            max: range.1,
287            step,
288            on_change: Some(Rc::new(on_change)),
289        },
290    )
291    .semantics(Semantics {
292        role: Role::Slider,
293        label: None,
294        focused: false,
295        enabled: true,
296    })
297}
298
299pub fn RangeSlider(
300    start: f32,
301    end: f32,
302    range: (f32, f32),
303    step: Option<f32>,
304    on_change: impl Fn(f32, f32) + 'static,
305) -> View {
306    View::new(
307        0,
308        ViewKind::RangeSlider {
309            start,
310            end,
311            min: range.0,
312            max: range.1,
313            step,
314            on_change: Some(Rc::new(on_change)),
315        },
316    )
317    .semantics(Semantics {
318        role: Role::Slider,
319        label: None,
320        focused: false,
321        enabled: true,
322    })
323}
324
325pub fn LinearProgress(value: Option<f32>) -> View {
326    View::new(
327        0,
328        ViewKind::ProgressBar {
329            value: value.unwrap_or(0.0),
330            min: 0.0,
331            max: 1.0,
332            circular: false,
333        },
334    )
335    .semantics(Semantics {
336        role: Role::ProgressBar,
337        label: None,
338        focused: false,
339        enabled: true,
340    })
341}
342
343pub fn ProgressBar(value: f32, range: (f32, f32)) -> View {
344    View::new(
345        0,
346        ViewKind::ProgressBar {
347            value,
348            min: range.0,
349            max: range.1,
350            circular: false,
351        },
352    )
353    .semantics(Semantics {
354        role: Role::ProgressBar,
355        label: None,
356        focused: false,
357        enabled: true,
358    })
359}
360
361pub fn Image(modifier: Modifier, handle: ImageHandle) -> View {
362    View::new(
363        0,
364        ViewKind::Image {
365            handle,
366            tint: Color::WHITE,
367            fit: ImageFit::Contain,
368        },
369    )
370    .modifier(modifier)
371}
372
373pub trait ImageExt {
374    fn image_tint(self, c: Color) -> View;
375    fn image_fit(self, fit: ImageFit) -> View;
376}
377impl ImageExt for View {
378    fn image_tint(mut self, c: Color) -> View {
379        if let ViewKind::Image { tint, .. } = &mut self.kind {
380            *tint = c;
381        }
382        self
383    }
384    fn image_fit(mut self, fit: ImageFit) -> View {
385        if let ViewKind::Image { fit: f, .. } = &mut self.kind {
386            *f = fit;
387        }
388        self
389    }
390}
391
392fn flex_dir_for(kind: &ViewKind) -> Option<FlexDirection> {
393    match kind {
394        ViewKind::Row => {
395            if repose_core::locals::text_direction() == repose_core::locals::TextDirection::Rtl {
396                Some(FlexDirection::RowReverse)
397            } else {
398                Some(FlexDirection::Row)
399            }
400        }
401        ViewKind::Column | ViewKind::Surface | ViewKind::ScrollV { .. } => {
402            Some(FlexDirection::Column)
403        }
404        _ => None,
405    }
406}
407
408/// Extension trait for child building
409pub trait ViewExt: Sized {
410    fn child(self, children: impl IntoChildren) -> Self;
411}
412
413impl ViewExt for View {
414    fn child(self, children: impl IntoChildren) -> Self {
415        self.with_children(children.into_children())
416    }
417}
418
419pub trait IntoChildren {
420    fn into_children(self) -> Vec<View>;
421}
422
423impl IntoChildren for View {
424    fn into_children(self) -> Vec<View> {
425        vec![self]
426    }
427}
428
429impl IntoChildren for Vec<View> {
430    fn into_children(self) -> Vec<View> {
431        self
432    }
433}
434
435impl<const N: usize> IntoChildren for [View; N] {
436    fn into_children(self) -> Vec<View> {
437        self.into()
438    }
439}
440
441// Tuple implementations
442macro_rules! impl_into_children_tuple {
443    ($($idx:tt $t:ident),+) => {
444        impl<$($t: IntoChildren),+> IntoChildren for ($($t,)+) {
445            fn into_children(self) -> Vec<View> {
446                let mut v = Vec::new();
447                $(v.extend(self.$idx.into_children());)+
448                v
449            }
450        }
451    };
452}
453
454impl_into_children_tuple!(0 A);
455impl_into_children_tuple!(0 A, 1 B);
456impl_into_children_tuple!(0 A, 1 B, 2 C);
457impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D);
458impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
459impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
460impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
461impl_into_children_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
462
463/// Layout and paint with TextField state injection (Taffy 0.9 API)
464pub fn layout_and_paint(
465    root: &View,
466    size_px_u32: (u32, u32),
467    textfield_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
468    interactions: &Interactions,
469    focused: Option<u64>,
470) -> (Scene, Vec<HitRegion>, Vec<SemNode>) {
471    LAYOUT_ENGINE.with(|engine| {
472        engine
473            .borrow_mut()
474            .layout_frame(root, size_px_u32, textfield_states, interactions, focused)
475    })
476}
477
478/// Method styling
479pub trait TextStyle {
480    fn color(self, c: Color) -> View;
481    fn size(self, px: f32) -> View;
482    fn max_lines(self, n: usize) -> View;
483    fn single_line(self) -> View;
484    fn overflow_ellipsize(self) -> View;
485    fn overflow_clip(self) -> View;
486    fn overflow_visible(self) -> View;
487}
488impl TextStyle for View {
489    fn color(mut self, c: Color) -> View {
490        if let ViewKind::Text {
491            color: text_color, ..
492        } = &mut self.kind
493        {
494            *text_color = c;
495        }
496        self
497    }
498    fn size(mut self, dp_font: f32) -> View {
499        if let ViewKind::Text {
500            font_size: text_size_dp,
501            ..
502        } = &mut self.kind
503        {
504            *text_size_dp = dp_font;
505        }
506        self
507    }
508    fn max_lines(mut self, n: usize) -> View {
509        if let ViewKind::Text {
510            max_lines,
511            soft_wrap,
512            ..
513        } = &mut self.kind
514        {
515            *max_lines = Some(n);
516            *soft_wrap = true;
517        }
518        self
519    }
520    fn single_line(mut self) -> View {
521        if let ViewKind::Text {
522            soft_wrap,
523            max_lines,
524            ..
525        } = &mut self.kind
526        {
527            *soft_wrap = false;
528            *max_lines = Some(1);
529        }
530        self
531    }
532    fn overflow_ellipsize(mut self) -> View {
533        if let ViewKind::Text { overflow, .. } = &mut self.kind {
534            *overflow = TextOverflow::Ellipsis;
535        }
536        self
537    }
538    fn overflow_clip(mut self) -> View {
539        if let ViewKind::Text { overflow, .. } = &mut self.kind {
540            *overflow = TextOverflow::Clip;
541        }
542        self
543    }
544    fn overflow_visible(mut self) -> View {
545        if let ViewKind::Text { overflow, .. } = &mut self.kind {
546            *overflow = TextOverflow::Visible;
547        }
548        self
549    }
550}