Skip to main content

repose_core/
view.rs

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