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, 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        /// Fill, outline, or both (faux-bold). Default `Fill`.
117        draw_style: DrawStyle,
118    },
119
120    Image {
121        handle: ImageHandle,
122        tint: Color, // multiplicative (WHITE = no tint)
123        fit: ImageFit,
124    },
125    /// A layout whose children are produced by calling `content` with the
126    /// current `SubcomposeScope`. The closure is invoked during reconciliation
127    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
128    /// identity used to reconcile the returned view across frames. This is
129    /// the building block for `BoxWithConstraints` and other
130    /// constraints-driven layouts.
131    ///
132    /// Note: any `Modifier::key` set on a returned view is overwritten by its
133    /// slot id so the slot's identity is stable across frames.
134    SubcomposeLayout {
135        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
136    },
137}
138
139impl std::fmt::Debug for ViewKind {
140    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
141        match self {
142            Self::Box => f.write_str("Box"),
143            Self::Row => f.write_str("Row"),
144            Self::Column => f.write_str("Column"),
145            Self::ZStack => f.write_str("ZStack"),
146            Self::OverlayHost => f.write_str("OverlayHost"),
147
148            Self::Image { .. } => f.write_str("Image"),
149            Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
150            Self::Text { text, .. } => write!(f, "Text({:?})", text),
151        }
152    }
153}
154
155#[derive(Clone, Debug)]
156pub struct View {
157    pub id: ViewId,
158    pub kind: ViewKind,
159    pub modifier: Modifier,
160    pub children: Vec<View>,
161    pub semantics: Option<crate::semantics::Semantics>,
162    /// Set by `scope!` macro to mark this as a scope boundary node.
163    /// Carries the scope key (e.g., "title", "color_buttons") for per-scope
164    /// TaffyTree isolation.
165    pub scope_key: Option<String>,
166}
167
168impl View {
169    pub fn new(id: ViewId, kind: ViewKind) -> Self {
170        View {
171            id,
172            kind,
173            modifier: Modifier::default(),
174            children: vec![],
175            semantics: None,
176            scope_key: None,
177        }
178    }
179    pub fn modifier(mut self, m: Modifier) -> Self {
180        self.modifier = m;
181        self
182    }
183    /// Mark this view as disabled - ignores pointer events.
184    pub fn disabled(mut self) -> Self {
185        self.modifier.disabled = true;
186        self
187    }
188    pub fn with_children(mut self, kids: Vec<View>) -> Self {
189        self.children = kids;
190        self
191    }
192    pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
193        self.children = kids.into();
194        self
195    }
196    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
197        self.semantics = Some(s);
198        self
199    }
200}
201
202/// Renderable scene
203#[derive(Clone, Debug, Default)]
204pub struct Scene {
205    pub clear_color: Color,
206    pub nodes: Vec<SceneNode>,
207}
208
209/// Rarely-tweaked text style properties bundled for ergonomic Default.
210#[derive(Clone, Debug, PartialEq)]
211pub struct TextExtraStyle {
212    pub text_direction: TextDirection,
213    pub font_synthesis: FontSynthesis,
214    pub baseline_shift: BaselineShift,
215    pub draw_style: DrawStyle,
216}
217
218impl Default for TextExtraStyle {
219    fn default() -> Self {
220        Self {
221            text_direction: TextDirection::Ltr,
222            font_synthesis: FontSynthesis::Unspecified,
223            baseline_shift: BaselineShift::Unspecified,
224            draw_style: DrawStyle::Fill,
225        }
226    }
227}
228
229#[derive(Clone, Copy, Debug)]
230pub struct PaintCallbackInfo {
231    /// Viewport in physical pixels where the callback should paint.
232    pub viewport: Rect,
233    /// Clip rect in physical pixels (intersection of callback rect and current clip).
234    pub clip_rect: Rect,
235    /// Pixels per point (DPI scale).
236    pub pixels_per_point: f32,
237    /// Screen size in physical pixels.
238    pub screen_size_px: [u32; 2],
239}
240
241pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
242
243/// Paint-space scene graph. `Rect`/`Vec2` compounds carry physical pixels
244/// (like Compose `Offset`/`Size`/`Rect`); scalar lengths use [`Px`] so the
245/// dp→px boundary is explicit instead of unitless `f32`.
246#[derive(Clone, Debug)]
247#[non_exhaustive]
248pub enum SceneNode {
249    Rect {
250        rect: Rect,
251        brush: Brush,
252        radius: [Px; 4],
253    },
254    Border {
255        rect: Rect,
256        brush: Brush,
257        width: Px,
258        radius: [Px; 4],
259    },
260    Text {
261        rect: Rect,
262        text: Arc<str>,
263        color: Color,
264        size: Px,
265        font_family: Option<&'static str>,
266        text_align: TextAlign,
267        font_weight: FontWeight,
268        font_style: FontStyle,
269        text_decoration: TextDecoration,
270        letter_spacing: Px,
271        line_height: Px,
272        /// Rarely-tweaked style properties, bundled for ergonomic Default.
273        extra_style: TextExtraStyle,
274        /// URL for clickable link text.
275        url: Option<Arc<str>>,
276        /// OpenType font variation settings (e.g. "wght 700, opsz 24").
277        font_variation_settings: Option<Arc<str>>,
278    },
279    Ellipse {
280        rect: Rect,
281        brush: Brush,
282    },
283    EllipseBorder {
284        rect: Rect,
285        brush: Brush,
286        width: Px,
287    },
288    PushClip {
289        rect: Rect,
290        radius: [Px; 4],
291        op: ClipOp,
292    },
293    PopClip,
294    PushTransform {
295        transform: Transform,
296    },
297    PopTransform,
298    Image {
299        rect: Rect,
300        handle: ImageHandle,
301        tint: Color,
302        fit: ImageFit,
303    },
304    /// Tinted A8 coverage mask: samples `handle` (registered with
305    /// `register_coverage_a8`) as coverage and composites `color` with
306    /// source-over. Lets hosts rasterize geometry once (e.g. cached glyph
307    /// coverage tiles) and re-composite it per frame with a new color or
308    /// position without re-uploading. `rect` positions the tile's top-left;
309    /// its size is the registered tile size.
310    Coverage {
311        rect: Rect,
312        handle: ImageHandle,
313        color: Color,
314    },
315    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
316    /// The `elevation` field controls offset and alpha.
317    Shadow {
318        rect: Rect,
319        radius: [Px; 4],
320        elevation: Px,
321        color: Color,
322    },
323    /// Mark the start of a graphics layer: the contained subtree is rendered
324    /// into an offscreen texture and then composited back into the parent.
325    /// `alpha` is the group-compositing alpha applied at composite time.
326    /// `blur_radius_x` / `blur_radius_y` are the gaussian blur radii in [`Px`]
327    /// applied to the layer before compositing (zero = no blur on that axis).
328    /// `rectangle_edge` true = clamp edge pixels (Rectangle). False = transparent out-of-bounds (Unbounded).
329    BeginLayer {
330        rect: Rect,
331        layer_id: u32,
332        alpha: f32,
333        blur_radius_x: Px,
334        blur_radius_y: Px,
335        rectangle_edge: bool,
336    },
337    /// Closes the graphics layer opened by the matching `BeginLayer`.
338    EndLayer {
339        layer_id: u32,
340    },
341    /// Draws a blurred drop shadow underneath a previously-rendered layer.
342    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
343    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
344    /// optional vertical offset.
345    CompositeShadow {
346        layer_id: u32,
347        blur_px: Px,
348        offset_px: (Px, Px),
349        color: Color,
350    },
351    /// Arc stroke
352    Arc {
353        rect: Rect,
354        start_angle: f32,
355        sweep_angle: f32,
356        stroke_width: Px,
357        brush: Brush,
358        cap: StrokeCap,
359    },
360    /// Pre-tessellated vector mesh (fill or stroke geometry produced by the
361    /// host, e.g. lyon tessellation). Vertices live in the mesh's own local
362    /// space; `transform` is a 2x3 affine that maps local -> world pixels as
363    /// `[m00, m01, m10, m11, tx, ty]` (`out = M * local + t`; identity is
364    /// `[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]`) and is applied in the vertex shader.
365    /// The current scene `PushTransform` stack is folded in on top of
366    /// `transform`.
367    VectorMesh {
368        mesh: Arc<VectorMeshData>,
369        transform: [f32; 6],
370        paint: PaintDesc,
371        /// Reserved for explicit clip assignment. Clipping is otherwise
372        /// structural via `PushVectorClip`/`PopVectorClip`.
373        clip: Option<u32>,
374        blend: BlendMode,
375    },
376    /// Screen-space overlays (handles, rubber bands, playhead). Each mesh
377    /// is positioned in final device pixels and ignores the world
378    /// PushTransform stack. Emit outside the viewport's world transform.
379    VectorOverlay {
380        meshes: Arc<[VectorMeshData]>,
381    },
382    /// Start a vector clip: the mesh is rendered into the stencil buffer
383    /// (increment) and subsequent content is masked to it. Mirrors the
384    /// rect-based `PushClip` but for arbitrary tessellated masks.
385    /// `op` selects intersection (keep content inside the mask, the common
386    /// case) or difference (cut the mask out, e.g. ASS `\iclip` drawings).
387    /// Difference is exact for a lone mask and for a mask nested inside
388    /// intersect clips; a normal clip nested inside a difference mask is
389    /// best-effort (see the renderer docs).
390    PushVectorClip {
391        mesh: Arc<VectorMeshData>,
392        op: ClipOp,
393    },
394    /// End a vector clip opened by `PushVectorClip`.
395    PopVectorClip,
396    /// Custom GPU paint callback (check `egui::PaintCallback`).
397    Callback {
398        rect: Rect,
399        payload: PaintCallbackPayload,
400    },
401}
402
403/// Shared vertex/index buffers for a tessellated vector mesh.
404#[derive(Clone, Debug, Default)]
405pub struct VectorMeshData {
406    pub vertices: Arc<[VectorVertex]>,
407    pub indices: Arc<[u32]>,
408}
409
410/// Pre-tessellated vertex: local position, premultiplied-linear color, and a
411/// free-form uv channel (unused for solid fills, reserved for texture/gradient
412/// sampling).
413#[derive(Clone, Copy, Debug, PartialEq)]
414#[repr(C)]
415pub struct VectorVertex {
416    pub pos: [f32; 2],
417    pub color: [f32; 4],
418    pub uv: [f32; 2],
419}
420
421/// How a `VectorMesh` is painted. Mirrors the [`Brush`] variants;
422/// gradient endpoints live in the mesh's local space.
423#[derive(Clone, Copy, Debug, PartialEq)]
424#[non_exhaustive]
425pub enum PaintDesc {
426    /// Use per-vertex color.
427    Solid,
428    /// Two-stop linear gradient in the mesh's local space.
429    Linear {
430        start: Vec2,
431        end: Vec2,
432        start_color: Color,
433        end_color: Color,
434    },
435    /// Radial gradient centered at `center` (mesh-local) with `radius`.
436    Radial {
437        center: Vec2,
438        radius: f32,
439        start_color: Color,
440        end_color: Color,
441    },
442    /// Angular sweep around `center` (mesh-local), clockwise from 3 o'clock.
443    Sweep {
444        center: Vec2,
445        start_color: Color,
446        end_color: Color,
447    },
448}
449
450/// Blend mode for a `VectorMesh`, following the CSS `mix-blend-mode`
451/// vocabulary used by SVG/Lottie-style artwork. All variants are wired into
452/// the renderer: separable modes use fixed-function blending, the rest
453/// (marked below) isolate the mesh into a graphics layer and composite it
454/// with a custom shader.
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456#[non_exhaustive]
457#[derive(Default)]
458pub enum BlendMode {
459    /// Standard premultiplied alpha blending (`normal`).
460    #[default]
461    Alpha,
462    /// `screen`: `1 - (1 - S) * (1 - D)`. Fixed-function.
463    Screen,
464    /// `overlay`: `Multiply` for dark backdrops, `Screen` for light ones.
465    Overlay,
466    /// `darken`: per-channel `min(S, D)`. Fixed-function.
467    Darken,
468    /// `lighten`: per-channel `max(S, D)`. Fixed-function.
469    Lighten,
470    /// `color-dodge`: brightens the backdrop toward the source color.
471    ColorDodge,
472    /// `color-burn`: darkens the backdrop toward the source color.
473    ColorBurn,
474    /// `hard-light`: `Overlay` with source and backdrop swapped.
475    HardLight,
476    /// `soft-light`: subtle `Overlay` variant.
477    SoftLight,
478    /// `difference`: per-channel `|S - D|`.
479    /// Needs the backdrop (fixed-function subtract only covers `S - D`),
480    /// so it composites via an isolated layer.
481    Difference,
482    /// `exclusion`: like `difference` with lower contrast.
483    Exclusion,
484    /// `hue`: source hue with backdrop saturation and luminosity.
485    Hue,
486    /// `saturation`: source saturation with backdrop hue and luminosity.
487    Saturation,
488    /// `color`: source hue and saturation with backdrop luminosity.
489    Color,
490    /// `luminosity`: source luminosity with backdrop hue and saturation.
491    Luminosity,
492    /// Additive (screen-space light). Fixed-function.
493    Add,
494    /// `multiply`: per-channel `S * D`. Fixed-function.
495    Multiply,
496}
497
498impl BlendMode {
499    /// Modes implemented with fixed-function hardware blending on the mesh
500    /// pipeline. Everything else isolates the mesh into a graphics layer and
501    /// composites it with the backdrop-blend shader.
502    pub fn needs_isolation(self) -> bool {
503        !matches!(
504            self,
505            BlendMode::Alpha
506                | BlendMode::Add
507                | BlendMode::Multiply
508                | BlendMode::Screen
509                | BlendMode::Darken
510                | BlendMode::Lighten
511        )
512    }
513
514    /// Discriminant consumed by `blend_layer.wgsl` (`mode` flat varying).
515    pub fn shader_mode(self) -> u32 {
516        match self {
517            BlendMode::Alpha => 0,
518            BlendMode::Add => 1,
519            BlendMode::Multiply => 2,
520            BlendMode::Screen => 3,
521            BlendMode::Overlay => 4,
522            BlendMode::Darken => 5,
523            BlendMode::Lighten => 6,
524            BlendMode::ColorDodge => 7,
525            BlendMode::ColorBurn => 8,
526            BlendMode::HardLight => 9,
527            BlendMode::SoftLight => 10,
528            BlendMode::Difference => 11,
529            BlendMode::Exclusion => 12,
530            BlendMode::Hue => 13,
531            BlendMode::Saturation => 14,
532            BlendMode::Color => 15,
533            BlendMode::Luminosity => 16,
534        }
535    }
536}
537
538#[derive(Clone, Copy, Debug, PartialEq, Eq)]
539#[non_exhaustive]
540pub enum TextOverflow {
541    Visible,
542    Clip,
543    Ellipsis,
544}
545
546/// Controls how line segments are joined in a stroked path.
547#[derive(Clone, Copy, Debug, PartialEq, Default)]
548pub enum StrokeJoin {
549    #[default]
550    /// Sharp corner joins.
551    Miter,
552    /// Semi-circular joins.
553    Round,
554    /// Beveled (flat) joins.
555    Bevel,
556}
557
558/// Controls how the endpoints of a stroked arc are drawn.
559#[derive(Clone, Copy, Debug, PartialEq, Default)]
560pub enum StrokeCap {
561    #[default]
562    /// Flat ends at the exact arc endpoint. No extension.
563    Butt,
564    /// Semicircle with diameter equal to the stroke width, centered at the
565    /// arc endpoint.
566    Round,
567    /// Flat-ended rectangle extending half the stroke width beyond the arc
568    /// endpoint.
569    Square,
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn subcompose_scope_unbounded_has_infinite_max() {
578        let s = SubcomposeScope::UNBOUNDED;
579        assert!(!s.max_width.0.is_finite());
580        assert!(!s.max_height.0.is_finite());
581        assert_eq!(s.min_width, Dp(0.0));
582        assert_eq!(s.min_height, Dp(0.0));
583    }
584
585    #[test]
586    fn subcompose_scope_new_round_trips() {
587        let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
588        assert_eq!(s.min_width, Dp(10.0));
589        assert_eq!(s.max_width, Dp(200.0));
590        assert_eq!(s.min_height, Dp(20.0));
591        assert_eq!(s.max_height, Dp(300.0));
592    }
593
594    #[test]
595    fn box_with_constraints_scope_bounded_predicates() {
596        let bounded = BoxWithConstraintsScope {
597            min_width: Dp(0.0),
598            max_width: Dp(360.0),
599            min_height: Dp(0.0),
600            max_height: Dp(640.0),
601        };
602        assert!(bounded.has_bounded_width());
603        assert!(bounded.has_bounded_height());
604
605        let unbounded = BoxWithConstraintsScope {
606            min_width: Dp(0.0),
607            max_width: Dp(f32::INFINITY),
608            min_height: Dp(0.0),
609            max_height: Dp(f32::INFINITY),
610        };
611        assert!(!unbounded.has_bounded_width());
612        assert!(!unbounded.has_bounded_height());
613    }
614
615    #[test]
616    fn view_kind_subcompose_layout_holds_closure() {
617        let v: View = View {
618            id: 0,
619            kind: ViewKind::SubcomposeLayout {
620                content: std::sync::Arc::new(|scope| {
621                    let _ = scope.max_width;
622                    vec![(0, View::new(0, ViewKind::Box))]
623                }),
624            },
625            modifier: Modifier::default(),
626            children: vec![],
627            scope_key: None,
628            semantics: None,
629        };
630        match &v.kind {
631            ViewKind::SubcomposeLayout { .. } => {}
632            _ => panic!("expected SubcomposeLayout"),
633        }
634    }
635
636    #[test]
637    fn view_kind_subcompose_layout_supports_multiple_slots() {
638        let v: View = View {
639            id: 0,
640            kind: ViewKind::SubcomposeLayout {
641                content: std::sync::Arc::new(|_scope| {
642                    vec![
643                        (1, View::new(0, ViewKind::Box)),
644                        (2, View::new(0, ViewKind::Box)),
645                        (3, View::new(0, ViewKind::Box)),
646                    ]
647                }),
648            },
649            modifier: Modifier::default(),
650            children: vec![],
651            scope_key: None,
652            semantics: None,
653        };
654        if let ViewKind::SubcomposeLayout { content } = &v.kind {
655            let slots = content(&SubcomposeScope::UNBOUNDED);
656            assert_eq!(slots.len(), 3);
657            assert_eq!(slots[0].0, 1);
658            assert_eq!(slots[1].0, 2);
659            assert_eq!(slots[2].0, 3);
660        } else {
661            panic!("expected SubcomposeLayout");
662        }
663    }
664}