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