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    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
342    /// The `elevation` field controls offset and alpha.
343    Shadow {
344        rect: Rect,
345        radius: [Px; 4],
346        elevation: Px,
347        color: Color,
348    },
349    /// Mark the start of a graphics layer: the contained subtree is rendered
350    /// into an offscreen texture and then composited back into the parent.
351    /// `alpha` is the group-compositing alpha applied at composite time.
352    /// `blur_radius_x` / `blur_radius_y` are the gaussian blur radii in [`Px`]
353    /// applied to the layer before compositing (zero = no blur on that axis).
354    /// `rectangle_edge` true = clamp edge pixels (Rectangle). False = transparent out-of-bounds (Unbounded).
355    BeginLayer {
356        rect: Rect,
357        layer_id: u32,
358        alpha: f32,
359        blur_radius_x: Px,
360        blur_radius_y: Px,
361        rectangle_edge: bool,
362    },
363    /// Closes the graphics layer opened by the matching `BeginLayer`.
364    EndLayer {
365        layer_id: u32,
366    },
367    /// Draws a blurred drop shadow underneath a previously-rendered layer.
368    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
369    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
370    /// optional vertical offset.
371    CompositeShadow {
372        layer_id: u32,
373        blur_px: Px,
374        offset_px: (Px, Px),
375        color: Color,
376    },
377    /// Arc stroke
378    Arc {
379        rect: Rect,
380        start_angle: f32,
381        sweep_angle: f32,
382        stroke_width: Px,
383        color: Color,
384        cap: StrokeCap,
385    },
386    /// Pre-tessellated vector mesh (fill or stroke geometry produced by the
387    /// host, e.g. lyon tessellation). Vertices live in the mesh's own local
388    /// space; `transform` is a 2x3 affine that maps local -> world pixels and
389    /// is applied in the vertex shader. The current scene `PushTransform`
390    /// stack is folded in on top of `transform`.
391    VectorMesh {
392        mesh: Arc<VectorMeshData>,
393        transform: [f32; 6],
394        paint: PaintDesc,
395        /// Reserved for explicit clip assignment. Clipping is otherwise
396        /// structural via `PushVectorClip`/`PopVectorClip`.
397        clip: Option<u32>,
398        blend: BlendMode,
399    },
400    /// Screen-space overlays (handles, rubber bands, playhead). Each mesh
401    /// is positioned in final device pixels and ignores the world
402    /// PushTransform stack. Emit outside the viewport's world transform.
403    VectorOverlay {
404        meshes: Arc<[VectorMeshData]>,
405    },
406    /// Start a vector clip: the mesh is rendered into the stencil buffer
407    /// (increment) and subsequent content is masked to it. Mirrors the
408    /// rect-based `PushClip` but for arbitrary tessellated masks.
409    PushVectorClip {
410        mesh: Arc<VectorMeshData>,
411    },
412    /// End a vector clip opened by `PushVectorClip`.
413    PopVectorClip,
414    /// Custom GPU paint callback (check `egui::PaintCallback`).
415    Callback {
416        rect: Rect,
417        payload: PaintCallbackPayload,
418    },
419}
420
421/// Shared vertex/index buffers for a tessellated vector mesh.
422#[derive(Clone, Debug, Default)]
423pub struct VectorMeshData {
424    pub vertices: Arc<[VectorVertex]>,
425    pub indices: Arc<[u32]>,
426}
427
428/// Pre-tessellated vertex: local position, premultiplied-linear color, and a
429/// free-form uv channel (unused for solid fills, reserved for texture/gradient
430/// sampling).
431#[derive(Clone, Copy, Debug, PartialEq)]
432#[repr(C)]
433pub struct VectorVertex {
434    pub pos: [f32; 2],
435    pub color: [f32; 4],
436    pub uv: [f32; 2],
437}
438
439/// How a `VectorMesh` is painted.
440#[derive(Clone, Copy, Debug, PartialEq)]
441#[non_exhaustive]
442pub enum PaintDesc {
443    /// Use per-vertex color.
444    Solid,
445    /// Two-stop linear gradient in the mesh's local space.
446    Linear {
447        start: Vec2,
448        end: Vec2,
449        start_color: Color,
450        end_color: Color,
451    },
452}
453
454/// Blend mode for a `VectorMesh`. Only `Alpha` (premultiplied alpha) is wired
455/// into the renderer today. The remaining variants are reserved.
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457#[non_exhaustive]
458#[derive(Default)]
459pub enum BlendMode {
460    /// Standard premultiplied alpha blending.
461    #[default]
462    Alpha,
463    /// Additive (screen-space light). Not yet implemented.
464    Add,
465    /// Multiply. Not yet implemented.
466    Multiply,
467    /// Overlay. Not yet implemented.
468    Overlay,
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472#[non_exhaustive]
473pub enum TextOverflow {
474    Visible,
475    Clip,
476    Ellipsis,
477}
478
479/// Controls how line segments are joined in a stroked path.
480#[derive(Clone, Copy, Debug, PartialEq, Default)]
481pub enum StrokeJoin {
482    #[default]
483    /// Sharp corner joins.
484    Miter,
485    /// Semi-circular joins.
486    Round,
487    /// Beveled (flat) joins.
488    Bevel,
489}
490
491/// Controls how the endpoints of a stroked arc are drawn.
492#[derive(Clone, Copy, Debug, PartialEq, Default)]
493pub enum StrokeCap {
494    #[default]
495    /// Flat ends at the exact arc endpoint. No extension.
496    Butt,
497    /// Semicircle with diameter equal to the stroke width, centered at the
498    /// arc endpoint.
499    Round,
500    /// Flat-ended rectangle extending half the stroke width beyond the arc
501    /// endpoint.
502    Square,
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn subcompose_scope_unbounded_has_infinite_max() {
511        let s = SubcomposeScope::UNBOUNDED;
512        assert!(!s.max_width.0.is_finite());
513        assert!(!s.max_height.0.is_finite());
514        assert_eq!(s.min_width, Dp(0.0));
515        assert_eq!(s.min_height, Dp(0.0));
516    }
517
518    #[test]
519    fn subcompose_scope_new_round_trips() {
520        let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
521        assert_eq!(s.min_width, Dp(10.0));
522        assert_eq!(s.max_width, Dp(200.0));
523        assert_eq!(s.min_height, Dp(20.0));
524        assert_eq!(s.max_height, Dp(300.0));
525    }
526
527    #[test]
528    fn box_with_constraints_scope_bounded_predicates() {
529        let bounded = BoxWithConstraintsScope {
530            min_width: Dp(0.0),
531            max_width: Dp(360.0),
532            min_height: Dp(0.0),
533            max_height: Dp(640.0),
534        };
535        assert!(bounded.has_bounded_width());
536        assert!(bounded.has_bounded_height());
537
538        let unbounded = BoxWithConstraintsScope {
539            min_width: Dp(0.0),
540            max_width: Dp(f32::INFINITY),
541            min_height: Dp(0.0),
542            max_height: Dp(f32::INFINITY),
543        };
544        assert!(!unbounded.has_bounded_width());
545        assert!(!unbounded.has_bounded_height());
546    }
547
548    #[test]
549    fn view_kind_subcompose_layout_holds_closure() {
550        let v: View = View {
551            id: 0,
552            kind: ViewKind::SubcomposeLayout {
553                content: std::sync::Arc::new(|scope| {
554                    let _ = scope.max_width;
555                    vec![(0, View::new(0, ViewKind::Box))]
556                }),
557            },
558            modifier: Modifier::default(),
559            children: vec![],
560            scope_key: None,
561            semantics: None,
562        };
563        match &v.kind {
564            ViewKind::SubcomposeLayout { .. } => {}
565            _ => panic!("expected SubcomposeLayout"),
566        }
567    }
568
569    #[test]
570    fn view_kind_subcompose_layout_supports_multiple_slots() {
571        let v: View = View {
572            id: 0,
573            kind: ViewKind::SubcomposeLayout {
574                content: std::sync::Arc::new(|_scope| {
575                    vec![
576                        (1, View::new(0, ViewKind::Box)),
577                        (2, View::new(0, ViewKind::Box)),
578                        (3, View::new(0, ViewKind::Box)),
579                    ]
580                }),
581            },
582            modifier: Modifier::default(),
583            children: vec![],
584            scope_key: None,
585            semantics: None,
586        };
587        if let ViewKind::SubcomposeLayout { content } = &v.kind {
588            let slots = content(&SubcomposeScope::UNBOUNDED);
589            assert_eq!(slots.len(), 3);
590            assert_eq!(slots[0].0, 1);
591            assert_eq!(slots[1].0, 2);
592            assert_eq!(slots[2].0, 3);
593        } else {
594            panic!("expected SubcomposeLayout");
595        }
596    }
597}