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