i_slint_core/graphics/
border_radius.rs

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            min(self.top_left, other.top_left),
430            min(self.top_right, other.top_right),
431            min(self.bottom_right, other.bottom_right),
432            min(self.bottom_left, 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            max(self.top_left, other.top_left),
441            max(self.top_right, other.top_right),
442            max(self.bottom_right, other.bottom_right),
443            max(self.bottom_left, 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), Some(top_right), Some(bottom_right), Some(bottom_left)) => {
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}