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 finite upper bounds. Values are density-independent
20    /// pixels (`Dp`).
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. Non-finite minimums
29    /// and negative values are coerced to zero, a NaN maximum becomes
30    /// unbounded, and inverted bounds collapse to the maximum.
31    pub fn new(min_width: Dp, max_width: Dp, min_height: Dp, max_height: Dp) -> Self {
32        fn minimum(value: f32) -> f32 {
33            if value.is_nan() || value < 0.0 || !value.is_finite() {
34                0.0
35            } else {
36                value
37            }
38        }
39        fn maximum(value: f32) -> f32 {
40            if value.is_nan() || value < 0.0 {
41                if value.is_nan() { f32::INFINITY } else { 0.0 }
42            } else {
43                value
44            }
45        }
46        let mut scope = Self {
47            min_width: Dp(minimum(min_width.0)),
48            max_width: Dp(maximum(max_width.0)),
49            min_height: Dp(minimum(min_height.0)),
50            max_height: Dp(maximum(max_height.0)),
51        };
52        if scope.min_width.0 > scope.max_width.0 {
53            scope.min_width = scope.max_width;
54        }
55        if scope.min_height.0 > scope.max_height.0 {
56            scope.min_height = scope.max_height;
57        }
58        scope
59    }
60
61    pub fn has_bounded_width(&self) -> bool {
62        self.max_width.0.is_finite()
63    }
64
65    pub fn has_bounded_height(&self) -> bool {
66        self.max_height.0.is_finite()
67    }
68}
69
70/// Scope passed to [`BoxWithConstraints`](crate::prelude::BoxWithConstraints)
71/// content. All values are in [`Dp`].
72#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct BoxWithConstraintsScope {
74    pub min_width: Dp,
75    pub max_width: Dp,
76    pub min_height: Dp,
77    pub max_height: Dp,
78}
79
80impl BoxWithConstraintsScope {
81    /// `true` if the width is bounded by the parent (i.e. not infinite).
82    pub fn has_bounded_width(&self) -> bool {
83        self.max_width.0.is_finite()
84    }
85
86    /// `true` if the height is bounded by the parent (i.e. not infinite).
87    pub fn has_bounded_height(&self) -> bool {
88        self.max_height.0.is_finite()
89    }
90}
91
92pub type ViewId = u64;
93
94pub type ImageHandle = u64;
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
96pub enum ImageFilter {
97    #[default]
98    Linear,
99    Nearest,
100}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
103pub struct ImageSourceRect {
104    pub x: u32,
105    pub y: u32,
106    pub width: u32,
107    pub height: u32,
108}
109
110impl ImageSourceRect {
111    pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
112        Self {
113            x,
114            y,
115            width,
116            height,
117        }
118    }
119
120    pub const fn is_empty(self) -> bool {
121        self.width == 0 || self.height == 0
122    }
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub enum ImageFit {
128    /// ContentScale.Fit - default in Compose Image
129    #[default]
130    Contain,
131    /// ContentScale.Crop
132    Cover,
133    /// ContentScale.FillWidth
134    FitWidth,
135    /// ContentScale.FillHeight
136    FitHeight,
137    /// ContentScale.FillBounds - stretch, ignore aspect
138    FillBounds,
139    /// ContentScale.Inside - like Contain but never upscales
140    Inside,
141    /// ContentScale.None - no scaling; placement comes from [`ImageAlignment`]
142    None,
143    /// Repeat the source to fill the bounds (Godot `STRETCH_SCALE` + tile).
144    /// Unlike every other mode this ignores aspect ratio and may draw past a
145    /// `source_rect` edge, wrapping instead of clipping.
146    Tile,
147}
148
149/// Placement of scaled content inside the view bounds. Mirrors Godot's
150/// `AspectRatioContainer::AlignmentMode` and covers every fixed `Alignment` in
151/// Compose (`Begin`/`End` are physical left/right and top/bottom, not
152/// direction-aware).
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
154pub enum ImageAlignment {
155    /// Alignment.Center
156    #[default]
157    Center,
158    /// Alignment.TopLeft / BottomStart - left and top
159    Begin,
160    /// Alignment.TopRight / BottomEnd - right and bottom
161    End,
162}
163
164impl ImageFit {
165    /// Per-axis scale factors for drawing a `src`-sized source into `dst`.
166    /// Mirrors Compose's `ContentScale.computeScaleFactor`, which layout and
167    /// paint both need: layout sizes to it, paint draws with it.
168    pub fn scale_factor(self, src: (f32, f32), dst: (f32, f32)) -> (f32, f32) {
169        let fill_w = dst.0 / src.0;
170        let fill_h = dst.1 / src.1;
171        let uniform = |v: f32| (v, v);
172        match self {
173            Self::Contain => uniform(fill_w.min(fill_h)),
174            Self::Cover => uniform(fill_w.max(fill_h)),
175            Self::FitWidth => uniform(fill_w),
176            Self::FitHeight => uniform(fill_h),
177            Self::FillBounds => (fill_w, fill_h),
178            Self::Inside => {
179                if src.0 <= dst.0 && src.1 <= dst.1 {
180                    uniform(1.0)
181                } else {
182                    uniform(fill_w.min(fill_h))
183                }
184            }
185            Self::None | Self::Tile => uniform(1.0),
186        }
187    }
188}
189
190#[derive(Clone)]
191pub struct OverlayEntry {
192    pub id: u64,
193    pub view: Box<View>,
194}
195
196#[derive(Clone)]
197#[non_exhaustive]
198pub enum ViewKind {
199    Box,
200    Row,
201    Column,
202    ZStack,
203    OverlayHost,
204    Text {
205        text: String,
206        color: Color,
207        font_size: Sp,
208        soft_wrap: bool,
209        max_lines: Option<usize>,
210        overflow: TextOverflow,
211        font_family: Option<&'static str>,
212        annotations: Option<Arc<[TextSpan]>>,
213        text_align: TextAlign,
214        font_weight: FontWeight,
215        font_style: FontStyle,
216        text_decoration: TextDecoration,
217        letter_spacing: Sp,
218        line_height: Sp,
219        /// URL for clickable link text.
220        url: Option<Arc<str>>,
221        /// OpenType font variation settings (e.g. "wght 700, opsz 24").
222        font_variation_settings: Option<Arc<str>>,
223        /// Fill, outline, or both (faux-bold). Default `Fill`.
224        draw_style: DrawStyle,
225    },
226
227    Image {
228        handle: ImageHandle,
229        tint: Color,
230        fit: ImageFit,
231        filter: ImageFilter,
232        source_rect: Option<ImageSourceRect>,
233        alignment: ImageAlignment,
234    },
235    /// A layout whose children are produced by calling `content` with the
236    /// current `SubcomposeScope`. The closure is invoked during reconciliation
237    /// and returns a list of `(slot_id, view)` pairs. Each slot id is a stable
238    /// identity used to reconcile the returned view across frames. This is
239    /// the building block for `BoxWithConstraints` and other
240    /// constraints-driven layouts.
241    ///
242    /// Note: any `Modifier::key` set on a returned view is overwritten by its
243    /// slot id so the slot's identity is stable across frames.
244    SubcomposeLayout {
245        content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
246    },
247}
248
249impl std::fmt::Debug for ViewKind {
250    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251        match self {
252            Self::Box => f.write_str("Box"),
253            Self::Row => f.write_str("Row"),
254            Self::Column => f.write_str("Column"),
255            Self::ZStack => f.write_str("ZStack"),
256            Self::OverlayHost => f.write_str("OverlayHost"),
257
258            Self::Image {
259                handle,
260                tint,
261                fit,
262                filter,
263                source_rect,
264                alignment,
265            } => f
266                .debug_struct("Image")
267                .field("handle", handle)
268                .field("tint", tint)
269                .field("fit", fit)
270                .field("filter", filter)
271                .field("source_rect", source_rect)
272                .field("alignment", alignment)
273                .finish(),
274            Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
275            Self::Text { text, .. } => write!(f, "Text({:?})", text),
276        }
277    }
278}
279
280#[derive(Clone, Debug)]
281pub struct View {
282    pub id: ViewId,
283    pub kind: ViewKind,
284    pub modifier: Modifier,
285    pub children: Vec<View>,
286    pub semantics: Option<crate::semantics::Semantics>,
287    /// Set by `scope!` macro to mark this as a scope boundary node.
288    /// Carries the scope key (e.g., "title", "color_buttons") for per-scope
289    /// TaffyTree isolation.
290    pub scope_key: Option<String>,
291}
292
293impl View {
294    pub fn new(id: ViewId, kind: ViewKind) -> Self {
295        View {
296            id,
297            kind,
298            modifier: Modifier::default(),
299            children: vec![],
300            semantics: None,
301            scope_key: None,
302        }
303    }
304    pub fn modifier(mut self, m: Modifier) -> Self {
305        self.modifier = m;
306        self
307    }
308    /// Mark this view as disabled - ignores pointer events.
309    pub fn disabled(mut self) -> Self {
310        self.modifier.disabled = true;
311        self
312    }
313    pub fn with_children(mut self, kids: Vec<View>) -> Self {
314        self.children = kids;
315        self
316    }
317    pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
318        self.children = kids.into();
319        self
320    }
321    pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
322        self.semantics = Some(s);
323        self
324    }
325}
326
327/// Renderable scene
328#[derive(Clone, Debug, Default)]
329pub struct Scene {
330    pub clear_color: Color,
331    pub nodes: Vec<SceneNode>,
332}
333
334/// Tunable text properties bundled for ergonomic [`Default`], so a
335/// [`SceneNode::Text`] stays `{ rect, text, color, size, style }` and simple
336/// nodes don't spell out every knob. `color` and `size` stay on the variant
337/// because a default for either would silently draw invisible text.
338#[derive(Clone, Debug, PartialEq, Default)]
339pub struct TextPaintStyle {
340    pub font_family: Option<&'static str>,
341    pub text_align: TextAlign,
342    pub font_weight: FontWeight,
343    pub font_style: FontStyle,
344    pub text_decoration: TextDecoration,
345    pub letter_spacing: Px,
346    pub line_height: Px,
347    pub text_direction: TextDirection,
348    /// Whether the font renderer may synthesize bold/italic/small-caps.
349    pub font_synthesis: FontSynthesis,
350    pub baseline_shift: BaselineShift,
351    /// Fill, outline, or both.
352    pub draw_style: DrawStyle,
353    /// URL for clickable link text.
354    pub url: Option<Arc<str>>,
355    /// OpenType font variation settings (e.g. "wght 700, opsz 24").
356    pub font_variation_settings: Option<Arc<str>>,
357}
358
359/// How an image is sampled and placed inside its [`Rect`], bundled for
360/// ergonomic [`Default`]. `tint` stays on [`SceneNode::Image`] because
361/// untinted is an explicit choice, not a neutral one.
362#[derive(Clone, Copy, Debug, PartialEq, Default)]
363pub struct ImagePaintStyle {
364    pub fit: ImageFit,
365    pub filter: ImageFilter,
366    /// Sub-region of the source texture to sample.
367    pub source_rect: Option<ImageSourceRect>,
368    pub alignment: ImageAlignment,
369}
370
371#[derive(Clone, Copy, Debug)]
372pub struct PaintCallbackInfo {
373    /// Viewport in physical pixels where the callback should paint.
374    pub viewport: Rect,
375    /// Clip rect in physical pixels (intersection of callback rect and current clip).
376    pub clip_rect: Rect,
377    /// Pixels per point (DPI scale).
378    pub pixels_per_point: f32,
379    /// Screen size in physical pixels.
380    pub screen_size_px: [u32; 2],
381}
382
383pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
384
385/// Paint-space scene graph. `Rect`/`Vec2` compounds carry physical pixels
386/// (like Compose `Offset`/`Size`/`Rect`); scalar lengths use [`Px`] so the
387/// dp→px boundary is explicit instead of unitless `f32`.
388#[derive(Clone, Debug)]
389#[non_exhaustive]
390pub enum SceneNode {
391    Rect {
392        rect: Rect,
393        brush: Brush,
394        radius: [Px; 4],
395    },
396    Border {
397        rect: Rect,
398        brush: Brush,
399        width: Px,
400        radius: [Px; 4],
401    },
402    Text {
403        rect: Rect,
404        text: Arc<str>,
405        color: Color,
406        size: Px,
407        style: TextPaintStyle,
408    },
409    Ellipse {
410        rect: Rect,
411        brush: Brush,
412    },
413    EllipseBorder {
414        rect: Rect,
415        brush: Brush,
416        width: Px,
417    },
418    PushClip {
419        rect: Rect,
420        radius: [Px; 4],
421        op: ClipOp,
422    },
423    PopClip,
424    PushTransform {
425        transform: Transform,
426    },
427    PopTransform,
428    Image {
429        rect: Rect,
430        handle: ImageHandle,
431        tint: Color,
432        style: ImagePaintStyle,
433    },
434    /// Tinted A8 coverage mask: samples `handle` (registered with
435    /// `register_coverage_a8`) as coverage and composites `color` with
436    /// source-over. Lets hosts rasterize geometry once (e.g. cached glyph
437    /// coverage tiles) and re-composite it per frame with a new color or
438    /// position without re-uploading. `rect` positions the tile's top-left;
439    /// its size is the registered tile size.
440    Coverage {
441        rect: Rect,
442        handle: ImageHandle,
443        color: Color,
444    },
445    /// Shadow behind a rounded rect, typically driven by `StateElevation`.
446    /// The `elevation` field controls offset and alpha.
447    Shadow {
448        rect: Rect,
449        radius: [Px; 4],
450        elevation: Px,
451        color: Color,
452    },
453    /// Mark the start of a graphics layer: the contained subtree is rendered
454    /// into an offscreen texture and then composited back into the parent.
455    /// `alpha` is the group-compositing alpha applied at composite time.
456    /// `blur_radius_x` / `blur_radius_y` are the gaussian blur radii in [`Px`]
457    /// applied to the layer before compositing (zero = no blur on that axis).
458    /// `rectangle_edge` true = clamp edge pixels (Rectangle). False = transparent out-of-bounds (Unbounded).
459    BeginLayer {
460        rect: Rect,
461        layer_id: u32,
462        alpha: f32,
463        blur_radius_x: Px,
464        blur_radius_y: Px,
465        rectangle_edge: bool,
466    },
467    /// Closes the graphics layer opened by the matching `BeginLayer`.
468    EndLayer {
469        layer_id: u32,
470    },
471    /// Draws a blurred drop shadow underneath a previously-rendered layer.
472    /// Emitted between `EndLayer` and the layer's `CompositeLayer`. The
473    /// quad samples the layer's texture with a 3x3 Gaussian blur and an
474    /// optional vertical offset.
475    CompositeShadow {
476        layer_id: u32,
477        blur_px: Px,
478        offset_px: (Px, Px),
479        color: Color,
480        /// Corner radii shaping the shadow silhouette, in px. All zero = the
481        /// silhouette follows the layer alpha unchanged.
482        shape_px: [Px; 4],
483    },
484    /// Arc stroke
485    Arc {
486        rect: Rect,
487        start_angle: f32,
488        sweep_angle: f32,
489        stroke_width: Px,
490        brush: Brush,
491        cap: StrokeCap,
492    },
493    /// Pre-tessellated vector mesh (fill or stroke geometry produced by the
494    /// host, e.g. lyon tessellation). Vertices live in the mesh's own local
495    /// space; `transform` is a 2x3 affine that maps local -> world pixels as
496    /// `[m00, m01, m10, m11, tx, ty]` (`out = M * local + t`; identity is
497    /// `[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]`) and is applied in the vertex shader.
498    /// The current scene `PushTransform` stack is folded in on top of
499    /// `transform`.
500    VectorMesh {
501        mesh: Arc<VectorMeshData>,
502        transform: [f32; 6],
503        paint: PaintDesc,
504        /// Reserved for explicit clip assignment. Clipping is otherwise
505        /// structural via `PushVectorClip`/`PopVectorClip`.
506        clip: Option<u32>,
507        blend: BlendMode,
508    },
509    /// Screen-space overlays (handles, rubber bands, playhead). Each mesh
510    /// is positioned in final device pixels and ignores the world
511    /// PushTransform stack. Emit outside the viewport's world transform.
512    VectorOverlay {
513        meshes: Arc<[VectorMeshData]>,
514    },
515    /// Start a vector clip: the mesh is rendered into the stencil buffer
516    /// (increment) and subsequent content is masked to it. Mirrors the
517    /// rect-based `PushClip` but for arbitrary tessellated masks.
518    /// `op` selects intersection (keep content inside the mask, the common
519    /// case) or difference (cut the mask out, e.g. ASS `\iclip` drawings).
520    /// Difference is exact for a lone mask and for a mask nested inside
521    /// intersect clips; a normal clip nested inside a difference mask is
522    /// best-effort (see the renderer docs).
523    PushVectorClip {
524        mesh: Arc<VectorMeshData>,
525        op: ClipOp,
526    },
527    /// End a vector clip opened by `PushVectorClip`.
528    PopVectorClip,
529    /// Custom GPU paint callback (check `egui::PaintCallback`).
530    Callback {
531        rect: Rect,
532        payload: PaintCallbackPayload,
533    },
534}
535
536/// Shared vertex/index buffers for a tessellated vector mesh.
537#[derive(Clone, Debug, Default)]
538pub struct VectorMeshData {
539    pub vertices: Arc<[VectorVertex]>,
540    pub indices: Arc<[u32]>,
541}
542
543/// Pre-tessellated vertex: local position, premultiplied-linear color, and a
544/// free-form uv channel (unused for solid fills, reserved for texture/gradient
545/// sampling).
546#[derive(Clone, Copy, Debug, PartialEq)]
547#[repr(C)]
548pub struct VectorVertex {
549    pub pos: [f32; 2],
550    pub color: [f32; 4],
551    pub uv: [f32; 2],
552}
553
554/// How a `VectorMesh` is painted. Mirrors the [`Brush`] variants;
555/// gradient endpoints live in the mesh's local space.
556#[derive(Clone, Copy, Debug, PartialEq)]
557#[non_exhaustive]
558pub enum PaintDesc {
559    /// Use per-vertex color.
560    Solid,
561    /// Two-stop linear gradient in the mesh's local space.
562    Linear {
563        start: Vec2,
564        end: Vec2,
565        start_color: Color,
566        end_color: Color,
567    },
568    /// Radial gradient centered at `center` (mesh-local) with `radius`.
569    Radial {
570        center: Vec2,
571        radius: f32,
572        start_color: Color,
573        end_color: Color,
574    },
575    /// Angular sweep around `center` (mesh-local), clockwise from 3 o'clock.
576    Sweep {
577        center: Vec2,
578        start_color: Color,
579        end_color: Color,
580    },
581}
582
583/// Blend mode for a `VectorMesh`, following the CSS `mix-blend-mode`
584/// vocabulary used by SVG/Lottie-style artwork. All variants are wired into
585/// the renderer: separable modes use fixed-function blending, the rest
586/// (marked below) isolate the mesh into a graphics layer and composite it
587/// with a custom shader.
588#[derive(Clone, Copy, Debug, PartialEq, Eq)]
589#[non_exhaustive]
590#[derive(Default)]
591pub enum BlendMode {
592    /// Standard premultiplied alpha blending (`normal`).
593    #[default]
594    Alpha,
595    /// `screen`: `1 - (1 - S) * (1 - D)`. Fixed-function.
596    Screen,
597    /// `overlay`: `Multiply` for dark backdrops, `Screen` for light ones.
598    Overlay,
599    /// `darken`: per-channel `min(S, D)`. Fixed-function.
600    Darken,
601    /// `lighten`: per-channel `max(S, D)`. Fixed-function.
602    Lighten,
603    /// `color-dodge`: brightens the backdrop toward the source color.
604    ColorDodge,
605    /// `color-burn`: darkens the backdrop toward the source color.
606    ColorBurn,
607    /// `hard-light`: `Overlay` with source and backdrop swapped.
608    HardLight,
609    /// `soft-light`: subtle `Overlay` variant.
610    SoftLight,
611    /// `difference`: per-channel `|S - D|`.
612    /// Needs the backdrop (fixed-function subtract only covers `S - D`),
613    /// so it composites via an isolated layer.
614    Difference,
615    /// `exclusion`: like `difference` with lower contrast.
616    Exclusion,
617    /// `hue`: source hue with backdrop saturation and luminosity.
618    Hue,
619    /// `saturation`: source saturation with backdrop hue and luminosity.
620    Saturation,
621    /// `color`: source hue and saturation with backdrop luminosity.
622    Color,
623    /// `luminosity`: source luminosity with backdrop hue and saturation.
624    Luminosity,
625    /// Additive (screen-space light). Fixed-function.
626    Add,
627    /// `multiply`: per-channel `S * D`. Fixed-function.
628    Multiply,
629}
630
631impl BlendMode {
632    /// Modes implemented with fixed-function hardware blending on the mesh
633    /// pipeline. Everything else isolates the mesh into a graphics layer and
634    /// composites it with the backdrop-blend shader.
635    pub fn needs_isolation(self) -> bool {
636        !matches!(
637            self,
638            BlendMode::Alpha
639                | BlendMode::Add
640                | BlendMode::Multiply
641                | BlendMode::Screen
642                | BlendMode::Darken
643                | BlendMode::Lighten
644        )
645    }
646
647    /// Discriminant consumed by `blend_layer.wgsl` (`mode` flat varying).
648    pub fn shader_mode(self) -> u32 {
649        match self {
650            BlendMode::Alpha => 0,
651            BlendMode::Add => 1,
652            BlendMode::Multiply => 2,
653            BlendMode::Screen => 3,
654            BlendMode::Overlay => 4,
655            BlendMode::Darken => 5,
656            BlendMode::Lighten => 6,
657            BlendMode::ColorDodge => 7,
658            BlendMode::ColorBurn => 8,
659            BlendMode::HardLight => 9,
660            BlendMode::SoftLight => 10,
661            BlendMode::Difference => 11,
662            BlendMode::Exclusion => 12,
663            BlendMode::Hue => 13,
664            BlendMode::Saturation => 14,
665            BlendMode::Color => 15,
666            BlendMode::Luminosity => 16,
667        }
668    }
669}
670
671#[derive(Clone, Copy, Debug, PartialEq, Eq)]
672#[non_exhaustive]
673pub enum TextOverflow {
674    Visible,
675    Clip,
676    Ellipsis,
677}
678
679/// Controls how line segments are joined in a stroked path.
680#[derive(Clone, Copy, Debug, PartialEq, Default)]
681pub enum StrokeJoin {
682    #[default]
683    /// Sharp corner joins.
684    Miter,
685    /// Semi-circular joins.
686    Round,
687    /// Beveled (flat) joins.
688    Bevel,
689}
690
691/// Controls how the endpoints of a stroked arc are drawn.
692#[derive(Clone, Copy, Debug, PartialEq, Default)]
693pub enum StrokeCap {
694    #[default]
695    /// Flat ends at the exact arc endpoint. No extension.
696    Butt,
697    /// Semicircle with diameter equal to the stroke width, centered at the
698    /// arc endpoint.
699    Round,
700    /// Flat-ended rectangle extending half the stroke width beyond the arc
701    /// endpoint.
702    Square,
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    #[test]
710    fn subcompose_scope_unbounded_has_infinite_max() {
711        let s = SubcomposeScope::UNBOUNDED;
712        assert!(!s.max_width.0.is_finite());
713        assert!(!s.max_height.0.is_finite());
714        assert_eq!(s.min_width, Dp(0.0));
715        assert_eq!(s.min_height, Dp(0.0));
716    }
717
718    #[test]
719    fn subcompose_scope_new_round_trips() {
720        let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
721        assert_eq!(s.min_width, Dp(10.0));
722        assert_eq!(s.max_width, Dp(200.0));
723        assert_eq!(s.min_height, Dp(20.0));
724        assert_eq!(s.max_height, Dp(300.0));
725    }
726
727    #[test]
728    fn box_with_constraints_scope_bounded_predicates() {
729        let bounded = BoxWithConstraintsScope {
730            min_width: Dp(0.0),
731            max_width: Dp(360.0),
732            min_height: Dp(0.0),
733            max_height: Dp(640.0),
734        };
735        assert!(bounded.has_bounded_width());
736        assert!(bounded.has_bounded_height());
737
738        let unbounded = BoxWithConstraintsScope {
739            min_width: Dp(0.0),
740            max_width: Dp(f32::INFINITY),
741            min_height: Dp(0.0),
742            max_height: Dp(f32::INFINITY),
743        };
744        assert!(!unbounded.has_bounded_width());
745        assert!(!unbounded.has_bounded_height());
746    }
747
748    #[test]
749    fn view_kind_subcompose_layout_holds_closure() {
750        let v: View = View {
751            id: 0,
752            kind: ViewKind::SubcomposeLayout {
753                content: std::sync::Arc::new(|scope| {
754                    let _ = scope.max_width;
755                    vec![(0, View::new(0, ViewKind::Box))]
756                }),
757            },
758            modifier: Modifier::default(),
759            children: vec![],
760            scope_key: None,
761            semantics: None,
762        };
763        match &v.kind {
764            ViewKind::SubcomposeLayout { .. } => {}
765            _ => panic!("expected SubcomposeLayout"),
766        }
767    }
768
769    #[test]
770    fn view_kind_subcompose_layout_supports_multiple_slots() {
771        let v: View = View {
772            id: 0,
773            kind: ViewKind::SubcomposeLayout {
774                content: std::sync::Arc::new(|_scope| {
775                    vec![
776                        (1, View::new(0, ViewKind::Box)),
777                        (2, View::new(0, ViewKind::Box)),
778                        (3, View::new(0, ViewKind::Box)),
779                    ]
780                }),
781            },
782            modifier: Modifier::default(),
783            children: vec![],
784            scope_key: None,
785            semantics: None,
786        };
787        if let ViewKind::SubcomposeLayout { content } = &v.kind {
788            let slots = content(&SubcomposeScope::UNBOUNDED);
789            assert_eq!(slots.len(), 3);
790            assert_eq!(slots[0].0, 1);
791            assert_eq!(slots[1].0, 2);
792            assert_eq!(slots[2].0, 3);
793        } else {
794            panic!("expected SubcomposeLayout");
795        }
796    }
797}