Skip to main content

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() { Some(self.top_left) } else { None }
148    }
149
150    /// Returns `true` if all radii are zero.
151    pub fn is_zero(&self) -> bool
152    where
153        T: ApproxEq<T> + Zero,
154    {
155        let zero = T::zero();
156        self.top_left.approx_eq(&zero)
157            && self.top_right.approx_eq(&zero)
158            && self.bottom_right.approx_eq(&zero)
159            && self.bottom_left.approx_eq(&zero)
160    }
161
162    /// Returns the outer radius.
163    ///
164    /// For any corner with a positive radius, the radius is ensured to be at
165    /// least `half_border_width`.
166    pub fn outer(&self, half_border_width: Length<T, U>) -> Self
167    where
168        T: Copy + PartialOrd + Zero,
169    {
170        let zero = T::zero();
171        BorderRadius::new(
172            if self.top_left > zero {
173                max(self.top_left, half_border_width.0)
174            } else {
175                self.top_left
176            },
177            if self.top_right > zero {
178                max(self.top_right, half_border_width.0)
179            } else {
180                self.top_right
181            },
182            if self.bottom_right > zero {
183                max(self.bottom_right, half_border_width.0)
184            } else {
185                self.bottom_right
186            },
187            if self.bottom_left > zero {
188                max(self.bottom_left, half_border_width.0)
189            } else {
190                self.bottom_left
191            },
192        )
193    }
194
195    /// Returns the inner radius.
196    ///
197    /// A positive radius of each corner is subtracted by `half_border_width`
198    /// and min-clamped to zero.
199    pub fn inner(&self, half_border_width: Length<T, U>) -> Self
200    where
201        T: Copy + PartialOrd + Sub<T, Output = T> + Zero,
202    {
203        BorderRadius::new(
204            self.top_left - half_border_width.0,
205            self.top_right - half_border_width.0,
206            self.bottom_right - half_border_width.0,
207            self.bottom_left - half_border_width.0,
208        )
209        .max(Self::zero())
210    }
211}
212
213/// Trait for testing approximate equality
214pub trait ApproxEq<Eps> {
215    /// Returns `true` is this object is approximately equal to the other one.
216    fn approx_eq(&self, other: &Self) -> bool;
217}
218
219macro_rules! approx_eq {
220    ($ty:ty, $eps:expr) => {
221        impl ApproxEq<$ty> for $ty {
222            #[inline]
223            fn approx_eq(&self, other: &$ty) -> bool {
224                num_traits::sign::abs(*self - *other) <= $eps
225            }
226        }
227    };
228}
229
230approx_eq!(i16, 0);
231approx_eq!(i32, 0);
232approx_eq!(f32, f32::EPSILON);
233
234impl<T, U> Add for BorderRadius<T, U>
235where
236    T: Add<T, Output = T>,
237{
238    type Output = Self;
239    fn add(self, other: Self) -> Self {
240        BorderRadius::new(
241            self.top_left + other.top_left,
242            self.top_right + other.top_right,
243            self.bottom_right + other.bottom_right,
244            self.bottom_left + other.bottom_left,
245        )
246    }
247}
248
249impl<T, U> AddAssign<Self> for BorderRadius<T, U>
250where
251    T: AddAssign<T>,
252{
253    fn add_assign(&mut self, other: Self) {
254        self.top_left += other.top_left;
255        self.top_right += other.top_right;
256        self.bottom_right += other.bottom_right;
257        self.bottom_left += other.bottom_left;
258    }
259}
260
261impl<T, U> Sub for BorderRadius<T, U>
262where
263    T: Sub<T, Output = T>,
264{
265    type Output = Self;
266    fn sub(self, other: Self) -> Self {
267        BorderRadius::new(
268            self.top_left - other.top_left,
269            self.top_right - other.top_right,
270            self.bottom_right - other.bottom_right,
271            self.bottom_left - other.bottom_left,
272        )
273    }
274}
275
276impl<T, U> SubAssign<Self> for BorderRadius<T, U>
277where
278    T: SubAssign<T>,
279{
280    fn sub_assign(&mut self, other: Self) {
281        self.top_left -= other.top_left;
282        self.top_right -= other.top_right;
283        self.bottom_right -= other.bottom_right;
284        self.bottom_left -= other.bottom_left;
285    }
286}
287
288impl<T, U> Neg for BorderRadius<T, U>
289where
290    T: Neg<Output = T>,
291{
292    type Output = Self;
293    fn neg(self) -> Self {
294        BorderRadius {
295            top_left: -self.top_left,
296            top_right: -self.top_right,
297            bottom_right: -self.bottom_right,
298            bottom_left: -self.bottom_left,
299            _unit: PhantomData,
300        }
301    }
302}
303
304impl<T, U> Mul<T> for BorderRadius<T, U>
305where
306    T: Copy + Mul,
307{
308    type Output = BorderRadius<T::Output, U>;
309
310    #[inline]
311    fn mul(self, scale: T) -> Self::Output {
312        BorderRadius::new(
313            self.top_left * scale,
314            self.top_right * scale,
315            self.bottom_right * scale,
316            self.bottom_left * scale,
317        )
318    }
319}
320
321impl<T, U> MulAssign<T> for BorderRadius<T, U>
322where
323    T: Copy + MulAssign,
324{
325    #[inline]
326    fn mul_assign(&mut self, other: T) {
327        self.top_left *= other;
328        self.top_right *= other;
329        self.bottom_right *= other;
330        self.bottom_left *= other;
331    }
332}
333
334impl<T, U1, U2> Mul<Scale<T, U1, U2>> for BorderRadius<T, U1>
335where
336    T: Copy + Mul,
337{
338    type Output = BorderRadius<T::Output, U2>;
339
340    #[inline]
341    fn mul(self, scale: Scale<T, U1, U2>) -> Self::Output {
342        BorderRadius::new(
343            self.top_left * scale.0,
344            self.top_right * scale.0,
345            self.bottom_right * scale.0,
346            self.bottom_left * scale.0,
347        )
348    }
349}
350
351impl<T, U> MulAssign<Scale<T, U, U>> for BorderRadius<T, U>
352where
353    T: Copy + MulAssign,
354{
355    #[inline]
356    fn mul_assign(&mut self, other: Scale<T, U, U>) {
357        *self *= other.0;
358    }
359}
360
361impl<T, U> Div<T> for BorderRadius<T, U>
362where
363    T: Copy + Div,
364{
365    type Output = BorderRadius<T::Output, U>;
366
367    #[inline]
368    fn div(self, scale: T) -> Self::Output {
369        BorderRadius::new(
370            self.top_left / scale,
371            self.top_right / scale,
372            self.bottom_right / scale,
373            self.bottom_left / scale,
374        )
375    }
376}
377
378impl<T, U> DivAssign<T> for BorderRadius<T, U>
379where
380    T: Copy + DivAssign,
381{
382    #[inline]
383    fn div_assign(&mut self, other: T) {
384        self.top_left /= other;
385        self.top_right /= other;
386        self.bottom_right /= other;
387        self.bottom_left /= other;
388    }
389}
390
391impl<T, U1, U2> Div<Scale<T, U1, U2>> for BorderRadius<T, U2>
392where
393    T: Copy + Div,
394{
395    type Output = BorderRadius<T::Output, U1>;
396
397    #[inline]
398    fn div(self, scale: Scale<T, U1, U2>) -> Self::Output {
399        BorderRadius::new(
400            self.top_left / scale.0,
401            self.top_right / scale.0,
402            self.bottom_right / scale.0,
403            self.bottom_left / scale.0,
404        )
405    }
406}
407
408impl<T, U> DivAssign<Scale<T, U, U>> for BorderRadius<T, U>
409where
410    T: Copy + DivAssign,
411{
412    fn div_assign(&mut self, other: Scale<T, U, U>) {
413        *self /= other.0;
414    }
415}
416
417impl<T, U> BorderRadius<T, U>
418where
419    T: PartialOrd,
420{
421    /// Returns the minimum of the two radii.
422    #[inline]
423    pub fn min(self, other: Self) -> Self {
424        BorderRadius::new(
425            min(self.top_left, other.top_left),
426            min(self.top_right, other.top_right),
427            min(self.bottom_right, other.bottom_right),
428            min(self.bottom_left, other.bottom_left),
429        )
430    }
431
432    /// Returns the maximum of the two radii.
433    #[inline]
434    pub fn max(self, other: Self) -> Self {
435        BorderRadius::new(
436            max(self.top_left, other.top_left),
437            max(self.top_right, other.top_right),
438            max(self.bottom_right, other.bottom_right),
439            max(self.bottom_left, other.bottom_left),
440        )
441    }
442}
443
444impl<T, U> BorderRadius<T, U>
445where
446    T: NumCast + Copy,
447{
448    /// Cast from one numeric representation to another, preserving the units.
449    #[inline]
450    pub fn cast<NewT: NumCast>(self) -> BorderRadius<NewT, U> {
451        self.try_cast().unwrap()
452    }
453
454    /// Fallible cast from one numeric representation to another, preserving the units.
455    pub fn try_cast<NewT: NumCast>(self) -> Option<BorderRadius<NewT, U>> {
456        match (
457            NumCast::from(self.top_left),
458            NumCast::from(self.top_right),
459            NumCast::from(self.bottom_right),
460            NumCast::from(self.bottom_left),
461        ) {
462            (Some(top_left), Some(top_right), Some(bottom_right), Some(bottom_left)) => {
463                Some(BorderRadius::new(top_left, top_right, bottom_right, bottom_left))
464            }
465            _ => None,
466        }
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use crate::lengths::{LogicalBorderRadius, LogicalLength, PhysicalPx, ScaleFactor};
473    use euclid::UnknownUnit;
474
475    type BorderRadius = super::BorderRadius<f32, UnknownUnit>;
476    type IntBorderRadius = super::BorderRadius<i16, UnknownUnit>;
477    type PhysicalBorderRadius = super::BorderRadius<f32, PhysicalPx>;
478
479    #[test]
480    fn test_eq() {
481        let a = BorderRadius::new(1., 2., 3., 4.);
482        let b = BorderRadius::new(1., 2., 3., 4.);
483        let c = BorderRadius::new(4., 3., 2., 1.);
484        let d = BorderRadius::new(
485            c.top_left + f32::EPSILON / 2.,
486            c.top_right - f32::EPSILON / 2.,
487            c.bottom_right - f32::EPSILON / 2.,
488            c.bottom_left + f32::EPSILON / 2.,
489        );
490        assert_eq!(a, b);
491        assert_ne!(a, c);
492        assert_eq!(c, d);
493    }
494
495    #[test]
496    fn test_min_max() {
497        let a = BorderRadius::new(1., 2., 3., 4.);
498        let b = BorderRadius::new(4., 3., 2., 1.);
499        assert_eq!(a.min(b), BorderRadius::new(1., 2., 2., 1.));
500        assert_eq!(a.max(b), BorderRadius::new(4., 3., 3., 4.));
501    }
502
503    #[test]
504    fn test_scale() {
505        let scale = ScaleFactor::new(2.);
506        let logical_radius = LogicalBorderRadius::new(1., 2., 3., 4.);
507        let physical_radius = PhysicalBorderRadius::new(2., 4., 6., 8.);
508        assert_eq!(logical_radius * scale, physical_radius);
509        assert_eq!(physical_radius / scale, logical_radius);
510    }
511
512    #[test]
513    fn test_zero() {
514        assert!(BorderRadius::new_uniform(0.).is_zero());
515        assert!(BorderRadius::new_uniform(1.0e-9).is_zero());
516        assert!(!BorderRadius::new_uniform(1.0e-3).is_zero());
517        assert!(IntBorderRadius::new_uniform(0).is_zero());
518        assert!(!IntBorderRadius::new_uniform(1).is_zero());
519    }
520
521    #[test]
522    fn test_inner_outer() {
523        let radius = LogicalBorderRadius::new(0., 2.5, 5., 10.);
524        let half_border_width = LogicalLength::new(5.);
525        assert_eq!(radius.inner(half_border_width), LogicalBorderRadius::new(0., 0., 0., 5.));
526        assert_eq!(radius.outer(half_border_width), LogicalBorderRadius::new(0., 5., 5., 10.));
527    }
528}