Skip to main content

inset_embedder/geometry/
rrect.rs

1//! Flutter counterpart: `RRect` / `RSuperellipse` in `engine/src/flutter/lib/ui/geometry.dart`.
2
3#![allow(clippy::too_many_arguments)]
4
5use std::fmt::{self, Debug};
6
7use super::geometry::{Offset, Radius, Rect};
8use super::lerp::lerp_double_non_null;
9
10/// Dart's private `_RRectLike` fields. Shared by [`RRect`] and [`RSuperellipse`].
11#[derive(Clone, Copy, PartialEq)]
12struct Rounded {
13    left: f64,
14    top: f64,
15    right: f64,
16    bottom: f64,
17    tl_radius_x: f64,
18    tl_radius_y: f64,
19    tr_radius_x: f64,
20    tr_radius_y: f64,
21    br_radius_x: f64,
22    br_radius_y: f64,
23    bl_radius_x: f64,
24    bl_radius_y: f64,
25}
26
27impl Rounded {
28    fn raw(
29        left: f64,
30        top: f64,
31        right: f64,
32        bottom: f64,
33        tl_radius_x: f64,
34        tl_radius_y: f64,
35        tr_radius_x: f64,
36        tr_radius_y: f64,
37        br_radius_x: f64,
38        br_radius_y: f64,
39        bl_radius_x: f64,
40        bl_radius_y: f64,
41    ) -> Rounded {
42        debug_assert!(tl_radius_x >= 0.0);
43        debug_assert!(tl_radius_y >= 0.0);
44        debug_assert!(tr_radius_x >= 0.0);
45        debug_assert!(tr_radius_y >= 0.0);
46        debug_assert!(br_radius_x >= 0.0);
47        debug_assert!(br_radius_y >= 0.0);
48        debug_assert!(bl_radius_x >= 0.0);
49        debug_assert!(bl_radius_y >= 0.0);
50        Rounded {
51            left,
52            top,
53            right,
54            bottom,
55            tl_radius_x,
56            tl_radius_y,
57            tr_radius_x,
58            tr_radius_y,
59            br_radius_x,
60            br_radius_y,
61            bl_radius_x,
62            bl_radius_y,
63        }
64    }
65
66    fn uniform(
67        left: f64,
68        top: f64,
69        right: f64,
70        bottom: f64,
71        radius_x: f64,
72        radius_y: f64,
73    ) -> Rounded {
74        Rounded::raw(
75            left, top, right, bottom, radius_x, radius_y, radius_x, radius_y, radius_x, radius_y,
76            radius_x, radius_y,
77        )
78    }
79
80    fn corners(
81        left: f64,
82        top: f64,
83        right: f64,
84        bottom: f64,
85        top_left: Radius,
86        top_right: Radius,
87        bottom_right: Radius,
88        bottom_left: Radius,
89    ) -> Rounded {
90        Rounded::raw(
91            left,
92            top,
93            right,
94            bottom,
95            top_left.x,
96            top_left.y,
97            top_right.x,
98            top_right.y,
99            bottom_right.x,
100            bottom_right.y,
101            bottom_left.x,
102            bottom_left.y,
103        )
104    }
105
106    fn shift(self, offset: Offset) -> Rounded {
107        Rounded::raw(
108            self.left + offset.dx(),
109            self.top + offset.dy(),
110            self.right + offset.dx(),
111            self.bottom + offset.dy(),
112            self.tl_radius_x,
113            self.tl_radius_y,
114            self.tr_radius_x,
115            self.tr_radius_y,
116            self.br_radius_x,
117            self.br_radius_y,
118            self.bl_radius_x,
119            self.bl_radius_y,
120        )
121    }
122
123    fn inflate(self, delta: f64) -> Rounded {
124        Rounded::raw(
125            self.left - delta,
126            self.top - delta,
127            self.right + delta,
128            self.bottom + delta,
129            (self.tl_radius_x + delta).max(0.0),
130            (self.tl_radius_y + delta).max(0.0),
131            (self.tr_radius_x + delta).max(0.0),
132            (self.tr_radius_y + delta).max(0.0),
133            (self.br_radius_x + delta).max(0.0),
134            (self.br_radius_y + delta).max(0.0),
135            (self.bl_radius_x + delta).max(0.0),
136            (self.bl_radius_y + delta).max(0.0),
137        )
138    }
139
140    fn width(self) -> f64 {
141        self.right - self.left
142    }
143
144    fn height(self) -> f64 {
145        self.bottom - self.top
146    }
147
148    fn get_min(min: f64, radius1: f64, radius2: f64, limit: f64) -> f64 {
149        let sum = radius1 + radius2;
150        if sum > limit && sum != 0.0 {
151            return min.min(limit / sum);
152        }
153        min
154    }
155
156    fn scale_radii(self) -> Rounded {
157        let mut scale = 1.0;
158        scale = Rounded::get_min(scale, self.bl_radius_y, self.tl_radius_y, self.height());
159        scale = Rounded::get_min(scale, self.tl_radius_x, self.tr_radius_x, self.width());
160        scale = Rounded::get_min(scale, self.tr_radius_y, self.br_radius_y, self.height());
161        scale = Rounded::get_min(scale, self.br_radius_x, self.bl_radius_x, self.width());
162        debug_assert!(scale >= 0.0);
163        if scale < 1.0 {
164            Rounded::raw(
165                self.left,
166                self.top,
167                self.right,
168                self.bottom,
169                self.tl_radius_x * scale,
170                self.tl_radius_y * scale,
171                self.tr_radius_x * scale,
172                self.tr_radius_y * scale,
173                self.br_radius_x * scale,
174                self.br_radius_y * scale,
175                self.bl_radius_x * scale,
176                self.bl_radius_y * scale,
177            )
178        } else {
179            self
180        }
181    }
182
183    fn lerp_to(self, b: Option<Rounded>, t: f64) -> Rounded {
184        match b {
185            None => {
186                let k = 1.0 - t;
187                Rounded::raw(
188                    self.left * k,
189                    self.top * k,
190                    self.right * k,
191                    self.bottom * k,
192                    (self.tl_radius_x * k).max(0.0),
193                    (self.tl_radius_y * k).max(0.0),
194                    (self.tr_radius_x * k).max(0.0),
195                    (self.tr_radius_y * k).max(0.0),
196                    (self.br_radius_x * k).max(0.0),
197                    (self.br_radius_y * k).max(0.0),
198                    (self.bl_radius_x * k).max(0.0),
199                    (self.bl_radius_y * k).max(0.0),
200                )
201            }
202            Some(b) => Rounded::raw(
203                lerp_double_non_null(self.left, b.left, t),
204                lerp_double_non_null(self.top, b.top, t),
205                lerp_double_non_null(self.right, b.right, t),
206                lerp_double_non_null(self.bottom, b.bottom, t),
207                lerp_double_non_null(self.tl_radius_x, b.tl_radius_x, t).max(0.0),
208                lerp_double_non_null(self.tl_radius_y, b.tl_radius_y, t).max(0.0),
209                lerp_double_non_null(self.tr_radius_x, b.tr_radius_x, t).max(0.0),
210                lerp_double_non_null(self.tr_radius_y, b.tr_radius_y, t).max(0.0),
211                lerp_double_non_null(self.br_radius_x, b.br_radius_x, t).max(0.0),
212                lerp_double_non_null(self.br_radius_y, b.br_radius_y, t).max(0.0),
213                lerp_double_non_null(self.bl_radius_x, b.bl_radius_x, t).max(0.0),
214                lerp_double_non_null(self.bl_radius_y, b.bl_radius_y, t).max(0.0),
215            ),
216        }
217    }
218
219    fn fmt_debug(self, class_name: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        let rect = format!(
221            "{:.1}, {:.1}, {:.1}, {:.1}",
222            self.left, self.top, self.right, self.bottom
223        );
224        let tl = Radius::elliptical(self.tl_radius_x, self.tl_radius_y);
225        let tr = Radius::elliptical(self.tr_radius_x, self.tr_radius_y);
226        let br = Radius::elliptical(self.br_radius_x, self.br_radius_y);
227        let bl = Radius::elliptical(self.bl_radius_x, self.bl_radius_y);
228        if tl == tr && tr == br && br == bl {
229            if tl.x == tl.y {
230                write!(f, "{class_name}.fromLTRBR({rect}, {:.1})", tl.x)
231            } else {
232                write!(
233                    f,
234                    "{class_name}.fromLTRBXY({rect}, {:.1}, {:.1})",
235                    tl.x, tl.y
236                )
237            }
238        } else {
239            write!(
240                f,
241                "{class_name}.fromLTRBAndCorners({rect}, topLeft: {tl:?}, topRight: {tr:?}, bottomRight: {br:?}, bottomLeft: {bl:?})"
242            )
243        }
244    }
245}
246
247macro_rules! rounded_type {
248    ($name:ident) => {
249        impl $name {
250            fn from_inner(inner: Rounded) -> $name {
251                $name {
252                    left: inner.left,
253                    top: inner.top,
254                    right: inner.right,
255                    bottom: inner.bottom,
256                    tl_radius_x: inner.tl_radius_x,
257                    tl_radius_y: inner.tl_radius_y,
258                    tr_radius_x: inner.tr_radius_x,
259                    tr_radius_y: inner.tr_radius_y,
260                    br_radius_x: inner.br_radius_x,
261                    br_radius_y: inner.br_radius_y,
262                    bl_radius_x: inner.bl_radius_x,
263                    bl_radius_y: inner.bl_radius_y,
264                }
265            }
266
267            fn inner(self) -> Rounded {
268                Rounded {
269                    left: self.left,
270                    top: self.top,
271                    right: self.right,
272                    bottom: self.bottom,
273                    tl_radius_x: self.tl_radius_x,
274                    tl_radius_y: self.tl_radius_y,
275                    tr_radius_x: self.tr_radius_x,
276                    tr_radius_y: self.tr_radius_y,
277                    br_radius_x: self.br_radius_x,
278                    br_radius_y: self.br_radius_y,
279                    bl_radius_x: self.bl_radius_x,
280                    bl_radius_y: self.bl_radius_y,
281                }
282            }
283
284            /// Construct from left, top, right, bottom, and the same radii along
285            /// the horizontal axis and the vertical axis.
286            pub fn from_ltrbxy(
287                left: f64,
288                top: f64,
289                right: f64,
290                bottom: f64,
291                radius_x: f64,
292                radius_y: f64,
293            ) -> $name {
294                $name::from_inner(Rounded::uniform(
295                    left, top, right, bottom, radius_x, radius_y,
296                ))
297            }
298
299            /// Construct from left, top, right, bottom, and the same radius in
300            /// each corner.
301            pub fn from_ltrbr(
302                left: f64,
303                top: f64,
304                right: f64,
305                bottom: f64,
306                radius: Radius,
307            ) -> $name {
308                $name::from_ltrbxy(left, top, right, bottom, radius.x, radius.y)
309            }
310
311            /// Construct from a bounding box and the same radii along each axis.
312            pub fn from_rect_xy(rect: Rect, radius_x: f64, radius_y: f64) -> $name {
313                $name::from_ltrbxy(
314                    rect.left,
315                    rect.top,
316                    rect.right,
317                    rect.bottom,
318                    radius_x,
319                    radius_y,
320                )
321            }
322
323            /// Construct from a bounding box and a radius that is the same in
324            /// each corner.
325            pub fn from_rect_and_radius(rect: Rect, radius: Radius) -> $name {
326                $name::from_rect_xy(rect, radius.x, radius.y)
327            }
328
329            /// Construct from left, top, right, bottom, and per-corner radii.
330            pub fn from_ltrb_and_corners(
331                left: f64,
332                top: f64,
333                right: f64,
334                bottom: f64,
335                top_left: Radius,
336                top_right: Radius,
337                bottom_right: Radius,
338                bottom_left: Radius,
339            ) -> $name {
340                $name::from_inner(Rounded::corners(
341                    left,
342                    top,
343                    right,
344                    bottom,
345                    top_left,
346                    top_right,
347                    bottom_right,
348                    bottom_left,
349                ))
350            }
351
352            /// Construct from a bounding box and per-corner radii.
353            pub fn from_rect_and_corners(
354                rect: Rect,
355                top_left: Radius,
356                top_right: Radius,
357                bottom_right: Radius,
358                bottom_left: Radius,
359            ) -> $name {
360                $name::from_ltrb_and_corners(
361                    rect.left,
362                    rect.top,
363                    rect.right,
364                    rect.bottom,
365                    top_left,
366                    top_right,
367                    bottom_right,
368                    bottom_left,
369                )
370            }
371
372            /// All values set to zero.
373            pub const ZERO: $name = $name {
374                left: 0.0,
375                top: 0.0,
376                right: 0.0,
377                bottom: 0.0,
378                tl_radius_x: 0.0,
379                tl_radius_y: 0.0,
380                tr_radius_x: 0.0,
381                tr_radius_y: 0.0,
382                br_radius_x: 0.0,
383                br_radius_y: 0.0,
384                bl_radius_x: 0.0,
385                bl_radius_y: 0.0,
386            };
387
388            /// The top-left [`Radius`].
389            pub fn tl_radius(self) -> Radius {
390                Radius::elliptical(self.tl_radius_x, self.tl_radius_y)
391            }
392
393            /// The top-right [`Radius`].
394            pub fn tr_radius(self) -> Radius {
395                Radius::elliptical(self.tr_radius_x, self.tr_radius_y)
396            }
397
398            /// The bottom-right [`Radius`].
399            pub fn br_radius(self) -> Radius {
400                Radius::elliptical(self.br_radius_x, self.br_radius_y)
401            }
402
403            /// The bottom-left [`Radius`].
404            pub fn bl_radius(self) -> Radius {
405                Radius::elliptical(self.bl_radius_x, self.bl_radius_y)
406            }
407
408            /// Returns a clone translated by the given offset.
409            pub fn shift(self, offset: Offset) -> $name {
410                $name::from_inner(self.inner().shift(offset))
411            }
412
413            /// Returns a clone with edges and radii moved outwards by `delta`.
414            pub fn inflate(self, delta: f64) -> $name {
415                $name::from_inner(self.inner().inflate(delta))
416            }
417
418            /// Returns a clone with edges and radii moved inwards by `delta`.
419            pub fn deflate(self, delta: f64) -> $name {
420                self.inflate(-delta)
421            }
422
423            /// The distance between the left and right edges.
424            pub fn width(self) -> f64 {
425                self.inner().width()
426            }
427
428            /// The distance between the top and bottom edges.
429            pub fn height(self) -> f64 {
430                self.inner().height()
431            }
432
433            /// The bounding box of this rounded rectangle.
434            pub fn outer_rect(self) -> Rect {
435                Rect::from_ltrb(self.left, self.top, self.right, self.bottom)
436            }
437
438            /// The non-rounded rectangle constrained by the smaller of the two
439            /// diagonals through the middle of the curve corners.
440            pub fn safe_inner_rect(self) -> Rect {
441                const K_INSET_FACTOR: f64 = 0.29289321881; // 1-cos(pi/4)
442                let left_radius = self.bl_radius_x.max(self.tl_radius_x);
443                let top_radius = self.tl_radius_y.max(self.tr_radius_y);
444                let right_radius = self.tr_radius_x.max(self.br_radius_x);
445                let bottom_radius = self.br_radius_y.max(self.bl_radius_y);
446                Rect::from_ltrb(
447                    self.left + left_radius * K_INSET_FACTOR,
448                    self.top + top_radius * K_INSET_FACTOR,
449                    self.right - right_radius * K_INSET_FACTOR,
450                    self.bottom - bottom_radius * K_INSET_FACTOR,
451                )
452            }
453
454            /// The rectangle formed from the inner-most centers of the corner
455            /// ellipses.
456            pub fn middle_rect(self) -> Rect {
457                let left_radius = self.bl_radius_x.max(self.tl_radius_x);
458                let top_radius = self.tl_radius_y.max(self.tr_radius_y);
459                let right_radius = self.tr_radius_x.max(self.br_radius_x);
460                let bottom_radius = self.br_radius_y.max(self.bl_radius_y);
461                Rect::from_ltrb(
462                    self.left + left_radius,
463                    self.top + top_radius,
464                    self.right - right_radius,
465                    self.bottom - bottom_radius,
466                )
467            }
468
469            /// The biggest rectangle entirely inside with the full width.
470            pub fn wide_middle_rect(self) -> Rect {
471                let top_radius = self.tl_radius_y.max(self.tr_radius_y);
472                let bottom_radius = self.br_radius_y.max(self.bl_radius_y);
473                Rect::from_ltrb(
474                    self.left,
475                    self.top + top_radius,
476                    self.right,
477                    self.bottom - bottom_radius,
478                )
479            }
480
481            /// The biggest rectangle entirely inside with the full height.
482            pub fn tall_middle_rect(self) -> Rect {
483                let left_radius = self.bl_radius_x.max(self.tl_radius_x);
484                let right_radius = self.tr_radius_x.max(self.br_radius_x);
485                Rect::from_ltrb(
486                    self.left + left_radius,
487                    self.top,
488                    self.right - right_radius,
489                    self.bottom,
490                )
491            }
492
493            /// Whether this rounded rectangle encloses a non-zero area.
494            pub fn is_empty(self) -> bool {
495                self.left >= self.right || self.top >= self.bottom
496            }
497
498            /// Whether all coordinates are finite.
499            pub fn is_finite(self) -> bool {
500                self.left.is_finite()
501                    && self.top.is_finite()
502                    && self.right.is_finite()
503                    && self.bottom.is_finite()
504            }
505
506            /// Whether this is a simple rectangle with zero corner radii.
507            pub fn is_rect(self) -> bool {
508                (self.tl_radius_x == 0.0 || self.tl_radius_y == 0.0)
509                    && (self.tr_radius_x == 0.0 || self.tr_radius_y == 0.0)
510                    && (self.bl_radius_x == 0.0 || self.bl_radius_y == 0.0)
511                    && (self.br_radius_x == 0.0 || self.br_radius_y == 0.0)
512            }
513
514            /// Whether this rounded rectangle has a side with no straight section.
515            pub fn is_stadium(self) -> bool {
516                self.tl_radius() == self.tr_radius()
517                    && self.tr_radius() == self.br_radius()
518                    && self.br_radius() == self.bl_radius()
519                    && (self.width() <= 2.0 * self.tl_radius_x
520                        || self.height() <= 2.0 * self.tl_radius_y)
521            }
522
523            /// Whether this rounded rectangle has no side with a straight section.
524            pub fn is_ellipse(self) -> bool {
525                self.tl_radius() == self.tr_radius()
526                    && self.tr_radius() == self.br_radius()
527                    && self.br_radius() == self.bl_radius()
528                    && self.width() <= 2.0 * self.tl_radius_x
529                    && self.height() <= 2.0 * self.tl_radius_y
530            }
531
532            /// Whether this rounded rectangle would draw as a circle.
533            pub fn is_circle(self) -> bool {
534                self.width() == self.height() && self.is_ellipse()
535            }
536
537            /// The lesser of the magnitudes of the width and the height.
538            pub fn shortest_side(self) -> f64 {
539                self.width().abs().min(self.height().abs())
540            }
541
542            /// The greater of the magnitudes of the width and the height.
543            pub fn longest_side(self) -> f64 {
544                self.width().abs().max(self.height().abs())
545            }
546
547            /// Whether any of the dimensions are `NaN`.
548            pub fn has_nan(self) -> bool {
549                self.left.is_nan()
550                    || self.top.is_nan()
551                    || self.right.is_nan()
552                    || self.bottom.is_nan()
553                    || self.tr_radius_x.is_nan()
554                    || self.tr_radius_y.is_nan()
555                    || self.tl_radius_x.is_nan()
556                    || self.tl_radius_y.is_nan()
557                    || self.br_radius_x.is_nan()
558                    || self.br_radius_y.is_nan()
559                    || self.bl_radius_x.is_nan()
560                    || self.bl_radius_y.is_nan()
561            }
562
563            /// The offset halfway between the left and right and the top and
564            /// bottom edges.
565            pub fn center(self) -> Offset {
566                Offset::new(
567                    self.left + self.width() / 2.0,
568                    self.top + self.height() / 2.0,
569                )
570            }
571
572            /// Scales all radii so that on each side their sum will not exceed
573            /// the width or height.
574            pub fn scale_radii(self) -> $name {
575                $name::from_inner(self.inner().scale_radii())
576            }
577
578            fn lerp_to(self, b: Option<$name>, t: f64) -> $name {
579                $name::from_inner(self.inner().lerp_to(b.map($name::inner), t))
580            }
581
582            /// Linearly interpolate between two rounded rectangles.
583            ///
584            /// If either is null, this function substitutes [`ZERO`](Self::ZERO).
585            pub fn lerp(a: Option<$name>, b: Option<$name>, t: f64) -> Option<$name> {
586                match a {
587                    None => b.map(|b| b.lerp_to(None, 1.0 - t)),
588                    Some(a) => Some(a.lerp_to(b, t)),
589                }
590            }
591        }
592    };
593}
594
595/// An immutable rounded rectangle with the custom radii for all four corners.
596#[derive(Clone, Copy, PartialEq)]
597pub struct RRect {
598    /// The offset of the left edge of this rectangle from the x axis.
599    pub left: f64,
600    /// The offset of the top edge of this rectangle from the y axis.
601    pub top: f64,
602    /// The offset of the right edge of this rectangle from the x axis.
603    pub right: f64,
604    /// The offset of the bottom edge of this rectangle from the y axis.
605    pub bottom: f64,
606    /// The top-left horizontal radius.
607    pub tl_radius_x: f64,
608    /// The top-left vertical radius.
609    pub tl_radius_y: f64,
610    /// The top-right horizontal radius.
611    pub tr_radius_x: f64,
612    /// The top-right vertical radius.
613    pub tr_radius_y: f64,
614    /// The bottom-right horizontal radius.
615    pub br_radius_x: f64,
616    /// The bottom-right vertical radius.
617    pub br_radius_y: f64,
618    /// The bottom-left horizontal radius.
619    pub bl_radius_x: f64,
620    /// The bottom-left vertical radius.
621    pub bl_radius_y: f64,
622}
623
624rounded_type!(RRect);
625
626impl RRect {
627    /// Whether the point specified by the given offset (which is assumed to be
628    /// relative to the origin) lies inside the rounded rectangle.
629    pub fn contains(self, point: Offset) -> bool {
630        if point.dx() < self.left
631            || point.dx() >= self.right
632            || point.dy() < self.top
633            || point.dy() >= self.bottom
634        {
635            return false;
636        }
637        let scaled = self.scale_radii();
638        let (x, y, radius_x, radius_y);
639        if point.dx() < scaled.left + scaled.tl_radius_x
640            && point.dy() < scaled.top + scaled.tl_radius_y
641        {
642            x = point.dx() - scaled.left - scaled.tl_radius_x;
643            y = point.dy() - scaled.top - scaled.tl_radius_y;
644            radius_x = scaled.tl_radius_x;
645            radius_y = scaled.tl_radius_y;
646        } else if point.dx() > scaled.right - scaled.tr_radius_x
647            && point.dy() < scaled.top + scaled.tr_radius_y
648        {
649            x = point.dx() - scaled.right + scaled.tr_radius_x;
650            y = point.dy() - scaled.top - scaled.tr_radius_y;
651            radius_x = scaled.tr_radius_x;
652            radius_y = scaled.tr_radius_y;
653        } else if point.dx() > scaled.right - scaled.br_radius_x
654            && point.dy() > scaled.bottom - scaled.br_radius_y
655        {
656            x = point.dx() - scaled.right + scaled.br_radius_x;
657            y = point.dy() - scaled.bottom + scaled.br_radius_y;
658            radius_x = scaled.br_radius_x;
659            radius_y = scaled.br_radius_y;
660        } else if point.dx() < scaled.left + scaled.bl_radius_x
661            && point.dy() > scaled.bottom - scaled.bl_radius_y
662        {
663            x = point.dx() - scaled.left - scaled.bl_radius_x;
664            y = point.dy() - scaled.bottom + scaled.bl_radius_y;
665            radius_x = scaled.bl_radius_x;
666            radius_y = scaled.bl_radius_y;
667        } else {
668            return true;
669        }
670        let x = x / radius_x;
671        let y = y / radius_y;
672        x * x + y * y <= 1.0
673    }
674}
675
676impl Debug for RRect {
677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678        self.inner().fmt_debug("RRect", f)
679    }
680}
681
682/// An immutable rounded superellipse.
683///
684/// A rounded superellipse (not to be confused with a standard superellipse) is
685/// a shape formed by replacing the four curved corners of a superellipse with
686/// circular arcs.
687#[derive(Clone, Copy, PartialEq)]
688pub struct RSuperellipse {
689    /// The offset of the left edge of this rectangle from the x axis.
690    pub left: f64,
691    /// The offset of the top edge of this rectangle from the y axis.
692    pub top: f64,
693    /// The offset of the right edge of this rectangle from the x axis.
694    pub right: f64,
695    /// The offset of the bottom edge of this rectangle from the y axis.
696    pub bottom: f64,
697    /// The top-left horizontal radius.
698    pub tl_radius_x: f64,
699    /// The top-left vertical radius.
700    pub tl_radius_y: f64,
701    /// The top-right horizontal radius.
702    pub tr_radius_x: f64,
703    /// The top-right vertical radius.
704    pub tr_radius_y: f64,
705    /// The bottom-right horizontal radius.
706    pub br_radius_x: f64,
707    /// The bottom-right vertical radius.
708    pub br_radius_y: f64,
709    /// The bottom-left horizontal radius.
710    pub bl_radius_x: f64,
711    /// The bottom-left vertical radius.
712    pub bl_radius_y: f64,
713}
714
715rounded_type!(RSuperellipse);
716
717impl RSuperellipse {
718    /// Whether the point specified by the given offset (which is assumed to be
719    /// relative to the origin) lies inside the rounded superellipse.
720    ///
721    /// Dart's method is engine FFI (Impeller). Ours uses valo's Impeller-port
722    /// [`valo_geometry::RoundSuperellipse::contains`].
723    pub fn contains(self, point: Offset) -> bool {
724        valo_geometry::RoundSuperellipse::new(
725            self.outer_rect().into(),
726            super::rsuperellipse_radii_elliptical(self),
727        )
728        .contains(point.into())
729    }
730}
731
732impl Debug for RSuperellipse {
733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734        self.inner().fmt_debug("RSuperellipse", f)
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    // engine geometry_test.dart 'RRect.fromRectXY'.
743    #[test]
744    fn from_rect_xy() {
745        let base_rect = Rect::from_ltwh(1.0, 3.0, 5.0, 7.0);
746        let r = RRect::from_rect_xy(base_rect, 1.0, 1.0);
747        assert_eq!(r.left, 1.0);
748        assert_eq!(r.top, 3.0);
749        assert_eq!(r.right, 6.0);
750        assert_eq!(r.bottom, 10.0);
751        assert_eq!(r.shortest_side(), 5.0);
752        assert_eq!(r.longest_side(), 7.0);
753    }
754
755    fn sample_rrect() -> RRect {
756        RRect::from_rect_and_corners(
757            Rect::from_ltrb(1.0, 1.0, 2.0, 2.0),
758            Radius::circular(0.5),
759            Radius::circular(0.25),
760            Radius::elliptical(0.25, 0.75),
761            Radius::ZERO,
762        )
763    }
764
765    // engine geometry_test.dart 'RRect.contains()'.
766    #[test]
767    fn contains_points() {
768        let rrect = sample_rrect();
769        assert!(!rrect.contains(Offset::new(1.0, 1.0)));
770        assert!(!rrect.contains(Offset::new(1.1, 1.1)));
771        assert!(rrect.contains(Offset::new(1.15, 1.15)));
772        assert!(!rrect.contains(Offset::new(2.0, 1.0)));
773        assert!(!rrect.contains(Offset::new(1.93, 1.07)));
774        assert!(!rrect.contains(Offset::new(1.97, 1.7)));
775        assert!(rrect.contains(Offset::new(1.7, 1.97)));
776        assert!(rrect.contains(Offset::new(1.0, 1.99)));
777    }
778
779    // engine geometry_test.dart 'RRect.contains() large radii'.
780    #[test]
781    fn contains_large_radii() {
782        let rrect = RRect::from_rect_and_corners(
783            Rect::from_ltrb(1.0, 1.0, 2.0, 2.0),
784            Radius::circular(5000.0),
785            Radius::circular(2500.0),
786            Radius::elliptical(2500.0, 7500.0),
787            Radius::ZERO,
788        );
789        assert!(!rrect.contains(Offset::new(1.0, 1.0)));
790        assert!(!rrect.contains(Offset::new(1.1, 1.1)));
791        assert!(rrect.contains(Offset::new(1.15, 1.15)));
792        assert!(!rrect.contains(Offset::new(2.0, 1.0)));
793        assert!(!rrect.contains(Offset::new(1.93, 1.07)));
794        assert!(!rrect.contains(Offset::new(1.97, 1.7)));
795        assert!(rrect.contains(Offset::new(1.7, 1.97)));
796        assert!(rrect.contains(Offset::new(1.0, 1.99)));
797    }
798
799    fn assert_scaled_corners(rrect: RRect) {
800        assert_eq!(rrect.left, 1.0);
801        assert_eq!(rrect.top, 1.0);
802        assert_eq!(rrect.right, 2.0);
803        assert_eq!(rrect.bottom, 2.0);
804        assert_eq!(rrect.tl_radius_x, 0.5);
805        assert_eq!(rrect.tl_radius_y, 0.5);
806        assert_eq!(rrect.tr_radius_x, 0.25);
807        assert_eq!(rrect.tr_radius_y, 0.25);
808        assert_eq!(rrect.bl_radius_x, 0.0);
809        assert_eq!(rrect.bl_radius_y, 0.0);
810        assert_eq!(rrect.br_radius_x, 0.25);
811        assert_eq!(rrect.br_radius_y, 0.75);
812    }
813
814    // engine geometry_test.dart 'RRect.scaleRadii() properly constrained radii should remain unchanged'.
815    #[test]
816    fn scale_radii_leaves_valid_radii_unchanged() {
817        assert_scaled_corners(sample_rrect().scale_radii());
818    }
819
820    // engine geometry_test.dart 'RRect.scaleRadii() sum of radii that exceed side length should properly scale'.
821    #[test]
822    fn scale_radii_when_sum_exceeds_side() {
823        let rrect = RRect::from_rect_and_corners(
824            Rect::from_ltrb(1.0, 1.0, 2.0, 2.0),
825            Radius::circular(5000.0),
826            Radius::circular(2500.0),
827            Radius::elliptical(2500.0, 7500.0),
828            Radius::ZERO,
829        )
830        .scale_radii();
831        assert_scaled_corners(rrect);
832    }
833
834    // engine geometry_test.dart 'RRect asserts when corner radii are negative'.
835    #[test]
836    #[cfg(debug_assertions)]
837    #[should_panic]
838    fn negative_corner_radii_panic_in_debug() {
839        let _ = RRect::from_rect_and_corners(
840            Rect::from_ltrb(10.0, 20.0, 30.0, 40.0),
841            Radius::circular(-1.0),
842            Radius::ZERO,
843            Radius::ZERO,
844            Radius::ZERO,
845        );
846    }
847
848    // engine geometry_test.dart 'RRect.inflate clamps when deflating past zero'.
849    #[test]
850    fn inflate_clamps_when_deflating_past_zero() {
851        let mut rrect = RRect::from_rect_and_corners(
852            Rect::from_ltrb(10.0, 20.0, 30.0, 40.0),
853            Radius::circular(1.0),
854            Radius::circular(2.0),
855            Radius::circular(4.0),
856            Radius::circular(3.0),
857        )
858        .inflate(-1.0);
859        assert_eq!(rrect.tl_radius_x, 0.0);
860        assert_eq!(rrect.tl_radius_y, 0.0);
861        assert_eq!(rrect.tr_radius_x, 1.0);
862        assert_eq!(rrect.tr_radius_y, 1.0);
863        assert_eq!(rrect.bl_radius_x, 2.0);
864        assert_eq!(rrect.bl_radius_y, 2.0);
865        assert_eq!(rrect.br_radius_x, 3.0);
866        assert_eq!(rrect.br_radius_y, 3.0);
867
868        rrect = rrect.inflate(-1.0);
869        assert_eq!(rrect.tl_radius_x, 0.0);
870        assert_eq!(rrect.tr_radius_x, 0.0);
871        assert_eq!(rrect.bl_radius_x, 1.0);
872        assert_eq!(rrect.br_radius_x, 2.0);
873
874        rrect = rrect.inflate(-1.0);
875        assert_eq!(rrect.bl_radius_x, 0.0);
876        assert_eq!(rrect.br_radius_x, 1.0);
877
878        rrect = rrect.inflate(-1.0);
879        assert_eq!(rrect.br_radius_x, 0.0);
880        assert_eq!(rrect.br_radius_y, 0.0);
881    }
882
883    #[test]
884    fn rrect_and_rsuperellipse_are_unequal_at_the_same_floats() {
885        let rect = Rect::from_ltrb(0.0, 0.0, 10.0, 10.0);
886        let rrect = RRect::from_rect_and_radius(rect, Radius::circular(2.0));
887        let rse = RSuperellipse::from_rect_and_radius(rect, Radius::circular(2.0));
888        assert_eq!(rrect.left, rse.left);
889        assert!(format!("{rrect:?}").starts_with("RRect"));
890        assert!(format!("{rse:?}").starts_with("RSuperellipse"));
891    }
892
893    #[test]
894    fn rsuperellipse_contains_the_center_and_rejects_the_outside() {
895        let rse = RSuperellipse::from_rect_and_radius(
896            Rect::from_ltrb(0.0, 0.0, 10.0, 10.0),
897            Radius::circular(2.0),
898        );
899        assert!(rse.contains(Offset::new(5.0, 5.0)));
900        assert!(!rse.contains(Offset::new(-1.0, 5.0)));
901    }
902}