Skip to main content

repose_core/
view.rs

1use crate::{Brush, Color, Modifier, Rect, TextSpan, Transform};
2use std::{fmt::Formatter, 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
119    Image {
120        handle: ImageHandle,
121        tint: Color, // multiplicative (WHITE = no tint)
122        fit: ImageFit,
123    },
124    Ellipse {
125        rect: Rect,
126        color: Color,
127    },
128    EllipseBorder {
129        rect: Rect,
130        color: Color,
131        width: f32, // screen-space width (px)
132    },
133    /// A layout whose children are produced by calling `content` with the
134    /// current `SubcomposeScope`. The closure is invoked during reconciliation
135    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
136    /// identity used to reconcile the returned view across frames. This is
137    /// the building block for `BoxWithConstraints` and other
138    /// constraints-driven layouts.
139    ///
140    /// Note: any `Modifier::key` set on a returned view is overwritten by its
141    /// slot id so the slot's identity is stable across frames.
142    SubcomposeLayout {
143        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
144    },
145    /// A collapsible section with a clickable header.
146    /// First child is the header content; remaining children shown only when expanded.
147    Expander {
148        expanded: bool,
149        on_toggle: Option<Callback>,
150    },
151    /// A single row in a tree view with indentation and expand/select support.
152    /// First child is rendered as the row label/content.
153    TreeRow {
154        depth: usize,
155        has_children: bool,
156        is_expanded: bool,
157        is_selected: bool,
158        on_toggle: Option<Callback>,
159        on_select: Option<Callback>,
160    },
161}
162
163impl std::fmt::Debug for ViewKind {
164    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165        match self {
166            Self::Surface => f.write_str("Surface"),
167            Self::Box => f.write_str("Box"),
168            Self::Row => f.write_str("Row"),
169            Self::Column => f.write_str("Column"),
170            Self::Stack => f.write_str("Stack"),
171            Self::ZStack => f.write_str("ZStack"),
172            Self::OverlayHost => f.write_str("OverlayHost"),
173            Self::ScrollV { .. } => f.write_str("ScrollV"),
174            Self::ScrollXY { .. } => f.write_str("ScrollXY"),
175            Self::Image { .. } => f.write_str("Image"),
176            Self::Ellipse { .. } => f.write_str("Ellipse"),
177            Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
178            Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
179            Self::Text { text, .. } => write!(f, "Text({:?})", text),
180
181            Self::Expander { expanded, .. } => {
182                if *expanded {
183                    write!(f, "Expander(expanded)")
184                } else {
185                    write!(f, "Expander(collapsed)")
186                }
187            }
188            Self::TreeRow {
189                depth,
190                has_children,
191                is_expanded,
192                is_selected,
193                ..
194            } => {
195                write!(
196                    f,
197                    "TreeRow(depth={}, children={}, expanded={}, selected={})",
198                    depth, has_children, is_expanded, is_selected
199                )
200            }
201        }
202    }
203}
204
205#[derive(Clone, Debug)]
206pub struct View {
207    pub id: ViewId,
208    pub kind: ViewKind,
209    pub modifier: Modifier,
210    pub children: Vec<View>,
211    pub semantics: Option<crate::semantics::Semantics>,
212}
213
214impl View {
215    pub fn new(id: ViewId, kind: ViewKind) -> Self {
216        View {
217            id,
218            kind,
219            modifier: Modifier::default(),
220            children: vec![],
221            semantics: None,
222        }
223    }
224    pub fn modifier(mut self, m: Modifier) -> Self {
225        self.modifier = m;
226        self
227    }
228    /// Mark this view as disabled - ignores pointer events.
229    pub fn disabled(mut self) -> Self {
230        self.modifier.disabled = true;
231        self
232    }
233    pub fn with_children(mut self, kids: Vec<View>) -> Self {
234        self.children = kids;
235        self
236    }
237    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
238        self.semantics = Some(s);
239        self
240    }
241}
242
243/// Renderable scene
244#[derive(Clone, Debug, Default)]
245pub struct Scene {
246    pub clear_color: Color,
247    pub nodes: Vec<SceneNode>,
248}
249
250#[derive(Clone, Debug)]
251#[non_exhaustive]
252pub enum SceneNode {
253    Rect {
254        rect: Rect,
255        brush: Brush,
256        radius: f32,
257    },
258    Border {
259        rect: Rect,
260        color: Color,
261        width: f32,
262        radius: f32,
263    },
264    Text {
265        rect: Rect,
266        text: Arc<str>,
267        color: Color,
268        size: f32,
269        font_family: Option<&'static str>,
270    },
271    Ellipse {
272        rect: Rect,
273        brush: Brush,
274    },
275    EllipseBorder {
276        rect: Rect,
277        color: Color,
278        width: f32, // screen-space width (px)
279    },
280    PushClip {
281        rect: Rect,
282        radius: f32,
283    },
284    PopClip,
285    PushTransform {
286        transform: Transform,
287    },
288    PopTransform,
289    Image {
290        rect: Rect,
291        handle: ImageHandle,
292        tint: Color,
293        fit: ImageFit,
294    },
295    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
296    /// The `elevation` field controls offset and alpha.
297    Shadow {
298        rect: Rect,
299        radius: f32,
300        elevation: f32,
301        color: Color,
302    },
303    /// Mark the start of a graphics layer: the contained subtree is rendered
304    /// into an offscreen texture and then composited back into the parent.
305    /// `alpha` is the group-compositing alpha applied at composite time.
306    BeginLayer {
307        rect: Rect,
308        layer_id: u32,
309        alpha: f32,
310    },
311    /// Closes the graphics layer opened by the matching `BeginLayer`.
312    EndLayer {
313        layer_id: u32,
314    },
315    /// Draws a blurred drop shadow underneath a previously-rendered layer.
316    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
317    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
318    /// optional vertical offset.
319    CompositeShadow {
320        layer_id: u32,
321        blur_px: f32,
322        offset_px: (f32, f32),
323        color: Color,
324    },
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328#[non_exhaustive]
329pub enum TextOverflow {
330    Visible,
331    Clip,
332    Ellipsis,
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn subcompose_scope_unbounded_has_infinite_max() {
341        let s = SubcomposeScope::UNBOUNDED;
342        assert!(!s.max_width.is_finite());
343        assert!(!s.max_height.is_finite());
344        assert_eq!(s.min_width, 0.0);
345        assert_eq!(s.min_height, 0.0);
346    }
347
348    #[test]
349    fn subcompose_scope_new_round_trips() {
350        let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
351        assert_eq!(s.min_width, 10.0);
352        assert_eq!(s.max_width, 200.0);
353        assert_eq!(s.min_height, 20.0);
354        assert_eq!(s.max_height, 300.0);
355    }
356
357    #[test]
358    fn box_with_constraints_scope_bounded_predicates() {
359        let bounded = BoxWithConstraintsScope {
360            min_width: 0.0,
361            max_width: 360.0,
362            min_height: 0.0,
363            max_height: 640.0,
364        };
365        assert!(bounded.has_bounded_width());
366        assert!(bounded.has_bounded_height());
367
368        let unbounded = BoxWithConstraintsScope {
369            min_width: 0.0,
370            max_width: f32::INFINITY,
371            min_height: 0.0,
372            max_height: f32::INFINITY,
373        };
374        assert!(!unbounded.has_bounded_width());
375        assert!(!unbounded.has_bounded_height());
376    }
377
378    #[test]
379    fn view_kind_subcompose_layout_holds_closure() {
380        let v: View = View {
381            id: 0,
382            kind: ViewKind::SubcomposeLayout {
383                content: std::sync::Arc::new(|scope| {
384                    let _ = scope.max_width;
385                    vec![(0, View::new(0, ViewKind::Box))]
386                }),
387            },
388            modifier: Modifier::default(),
389            children: vec![],
390            semantics: None,
391        };
392        match &v.kind {
393            ViewKind::SubcomposeLayout { .. } => {}
394            _ => panic!("expected SubcomposeLayout"),
395        }
396    }
397
398    #[test]
399    fn view_kind_subcompose_layout_supports_multiple_slots() {
400        let v: View = View {
401            id: 0,
402            kind: ViewKind::SubcomposeLayout {
403                content: std::sync::Arc::new(|_scope| {
404                    vec![
405                        (1, View::new(0, ViewKind::Box)),
406                        (2, View::new(0, ViewKind::Box)),
407                        (3, View::new(0, ViewKind::Box)),
408                    ]
409                }),
410            },
411            modifier: Modifier::default(),
412            children: vec![],
413            semantics: None,
414        };
415        if let ViewKind::SubcomposeLayout { content } = &v.kind {
416            let slots = content(&SubcomposeScope::UNBOUNDED);
417            assert_eq!(slots.len(), 3);
418            assert_eq!(slots[0].0, 1);
419            assert_eq!(slots[1].0, 2);
420            assert_eq!(slots[2].0, 3);
421        } else {
422            panic!("expected SubcomposeLayout");
423        }
424    }
425}