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