Skip to main content

ferroui_base/media/
geometry_builder.rs

1// Portions of this source file are adapted from the Windows Presentation Foundation (WPF) project
2// (https://github.com/dotnet/wpf) and from the WinUI project
3// (https://github.com/microsoft/microsoft-ui-xaml/tree/winui3/main), both under the MIT License.
4
5use crate::media::{BackgroundSizing, StreamGeometryContext, SweepDirection};
6use crate::platform::IGeometryContext;
7use crate::utilities::{math_utilities, MathUtilities};
8use crate::{CornerRadius, Point, Rect, RoundedRect, Size, Thickness, Vector};
9
10/// Same as the layout epsilon of the layout helper.
11const EPSILON: f64 = 0.00000153;
12
13/// Contains internal helpers used to build and draw various geometries.
14pub struct GeometryBuilder;
15
16impl GeometryBuilder {
17    /// Draws a new rounded rectangle within the given geometry context.
18    /// Warning: The caller must manage and dispose the [`StreamGeometryContext`] externally.
19    ///
20    /// WinUI: https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L1072-L1079
21    ///
22    /// * `context` - The geometry context to draw into.
23    /// * `keypoints` - The rounded rectangle keypoints defining the rectangle to draw.
24    pub fn draw_rounded_corners_rectangle_keypoints(
25        context: &mut StreamGeometryContext,
26        keypoints: &RoundedRectKeypoints,
27    ) {
28        let mut radius_x;
29        let mut radius_y;
30
31        context.begin_figure(keypoints.top_left, true);
32
33        // Top
34        context.line_to(keypoints.top_right, true);
35
36        // TopRight corner
37        radius_x = keypoints.right_top.x - keypoints.top_right.x;
38        radius_y = keypoints.top_right.y - keypoints.right_top.y;
39        radius_x = if radius_x > 0.0 { radius_x } else { -radius_x };
40        radius_y = if radius_y > 0.0 { radius_y } else { -radius_y };
41
42        context.arc_to(
43            keypoints.right_top,
44            Size::new(radius_x, radius_y),
45            0.0,
46            false,
47            SweepDirection::Clockwise,
48            true,
49        );
50
51        // Right
52        context.line_to(keypoints.right_bottom, true);
53
54        // BottomRight corner
55        radius_x = keypoints.right_bottom.x - keypoints.bottom_right.x;
56        radius_y = keypoints.bottom_right.y - keypoints.right_bottom.y;
57        radius_x = if radius_x > 0.0 { radius_x } else { -radius_x };
58        radius_y = if radius_y > 0.0 { radius_y } else { -radius_y };
59
60        if radius_x != 0.0 || radius_y != 0.0 {
61            context.arc_to(
62                keypoints.bottom_right,
63                Size::new(radius_x, radius_y),
64                0.0,
65                false,
66                SweepDirection::Clockwise,
67                true,
68            );
69        }
70
71        // Bottom
72        context.line_to(keypoints.bottom_left, true);
73
74        // BottomLeft corner
75        radius_x = keypoints.bottom_left.x - keypoints.left_bottom.x;
76        radius_y = keypoints.bottom_left.y - keypoints.left_bottom.y;
77        radius_x = if radius_x > 0.0 { radius_x } else { -radius_x };
78        radius_y = if radius_y > 0.0 { radius_y } else { -radius_y };
79
80        if radius_x != 0.0 || radius_y != 0.0 {
81            context.arc_to(
82                keypoints.left_bottom,
83                Size::new(radius_x, radius_y),
84                0.0,
85                false,
86                SweepDirection::Clockwise,
87                true,
88            );
89        }
90
91        // Left
92        context.line_to(keypoints.left_top, true);
93
94        // TopLeft corner
95        radius_x = keypoints.top_left.x - keypoints.left_top.x;
96        radius_y = keypoints.top_left.y - keypoints.left_top.y;
97        radius_x = if radius_x > 0.0 { radius_x } else { -radius_x };
98        radius_y = if radius_y > 0.0 { radius_y } else { -radius_y };
99
100        if radius_x != 0.0 || radius_y != 0.0 {
101            context.arc_to(
102                keypoints.top_left,
103                Size::new(radius_x, radius_y),
104                0.0,
105                false,
106                SweepDirection::Clockwise,
107                true,
108            );
109        }
110
111        context.end_figure(true);
112    }
113
114    /// Draws a new rounded rectangle within the given geometry context.
115    /// Warning: The caller must manage and dispose the [`StreamGeometryContext`] externally.
116    ///
117    /// * `context` - The geometry context to draw into.
118    /// * `rect` - The existing rectangle dimensions without corner radii.
119    /// * `radius_x` - The radius on the X-axis used to round the corners of the rectangle.
120    /// * `radius_y` - The radius on the Y-axis used to round the corners of the rectangle.
121    pub fn draw_rounded_corners_rectangle(context: &mut dyn IGeometryContext, rect: Rect, radius_x: f64, radius_y: f64) {
122        let arc_size = Size::new(radius_x, radius_y);
123
124        // The rectangle is constructed as follows:
125        //
126        //   (origin)
127        //   Corner 4            Corner 1
128        //   Top/Left  Line 1    Top/Right
129        //      \_   __________   _/
130        //          |          |
131        //   Line 4 |          | Line 2
132        //       _  |__________|  _
133        //      /      Line 3      \
134        //   Corner 3            Corner 2
135        //   Bottom/Left         Bottom/Right
136        //
137        // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX
138        // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY
139        // - All corners are constructed using elliptical arcs
140
141        context.begin_figure(Point::new(rect.left() + radius_x, rect.top()), true);
142
143        // Line 1 + Corner 1
144        context.line_to(Point::new(rect.right() - radius_x, rect.top()), true);
145        context.arc_to(
146            Point::new(rect.right(), rect.top() + radius_y),
147            arc_size,
148            0.0,
149            false,
150            SweepDirection::Clockwise,
151            true,
152        );
153
154        // Line 2 + Corner 2
155        context.line_to(Point::new(rect.right(), rect.bottom() - radius_y), true);
156        context.arc_to(
157            Point::new(rect.right() - radius_x, rect.bottom()),
158            arc_size,
159            0.0,
160            false,
161            SweepDirection::Clockwise,
162            true,
163        );
164
165        // Line 3 + Corner 3
166        context.line_to(Point::new(rect.left() + radius_x, rect.bottom()), true);
167        context.arc_to(
168            Point::new(rect.left(), rect.bottom() - radius_y),
169            arc_size,
170            0.0,
171            false,
172            SweepDirection::Clockwise,
173            true,
174        );
175
176        // Line 4 + Corner 4
177        context.line_to(Point::new(rect.left(), rect.top() + radius_y), true);
178        context.arc_to(
179            Point::new(rect.left() + radius_x, rect.top()),
180            arc_size,
181            0.0,
182            false,
183            SweepDirection::Clockwise,
184            true,
185        );
186
187        context.end_figure(true);
188    }
189
190    /// Calculates the keypoints of a rounded rectangle based on the algorithm in WinUI.
191    /// These keypoints may then be drawn or transformed into other types.
192    ///
193    /// * `outer_bounds` - The outer bounds of the rounded rectangle.
194    ///   This should be the overall bounds and size of the shape/control without any
195    ///   corner radii or border thickness adjustments.
196    /// * `border_thickness` - The unadjusted border thickness of the rounded rectangle.
197    /// * `corner_radius` - The unadjusted corner radii of the rounded rectangle.
198    ///   The corner radius is defined to be the middle of the border stroke (center of the border).
199    /// * `sizing` - The sizing mode used to calculate the final rounded rectangle size.
200    ///
201    /// Returns new rounded rectangle keypoints.
202    pub fn calculate_rounded_corners_rectangle_win_ui(
203        outer_bounds: Rect,
204        border_thickness: Thickness,
205        corner_radius: CornerRadius,
206        sizing: BackgroundSizing,
207    ) -> RoundedRectKeypoints {
208        // This was initially derived from WinUI:
209        //  - CGeometryBuilder::CalculateRoundedCornersRectangle
210        //    https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L862-L869
211        //
212        // It has been modified to accept a BackgroundSizing parameter directly as well
213        // as to support BackgroundSizing::CenterBorder.
214        //
215        // Keep in mind:
216        //   > In Xaml, the corner radius is defined to be the middle of the stroke
217        //   > (i.e. half the border thickness extends to either side).
218
219        let f_outer;
220        let mut bound_rect = outer_bounds;
221
222        if sizing == BackgroundSizing::InnerBorderEdge {
223            bound_rect = outer_bounds.deflate_thickness(border_thickness);
224            f_outer = false;
225        } else if sizing == BackgroundSizing::OuterBorderEdge {
226            f_outer = true;
227        } else {
228            // CenterBorder
229            // This is a trick to support a 3rd state (CenterBorder) using the same WinUI-based algorithm.
230            // The WinUI algorithm only supports the fOuter = True|False parameter.
231            bound_rect = outer_bounds.deflate_thickness(border_thickness * 0.5);
232            f_outer = false;
233        }
234
235        // Start of WinUI converted code
236        // Doubles are used for calculation so multiple Point structs aren't
237        // required during calculations -- everything can be done with these double variables.
238        let f_left_top;
239        let f_left_bottom;
240        let f_top_left;
241        let f_top_right;
242        let f_right_top;
243        let f_right_bottom;
244        let f_bottom_left;
245        let f_bottom_right;
246
247        let left;
248        let right;
249        let top;
250        let bottom;
251
252        // If the caller wants to take the border into account
253        // initialize the borders variables
254        if border_thickness != Thickness::default() {
255            left = 0.5 * border_thickness.left;
256            right = 0.5 * border_thickness.right;
257            top = 0.5 * border_thickness.top;
258            bottom = 0.5 * border_thickness.bottom;
259        } else {
260            left = 0.0;
261            right = 0.0;
262            top = 0.0;
263            bottom = 0.0;
264        }
265
266        // The following if/else block initializes the variables
267        // of which the points of the path will be created
268        // In case of outer, add the border - if any.
269        // Otherwise (inner rectangle) subtract the border - if any
270        if f_outer {
271            if MathUtilities::are_close_eps(corner_radius.top_left, 0.0, EPSILON) {
272                f_left_top = 0.0;
273                f_top_left = 0.0;
274            } else {
275                f_left_top = corner_radius.top_left + left;
276                f_top_left = corner_radius.top_left + top;
277            }
278
279            if MathUtilities::are_close_eps(corner_radius.top_right, 0.0, EPSILON) {
280                f_top_right = 0.0;
281                f_right_top = 0.0;
282            } else {
283                f_top_right = corner_radius.top_right + top;
284                f_right_top = corner_radius.top_right + right;
285            }
286
287            if MathUtilities::are_close_eps(corner_radius.bottom_right, 0.0, EPSILON) {
288                f_right_bottom = 0.0;
289                f_bottom_right = 0.0;
290            } else {
291                f_right_bottom = corner_radius.bottom_right + right;
292                f_bottom_right = corner_radius.bottom_right + bottom;
293            }
294
295            if MathUtilities::are_close_eps(corner_radius.bottom_left, 0.0, EPSILON) {
296                f_bottom_left = 0.0;
297                f_left_bottom = 0.0;
298            } else {
299                f_bottom_left = corner_radius.bottom_left + bottom;
300                f_left_bottom = corner_radius.bottom_left + left;
301            }
302        } else {
303            f_left_top = math_utilities::max(0.0, corner_radius.top_left - left);
304            f_top_left = math_utilities::max(0.0, corner_radius.top_left - top);
305            f_top_right = math_utilities::max(0.0, corner_radius.top_right - top);
306            f_right_top = math_utilities::max(0.0, corner_radius.top_right - right);
307            f_right_bottom = math_utilities::max(0.0, corner_radius.bottom_right - right);
308            f_bottom_right = math_utilities::max(0.0, corner_radius.bottom_right - bottom);
309            f_bottom_left = math_utilities::max(0.0, corner_radius.bottom_left - bottom);
310            f_left_bottom = math_utilities::max(0.0, corner_radius.bottom_left - left);
311        }
312
313        let mut top_left_x = f_left_top;
314        let top_left_y = 0.0;
315
316        let mut top_right_x = bound_rect.width - f_right_top;
317        let top_right_y = 0.0;
318
319        let right_top_x = bound_rect.width;
320        let mut right_top_y = f_top_right;
321
322        let right_bottom_x = bound_rect.width;
323        let mut right_bottom_y = bound_rect.height - f_bottom_right;
324
325        let mut bottom_right_x = bound_rect.width - f_right_bottom;
326        let bottom_right_y = bound_rect.height;
327
328        let mut bottom_left_x = f_left_bottom;
329        let bottom_left_y = bound_rect.height;
330
331        let left_bottom_x = 0.0;
332        let mut left_bottom_y = bound_rect.height - f_bottom_left;
333
334        let left_top_x = 0.0;
335        let mut left_top_y = f_top_left;
336
337        // check keypoints for overlap and resolve by partitioning radii according to
338        // the percentage of each one.
339
340        // top edge
341        if top_left_x > top_right_x {
342            let v = (f_left_top) / (f_left_top + f_right_top) * bound_rect.width;
343            top_left_x = v;
344            top_right_x = v;
345        }
346        // right edge
347        if right_top_y > right_bottom_y {
348            let v = (f_top_right) / (f_top_right + f_bottom_right) * bound_rect.height;
349            right_top_y = v;
350            right_bottom_y = v;
351        }
352        // bottom edge
353        if bottom_right_x < bottom_left_x {
354            let v = (f_left_bottom) / (f_left_bottom + f_right_bottom) * bound_rect.width;
355            bottom_right_x = v;
356            bottom_left_x = v;
357        }
358        // left edge
359        if left_bottom_y < left_top_y {
360            let v = (f_top_left) / (f_top_left + f_bottom_left) * bound_rect.height;
361            left_bottom_y = v;
362            left_top_y = v;
363        }
364
365        // The above code does all calculations without taking into consideration X/Y absolute position.
366        // In WinUI, this is compensated for in DrawRoundedCornersRectangle(); however, we do this here directly
367        // when the final keypoints are being created.
368        let mut keypoints = RoundedRectKeypoints::new();
369        keypoints.top_left = Point::new(bound_rect.x + top_left_x, bound_rect.y + top_left_y);
370        keypoints.top_right = Point::new(bound_rect.x + top_right_x, bound_rect.y + top_right_y);
371
372        keypoints.right_top = Point::new(bound_rect.x + right_top_x, bound_rect.y + right_top_y);
373        keypoints.right_bottom = Point::new(bound_rect.x + right_bottom_x, bound_rect.y + right_bottom_y);
374
375        keypoints.bottom_right = Point::new(bound_rect.x + bottom_right_x, bound_rect.y + bottom_right_y);
376        keypoints.bottom_left = Point::new(bound_rect.x + bottom_left_x, bound_rect.y + bottom_left_y);
377
378        keypoints.left_bottom = Point::new(bound_rect.x + left_bottom_x, bound_rect.y + left_bottom_y);
379        keypoints.left_top = Point::new(bound_rect.x + left_top_x, bound_rect.y + left_top_y);
380
381        keypoints
382    }
383}
384
385/// Represents the keypoints of a rounded rectangle.
386/// These keypoints can be shared between methods and turned into geometry.
387///
388/// A rounded rectangle is the base geometric shape used when drawing borders.
389/// It is a superset of a simple rectangle (which has corner radii set to zero).
390/// These keypoints can be combined together to produce geometries for both background
391/// and border elements.
392//
393// The following keypoints are defined for a rounded rectangle:
394//
395//       TopLeft                                  TopRight
396//              *--------------------------------*
397// (start)     /                                  \
398//    LeftTop *                                    * RightTop
399//            |                                    |
400//            |                                    |
401// LeftBottom *                                    * RightBottom
402//             \                                  /
403//              *--------------------------------*
404//    BottomLeft                                  BottomRight
405//
406// Or, for a simple rectangle without corner radii:
407//
408//    TopLeft = LeftTop                   TopRight = RightTop
409//  (start)   *------------------------------------*
410//            |                                    |
411//            |                                    |
412//            *------------------------------------*
413// BottomLeft = LeftBottom             BottomRight = RightBottom
414#[derive(Clone, Copy, Debug, PartialEq, Default)]
415pub struct RoundedRectKeypoints {
416    /// The topmost point in the left line segment of the rectangle.
417    pub left_top: Point,
418    /// The leftmost point in the top line segment of the rectangle.
419    pub top_left: Point,
420    /// The rightmost point in the top line segment of the rectangle.
421    pub top_right: Point,
422    /// The topmost point in the right line segment of the rectangle.
423    pub right_top: Point,
424    /// The bottommost point in the right line segment of the rectangle.
425    pub right_bottom: Point,
426    /// The rightmost point in the bottom line segment of the rectangle.
427    pub bottom_right: Point,
428    /// The leftmost point in the bottom line segment of the rectangle.
429    pub bottom_left: Point,
430    /// The bottommost point in the left line segment of the rectangle.
431    pub left_bottom: Point,
432}
433
434impl RoundedRectKeypoints {
435    /// Initializes a new instance of the [`RoundedRectKeypoints`] struct.
436    pub fn new() -> Self {
437        Self::default()
438    }
439
440    /// Initializes a new instance of the [`RoundedRectKeypoints`] struct.
441    ///
442    /// * `rounded_rect` - An existing [`RoundedRect`] to initialize keypoints with.
443    pub fn from_rounded_rect(rounded_rect: RoundedRect) -> Self {
444        let rect = rounded_rect.rect;
445        Self {
446            left_top: Point::new(rect.top_left().x, rect.top_left().y + rounded_rect.radii_top_left.y),
447            top_left: Point::new(rect.top_left().x + rounded_rect.radii_top_left.x, rect.top_left().y),
448            top_right: Point::new(rect.top_right().x - rounded_rect.radii_top_right.x, rect.top_right().y),
449            right_top: Point::new(rect.top_right().x, rect.top_right().y + rounded_rect.radii_top_right.y),
450            right_bottom: Point::new(
451                rect.bottom_right().x,
452                rect.bottom_right().y - rounded_rect.radii_bottom_right.y,
453            ),
454            bottom_right: Point::new(
455                rect.bottom_right().x - rounded_rect.radii_bottom_right.x,
456                rect.bottom_right().y,
457            ),
458            bottom_left: Point::new(rect.bottom_left().x + rounded_rect.radii_bottom_left.x, rect.bottom_left().y),
459            left_bottom: Point::new(rect.bottom_left().x, rect.bottom_right().y - rounded_rect.radii_bottom_left.y),
460        }
461    }
462
463    /// Gets a value indicating whether the rounded rectangle is actually rounded on
464    /// any corner. If false the key points represent a simple rectangle.
465    pub fn is_rounded(&self) -> bool {
466        self.top_left != self.left_top
467            || self.top_right != self.right_top
468            || self.bottom_left != self.left_bottom
469            || self.bottom_right != self.right_bottom
470    }
471
472    /// Converts the keypoints into a simple rectangle (with no corners).
473    /// This is equivalent to the outer rectangle with zero corner radii.
474    ///
475    /// Warning: This will force the keypoints into a simple rectangle without
476    /// any rounded corners. Use [`is_rounded`](Self::is_rounded) to determine if corner
477    /// information is otherwise available.
478    pub fn to_rect(&self) -> Rect {
479        Rect::from_points(
480            Point::new(self.left_top.x, self.top_left.y),
481            Point::new(self.right_bottom.x, self.bottom_right.y),
482        )
483    }
484
485    /// Converts the keypoints into a rounded rectangle with elliptical corner radii.
486    ///
487    /// Elliptical corner radius (represented by [`Vector`]) is more powerful
488    /// than circular corner radius (represented by a [`CornerRadius`]).
489    /// Elliptical is a superset of circular.
490    pub fn to_rounded_rect(&self) -> RoundedRect {
491        RoundedRect::new(
492            self.to_rect(),
493            Vector::new(self.top_left.x - self.left_top.x, self.left_top.y - self.top_left.y),
494            Vector::new(self.right_top.x - self.top_right.x, self.right_top.y - self.top_right.y),
495            Vector::new(self.right_bottom.x - self.bottom_right.x, self.bottom_right.y - self.right_bottom.y),
496            Vector::new(self.bottom_left.x - self.left_bottom.x, self.bottom_left.y - self.left_bottom.y),
497        )
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use crate::media::FillRule;
505    use std::cell::RefCell;
506    use std::rc::Rc;
507
508    fn inner_border_edge_borders_larger_than_corners(uniform_borders: f64, uniform_corners: f64) {
509        let bounds = Rect::from_size(Size::new(100.0, 100.0));
510        let border_thickness = Thickness::uniform(uniform_borders);
511        let corner_radius = CornerRadius::uniform(uniform_corners);
512
513        let points = GeometryBuilder::calculate_rounded_corners_rectangle_win_ui(
514            bounds,
515            border_thickness,
516            corner_radius,
517            BackgroundSizing::InnerBorderEdge,
518        );
519
520        assert_eq!(Point::new(uniform_borders, uniform_borders), points.left_top);
521        assert_eq!(Point::new(uniform_borders, uniform_borders), points.top_left);
522        assert_eq!(Point::new(100.0 - uniform_borders, uniform_borders), points.top_right);
523        assert_eq!(Point::new(100.0 - uniform_borders, uniform_borders), points.right_top);
524        assert_eq!(Point::new(100.0 - uniform_borders, 100.0 - uniform_borders), points.right_bottom);
525        assert_eq!(Point::new(100.0 - uniform_borders, 100.0 - uniform_borders), points.bottom_right);
526        assert_eq!(Point::new(uniform_borders, 100.0 - uniform_borders), points.bottom_left);
527        assert_eq!(Point::new(uniform_borders, 100.0 - uniform_borders), points.left_bottom);
528
529        assert!(!points.is_rounded());
530    }
531
532    #[test]
533    fn calculate_rounded_corners_rectangle_win_ui_inner_border_edge_borders_larger_than_corners_test() {
534        for (uniform_borders, uniform_corners) in [(20.0, 10.0), (10.0, 5.0), (2.0, 1.0), (1.0, 0.0)] {
535            inner_border_edge_borders_larger_than_corners(uniform_borders, uniform_corners);
536        }
537    }
538
539    fn outer_border_edge_borders_larger_than_corners(uniform_borders: f64, uniform_corners: f64) {
540        let bounds = Rect::from_size(Size::new(100.0, 100.0));
541        let border_thickness = Thickness::uniform(uniform_borders);
542        let corner_radius = CornerRadius::uniform(uniform_corners);
543
544        let points = GeometryBuilder::calculate_rounded_corners_rectangle_win_ui(
545            bounds,
546            border_thickness,
547            corner_radius,
548            BackgroundSizing::OuterBorderEdge,
549        );
550
551        assert_eq!(Point::new(0.0, uniform_borders), points.left_top);
552        assert_eq!(Point::new(uniform_borders, 0.0), points.top_left);
553        assert_eq!(Point::new(100.0 - uniform_borders, 0.0), points.top_right);
554        assert_eq!(Point::new(100.0, uniform_borders), points.right_top);
555        assert_eq!(Point::new(100.0, 100.0 - uniform_borders), points.right_bottom);
556        assert_eq!(Point::new(100.0 - uniform_borders, 100.0), points.bottom_right);
557        assert_eq!(Point::new(uniform_borders, 100.0), points.bottom_left);
558        assert_eq!(Point::new(0.0, 100.0 - uniform_borders), points.left_bottom);
559
560        assert!(points.is_rounded());
561    }
562
563    #[test]
564    fn calculate_rounded_corners_rectangle_win_ui_outer_border_edge_borders_larger_than_corners_test() {
565        for (uniform_borders, uniform_corners) in [(20.0, 10.0), (10.0, 5.0), (2.0, 1.0)] {
566            outer_border_edge_borders_larger_than_corners(uniform_borders, uniform_corners);
567        }
568    }
569
570    // --- The tests below are not upstream tests; expected values are derived by hand from the
571    // --- upstream algorithm.
572
573    #[derive(Default)]
574    struct Recorder {
575        log: Rc<RefCell<Vec<String>>>,
576    }
577
578    impl IGeometryContext for Recorder {
579        fn arc_to(&mut self, point: Point, size: Size, angle: f64, large: bool, sweep: SweepDirection, stroked: bool) {
580            self.log.borrow_mut().push(format!("arc {point} {size} {angle} {large} {sweep:?} {stroked}"));
581        }
582        fn begin_figure(&mut self, start_point: Point, is_filled: bool) {
583            self.log.borrow_mut().push(format!("begin {start_point} {is_filled}"));
584        }
585        fn cubic_bezier_to(&mut self, p1: Point, p2: Point, p3: Point, stroked: bool) {
586            self.log.borrow_mut().push(format!("cubic {p1} {p2} {p3} {stroked}"));
587        }
588        fn quadratic_bezier_to(&mut self, p1: Point, p2: Point, stroked: bool) {
589            self.log.borrow_mut().push(format!("quad {p1} {p2} {stroked}"));
590        }
591        fn line_to(&mut self, point: Point, stroked: bool) {
592            self.log.borrow_mut().push(format!("line {point} {stroked}"));
593        }
594        fn end_figure(&mut self, is_closed: bool) {
595            self.log.borrow_mut().push(format!("end {is_closed}"));
596        }
597        fn set_fill_rule(&mut self, _: FillRule) {}
598        fn dispose(&mut self) {}
599    }
600
601    fn record(draw: impl FnOnce(&mut StreamGeometryContext)) -> Vec<String> {
602        let log = Rc::new(RefCell::new(Vec::new()));
603        let mut context = StreamGeometryContext::from_context(Box::new(Recorder { log: log.clone() }));
604        draw(&mut context);
605        drop(context);
606        let result = log.borrow().clone();
607        result
608    }
609
610    #[test]
611    fn keypoints_from_rounded_rect_round_trip() {
612        let rounded_rect = RoundedRect::new(
613            Rect::new(10.0, 20.0, 100.0, 50.0),
614            Vector::new(1.0, 2.0),
615            Vector::new(3.0, 4.0),
616            Vector::new(5.0, 6.0),
617            Vector::new(7.0, 8.0),
618        );
619
620        let keypoints = RoundedRectKeypoints::from_rounded_rect(rounded_rect);
621
622        assert_eq!(Point::new(10.0, 22.0), keypoints.left_top);
623        assert_eq!(Point::new(11.0, 20.0), keypoints.top_left);
624        assert_eq!(Point::new(107.0, 20.0), keypoints.top_right);
625        assert_eq!(Point::new(110.0, 24.0), keypoints.right_top);
626        assert_eq!(Point::new(110.0, 64.0), keypoints.right_bottom);
627        assert_eq!(Point::new(105.0, 70.0), keypoints.bottom_right);
628        assert_eq!(Point::new(17.0, 70.0), keypoints.bottom_left);
629        assert_eq!(Point::new(10.0, 62.0), keypoints.left_bottom);
630        assert!(keypoints.is_rounded());
631        assert_eq!(Rect::new(10.0, 20.0, 100.0, 50.0), keypoints.to_rect());
632        assert_eq!(rounded_rect, keypoints.to_rounded_rect());
633    }
634
635    #[test]
636    fn default_keypoints_are_not_rounded() {
637        let keypoints = RoundedRectKeypoints::new();
638        assert!(!keypoints.is_rounded());
639        assert_eq!(Rect::default(), keypoints.to_rect());
640    }
641
642    #[test]
643    fn center_border_deflates_by_half_the_border() {
644        let points = GeometryBuilder::calculate_rounded_corners_rectangle_win_ui(
645            Rect::new(10.0, 10.0, 100.0, 60.0),
646            Thickness::uniform(4.0),
647            CornerRadius::uniform(10.0),
648            BackgroundSizing::CenterBorder,
649        );
650
651        // bounds deflated by 2 -> (12, 12, 96, 56); radii reduced by half the border -> 8
652        assert_eq!(Point::new(12.0, 20.0), points.left_top);
653        assert_eq!(Point::new(20.0, 12.0), points.top_left);
654        assert_eq!(Point::new(100.0, 12.0), points.top_right);
655        assert_eq!(Point::new(108.0, 20.0), points.right_top);
656        assert_eq!(Point::new(108.0, 60.0), points.right_bottom);
657        assert_eq!(Point::new(100.0, 68.0), points.bottom_right);
658        assert_eq!(Point::new(20.0, 68.0), points.bottom_left);
659        assert_eq!(Point::new(12.0, 60.0), points.left_bottom);
660    }
661
662    #[test]
663    fn overlapping_radii_are_partitioned_proportionally() {
664        let points = GeometryBuilder::calculate_rounded_corners_rectangle_win_ui(
665            Rect::new(0.0, 0.0, 100.0, 100.0),
666            Thickness::default(),
667            CornerRadius::new(90.0, 30.0, 30.0, 90.0),
668            BackgroundSizing::OuterBorderEdge,
669        );
670
671        // top edge: 90 + 30 > 100 -> split at 90 / 120 * 100 = 75
672        assert_eq!(Point::new(75.0, 0.0), points.top_left);
673        assert_eq!(Point::new(75.0, 0.0), points.top_right);
674        // bottom edge: same split
675        assert_eq!(Point::new(75.0, 100.0), points.bottom_left);
676        assert_eq!(Point::new(75.0, 100.0), points.bottom_right);
677        // right edge: 30 + 30 <= 100 -> untouched
678        assert_eq!(Point::new(100.0, 30.0), points.right_top);
679        assert_eq!(Point::new(100.0, 70.0), points.right_bottom);
680        // left edge: 90 + 90 > 100 -> split at 90 / 180 * 100 = 50
681        assert_eq!(Point::new(0.0, 50.0), points.left_top);
682        assert_eq!(Point::new(0.0, 50.0), points.left_bottom);
683    }
684
685    #[test]
686    fn keypoints_are_drawn_with_arcs_only_on_rounded_corners() {
687        let keypoints = RoundedRectKeypoints::from_rounded_rect(RoundedRect::new(
688            Rect::new(0.0, 0.0, 100.0, 50.0),
689            Vector::new(10.0, 5.0),
690            Vector::new(0.0, 0.0),
691            Vector::new(20.0, 10.0),
692            Vector::new(0.0, 0.0),
693        ));
694
695        let log = record(|context| GeometryBuilder::draw_rounded_corners_rectangle_keypoints(context, &keypoints));
696
697        assert_eq!(
698            vec![
699                "begin 10, 0 true",
700                "line 100, 0 true",
701                // The top right corner is always drawn, even with zero radii (which the upstream
702                // sign flip turns into negative zeros).
703                "arc 100, 0 -0, -0 0 false Clockwise true",
704                "line 100, 40 true",
705                "arc 80, 50 20, 10 0 false Clockwise true",
706                "line 0, 50 true",
707                "line 0, 5 true",
708                "arc 10, 0 10, 5 0 false Clockwise true",
709                "end true",
710            ],
711            log
712        );
713    }
714
715    #[test]
716    fn uniform_radii_rectangle_is_drawn_with_four_arcs() {
717        let log = record(|context| {
718            GeometryBuilder::draw_rounded_corners_rectangle(context, Rect::new(0.0, 0.0, 100.0, 50.0), 10.0, 5.0)
719        });
720
721        assert_eq!(
722            vec![
723                "begin 10, 0 true",
724                "line 90, 0 true",
725                "arc 100, 5 10, 5 0 false Clockwise true",
726                "line 100, 45 true",
727                "arc 90, 50 10, 5 0 false Clockwise true",
728                "line 10, 50 true",
729                "arc 0, 45 10, 5 0 false Clockwise true",
730                "line 0, 5 true",
731                "arc 10, 0 10, 5 0 false Clockwise true",
732                "end true",
733            ],
734            log
735        );
736    }
737}