gpui: Add more shapes for `PathBuilder` (#30904)

Floyd Wang created

- Add `arc` for drawing elliptical arc.
- Add `polygon` support.

<img width="1136" alt="image"
src="https://github.com/user-attachments/assets/97032b02-e6ff-4985-a587-3689500bfd56"
/>

Release Notes:

- N/A

Change summary

crates/gpui/examples/painting.rs | 55 ++++++++++++++++++++++++++++--
crates/gpui/src/path_builder.rs  | 60 +++++++++++++++++++++++++++++++++
2 files changed, 110 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/examples/painting.rs 🔗

@@ -27,10 +27,15 @@ impl PaintingViewer {
 
         // draw a lightening bolt ⚡
         let mut builder = PathBuilder::fill();
-        builder.move_to(point(px(150.), px(200.)));
-        builder.line_to(point(px(200.), px(125.)));
-        builder.line_to(point(px(200.), px(175.)));
-        builder.line_to(point(px(250.), px(100.)));
+        builder.add_polygon(
+            &[
+                point(px(150.), px(200.)),
+                point(px(200.), px(125.)),
+                point(px(200.), px(175.)),
+                point(px(250.), px(100.)),
+            ],
+            false,
+        );
         let path = builder.build().unwrap();
         lines.push((path, rgb(0x1d4ed8).into()));
 
@@ -58,6 +63,7 @@ impl PaintingViewer {
             .color_space(ColorSpace::Oklab),
         ));
 
+        // draw linear gradient
         let square_bounds = Bounds {
             origin: point(px(450.), px(100.)),
             size: size(px(200.), px(80.)),
@@ -87,6 +93,47 @@ impl PaintingViewer {
             ),
         ));
 
+        // draw a pie chart
+        let center = point(px(96.), px(96.));
+        let pie_center = point(px(775.), px(155.));
+        let segments = [
+            (
+                point(px(871.), px(155.)),
+                point(px(747.), px(63.)),
+                rgb(0x1374e9),
+            ),
+            (
+                point(px(747.), px(63.)),
+                point(px(679.), px(163.)),
+                rgb(0xe13527),
+            ),
+            (
+                point(px(679.), px(163.)),
+                point(px(754.), px(249.)),
+                rgb(0x0751ce),
+            ),
+            (
+                point(px(754.), px(249.)),
+                point(px(854.), px(210.)),
+                rgb(0x209742),
+            ),
+            (
+                point(px(854.), px(210.)),
+                point(px(871.), px(155.)),
+                rgb(0xfbc10a),
+            ),
+        ];
+
+        for (start, end, color) in segments {
+            let mut builder = PathBuilder::fill();
+            builder.move_to(start);
+            builder.arc_to(center, px(0.), false, false, end);
+            builder.line_to(pie_center);
+            builder.close();
+            let path = builder.build().unwrap();
+            lines.push((path, color.into()));
+        }
+
         // draw a wave
         let options = StrokeOptions::default()
             .with_line_width(1.)

crates/gpui/src/path_builder.rs 🔗

@@ -1,6 +1,9 @@
 use anyhow::Error;
-use etagere::euclid::Vector2D;
+use etagere::euclid::{Point2D, Vector2D};
 use lyon::geom::Angle;
+use lyon::math::{Vector, vector};
+use lyon::path::traits::SvgPathBuilder;
+use lyon::path::{ArcFlags, Polygon};
 use lyon::tessellation::{
     BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers,
 };
@@ -56,6 +59,18 @@ impl From<Point<Pixels>> for lyon::math::Point {
     }
 }
 
+impl From<Point<Pixels>> for Vector {
+    fn from(p: Point<Pixels>) -> Self {
+        vector(p.x.0, p.y.0)
+    }
+}
+
+impl From<Point<Pixels>> for Point2D<f32, Pixels> {
+    fn from(p: Point<Pixels>) -> Self {
+        Point2D::new(p.x.0, p.y.0)
+    }
+}
+
 impl Default for PathBuilder {
     fn default() -> Self {
         Self {
@@ -116,6 +131,49 @@ impl PathBuilder {
             .cubic_bezier_to(control_a.into(), control_b.into(), to.into());
     }
 
+    /// Adds an elliptical arc.
+    pub fn arc_to(
+        &mut self,
+        radii: Point<Pixels>,
+        x_rotation: Pixels,
+        large_arc: bool,
+        sweep: bool,
+        to: Point<Pixels>,
+    ) {
+        self.raw.arc_to(
+            radii.into(),
+            Angle::degrees(x_rotation.into()),
+            ArcFlags { large_arc, sweep },
+            to.into(),
+        );
+    }
+
+    /// Equivalent to `arc_to` in relative coordinates.
+    pub fn relative_arc_to(
+        &mut self,
+        radii: Point<Pixels>,
+        x_rotation: Pixels,
+        large_arc: bool,
+        sweep: bool,
+        to: Point<Pixels>,
+    ) {
+        self.raw.relative_arc_to(
+            radii.into(),
+            Angle::degrees(x_rotation.into()),
+            ArcFlags { large_arc, sweep },
+            to.into(),
+        );
+    }
+
+    /// Adds a polygon.
+    pub fn add_polygon(&mut self, points: &[Point<Pixels>], closed: bool) {
+        let points = points.iter().copied().map(|p| p.into()).collect::<Vec<_>>();
+        self.raw.add_polygon(Polygon {
+            points: points.as_ref(),
+            closed,
+        });
+    }
+
     /// Close the current sub-path.
     #[inline]
     pub fn close(&mut self) {