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