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)]
61pub enum ImageFit {
62    Contain,
63    Cover,
64    FitWidth,
65    FitHeight,
66}
67
68pub type Callback = Rc<dyn Fn()>;
69pub type ScrollCallback = Rc<dyn Fn(crate::Vec2) -> crate::Vec2>;
70
71#[derive(Clone)]
72pub struct OverlayEntry {
73    pub id: u64,
74    pub view: Box<View>,
75}
76
77#[derive(Clone)]
78pub enum ViewKind {
79    Surface,
80    Box,
81    Row,
82    Column,
83    Stack,
84    ZStack,
85    OverlayHost,
86    ScrollV {
87        on_scroll: Option<ScrollCallback>,
88        set_viewport_height: Option<Rc<dyn Fn(f32)>>,
89        set_content_height: Option<Rc<dyn Fn(f32)>>,
90        get_scroll_offset: Option<Rc<dyn Fn() -> f32>>,
91        set_scroll_offset: Option<Rc<dyn Fn(f32)>>,
92        show_scrollbar: bool,
93    },
94    ScrollXY {
95        on_scroll: Option<ScrollCallback>,
96        set_viewport_width: Option<Rc<dyn Fn(f32)>>,
97        set_viewport_height: Option<Rc<dyn Fn(f32)>>,
98        set_content_width: Option<Rc<dyn Fn(f32)>>,
99        set_content_height: Option<Rc<dyn Fn(f32)>>,
100        get_scroll_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
101        set_scroll_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
102        show_scrollbar: bool,
103    },
104    Text {
105        text: String,
106        color: Color,
107        font_size: f32,
108        soft_wrap: bool,
109        max_lines: Option<usize>,
110        overflow: TextOverflow,
111        font_family: Option<&'static str>,
112        annotations: Option<Arc<[TextSpan]>>,
113    },
114    Button {
115        on_click: Option<Callback>,
116    },
117    TextField {
118        state_key: ViewId,
119        hint: String,
120        multiline: bool,
121        on_change: Option<Rc<dyn Fn(String)>>,
122        on_submit: Option<Rc<dyn Fn(String)>>,
123        /// Set by the component (e.g. OutlinedTextField) to receive focus-change
124        /// signals from the layout/paint phase.
125        focus_tracker: Option<Rc<Cell<bool>>>,
126        /// Current text content, supplied by the caller. The platform syncs
127        value: String,
128    },
129    Slider {
130        value: f32,
131        min: f32,
132        max: f32,
133        step: Option<f32>,
134        on_change: Option<CallbackF32>,
135    },
136    RangeSlider {
137        start: f32,
138        end: f32,
139        min: f32,
140        max: f32,
141        step: Option<f32>,
142        on_change: Option<CallbackRange>,
143    },
144    ProgressBar {
145        value: f32,
146        min: f32,
147        max: f32,
148        circular: bool,
149    },
150    Image {
151        handle: ImageHandle,
152        tint: Color, // multiplicative (WHITE = no tint)
153        fit: ImageFit,
154    },
155    Ellipse {
156        rect: Rect,
157        color: Color,
158    },
159    EllipseBorder {
160        rect: Rect,
161        color: Color,
162        width: f32, // screen-space width (px)
163    },
164    /// A layout whose children are produced by calling `content` with the
165    /// current `SubcomposeScope`. The closure is invoked during reconciliation
166    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
167    /// identity used to reconcile the returned view across frames. This is
168    /// the building block for `BoxWithConstraints` and other
169    /// constraints-driven layouts.
170    ///
171    /// Note: any `Modifier::key` set on a returned view is overwritten by its
172    /// slot id so the slot's identity is stable across frames.
173    SubcomposeLayout {
174        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
175    },
176}
177
178impl std::fmt::Debug for ViewKind {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self {
181            Self::Surface => f.write_str("Surface"),
182            Self::Box => f.write_str("Box"),
183            Self::Row => f.write_str("Row"),
184            Self::Column => f.write_str("Column"),
185            Self::Stack => f.write_str("Stack"),
186            Self::ZStack => f.write_str("ZStack"),
187            Self::OverlayHost => f.write_str("OverlayHost"),
188            Self::ScrollV { .. } => f.write_str("ScrollV"),
189            Self::ScrollXY { .. } => f.write_str("ScrollXY"),
190            Self::Button { .. } => f.write_str("Button"),
191            Self::Image { .. } => f.write_str("Image"),
192            Self::Ellipse { .. } => f.write_str("Ellipse"),
193            Self::EllipseBorder { .. } => f.write_str("EllipseBorder"),
194            Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
195            Self::Text { text, .. } => write!(f, "Text({:?})", text),
196            Self::TextField { hint, .. } => write!(f, "TextField({:?})", hint),
197            Self::Slider { value, .. } => write!(f, "Slider({})", value),
198            Self::RangeSlider { start, end, .. } => write!(f, "Range({}..{})", start, end),
199            Self::ProgressBar { value, .. } => write!(f, "Progress({})", value),
200        }
201    }
202}
203
204#[derive(Clone, Debug)]
205pub struct View {
206    pub id: ViewId,
207    pub kind: ViewKind,
208    pub modifier: Modifier,
209    pub children: Vec<View>,
210    pub semantics: Option<crate::semantics::Semantics>,
211}
212
213impl View {
214    pub fn new(id: ViewId, kind: ViewKind) -> Self {
215        View {
216            id,
217            kind,
218            modifier: Modifier::default(),
219            children: vec![],
220            semantics: None,
221        }
222    }
223    pub fn modifier(mut self, m: Modifier) -> Self {
224        self.modifier = m;
225        self
226    }
227    /// Mark this view as disabled - ignores pointer events.
228    pub fn disabled(mut self) -> Self {
229        self.modifier.disabled = true;
230        self
231    }
232    pub fn with_children(mut self, kids: Vec<View>) -> Self {
233        self.children = kids;
234        self
235    }
236    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
237        self.semantics = Some(s);
238        self
239    }
240}
241
242/// Renderable scene
243#[derive(Clone, Debug, Default)]
244pub struct Scene {
245    pub clear_color: Color,
246    pub nodes: Vec<SceneNode>,
247}
248
249#[derive(Clone, Debug)]
250pub enum SceneNode {
251    Rect {
252        rect: Rect,
253        brush: Brush,
254        radius: f32,
255    },
256    Border {
257        rect: Rect,
258        color: Color,
259        width: f32,
260        radius: f32,
261    },
262    Text {
263        rect: Rect,
264        text: Arc<str>,
265        color: Color,
266        size: f32,
267        font_family: Option<&'static str>,
268    },
269    Ellipse {
270        rect: Rect,
271        brush: Brush,
272    },
273    EllipseBorder {
274        rect: Rect,
275        color: Color,
276        width: f32, // screen-space width (px)
277    },
278    PushClip {
279        rect: Rect,
280        radius: f32,
281    },
282    PopClip,
283    PushTransform {
284        transform: Transform,
285    },
286    PopTransform,
287    Image {
288        rect: Rect,
289        handle: ImageHandle,
290        tint: Color,
291        fit: ImageFit,
292    },
293    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
294    /// The `elevation` field controls offset and alpha.
295    Shadow {
296        rect: Rect,
297        radius: f32,
298        elevation: f32,
299        color: Color,
300    },
301    /// Mark the start of a graphics layer: the contained subtree is rendered
302    /// into an offscreen texture and then composited back into the parent.
303    /// `alpha` is the group-compositing alpha applied at composite time.
304    BeginLayer {
305        rect: Rect,
306        layer_id: u32,
307        alpha: f32,
308    },
309    /// Closes the graphics layer opened by the matching `BeginLayer`.
310    EndLayer {
311        layer_id: u32,
312    },
313    /// Draws a blurred drop shadow underneath a previously-rendered layer.
314    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
315    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
316    /// optional vertical offset.
317    CompositeShadow {
318        layer_id: u32,
319        blur_px: f32,
320        offset_px: (f32, f32),
321        color: Color,
322    },
323}
324
325pub type CallbackF32 = Rc<dyn Fn(f32)>;
326pub type CallbackRange = Rc<dyn Fn(f32, f32)>;
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
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}