Skip to main content

repose_core/
view.rs

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