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, Vec2,
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    ZStack,
87    OverlayHost,
88    Text {
89        text: String,
90        color: Color,
91        font_size: f32,
92        soft_wrap: bool,
93        max_lines: Option<usize>,
94        overflow: TextOverflow,
95        font_family: Option<&'static str>,
96        annotations: Option<Arc<[TextSpan]>>,
97        text_align: TextAlign,
98        font_weight: FontWeight,
99        font_style: FontStyle,
100        text_decoration: TextDecoration,
101        letter_spacing: f32,
102        line_height: f32,
103        /// URL for clickable link text.
104        url: Option<Arc<str>>,
105        /// OpenType font variation settings (e.g. "wght 700, opsz 24").
106        font_variation_settings: Option<Arc<str>>,
107    },
108
109    Image {
110        handle: ImageHandle,
111        tint: Color, // multiplicative (WHITE = no tint)
112        fit: ImageFit,
113    },
114    /// A layout whose children are produced by calling `content` with the
115    /// current `SubcomposeScope`. The closure is invoked during reconciliation
116    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
117    /// identity used to reconcile the returned view across frames. This is
118    /// the building block for `BoxWithConstraints` and other
119    /// constraints-driven layouts.
120    ///
121    /// Note: any `Modifier::key` set on a returned view is overwritten by its
122    /// slot id so the slot's identity is stable across frames.
123    SubcomposeLayout {
124        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
125    },
126    /// A collapsible section with a clickable header.
127    /// First child is the header content; remaining children shown only when expanded.
128    Expander {
129        expanded: bool,
130        on_toggle: Option<Callback>,
131    },
132    /// A single row in a tree view with indentation and expand/select support.
133    /// First child is rendered as the row label/content.
134    TreeRow {
135        depth: usize,
136        has_children: bool,
137        is_expanded: bool,
138        is_selected: bool,
139        on_toggle: Option<Callback>,
140        on_select: Option<Callback>,
141    },
142}
143
144impl std::fmt::Debug for ViewKind {
145    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::Box => f.write_str("Box"),
148            Self::Row => f.write_str("Row"),
149            Self::Column => f.write_str("Column"),
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 children(mut self, kids: impl Into<Vec<View>>) -> Self {
219        self.children = kids.into();
220        self
221    }
222    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
223        self.semantics = Some(s);
224        self
225    }
226}
227
228/// Renderable scene
229#[derive(Clone, Debug, Default)]
230pub struct Scene {
231    pub clear_color: Color,
232    pub nodes: Vec<SceneNode>,
233}
234
235/// Rarely-tweaked text style properties bundled for ergonomic Default.
236#[derive(Clone, Debug, PartialEq)]
237pub struct TextExtraStyle {
238    pub text_direction: TextDirection,
239    pub font_synthesis: FontSynthesis,
240    pub baseline_shift: BaselineShift,
241    pub draw_style: DrawStyle,
242}
243
244impl Default for TextExtraStyle {
245    fn default() -> Self {
246        Self {
247            text_direction: TextDirection::Ltr,
248            font_synthesis: FontSynthesis::Unspecified,
249            baseline_shift: BaselineShift::Unspecified,
250            draw_style: DrawStyle::Fill,
251        }
252    }
253}
254
255#[derive(Clone, Debug)]
256#[non_exhaustive]
257pub enum SceneNode {
258    Rect {
259        rect: Rect,
260        brush: Brush,
261        radius: [f32; 4],
262    },
263    Border {
264        rect: Rect,
265        color: Color,
266        width: f32,
267        radius: [f32; 4],
268    },
269    Text {
270        rect: Rect,
271        text: Arc<str>,
272        color: Color,
273        size: f32,
274        font_family: Option<&'static str>,
275        text_align: TextAlign,
276        font_weight: FontWeight,
277        font_style: FontStyle,
278        text_decoration: TextDecoration,
279        letter_spacing: f32,
280        line_height: f32,
281        /// Rarely-tweaked style properties, bundled for ergonomic Default.
282        extra_style: TextExtraStyle,
283        /// URL for clickable link text.
284        url: Option<Arc<str>>,
285        /// OpenType font variation settings (e.g. "wght 700, opsz 24").
286        font_variation_settings: Option<Arc<str>>,
287    },
288    Ellipse {
289        rect: Rect,
290        brush: Brush,
291    },
292    EllipseBorder {
293        rect: Rect,
294        color: Color,
295        width: f32, // screen-space width (px)
296    },
297    PushClip {
298        rect: Rect,
299        radius: [f32; 4],
300        op: ClipOp,
301    },
302    PopClip,
303    PushTransform {
304        transform: Transform,
305    },
306    PopTransform,
307    Image {
308        rect: Rect,
309        handle: ImageHandle,
310        tint: Color,
311        fit: ImageFit,
312    },
313    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
314    /// The `elevation` field controls offset and alpha.
315    Shadow {
316        rect: Rect,
317        radius: [f32; 4],
318        elevation: f32,
319        color: Color,
320    },
321    /// Mark the start of a graphics layer: the contained subtree is rendered
322    /// into an offscreen texture and then composited back into the parent.
323    /// `alpha` is the group-compositing alpha applied at composite time.
324    /// `blur_radius_x` / `blur_radius_y` are the gaussian blur radii in pixels
325    /// applied to the layer before compositing (0.0 = no blur on that axis).
326    /// `rectangle_edge` when true means clamp edge pixels (Rectangle treatment);
327    /// when false, out-of-bounds samples are transparent (Unbounded).
328    BeginLayer {
329        rect: Rect,
330        layer_id: u32,
331        alpha: f32,
332        blur_radius_x: f32,
333        blur_radius_y: f32,
334        rectangle_edge: bool,
335    },
336    /// Closes the graphics layer opened by the matching `BeginLayer`.
337    EndLayer {
338        layer_id: u32,
339    },
340    /// Draws a blurred drop shadow underneath a previously-rendered layer.
341    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
342    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
343    /// optional vertical offset.
344    CompositeShadow {
345        layer_id: u32,
346        blur_px: f32,
347        offset_px: (f32, f32),
348        color: Color,
349    },
350    /// Arc stroke
351    Arc {
352        rect: Rect,
353        start_angle: f32,
354        sweep_angle: f32,
355        stroke_width: f32,
356        color: Color,
357        cap: StrokeCap,
358    },
359    /// Pre-tessellated vector mesh (fill or stroke geometry produced by the
360    /// host, e.g. lyon tessellation). Vertices live in the mesh's own local
361    /// space; `transform` is a 2x3 affine that maps local -> world pixels and
362    /// is applied in the vertex shader. The current scene `PushTransform`
363    /// stack is folded in on top of `transform`.
364    VectorMesh {
365        mesh: Arc<VectorMeshData>,
366        transform: [f32; 6],
367        paint: PaintDesc,
368        /// Reserved for explicit clip assignment; clipping is otherwise
369        /// structural via `PushVectorClip`/`PopVectorClip`.
370        clip: Option<u32>,
371        blend: BlendMode,
372    },
373    /// Screen-space overlays (handles, rubber bands, playhead, …). Each mesh
374    /// is positioned in final device pixels and is *not* affected by the
375    /// world `PushTransform` stack — emit these outside the viewport's world
376    /// transform scope.
377    VectorOverlay {
378        meshes: Arc<[VectorMeshData]>,
379    },
380    /// Start a vector clip: the mesh is rendered into the stencil buffer
381    /// (increment) and subsequent content is masked to it. Mirrors the
382    /// rect-based `PushClip` but for arbitrary tessellated masks.
383    PushVectorClip {
384        mesh: Arc<VectorMeshData>,
385    },
386    /// End a vector clip opened by `PushVectorClip`.
387    PopVectorClip,
388}
389
390/// Shared vertex/index buffers for a tessellated vector mesh.
391#[derive(Clone, Debug, Default)]
392pub struct VectorMeshData {
393    pub vertices: Arc<[VectorVertex]>,
394    pub indices: Arc<[u32]>,
395}
396
397/// Pre-tessellated vertex: local position, premultiplied-linear color, and a
398/// free-form uv channel (unused for solid fills, reserved for texture/gradient
399/// sampling).
400#[derive(Clone, Copy, Debug, PartialEq)]
401#[repr(C)]
402pub struct VectorVertex {
403    pub pos: [f32; 2],
404    pub color: [f32; 4],
405    pub uv: [f32; 2],
406}
407
408/// How a `VectorMesh` is painted.
409#[derive(Clone, Copy, Debug, PartialEq)]
410#[non_exhaustive]
411pub enum PaintDesc {
412    /// Use per-vertex color.
413    Solid,
414    /// Two-stop linear gradient in the mesh's local space.
415    Linear {
416        start: Vec2,
417        end: Vec2,
418        start_color: Color,
419        end_color: Color,
420    },
421}
422
423/// Blend mode for a `VectorMesh`. Only `Alpha` (premultiplied alpha) is wired
424/// into the renderer today; the remaining variants are reserved.
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum BlendMode {
428    /// Standard premultiplied alpha blending.
429    Alpha,
430    /// Additive (screen-space light). Not yet implemented.
431    Add,
432    /// Multiply. Not yet implemented.
433    Multiply,
434    /// Overlay. Not yet implemented.
435    Overlay,
436}
437
438impl Default for BlendMode {
439    fn default() -> Self {
440        BlendMode::Alpha
441    }
442}
443
444#[derive(Clone, Copy, Debug, PartialEq, Eq)]
445#[non_exhaustive]
446pub enum TextOverflow {
447    Visible,
448    Clip,
449    Ellipsis,
450}
451
452/// Controls how line segments are joined in a stroked path.
453#[derive(Clone, Copy, Debug, PartialEq, Default)]
454pub enum StrokeJoin {
455    #[default]
456    /// Sharp corner joins.
457    Miter,
458    /// Semi-circular joins.
459    Round,
460    /// Beveled (flat) joins.
461    Bevel,
462}
463
464/// Controls how the endpoints of a stroked arc are drawn.
465#[derive(Clone, Copy, Debug, PartialEq, Default)]
466pub enum StrokeCap {
467    #[default]
468    /// Flat ends at the exact arc endpoint. No extension.
469    Butt,
470    /// Semicircle with diameter equal to the stroke width, centered at the
471    /// arc endpoint.
472    Round,
473    /// Flat-ended rectangle extending half the stroke width beyond the arc
474    /// endpoint.
475    Square,
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn subcompose_scope_unbounded_has_infinite_max() {
484        let s = SubcomposeScope::UNBOUNDED;
485        assert!(!s.max_width.is_finite());
486        assert!(!s.max_height.is_finite());
487        assert_eq!(s.min_width, 0.0);
488        assert_eq!(s.min_height, 0.0);
489    }
490
491    #[test]
492    fn subcompose_scope_new_round_trips() {
493        let s = SubcomposeScope::new(10.0, 200.0, 20.0, 300.0);
494        assert_eq!(s.min_width, 10.0);
495        assert_eq!(s.max_width, 200.0);
496        assert_eq!(s.min_height, 20.0);
497        assert_eq!(s.max_height, 300.0);
498    }
499
500    #[test]
501    fn box_with_constraints_scope_bounded_predicates() {
502        let bounded = BoxWithConstraintsScope {
503            min_width: 0.0,
504            max_width: 360.0,
505            min_height: 0.0,
506            max_height: 640.0,
507        };
508        assert!(bounded.has_bounded_width());
509        assert!(bounded.has_bounded_height());
510
511        let unbounded = BoxWithConstraintsScope {
512            min_width: 0.0,
513            max_width: f32::INFINITY,
514            min_height: 0.0,
515            max_height: f32::INFINITY,
516        };
517        assert!(!unbounded.has_bounded_width());
518        assert!(!unbounded.has_bounded_height());
519    }
520
521    #[test]
522    fn view_kind_subcompose_layout_holds_closure() {
523        let v: View = View {
524            id: 0,
525            kind: ViewKind::SubcomposeLayout {
526                content: std::sync::Arc::new(|scope| {
527                    let _ = scope.max_width;
528                    vec![(0, View::new(0, ViewKind::Box))]
529                }),
530            },
531            modifier: Modifier::default(),
532            children: vec![],
533            scope_key: None,
534            semantics: None,
535        };
536        match &v.kind {
537            ViewKind::SubcomposeLayout { .. } => {}
538            _ => panic!("expected SubcomposeLayout"),
539        }
540    }
541
542    #[test]
543    fn view_kind_subcompose_layout_supports_multiple_slots() {
544        let v: View = View {
545            id: 0,
546            kind: ViewKind::SubcomposeLayout {
547                content: std::sync::Arc::new(|_scope| {
548                    vec![
549                        (1, View::new(0, ViewKind::Box)),
550                        (2, View::new(0, ViewKind::Box)),
551                        (3, View::new(0, ViewKind::Box)),
552                    ]
553                }),
554            },
555            modifier: Modifier::default(),
556            children: vec![],
557            scope_key: None,
558            semantics: None,
559        };
560        if let ViewKind::SubcomposeLayout { content } = &v.kind {
561            let slots = content(&SubcomposeScope::UNBOUNDED);
562            assert_eq!(slots.len(), 3);
563            assert_eq!(slots[0].0, 1);
564            assert_eq!(slots[1].0, 2);
565            assert_eq!(slots[2].0, 3);
566        } else {
567            panic!("expected SubcomposeLayout");
568        }
569    }
570}