Skip to main content

repose_core/
view.rs

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