Skip to main content

tessera_foundation/
shape_def.rs

1//! Shape definitions shared by foundational visual APIs.
2//!
3//! ## Usage
4//!
5//! Describe rounded rectangles and ellipses for surfaces, outlines, and
6//! effects.
7
8use tessera_ui::{PxSize, dp::Dp};
9
10/// Capsule shapes use a constant `g2_k_value` to maintain circular ends.
11pub const CAPSULE_G2_K_VALUE: f32 = 2.0;
12
13/// A fixed or size-relative corner radius with per-corner curvature.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum RoundedCorner {
16    /// Capsule radius derived from `min(width, height) / 2.0`, with
17    /// `CAPSULE_G2_K_VALUE`.
18    Capsule,
19    /// Manual radius (in `Dp`) with per-corner G2.
20    Manual {
21        /// Corner radius in device-independent pixels.
22        radius: Dp,
23        /// Corner G2 value (2.0 yields circular curvature).
24        g2_k_value: f32,
25    },
26    /// A fraction of the shortest edge plus a fixed offset, clamped to fit.
27    Relative {
28        /// Fraction of the shortest measured edge.
29        fraction: f32,
30        /// Additional radius in device-independent pixels.
31        offset: Dp,
32        /// Corner G2 value (2.0 yields circular curvature).
33        g2_k_value: f32,
34    },
35}
36
37impl RoundedCorner {
38    /// A corner with zero radius.
39    pub const ZERO: Self = RoundedCorner::Manual {
40        radius: Dp(0.0),
41        g2_k_value: 3.0,
42    };
43
44    /// Creates a rounded corner with the specified radius and a default G2
45    /// k value.
46    pub const fn new(radius: Dp) -> Self {
47        RoundedCorner::Manual {
48            radius,
49            g2_k_value: 3.0,
50        }
51    }
52
53    /// Helper to create a manual corner.
54    pub const fn manual(radius: Dp, g2_k_value: f32) -> Self {
55        Self::Manual { radius, g2_k_value }
56    }
57
58    /// Creates a corner that responds to the actual measured size.
59    ///
60    /// The final radius is limited to half the shortest edge. Non-finite
61    /// fractions and offsets contribute zero to the radius.
62    pub const fn relative(fraction: f32, offset: Dp, g2_k_value: f32) -> Self {
63        Self::Relative {
64            fraction,
65            offset,
66            g2_k_value,
67        }
68    }
69
70    /// Resolves into `(radius_px, g2)` using the provided size.
71    pub fn resolve(self, size: PxSize) -> (f32, f32) {
72        match self {
73            RoundedCorner::Capsule => (
74                size.width.to_f32().min(size.height.to_f32()) / 2.0,
75                CAPSULE_G2_K_VALUE,
76            ),
77            RoundedCorner::Manual { radius, g2_k_value } => (radius.to_pixels_f32(), g2_k_value),
78            RoundedCorner::Relative {
79                fraction,
80                offset,
81                g2_k_value,
82            } => {
83                let edge = size.width.to_f32().min(size.height.to_f32()).max(0.0);
84                let fraction = if fraction.is_finite() { fraction } else { 0.0 };
85                let offset = offset.to_pixels_f32();
86                let offset = if offset.is_finite() { offset } else { 0.0 };
87                (
88                    (edge * fraction + offset).clamp(0.0, edge / 2.0),
89                    g2_k_value,
90                )
91            }
92        }
93    }
94}
95
96/// Resolved representation of a shape for rendering.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum ResolvedShape {
99    /// Rounded rect with resolved radii and G2 values.
100    Rounded {
101        /// Pixel radii for each corner.
102        corner_radii: [f32; 4],
103        /// G2 parameters per corner.
104        corner_g2: [f32; 4],
105    },
106    /// Ellipse occupies the full bounds.
107    Ellipse,
108}
109
110/// Shape definitions for UI components.
111#[derive(Clone, Copy, Debug, PartialEq)]
112pub enum Shape {
113    /// Rounded rectangle with independent fixed or size-relative corners.
114    RoundedRectangle {
115        /// Top-left corner definition.
116        top_left: RoundedCorner,
117        /// Top-right corner definition.
118        top_right: RoundedCorner,
119        /// Bottom-right corner definition.
120        bottom_right: RoundedCorner,
121        /// Bottom-left corner definition.
122        bottom_left: RoundedCorner,
123    },
124    /// Ellipse fitting the component bounds.
125    Ellipse,
126}
127
128impl Default for Shape {
129    /// Returns a rectangle with zero corner radius.
130    fn default() -> Self {
131        Self::RECTANGLE
132    }
133}
134
135impl Shape {
136    /// A pure rectangle shape with no rounded corners.
137    pub const RECTANGLE: Self = Shape::RoundedRectangle {
138        top_left: RoundedCorner::manual(Dp(0.0), 3.0),
139        top_right: RoundedCorner::manual(Dp(0.0), 3.0),
140        bottom_right: RoundedCorner::manual(Dp(0.0), 3.0),
141        bottom_left: RoundedCorner::manual(Dp(0.0), 3.0),
142    };
143
144    /// A helper to create a uniform rounded rectangle shape with manual
145    /// corners.
146    pub const fn rounded_rectangle(radius: Dp) -> Self {
147        Shape::RoundedRectangle {
148            top_left: RoundedCorner::manual(radius, 3.0),
149            top_right: RoundedCorner::manual(radius, 3.0),
150            bottom_right: RoundedCorner::manual(radius, 3.0),
151            bottom_left: RoundedCorner::manual(radius, 3.0),
152        }
153    }
154
155    /// A uniform capsule on all corners.
156    pub const CAPSULE: Self = Shape::RoundedRectangle {
157        top_left: RoundedCorner::Capsule,
158        top_right: RoundedCorner::Capsule,
159        bottom_right: RoundedCorner::Capsule,
160        bottom_left: RoundedCorner::Capsule,
161    };
162
163    /// Resolves a shape into pixel radii and per-corner G2 parameters for a
164    /// given size.
165    pub fn resolve_for_size(self, size: PxSize) -> ResolvedShape {
166        match self {
167            Shape::RoundedRectangle {
168                top_left,
169                top_right,
170                bottom_right,
171                bottom_left,
172            } => {
173                let (tl_r, tl_g2) = top_left.resolve(size);
174                let (tr_r, tr_g2) = top_right.resolve(size);
175                let (br_r, br_g2) = bottom_right.resolve(size);
176                let (bl_r, bl_g2) = bottom_left.resolve(size);
177
178                ResolvedShape::Rounded {
179                    corner_radii: [tl_r, tr_r, br_r, bl_r],
180                    corner_g2: [tl_g2, tr_g2, br_g2, bl_g2],
181                }
182            }
183            Shape::Ellipse => ResolvedShape::Ellipse,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use tessera_ui::Px;
191
192    use super::*;
193
194    #[test]
195    fn relative_corners_follow_measured_bounds_and_clamp_after_interpolation() {
196        for (width, height) in [(260, 32), (260, 136), (20, 200), (0, 0)] {
197            let size = PxSize::new(Px(width), Px(height));
198            let half = width.min(height) as f32 / 2.0;
199            assert_eq!(
200                RoundedCorner::relative(0.5, Dp(0.0), 2.0).resolve(size),
201                (half, 2.0)
202            );
203            let fixed = Dp(12.0).to_pixels_f32();
204            assert_eq!(
205                RoundedCorner::relative(0.0, Dp(12.0), 2.0).resolve(size).0,
206                fixed.min(half)
207            );
208            let interpolated = RoundedCorner::relative(0.25, Dp(6.0), 2.0).resolve(size).0;
209            assert_eq!(
210                interpolated,
211                (half / 2.0 + Dp(6.0).to_pixels_f32()).min(half)
212            );
213            assert_eq!(
214                RoundedCorner::relative(-0.5, Dp(-12.0), 2.0)
215                    .resolve(size)
216                    .0,
217                0.0
218            );
219            assert_eq!(
220                RoundedCorner::relative(1.0, Dp(12.0), 2.0).resolve(size).0,
221                half
222            );
223        }
224        let size = PxSize::new(Px(100), Px(100));
225        assert_eq!(
226            RoundedCorner::relative(f32::NAN, Dp(10.0), 2.0)
227                .resolve(size)
228                .0,
229            Dp(10.0).to_pixels_f32().min(50.0)
230        );
231        assert_eq!(
232            RoundedCorner::relative(0.5, Dp(f64::INFINITY), 2.0)
233                .resolve(size)
234                .0,
235            50.0
236        );
237    }
238}