Skip to main content

repose_core/
view.rs

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