Skip to main content

repose_core/
view.rs

1use crate::{Brush, Color, Modifier, Rect, TextSpan, Transform};
2use std::{cell::Cell, rc::Rc, sync::Arc};
3
4/// The constraints that will be passed to a subcomposed child. Values are in
5/// device-independent pixels (dp), matching the units used by `Modifier`.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct SubcomposeScope {
8    pub min_width: f32,
9    pub max_width: f32,
10    pub min_height: f32,
11    pub max_height: f32,
12}
13
14impl SubcomposeScope {
15    /// A scope with no constraints: unbounded in both dimensions. Use this as
16    /// a default when the parent constraints are not yet known.
17    pub const UNBOUNDED: Self = Self {
18        min_width: 0.0,
19        max_width: f32::INFINITY,
20        min_height: 0.0,
21        max_height: f32::INFINITY,
22    };
23
24    /// Construct a scope from raw min/max dp values.
25    pub fn new(min_width: f32, max_width: f32, min_height: f32, max_height: f32) -> Self {
26        Self {
27            min_width,
28            max_width,
29            min_height,
30            max_height,
31        }
32    }
33}
34
35/// Scope passed to [`BoxWithConstraints`](crate::prelude::BoxWithConstraints)
36/// content. All values are in dp.
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct BoxWithConstraintsScope {
39    pub min_width: f32,
40    pub max_width: f32,
41    pub min_height: f32,
42    pub max_height: f32,
43}
44
45impl BoxWithConstraintsScope {
46    /// `true` if the width is bounded by the parent (i.e. not infinite).
47    pub fn has_bounded_width(&self) -> bool {
48        self.max_width.is_finite()
49    }
50
51    /// `true` if the height is bounded by the parent (i.e. not infinite).
52    pub fn has_bounded_height(&self) -> bool {
53        self.max_height.is_finite()
54    }
55}
56
57pub type ViewId = u64;
58
59pub type ImageHandle = u64;
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ImageFit {
63    Contain,
64    Cover,
65    FitWidth,
66    FitHeight,
67}
68
69pub type Callback = Rc<dyn Fn()>;
70pub type ScrollCallback = Rc<dyn Fn(crate::Vec2) -> crate::Vec2>;
71
72#[derive(Clone)]
73pub struct OverlayEntry {
74    pub id: u64,
75    pub view: Box<View>,
76}
77
78#[derive(Clone)]
79#[non_exhaustive]
80pub enum ViewKind {
81    Surface,
82    Box,
83    Row,
84    Column,
85    Stack,
86    ZStack,
87    OverlayHost,
88    ScrollV {
89        on_scroll: Option<ScrollCallback>,
90        set_viewport_height: Option<Rc<dyn Fn(f32)>>,
91        set_content_height: Option<Rc<dyn Fn(f32)>>,
92        get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
93        set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
94        show_scrollbar: bool,
95        tick_scroll: Option<Rc<dyn Fn()>>,
96    },
97    ScrollXY {
98        on_scroll: Option<ScrollCallback>,
99        set_viewport_width: Option<Rc<dyn Fn(f32)>>,
100        set_viewport_height: Option<Rc<dyn Fn(f32)>>,
101        set_content_width: Option<Rc<dyn Fn(f32)>>,
102        set_content_height: Option<Rc<dyn Fn(f32)>>,
103        get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
104        set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
105        show_scrollbar: bool,
106        tick_scroll: Option<Rc<dyn Fn()>>,
107    },
108    Text {
109        text: String,
110        color: Color,
111        font_size: f32,
112        soft_wrap: bool,
113        max_lines: Option<usize>,
114        overflow: TextOverflow,
115        font_family: Option<&'static str>,
116        annotations: Option<Arc<[TextSpan]>>,
117    },
118    Button {
119        on_click: Option<Callback>,
120    },
121    TextField {
122        state_key: ViewId,
123        hint: String,
124        multiline: bool,
125        on_change: Option<Rc<dyn Fn(String)>>,
126        on_submit: Option<Rc<dyn Fn(String)>>,
127        /// Set by the component (e.g. OutlinedTextField) to receive focus-change
128        /// signals from the layout/paint phase.
129        focus_tracker: Option<Rc<Cell<bool>>>,
130        /// Current text content, supplied by the caller. The platform syncs
131        value: String,
132        /// Optional text display transformation (e.g., password masking).
133        visual_transformation: Option<Rc<dyn crate::text::VisualTransformation>>,
134        /// Keyboard type hint for the platform IME.
135        keyboard_type: Option<crate::text::KeyboardType>,
136        /// IME action button configuration.
137        ime_action: Option<crate::text::ImeAction>,
138    },
139    Slider {
140        value: f32,
141        min: f32,
142        max: f32,
143        step: Option<f32>,
144        on_change: Option<CallbackF32>,
145    },
146    RangeSlider {
147        start: f32,
148        end: f32,
149        min: f32,
150        max: f32,
151        step: Option<f32>,
152        on_change: Option<CallbackRange>,
153    },
154    ProgressBar {
155        value: f32,
156        min: f32,
157        max: f32,
158        circular: bool,
159    },
160    Image {
161        handle: ImageHandle,
162        tint: Color, // multiplicative (WHITE = no tint)
163        fit: ImageFit,
164    },
165    Ellipse {
166        rect: Rect,
167        color: Color,
168    },
169    EllipseBorder {
170        rect: Rect,
171        color: Color,
172        width: f32, // screen-space width (px)
173    },
174    /// A layout whose children are produced by calling `content` with the
175    /// current `SubcomposeScope`. The closure is invoked during reconciliation
176    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
177    /// identity used to reconcile the returned view across frames. This is
178    /// the building block for `BoxWithConstraints` and other
179    /// constraints-driven layouts.
180    ///
181    /// Note: any `Modifier::key` set on a returned view is overwritten by its
182    /// slot id so the slot's identity is stable across frames.
183    SubcomposeLayout {
184        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
185    },
186    /// A collapsible section with a clickable header.
187    /// First child is the header content; remaining children shown only when expanded.
188    Expander {
189        expanded: bool,
190        on_toggle: Option<Callback>,
191    },
192    /// A single row in a tree view with indentation and expand/select support.
193    /// First child is rendered as the row label/content.
194    TreeRow {
195        depth: usize,
196        has_children: bool,
197        is_expanded: bool,
198        is_selected: bool,
199        on_toggle: Option<Callback>,
200        on_select: Option<Callback>,
201    },
202}
203
204impl std::fmt::Debug for ViewKind {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            Self::Surface => f.write_str("Surface"),
208            Self::Box => f.write_str("Box"),
209            Self::Row => f.write_str("Row"),
210            Self::Column => f.write_str("Column"),
211            Self::Stack => f.write_str("Stack"),
212            Self::ZStack => f.write_str("ZStack"),
213            Self::OverlayHost => f.write_str("OverlayHost"),
214            Self::ScrollV { .. } => f.write_str("ScrollV"),
215            Self::ScrollXY { .. } => f.write_str("ScrollXY"),
216            Self::Button { .. } => f.write_str("Button"),
217            Self::Image { .. } => f.write_str("Image"),
218            Self::Ellipse { .. } => f.write_str("Ellipse"),
219            Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
220            Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
221            Self::Text { text, .. } => write!(f, "Text({:?})", text),
222            Self::TextField {
223                hint,
224                visual_transformation,
225                keyboard_type,
226                ime_action,
227                ..
228            } => {
229                let mut s = f.debug_struct("TextField");
230                s.field("hint", hint);
231                if visual_transformation.is_some() {
232                    s.field("visual_transformation", &"…");
233                }
234                if let Some(kt) = keyboard_type {
235                    s.field("keyboard_type", kt);
236                }
237                if let Some(ia) = ime_action {
238                    s.field("ime_action", ia);
239                }
240                s.finish()
241            }
242            Self::Slider { value, .. } => write!(f, "Slider({})", value),
243            Self::RangeSlider { start, end, .. } => write!(f, "Range({}..{})", start, end),
244            Self::ProgressBar { value, .. } => write!(f, "Progress({})", value),
245            Self::Expander { expanded, .. } => {
246                if *expanded { write!(f, "Expander(expanded)") } else { write!(f, "Expander(collapsed)") }
247            }
248            Self::TreeRow { depth, has_children, is_expanded, is_selected, .. } => {
249                write!(f, "TreeRow(depth={}, children={}, expanded={}, selected={})",
250                    depth, has_children, is_expanded, is_selected)
251            }
252        }
253    }
254}
255
256#[derive(Clone, Debug)]
257pub struct View {
258    pub id: ViewId,
259    pub kind: ViewKind,
260    pub modifier: Modifier,
261    pub children: Vec<View>,
262    pub semantics: Option<crate::semantics::Semantics>,
263}
264
265impl View {
266    pub fn new(id: ViewId, kind: ViewKind) -> Self {
267        View {
268            id,
269            kind,
270            modifier: Modifier::default(),
271            children: vec![],
272            semantics: None,
273        }
274    }
275    pub fn modifier(mut self, m: Modifier) -> Self {
276        self.modifier = m;
277        self
278    }
279    /// Mark this view as disabled - ignores pointer events.
280    pub fn disabled(mut self) -> Self {
281        self.modifier.disabled = true;
282        self
283    }
284    pub fn with_children(mut self, kids: Vec<View>) -> Self {
285        self.children = kids;
286        self
287    }
288    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
289        self.semantics = Some(s);
290        self
291    }
292}
293
294/// Renderable scene
295#[derive(Clone, Debug, Default)]
296pub struct Scene {
297    pub clear_color: Color,
298    pub nodes: Vec<SceneNode>,
299}
300
301#[derive(Clone, Debug)]
302#[non_exhaustive]
303pub enum SceneNode {
304    Rect {
305        rect: Rect,
306        brush: Brush,
307        radius: f32,
308    },
309    Border {
310        rect: Rect,
311        color: Color,
312        width: f32,
313        radius: f32,
314    },
315    Text {
316        rect: Rect,
317        text: Arc<str>,
318        color: Color,
319        size: f32,
320        font_family: Option<&'static str>,
321    },
322    Ellipse {
323        rect: Rect,
324        brush: Brush,
325    },
326    EllipseBorder {
327        rect: Rect,
328        color: Color,
329        width: f32, // screen-space width (px)
330    },
331    PushClip {
332        rect: Rect,
333        radius: f32,
334    },
335    PopClip,
336    PushTransform {
337        transform: Transform,
338    },
339    PopTransform,
340    Image {
341        rect: Rect,
342        handle: ImageHandle,
343        tint: Color,
344        fit: ImageFit,
345    },
346    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
347    /// The `elevation` field controls offset and alpha.
348    Shadow {
349        rect: Rect,
350        radius: f32,
351        elevation: f32,
352        color: Color,
353    },
354    /// Mark the start of a graphics layer: the contained subtree is rendered
355    /// into an offscreen texture and then composited back into the parent.
356    /// `alpha` is the group-compositing alpha applied at composite time.
357    BeginLayer {
358        rect: Rect,
359        layer_id: u32,
360        alpha: f32,
361    },
362    /// Closes the graphics layer opened by the matching `BeginLayer`.
363    EndLayer {
364        layer_id: u32,
365    },
366    /// Draws a blurred drop shadow underneath a previously-rendered layer.
367    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
368    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
369    /// optional vertical offset.
370    CompositeShadow {
371        layer_id: u32,
372        blur_px: f32,
373        offset_px: (f32, f32),
374        color: Color,
375    },
376}
377
378pub type CallbackF32 = Rc<dyn Fn(f32)>;
379pub type CallbackRange = Rc<dyn Fn(f32, f32)>;
380
381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
382#[non_exhaustive]
383pub enum TextOverflow {
384    Visible,
385    Clip,
386    Ellipsis,
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn subcompose_scope_unbounded_has_infinite_max() {
395        let s = SubcomposeScope::UNBOUNDED;
396        assert!(!s.max_width.is_finite());
397        assert!(!s.max_height.is_finite());
398        assert_eq!(s.min_width, 0.0);
399        assert_eq!(s.min_height, 0.0);
400    }
401
402    #[test]
403    fn subcompose_scope_new_round_trips() {
404        let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
405        assert_eq!(s.min_width, 10.0);
406        assert_eq!(s.max_width, 200.0);
407        assert_eq!(s.min_height, 20.0);
408        assert_eq!(s.max_height, 300.0);
409    }
410
411    #[test]
412    fn box_with_constraints_scope_bounded_predicates() {
413        let bounded = BoxWithConstraintsScope {
414            min_width: 0.0,
415            max_width: 360.0,
416            min_height: 0.0,
417            max_height: 640.0,
418        };
419        assert!(bounded.has_bounded_width());
420        assert!(bounded.has_bounded_height());
421
422        let unbounded = BoxWithConstraintsScope {
423            min_width: 0.0,
424            max_width: f32::INFINITY,
425            min_height: 0.0,
426            max_height: f32::INFINITY,
427        };
428        assert!(!unbounded.has_bounded_width());
429        assert!(!unbounded.has_bounded_height());
430    }
431
432    #[test]
433    fn view_kind_subcompose_layout_holds_closure() {
434        let v: View = View {
435            id: 0,
436            kind: ViewKind::SubcomposeLayout {
437                content: std::sync::Arc::new(|scope| {
438                    let _ = scope.max_width;
439                    vec![(0, View::new(0, ViewKind::Box))]
440                }),
441            },
442            modifier: Modifier::default(),
443            children: vec![],
444            semantics: None,
445        };
446        match &v.kind {
447            ViewKind::SubcomposeLayout { .. } => {}
448            _ => panic!("expected SubcomposeLayout"),
449        }
450    }
451
452    #[test]
453    fn view_kind_subcompose_layout_supports_multiple_slots() {
454        let v: View = View {
455            id: 0,
456            kind: ViewKind::SubcomposeLayout {
457                content: std::sync::Arc::new(|_scope| {
458                    vec![
459                        (1, View::new(0, ViewKind::Box)),
460                        (2, View::new(0, ViewKind::Box)),
461                        (3, View::new(0, ViewKind::Box)),
462                    ]
463                }),
464            },
465            modifier: Modifier::default(),
466            children: vec![],
467            semantics: None,
468        };
469        if let ViewKind::SubcomposeLayout { content } = &v.kind {
470            let slots = content(&SubcomposeScope::UNBOUNDED);
471            assert_eq!(slots.len(), 3);
472            assert_eq!(slots[0].0, 1);
473            assert_eq!(slots[1].0, 2);
474            assert_eq!(slots[2].0, 3);
475        } else {
476            panic!("expected SubcomposeLayout");
477        }
478    }
479}