1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This module contains border radius related types for the run-time library.
6*/
7
8use core::fmt;
9use core::marker::PhantomData;
10use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
11use euclid::approxord::{max, min};
12use euclid::num::Zero;
13use euclid::{Length, Scale};
14use num_traits::NumCast;
15
16/// Top-left, top-right, bottom-right, and bottom-left border radius, optionally
17/// tagged with a unit.
18#[repr(C)]
19pub struct BorderRadius<T, U> {
20 /// The top-left radius.
21 pub top_left: T,
22 /// The top-right radius.
23 pub top_right: T,
24 /// The bottom-right radius.
25 pub bottom_right: T,
26 /// The bottom-left radius.
27 pub bottom_left: T,
28 #[doc(hidden)]
29 pub _unit: PhantomData<U>,
30}
31
32impl<T, U> Copy for BorderRadius<T, U> where T: Copy {}
33
34impl<T, U> Clone for BorderRadius<T, U>
35where
36 T: Clone,
37{
38 fn clone(&self) -> Self {
39 BorderRadius {
40 top_left: self.top_left.clone(),
41 top_right: self.top_right.clone(),
42 bottom_right: self.bottom_right.clone(),
43 bottom_left: self.bottom_left.clone(),
44 _unit: PhantomData,
45 }
46 }
47}
48
49impl<T, U> Eq for BorderRadius<T, U> where T: Eq {}
50
51impl<T, U> PartialEq for BorderRadius<T, U>
52where
53 T: PartialEq,
54{
55 fn eq(&self, other: &Self) -> bool {
56 self.top_left == other.top_left
57 && self.top_right == other.top_right
58 && self.bottom_right == other.bottom_right
59 && self.bottom_left == other.bottom_left
60 }
61}
62
63impl<T, U> fmt::Debug for BorderRadius<T, U>
64where
65 T: fmt::Debug,
66{
67 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68 write!(
69 f,
70 "BorderRadius({:?}, {:?}, {:?}, {:?})",
71 self.top_left, self.top_right, self.bottom_right, self.bottom_left
72 )
73 }
74}
75
76impl<T, U> Default for BorderRadius<T, U>
77where
78 T: Default,
79{
80 fn default() -> Self {
81 BorderRadius::new(T::default(), T::default(), T::default(), T::default())
82 }
83}
84
85impl<T, U> Zero for BorderRadius<T, U>
86where
87 T: Zero,
88{
89 fn zero() -> Self {
90 BorderRadius::new(T::zero(), T::zero(), T::zero(), T::zero())
91 }
92}
93
94impl<T, U> BorderRadius<T, U> {
95 /// Constructor taking a scalar for each radius.
96 ///
97 /// Radii are specified in top-left, top-right, bottom-right, bottom-left
98 /// order following CSS's convention.
99 pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self {
100 BorderRadius { top_left, top_right, bottom_right, bottom_left, _unit: PhantomData }
101 }
102
103 /// Constructor taking a typed Length for each radius.
104 ///
105 /// Radii are specified in top-left, top-right, bottom-right, bottom-left
106 /// order following CSS's convention.
107 pub fn from_lengths(
108 top_left: Length<T, U>,
109 top_right: Length<T, U>,
110 bottom_right: Length<T, U>,
111 bottom_left: Length<T, U>,
112 ) -> Self {
113 BorderRadius::new(top_left.0, top_right.0, bottom_right.0, bottom_left.0)
114 }
115
116 /// Constructor taking the same scalar value for all radii.
117 pub fn new_uniform(all: T) -> Self
118 where
119 T: Copy,
120 {
121 BorderRadius::new(all, all, all, all)
122 }
123
124 /// Constructor taking the same typed Length for all radii.
125 pub fn from_length(all: Length<T, U>) -> Self
126 where
127 T: Copy,
128 {
129 BorderRadius::new_uniform(all.0)
130 }
131
132 /// Returns `true` if all radii are equal.
133 pub fn is_uniform(&self) -> bool
134 where
135 T: ApproxEq<T>,
136 {
137 self.top_left.approx_eq(&self.top_right)
138 && self.top_left.approx_eq(&self.bottom_right)
139 && self.top_left.approx_eq(&self.bottom_left)
140 }
141
142 /// Returns the uniform radius if all are equal, or `None` otherwise.
143 pub fn as_uniform(&self) -> Option<T>
144 where
145 T: Copy + ApproxEq<T>,
146 {
147 if self.is_uniform() {
148 Some(self.top_left)
149 } else {
150 None
151 }
152 }
153
154 /// Returns `true` if all radii are zero.
155 pub fn is_zero(&self) -> bool
156 where
157 T: ApproxEq<T> + Zero,
158 {
159 let zero = T::zero();
160 self.top_left.approx_eq(&zero)
161 && self.top_right.approx_eq(&zero)
162 && self.bottom_right.approx_eq(&zero)
163 && self.bottom_left.approx_eq(&zero)
164 }
165
166 /// Returns the outer radius.
167 ///
168 /// For any corner with a positive radius, the radius is ensured to be at
169 /// least `half_border_width`.
170 pub fn outer(&self, half_border_width: Length<T, U>) -> Self
171 where
172 T: Copy + PartialOrd + Zero,
173 {
174 let zero = T::zero();
175 BorderRadius::new(
176 if self.top_left > zero {
177 max(self.top_left, half_border_width.0)
178 } else {
179 self.top_left
180 },
181 if self.top_right > zero {
182 max(self.top_right, half_border_width.0)
183 } else {
184 self.top_right
185 },
186 if self.bottom_right > zero {
187 max(self.bottom_right, half_border_width.0)
188 } else {
189 self.bottom_right
190 },
191 if self.bottom_left > zero {
192 max(self.bottom_left, half_border_width.0)
193 } else {
194 self.bottom_left
195 },
196 )
197 }
198
199 /// Returns the inner radius.
200 ///
201 /// A positive radius of each corner is subtracted by `half_border_width`
202 /// and min-clamped to zero.
203 pub fn inner(&self, half_border_width: Length<T, U>) -> Self
204 where
205 T: Copy + PartialOrd + Sub<T, Output = T> + Zero,
206 {
207 BorderRadius::new(
208 self.top_left - half_border_width.0,
209 self.top_right - half_border_width.0,
210 self.bottom_right - half_border_width.0,
211 self.bottom_left - half_border_width.0,
212 )
213 .max(Self::zero())
214 }
215}
216
217/// Trait for testing approximate equality
218pub trait ApproxEq<Eps> {
219 /// Returns `true` is this object is approximately equal to the other one.
220 fn approx_eq(&self, other: &Self) -> bool;
221}
222
223macro_rules! approx_eq {
224 ($ty:ty, $eps:expr) => {
225 impl ApproxEq<$ty> for $ty {
226 #[inline]
227 fn approx_eq(&self, other: &$ty) -> bool {
228 num_traits::sign::abs(*self - *other) <= $eps
229 }
230 }
231 };
232}
233
234approx_eq!(i16, 0);
235approx_eq!(i32, 0);
236approx_eq!(f32, f32::EPSILON);
237
238impl<T, U> Add for BorderRadius<T, U>
239where
240 T: Add<T, Output = T>,
241{
242 type Output = Self;
243 fn add(self, other: Self) -> Self {
244 BorderRadius::new(
245 self.top_left + other.top_left,
246 self.top_right + other.top_right,
247 self.bottom_right + other.bottom_right,
248 self.bottom_left + other.bottom_left,
249 )
250 }
251}
252
253impl<T, U> AddAssign<Self> for BorderRadius<T, U>
254where
255 T: AddAssign<T>,
256{
257 fn add_assign(&mut self, other: Self) {
258 self.top_left += other.top_left;
259 self.top_right += other.top_right;
260 self.bottom_right += other.bottom_right;
261 self.bottom_left += other.bottom_left;
262 }
263}
264
265impl<T, U> Sub for BorderRadius<T, U>
266where
267 T: Sub<T, Output = T>,
268{
269 type Output = Self;
270 fn sub(self, other: Self) -> Self {
271 BorderRadius::new(
272 self.top_left - other.top_left,
273 self.top_right - other.top_right,
274 self.bottom_right - other.bottom_right,
275 self.bottom_left - other.bottom_left,
276 )
277 }
278}
279
280impl<T, U> SubAssign<Self> for BorderRadius<T, U>
281where
282 T: SubAssign<T>,
283{
284 fn sub_assign(&mut self, other: Self) {
285 self.top_left -= other.top_left;
286 self.top_right -= other.top_right;
287 self.bottom_right -= other.bottom_right;
288 self.bottom_left -= other.bottom_left;
289 }
290}
291
292impl<T, U> Neg for BorderRadius<T, U>
293where
294 T: Neg<Output = T>,
295{
296 type Output = Self;
297 fn neg(self) -> Self {
298 BorderRadius {
299 top_left: -self.top_left,
300 top_right: -self.top_right,
301 bottom_right: -self.bottom_right,
302 bottom_left: -self.bottom_left,
303 _unit: PhantomData,
304 }
305 }
306}
307
308impl<T, U> Mul<T> for BorderRadius<T, U>
309where
310 T: Copy + Mul,
311{
312 type Output = BorderRadius<T::Output, U>;
313
314 #[inline]
315 fn mul(self, scale: T) -> Self::Output {
316 BorderRadius::new(
317 self.top_left * scale,
318 self.top_right * scale,
319 self.bottom_right * scale,
320 self.bottom_left * scale,
321 )
322 }
323}
324
325impl<T, U> MulAssign<T> for BorderRadius<T, U>
326where
327 T: Copy + MulAssign,
328{
329 #[inline]
330 fn mul_assign(&mut self, other: T) {
331 self.top_left *= other;
332 self.top_right *= other;
333 self.bottom_right *= other;
334 self.bottom_left *= other;
335 }
336}
337
338impl<T, U1, U2> Mul<Scale<T, U1, U2>> for BorderRadius<T, U1>
339where
340 T: Copy + Mul,
341{
342 type Output = BorderRadius<T::Output, U2>;
343
344 #[inline]
345 fn mul(self, scale: Scale<T, U1, U2>) -> Self::Output {
346 BorderRadius::new(
347 self.top_left * scale.0,
348 self.top_right * scale.0,
349 self.bottom_right * scale.0,
350 self.bottom_left * scale.0,
351 )
352 }
353}
354
355impl<T, U> MulAssign<Scale<T, U, U>> for BorderRadius<T, U>
356where
357 T: Copy + MulAssign,
358{
359 #[inline]
360 fn mul_assign(&mut self, other: Scale<T, U, U>) {
361 *self *= other.0;
362 }
363}
364
365impl<T, U> Div<T> for BorderRadius<T, U>
366where
367 T: Copy + Div,
368{
369 type Output = BorderRadius<T::Output, U>;
370
371 #[inline]
372 fn div(self, scale: T) -> Self::Output {
373 BorderRadius::new(
374 self.top_left / scale,
375 self.top_right / scale,
376 self.bottom_right / scale,
377 self.bottom_left / scale,
378 )
379 }
380}
381
382impl<T, U> DivAssign<T> for BorderRadius<T, U>
383where
384 T: Copy + DivAssign,
385{
386 #[inline]
387 fn div_assign(&mut self, other: T) {
388 self.top_left /= other;
389 self.top_right /= other;
390 self.bottom_right /= other;
391 self.bottom_left /= other;
392 }
393}
394
395impl<T, U1, U2> Div<Scale<T, U1, U2>> for BorderRadius<T, U2>
396where
397 T: Copy + Div,
398{
399 type Output = BorderRadius<T::Output, U1>;
400
401 #[inline]
402 fn div(self, scale: Scale<T, U1, U2>) -> Self::Output {
403 BorderRadius::new(
404 self.top_left / scale.0,
405 self.top_right / scale.0,
406 self.bottom_right / scale.0,
407 self.bottom_left / scale.0,
408 )
409 }
410}
411
412impl<T, U> DivAssign<Scale<T, U, U>> for BorderRadius<T, U>
413where
414 T: Copy + DivAssign,
415{
416 fn div_assign(&mut self, other: Scale<T, U, U>) {
417 *self /= other.0;
418 }
419}
420
421impl<T, U> BorderRadius<T, U>
422where
423 T: PartialOrd,
424{
425 /// Returns the minimum of the two radii.
426 #[inline]
427 pub fn min(self, other: Self) -> Self {
428 BorderRadius::new(
429 top_left:min(self.top_left, other.top_left),
430 top_right:min(self.top_right, other.top_right),
431 bottom_right:min(self.bottom_right, other.bottom_right),
432 bottom_left:min(self.bottom_left, y:other.bottom_left),
433 )
434 }
435
436 /// Returns the maximum of the two radii.
437 #[inline]
438 pub fn max(self, other: Self) -> Self {
439 BorderRadius::new(
440 top_left:max(self.top_left, other.top_left),
441 top_right:max(self.top_right, other.top_right),
442 bottom_right:max(self.bottom_right, other.bottom_right),
443 bottom_left:max(self.bottom_left, y:other.bottom_left),
444 )
445 }
446}
447
448impl<T, U> BorderRadius<T, U>
449where
450 T: NumCast + Copy,
451{
452 /// Cast from one numeric representation to another, preserving the units.
453 #[inline]
454 pub fn cast<NewT: NumCast>(self) -> BorderRadius<NewT, U> {
455 self.try_cast().unwrap()
456 }
457
458 /// Fallible cast from one numeric representation to another, preserving the units.
459 pub fn try_cast<NewT: NumCast>(self) -> Option<BorderRadius<NewT, U>> {
460 match (
461 NumCast::from(self.top_left),
462 NumCast::from(self.top_right),
463 NumCast::from(self.bottom_right),
464 NumCast::from(self.bottom_left),
465 ) {
466 (Some(top_left: NewT), Some(top_right: NewT), Some(bottom_right: NewT), Some(bottom_left: NewT)) => {
467 Some(BorderRadius::new(top_left, top_right, bottom_right, bottom_left))
468 }
469 _ => None,
470 }
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use crate::lengths::{LogicalBorderRadius, LogicalLength, PhysicalPx, ScaleFactor};
477 use euclid::UnknownUnit;
478
479 type BorderRadius = super::BorderRadius<f32, UnknownUnit>;
480 type IntBorderRadius = super::BorderRadius<i16, UnknownUnit>;
481 type PhysicalBorderRadius = super::BorderRadius<f32, PhysicalPx>;
482
483 #[test]
484 fn test_eq() {
485 let a = BorderRadius::new(1., 2., 3., 4.);
486 let b = BorderRadius::new(1., 2., 3., 4.);
487 let c = BorderRadius::new(4., 3., 2., 1.);
488 let d = BorderRadius::new(
489 c.top_left + f32::EPSILON / 2.,
490 c.top_right - f32::EPSILON / 2.,
491 c.bottom_right - f32::EPSILON / 2.,
492 c.bottom_left + f32::EPSILON / 2.,
493 );
494 assert_eq!(a, b);
495 assert_ne!(a, c);
496 assert_eq!(c, d);
497 }
498
499 #[test]
500 fn test_min_max() {
501 let a = BorderRadius::new(1., 2., 3., 4.);
502 let b = BorderRadius::new(4., 3., 2., 1.);
503 assert_eq!(a.min(b), BorderRadius::new(1., 2., 2., 1.));
504 assert_eq!(a.max(b), BorderRadius::new(4., 3., 3., 4.));
505 }
506
507 #[test]
508 fn test_scale() {
509 let scale = ScaleFactor::new(2.);
510 let logical_radius = LogicalBorderRadius::new(1., 2., 3., 4.);
511 let physical_radius = PhysicalBorderRadius::new(2., 4., 6., 8.);
512 assert_eq!(logical_radius * scale, physical_radius);
513 assert_eq!(physical_radius / scale, logical_radius);
514 }
515
516 #[test]
517 fn test_zero() {
518 assert!(BorderRadius::new_uniform(0.).is_zero());
519 assert!(BorderRadius::new_uniform(1.0e-9).is_zero());
520 assert!(!BorderRadius::new_uniform(1.0e-3).is_zero());
521 assert!(IntBorderRadius::new_uniform(0).is_zero());
522 assert!(!IntBorderRadius::new_uniform(1).is_zero());
523 }
524
525 #[test]
526 fn test_inner_outer() {
527 let radius = LogicalBorderRadius::new(0., 2.5, 5., 10.);
528 let half_border_width = LogicalLength::new(5.);
529 assert_eq!(radius.inner(half_border_width), LogicalBorderRadius::new(0., 0., 0., 5.));
530 assert_eq!(radius.outer(half_border_width), LogicalBorderRadius::new(0., 5., 5., 10.));
531 }
532}
533