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