Skip to main content

freya_core/style/
corner_radius.rs

1use std::f32::consts::SQRT_2;
2
3use freya_engine::prelude::*;
4use torin::scaled::Scaled;
5
6/// Radius applied to each corner of an element to round it, plus an optional
7/// `smoothing` factor (`0.0..=1.0`) that turns sharp rounding into a squircle.
8///
9/// Use [`CornerRadius::new_all`] for a uniform radius. It also implements `From<f32>`,
10/// applied to every corner.
11///
12/// ```
13/// # use freya::prelude::*;
14/// let radius = CornerRadius::new_all(8.0);
15/// ```
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[derive(PartialEq, Clone, Debug, Default, Copy)]
18pub struct CornerRadius {
19    pub top_left: f32,
20    pub top_right: f32,
21    pub bottom_right: f32,
22    pub bottom_left: f32,
23    pub smoothing: f32,
24}
25
26impl From<f32> for CornerRadius {
27    fn from(value: f32) -> Self {
28        CornerRadius::new_all(value)
29    }
30}
31
32impl CornerRadius {
33    /// Create a [`CornerRadius`] with the same radius on all four corners.
34    pub const fn new_all(radius: f32) -> Self {
35        Self {
36            top_left: radius,
37            top_right: radius,
38            bottom_right: radius,
39            bottom_left: radius,
40            smoothing: 0.,
41        }
42    }
43
44    pub fn fill_top(&mut self, value: f32) {
45        self.top_left = value;
46        self.top_right = value;
47    }
48
49    pub fn fill_bottom(&mut self, value: f32) {
50        self.bottom_left = value;
51        self.bottom_right = value;
52    }
53
54    pub fn fill_all(&mut self, value: f32) {
55        self.fill_bottom(value);
56        self.fill_top(value);
57    }
58
59    /// Return a copy of this [`CornerRadius`] with the given `smoothing` (clamped to `0.0..=1.0`).
60    pub fn with_smoothing(mut self, smoothing: f32) -> Self {
61        self.smoothing = smoothing.clamp(0.0, 1.0);
62        self
63    }
64
65    // https://github.com/aloisdeniel/figma_squircle/blob/main/lib/src/path_smooth_corners.dart
66    pub fn smoothed_path(&self, rect: RRect) -> Path {
67        let mut path = PathBuilder::new();
68
69        let left = rect.rect().left();
70        let top = rect.rect().top();
71        let width = rect.width();
72        let height = rect.height();
73
74        let top_right = rect.radii(SkCorner::UpperRight).x;
75        if top_right > 0.0 {
76            let (a, b, c, d, l, p, radius) =
77                compute_smooth_corner(top_right, self.smoothing, width, height);
78
79            path.move_to((f32::max(width / 2.0, width - p), 0.0))
80                .cubic_to(
81                    (width - (p - a), 0.0),
82                    (width - (p - a - b), 0.0),
83                    (width - (p - a - b - c), d),
84                )
85                .r_arc_to(
86                    (radius, radius),
87                    0.0,
88                    ArcSize::Small,
89                    PathDirection::CW,
90                    (l, l),
91                )
92                .cubic_to(
93                    (width, p - a - b),
94                    (width, p - a),
95                    (width, f32::min(height / 2.0, p)),
96                );
97        } else {
98            path.move_to((width / 2.0, 0.0))
99                .line_to((width, 0.0))
100                .line_to((width, height / 2.0));
101        }
102
103        let bottom_right = rect.radii(SkCorner::LowerRight).x;
104        if bottom_right > 0.0 {
105            let (a, b, c, d, l, p, radius) =
106                compute_smooth_corner(bottom_right, self.smoothing, width, height);
107
108            path.line_to((width, f32::max(height / 2.0, height - p)))
109                .cubic_to(
110                    (width, height - (p - a)),
111                    (width, height - (p - a - b)),
112                    (width - d, height - (p - a - b - c)),
113                )
114                .r_arc_to(
115                    (radius, radius),
116                    0.0,
117                    ArcSize::Small,
118                    PathDirection::CW,
119                    (-l, l),
120                )
121                .cubic_to(
122                    (width - (p - a - b), height),
123                    (width - (p - a), height),
124                    (f32::max(width / 2.0, width - p), height),
125                );
126        } else {
127            path.line_to((width, height)).line_to((width / 2.0, height));
128        }
129
130        let bottom_left = rect.radii(SkCorner::LowerLeft).x;
131        if bottom_left > 0.0 {
132            let (a, b, c, d, l, p, radius) =
133                compute_smooth_corner(bottom_left, self.smoothing, width, height);
134
135            path.line_to((f32::min(width / 2.0, p), height))
136                .cubic_to(
137                    (p - a, height),
138                    (p - a - b, height),
139                    (p - a - b - c, height - d),
140                )
141                .r_arc_to(
142                    (radius, radius),
143                    0.0,
144                    ArcSize::Small,
145                    PathDirection::CW,
146                    (-l, -l),
147                )
148                .cubic_to(
149                    (0.0, height - (p - a - b)),
150                    (0.0, height - (p - a)),
151                    (0.0, f32::max(height / 2.0, height - p)),
152                );
153        } else {
154            path.line_to((0.0, height)).line_to((0.0, height / 2.0));
155        }
156
157        let top_left = rect.radii(SkCorner::UpperLeft).x;
158        if top_left > 0.0 {
159            let (a, b, c, d, l, p, radius) =
160                compute_smooth_corner(top_left, self.smoothing, width, height);
161
162            path.line_to((0.0, f32::min(height / 2.0, p)))
163                .cubic_to((0.0, p - a), (0.0, p - a - b), (d, p - a - b - c))
164                .r_arc_to(
165                    (radius, radius),
166                    0.0,
167                    ArcSize::Small,
168                    PathDirection::CW,
169                    (l, -l),
170                )
171                .cubic_to(
172                    (p - a - b, 0.0),
173                    (p - a, 0.0),
174                    (f32::min(width / 2.0, p), 0.0),
175                );
176        } else {
177            path.line_to((0.0, 0.0));
178        }
179
180        path.detach()
181            .make_transform(&Matrix::translate((left, top)))
182    }
183
184    pub fn pretty(&self) -> String {
185        format!(
186            "({}, {}, {}, {})",
187            self.top_left, self.top_right, self.bottom_right, self.bottom_left
188        )
189    }
190
191    pub fn is_round(&self) -> bool {
192        self.top_left > 0. || self.top_right > 0. || self.bottom_right > 0. || self.bottom_left > 0.
193    }
194}
195
196// https://www.figma.com/blog/desperately-seeking-squircles/
197fn compute_smooth_corner(
198    corner_radius: f32,
199    smoothing: f32,
200    width: f32,
201    height: f32,
202) -> (f32, f32, f32, f32, f32, f32, f32) {
203    let max_p = f32::min(width, height) / 2.0;
204    let corner_radius = f32::min(corner_radius, max_p);
205
206    let p = f32::min((1.0 + smoothing) * corner_radius, max_p);
207
208    let angle_alpha: f32;
209    let angle_beta: f32;
210
211    if corner_radius <= max_p / 2.0 {
212        angle_alpha = 45.0 * smoothing;
213        angle_beta = 90.0 * (1.0 - smoothing);
214    } else {
215        let diff_ratio = (corner_radius - max_p / 2.0) / (max_p / 2.0);
216
217        angle_alpha = 45.0 * smoothing * (1.0 - diff_ratio);
218        angle_beta = 90.0 * (1.0 - smoothing * (1.0 - diff_ratio));
219    }
220
221    let angle_theta = (90.0 - angle_beta) / 2.0;
222    let dist_p3_p4 = corner_radius * (angle_theta / 2.0).to_radians().tan();
223
224    let l = (angle_beta / 2.0).to_radians().sin() * corner_radius * SQRT_2;
225    let c = dist_p3_p4 * angle_alpha.to_radians().cos();
226    let d = c * angle_alpha.to_radians().tan();
227    let b = (p - l - c - d) / 3.0;
228    let a = 2.0 * b;
229
230    (a, b, c, d, l, p, corner_radius)
231}
232
233impl Scaled for CornerRadius {
234    fn scale(&mut self, scale: f32) {
235        self.top_left *= scale;
236        self.top_right *= scale;
237        self.bottom_left *= scale;
238        self.bottom_right *= scale;
239    }
240}