1use tessera_ui::{PxSize, dp::Dp};
9
10pub const CAPSULE_G2_K_VALUE: f32 = 2.0;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum RoundedCorner {
16 Capsule,
19 Manual {
21 radius: Dp,
23 g2_k_value: f32,
25 },
26 Relative {
28 fraction: f32,
30 offset: Dp,
32 g2_k_value: f32,
34 },
35}
36
37impl RoundedCorner {
38 pub const ZERO: Self = RoundedCorner::Manual {
40 radius: Dp(0.0),
41 g2_k_value: 3.0,
42 };
43
44 pub const fn new(radius: Dp) -> Self {
47 RoundedCorner::Manual {
48 radius,
49 g2_k_value: 3.0,
50 }
51 }
52
53 pub const fn manual(radius: Dp, g2_k_value: f32) -> Self {
55 Self::Manual { radius, g2_k_value }
56 }
57
58 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 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#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum ResolvedShape {
99 Rounded {
101 corner_radii: [f32; 4],
103 corner_g2: [f32; 4],
105 },
106 Ellipse,
108}
109
110#[derive(Clone, Copy, Debug, PartialEq)]
112pub enum Shape {
113 RoundedRectangle {
115 top_left: RoundedCorner,
117 top_right: RoundedCorner,
119 bottom_right: RoundedCorner,
121 bottom_left: RoundedCorner,
123 },
124 Ellipse,
126}
127
128impl Default for Shape {
129 fn default() -> Self {
131 Self::RECTANGLE
132 }
133}
134
135impl Shape {
136 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 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 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 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}