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    // https://github.com/aloisdeniel/figma_squircle/blob/main/lib/src/path_smooth_corners.dart
60    pub fn smoothed_path(&self, rect: RRect) -> Path {
61        let mut path = PathBuilder::new();
62
63        let width = rect.width();
64        let height = rect.height();
65
66        let top_right = rect.radii(SkCorner::UpperRight).x;
67        if top_right > 0.0 {
68            let (a, b, c, d, l, p, radius) =
69                compute_smooth_corner(top_right, self.smoothing, width, height);
70
71            path.move_to((f32::max(width / 2.0, width - p), 0.0))
72                .cubic_to(
73                    (width - (p - a), 0.0),
74                    (width - (p - a - b), 0.0),
75                    (width - (p - a - b - c), d),
76                )
77                .r_arc_to(
78                    (radius, radius),
79                    0.0,
80                    ArcSize::Small,
81                    PathDirection::CW,
82                    (l, l),
83                )
84                .cubic_to(
85                    (width, p - a - b),
86                    (width, p - a),
87                    (width, f32::min(height / 2.0, p)),
88                );
89        } else {
90            path.move_to((width / 2.0, 0.0))
91                .line_to((width, 0.0))
92                .line_to((width, height / 2.0));
93        }
94
95        let bottom_right = rect.radii(SkCorner::LowerRight).x;
96        if bottom_right > 0.0 {
97            let (a, b, c, d, l, p, radius) =
98                compute_smooth_corner(bottom_right, self.smoothing, width, height);
99
100            path.line_to((width, f32::max(height / 2.0, height - p)))
101                .cubic_to(
102                    (width, height - (p - a)),
103                    (width, height - (p - a - b)),
104                    (width - d, height - (p - a - b - c)),
105                )
106                .r_arc_to(
107                    (radius, radius),
108                    0.0,
109                    ArcSize::Small,
110                    PathDirection::CW,
111                    (-l, l),
112                )
113                .cubic_to(
114                    (width - (p - a - b), height),
115                    (width - (p - a), height),
116                    (f32::max(width / 2.0, width - p), height),
117                );
118        } else {
119            path.line_to((width, height)).line_to((width / 2.0, height));
120        }
121
122        let bottom_left = rect.radii(SkCorner::LowerLeft).x;
123        if bottom_left > 0.0 {
124            let (a, b, c, d, l, p, radius) =
125                compute_smooth_corner(bottom_left, self.smoothing, width, height);
126
127            path.line_to((f32::min(width / 2.0, p), height))
128                .cubic_to(
129                    (p - a, height),
130                    (p - a - b, height),
131                    (p - a - b - c, height - d),
132                )
133                .r_arc_to(
134                    (radius, radius),
135                    0.0,
136                    ArcSize::Small,
137                    PathDirection::CW,
138                    (-l, -l),
139                )
140                .cubic_to(
141                    (0.0, height - (p - a - b)),
142                    (0.0, height - (p - a)),
143                    (0.0, f32::max(height / 2.0, height - p)),
144                );
145        } else {
146            path.line_to((0.0, height)).line_to((0.0, height / 2.0));
147        }
148
149        let top_left = rect.radii(SkCorner::UpperLeft).x;
150        if top_left > 0.0 {
151            let (a, b, c, d, l, p, radius) =
152                compute_smooth_corner(top_left, self.smoothing, width, height);
153
154            path.line_to((0.0, f32::min(height / 2.0, p)))
155                .cubic_to((0.0, p - a), (0.0, p - a - b), (d, p - a - b - c))
156                .r_arc_to(
157                    (radius, radius),
158                    0.0,
159                    ArcSize::Small,
160                    PathDirection::CW,
161                    (l, -l),
162                )
163                .cubic_to(
164                    (p - a - b, 0.0),
165                    (p - a, 0.0),
166                    (f32::min(width / 2.0, p), 0.0),
167                );
168        } else {
169            path.line_to((0.0, 0.0));
170        }
171
172        path.detach()
173    }
174
175    pub fn pretty(&self) -> String {
176        format!(
177            "({}, {}, {}, {})",
178            self.top_left, self.top_right, self.bottom_right, self.bottom_left
179        )
180    }
181
182    pub fn is_round(&self) -> bool {
183        self.top_left > 0. || self.top_right > 0. || self.bottom_right > 0. || self.bottom_left > 0.
184    }
185}
186
187// https://www.figma.com/blog/desperately-seeking-squircles/
188fn compute_smooth_corner(
189    corner_radius: f32,
190    smoothing: f32,
191    width: f32,
192    height: f32,
193) -> (f32, f32, f32, f32, f32, f32, f32) {
194    let max_p = f32::min(width, height) / 2.0;
195    let corner_radius = f32::min(corner_radius, max_p);
196
197    let p = f32::min((1.0 + smoothing) * corner_radius, max_p);
198
199    let angle_alpha: f32;
200    let angle_beta: f32;
201
202    if corner_radius <= max_p / 2.0 {
203        angle_alpha = 45.0 * smoothing;
204        angle_beta = 90.0 * (1.0 - smoothing);
205    } else {
206        let diff_ratio = (corner_radius - max_p / 2.0) / (max_p / 2.0);
207
208        angle_alpha = 45.0 * smoothing * (1.0 - diff_ratio);
209        angle_beta = 90.0 * (1.0 - smoothing * (1.0 - diff_ratio));
210    }
211
212    let angle_theta = (90.0 - angle_beta) / 2.0;
213    let dist_p3_p4 = corner_radius * (angle_theta / 2.0).to_radians().tan();
214
215    let l = (angle_beta / 2.0).to_radians().sin() * corner_radius * SQRT_2;
216    let c = dist_p3_p4 * angle_alpha.to_radians().cos();
217    let d = c * angle_alpha.to_radians().tan();
218    let b = (p - l - c - d) / 3.0;
219    let a = 2.0 * b;
220
221    (a, b, c, d, l, p, corner_radius)
222}
223
224impl Scaled for CornerRadius {
225    fn scale(&mut self, scale: f32) {
226        self.top_left *= scale;
227        self.top_right *= scale;
228        self.bottom_left *= scale;
229        self.bottom_right *= scale;
230    }
231}