// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 /*! This module contains border radius related types for the run-time library. */ use core::fmt; use core::marker::PhantomData; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use euclid::approxord::{max, min}; use euclid::num::Zero; use euclid::{Length, Scale}; use num_traits::NumCast; /// Top-left, top-right, bottom-right, and bottom-left border radius, optionally /// tagged with a unit. #[repr(C)] pub struct BorderRadius { /// The top-left radius. pub top_left: T, /// The top-right radius. pub top_right: T, /// The bottom-right radius. pub bottom_right: T, /// The bottom-left radius. pub bottom_left: T, #[doc(hidden)] pub _unit: PhantomData, } impl Copy for BorderRadius where T: Copy {} impl Clone for BorderRadius where T: Clone, { fn clone(&self) -> Self { BorderRadius { top_left: self.top_left.clone(), top_right: self.top_right.clone(), bottom_right: self.bottom_right.clone(), bottom_left: self.bottom_left.clone(), _unit: PhantomData, } } } impl Eq for BorderRadius where T: Eq {} impl PartialEq for BorderRadius where T: PartialEq, { fn eq(&self, other: &Self) -> bool { self.top_left == other.top_left && self.top_right == other.top_right && self.bottom_right == other.bottom_right && self.bottom_left == other.bottom_left } } impl fmt::Debug for BorderRadius where T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "BorderRadius({:?}, {:?}, {:?}, {:?})", self.top_left, self.top_right, self.bottom_right, self.bottom_left ) } } impl Default for BorderRadius where T: Default, { fn default() -> Self { BorderRadius::new(T::default(), T::default(), T::default(), T::default()) } } impl Zero for BorderRadius where T: Zero, { fn zero() -> Self { BorderRadius::new(T::zero(), T::zero(), T::zero(), T::zero()) } } impl BorderRadius { /// Constructor taking a scalar for each radius. /// /// Radii are specified in top-left, top-right, bottom-right, bottom-left /// order following CSS's convention. pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self { BorderRadius { top_left, top_right, bottom_right, bottom_left, _unit: PhantomData } } /// Constructor taking a typed Length for each radius. /// /// Radii are specified in top-left, top-right, bottom-right, bottom-left /// order following CSS's convention. pub fn from_lengths( top_left: Length, top_right: Length, bottom_right: Length, bottom_left: Length, ) -> Self { BorderRadius::new(top_left.0, top_right.0, bottom_right.0, bottom_left.0) } /// Constructor taking the same scalar value for all radii. pub fn new_uniform(all: T) -> Self where T: Copy, { BorderRadius::new(all, all, all, all) } /// Constructor taking the same typed Length for all radii. pub fn from_length(all: Length) -> Self where T: Copy, { BorderRadius::new_uniform(all.0) } /// Returns `true` if all radii are equal. pub fn is_uniform(&self) -> bool where T: ApproxEq, { self.top_left.approx_eq(&self.top_right) && self.top_left.approx_eq(&self.bottom_right) && self.top_left.approx_eq(&self.bottom_left) } /// Returns the uniform radius if all are equal, or `None` otherwise. pub fn as_uniform(&self) -> Option where T: Copy + ApproxEq, { if self.is_uniform() { Some(self.top_left) } else { None } } /// Returns `true` if all radii are zero. pub fn is_zero(&self) -> bool where T: ApproxEq + Zero, { let zero = T::zero(); self.top_left.approx_eq(&zero) && self.top_right.approx_eq(&zero) && self.bottom_right.approx_eq(&zero) && self.bottom_left.approx_eq(&zero) } /// Returns the outer radius. /// /// For any corner with a positive radius, the radius is ensured to be at /// least `half_border_width`. pub fn outer(&self, half_border_width: Length) -> Self where T: Copy + PartialOrd + Zero, { let zero = T::zero(); BorderRadius::new( if self.top_left > zero { max(self.top_left, half_border_width.0) } else { self.top_left }, if self.top_right > zero { max(self.top_right, half_border_width.0) } else { self.top_right }, if self.bottom_right > zero { max(self.bottom_right, half_border_width.0) } else { self.bottom_right }, if self.bottom_left > zero { max(self.bottom_left, half_border_width.0) } else { self.bottom_left }, ) } /// Returns the inner radius. /// /// A positive radius of each corner is subtracted by `half_border_width` /// and min-clamped to zero. pub fn inner(&self, half_border_width: Length) -> Self where T: Copy + PartialOrd + Sub + Zero, { BorderRadius::new( self.top_left - half_border_width.0, self.top_right - half_border_width.0, self.bottom_right - half_border_width.0, self.bottom_left - half_border_width.0, ) .max(Self::zero()) } } /// Trait for testing approximate equality pub trait ApproxEq { /// Returns `true` is this object is approximately equal to the other one. fn approx_eq(&self, other: &Self) -> bool; } macro_rules! approx_eq { ($ty:ty, $eps:expr) => { impl ApproxEq<$ty> for $ty { #[inline] fn approx_eq(&self, other: &$ty) -> bool { num_traits::sign::abs(*self - *other) <= $eps } } }; } approx_eq!(i16, 0); approx_eq!(i32, 0); approx_eq!(f32, f32::EPSILON); impl Add for BorderRadius where T: Add, { type Output = Self; fn add(self, other: Self) -> Self { BorderRadius::new( self.top_left + other.top_left, self.top_right + other.top_right, self.bottom_right + other.bottom_right, self.bottom_left + other.bottom_left, ) } } impl AddAssign for BorderRadius where T: AddAssign, { fn add_assign(&mut self, other: Self) { self.top_left += other.top_left; self.top_right += other.top_right; self.bottom_right += other.bottom_right; self.bottom_left += other.bottom_left; } } impl Sub for BorderRadius where T: Sub, { type Output = Self; fn sub(self, other: Self) -> Self { BorderRadius::new( self.top_left - other.top_left, self.top_right - other.top_right, self.bottom_right - other.bottom_right, self.bottom_left - other.bottom_left, ) } } impl SubAssign for BorderRadius where T: SubAssign, { fn sub_assign(&mut self, other: Self) { self.top_left -= other.top_left; self.top_right -= other.top_right; self.bottom_right -= other.bottom_right; self.bottom_left -= other.bottom_left; } } impl Neg for BorderRadius where T: Neg, { type Output = Self; fn neg(self) -> Self { BorderRadius { top_left: -self.top_left, top_right: -self.top_right, bottom_right: -self.bottom_right, bottom_left: -self.bottom_left, _unit: PhantomData, } } } impl Mul for BorderRadius where T: Copy + Mul, { type Output = BorderRadius; #[inline] fn mul(self, scale: T) -> Self::Output { BorderRadius::new( self.top_left * scale, self.top_right * scale, self.bottom_right * scale, self.bottom_left * scale, ) } } impl MulAssign for BorderRadius where T: Copy + MulAssign, { #[inline] fn mul_assign(&mut self, other: T) { self.top_left *= other; self.top_right *= other; self.bottom_right *= other; self.bottom_left *= other; } } impl Mul> for BorderRadius where T: Copy + Mul, { type Output = BorderRadius; #[inline] fn mul(self, scale: Scale) -> Self::Output { BorderRadius::new( self.top_left * scale.0, self.top_right * scale.0, self.bottom_right * scale.0, self.bottom_left * scale.0, ) } } impl MulAssign> for BorderRadius where T: Copy + MulAssign, { #[inline] fn mul_assign(&mut self, other: Scale) { *self *= other.0; } } impl Div for BorderRadius where T: Copy + Div, { type Output = BorderRadius; #[inline] fn div(self, scale: T) -> Self::Output { BorderRadius::new( self.top_left / scale, self.top_right / scale, self.bottom_right / scale, self.bottom_left / scale, ) } } impl DivAssign for BorderRadius where T: Copy + DivAssign, { #[inline] fn div_assign(&mut self, other: T) { self.top_left /= other; self.top_right /= other; self.bottom_right /= other; self.bottom_left /= other; } } impl Div> for BorderRadius where T: Copy + Div, { type Output = BorderRadius; #[inline] fn div(self, scale: Scale) -> Self::Output { BorderRadius::new( self.top_left / scale.0, self.top_right / scale.0, self.bottom_right / scale.0, self.bottom_left / scale.0, ) } } impl DivAssign> for BorderRadius where T: Copy + DivAssign, { fn div_assign(&mut self, other: Scale) { *self /= other.0; } } impl BorderRadius where T: PartialOrd, { /// Returns the minimum of the two radii. #[inline] pub fn min(self, other: Self) -> Self { BorderRadius::new( min(self.top_left, other.top_left), min(self.top_right, other.top_right), min(self.bottom_right, other.bottom_right), min(self.bottom_left, other.bottom_left), ) } /// Returns the maximum of the two radii. #[inline] pub fn max(self, other: Self) -> Self { BorderRadius::new( max(self.top_left, other.top_left), max(self.top_right, other.top_right), max(self.bottom_right, other.bottom_right), max(self.bottom_left, other.bottom_left), ) } } impl BorderRadius where T: NumCast + Copy, { /// Cast from one numeric representation to another, preserving the units. #[inline] pub fn cast(self) -> BorderRadius { self.try_cast().unwrap() } /// Fallible cast from one numeric representation to another, preserving the units. pub fn try_cast(self) -> Option> { match ( NumCast::from(self.top_left), NumCast::from(self.top_right), NumCast::from(self.bottom_right), NumCast::from(self.bottom_left), ) { (Some(top_left), Some(top_right), Some(bottom_right), Some(bottom_left)) => { Some(BorderRadius::new(top_left, top_right, bottom_right, bottom_left)) } _ => None, } } } #[cfg(test)] mod tests { use crate::lengths::{LogicalBorderRadius, LogicalLength, PhysicalPx, ScaleFactor}; use euclid::UnknownUnit; type BorderRadius = super::BorderRadius; type IntBorderRadius = super::BorderRadius; type PhysicalBorderRadius = super::BorderRadius; #[test] fn test_eq() { let a = BorderRadius::new(1., 2., 3., 4.); let b = BorderRadius::new(1., 2., 3., 4.); let c = BorderRadius::new(4., 3., 2., 1.); let d = BorderRadius::new( c.top_left + f32::EPSILON / 2., c.top_right - f32::EPSILON / 2., c.bottom_right - f32::EPSILON / 2., c.bottom_left + f32::EPSILON / 2., ); assert_eq!(a, b); assert_ne!(a, c); assert_eq!(c, d); } #[test] fn test_min_max() { let a = BorderRadius::new(1., 2., 3., 4.); let b = BorderRadius::new(4., 3., 2., 1.); assert_eq!(a.min(b), BorderRadius::new(1., 2., 2., 1.)); assert_eq!(a.max(b), BorderRadius::new(4., 3., 3., 4.)); } #[test] fn test_scale() { let scale = ScaleFactor::new(2.); let logical_radius = LogicalBorderRadius::new(1., 2., 3., 4.); let physical_radius = PhysicalBorderRadius::new(2., 4., 6., 8.); assert_eq!(logical_radius * scale, physical_radius); assert_eq!(physical_radius / scale, logical_radius); } #[test] fn test_zero() { assert!(BorderRadius::new_uniform(0.).is_zero()); assert!(BorderRadius::new_uniform(1.0e-9).is_zero()); assert!(!BorderRadius::new_uniform(1.0e-3).is_zero()); assert!(IntBorderRadius::new_uniform(0).is_zero()); assert!(!IntBorderRadius::new_uniform(1).is_zero()); } #[test] fn test_inner_outer() { let radius = LogicalBorderRadius::new(0., 2.5, 5., 10.); let half_border_width = LogicalLength::new(5.); assert_eq!(radius.inner(half_border_width), LogicalBorderRadius::new(0., 0., 0., 5.)); assert_eq!(radius.outer(half_border_width), LogicalBorderRadius::new(0., 5., 5., 10.)); } }