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