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