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