Skip to main content

gpui/
path_builder.rs

1use anyhow::Error;
2use etagere::euclid::{Point2D, Vector2D};
3use lyon::geom::Angle;
4use lyon::math::{Vector, vector};
5use lyon::path::traits::SvgPathBuilder;
6use lyon::path::{ArcFlags, Polygon};
7use lyon::tessellation::{
8    BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers,
9};
10
11pub use lyon::math::Transform;
12pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions};
13
14use crate::{Path, Pixels, Point, point, px};
15
16/// How a [`crate::PathBuilder`] tessellates its path: as an outline, or as a filled shape.
17pub enum PathStyle {
18    /// Draws the outline as a line, with these [`StrokeOptions`].
19    Stroke(StrokeOptions),
20    /// Fills the interior, with these [`FillOptions`].
21    Fill(FillOptions),
22}
23
24/// Builds a [`Path`] from lines and curves, and tessellates it into triangles.
25///
26/// Create a builder with [`crate::PathBuilder::fill`] or [`crate::PathBuilder::stroke`], add commands, and call
27/// [`crate::PathBuilder::build`]. Paint the result with [`crate::Window::paint_path`]. Coordinates are in window
28/// pixels, so use the bounds that a [`canvas`](crate::canvas) gives you.
29///
30/// # Examples
31///
32/// ```
33/// # use gpui::{PathBuilder, point, px};
34/// let mut builder = PathBuilder::fill();
35/// builder.move_to(point(px(0.), px(0.)));
36/// builder.line_to(point(px(10.), px(0.)));
37/// builder.line_to(point(px(0.), px(10.)));
38/// builder.close();
39/// let path = builder.build().unwrap();
40/// assert_eq!(path.vertices.len() % 3, 0);
41/// assert_eq!(path.bounds.size.width, px(10.));
42/// assert_eq!(path.bounds.size.height, px(10.));
43/// ```
44///
45/// See [drawing custom graphics](crate::_documentation::guides::custom_drawing) for more.
46pub struct PathBuilder {
47    raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>,
48    transform: Option<lyon::math::Transform>,
49    /// How [`crate::PathBuilder::build`] tessellates the path. [`crate::PathBuilder::stroke`],
50    /// [`crate::PathBuilder::fill`] and [`crate::PathBuilder::with_style`] set it.
51    pub style: PathStyle,
52    dash_array: Option<Vec<Pixels>>,
53}
54
55impl From<lyon::path::Builder> for PathBuilder {
56    fn from(builder: lyon::path::Builder) -> Self {
57        Self {
58            raw: builder.with_svg(),
59            ..Default::default()
60        }
61    }
62}
63
64impl From<lyon::path::builder::WithSvg<lyon::path::BuilderImpl>> for PathBuilder {
65    fn from(raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>) -> Self {
66        Self {
67            raw,
68            ..Default::default()
69        }
70    }
71}
72
73impl From<lyon::math::Point> for Point<Pixels> {
74    fn from(p: lyon::math::Point) -> Self {
75        point(px(p.x), px(p.y))
76    }
77}
78
79impl From<Point<Pixels>> for lyon::math::Point {
80    fn from(p: Point<Pixels>) -> Self {
81        lyon::math::point(p.x.0, p.y.0)
82    }
83}
84
85impl From<Point<Pixels>> for Vector {
86    fn from(p: Point<Pixels>) -> Self {
87        vector(p.x.0, p.y.0)
88    }
89}
90
91impl From<Point<Pixels>> for Point2D<f32, Pixels> {
92    fn from(p: Point<Pixels>) -> Self {
93        Point2D::new(p.x.0, p.y.0)
94    }
95}
96
97impl Default for PathBuilder {
98    fn default() -> Self {
99        Self {
100            raw: lyon::path::Path::builder().with_svg(),
101            style: PathStyle::Fill(FillOptions::default()),
102            transform: None,
103            dash_array: None,
104        }
105    }
106}
107
108impl PathBuilder {
109    /// Returns a builder that strokes the path with a line `width` logical pixels wide.
110    ///
111    /// The stroke has butt caps and miter joins, with a miter limit of 4. Use [`crate::PathBuilder::fill`] for a
112    /// filled shape.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// # use gpui::{PathBuilder, point, px, size};
118    /// let mut builder = PathBuilder::stroke(px(2.));
119    /// builder.move_to(point(px(0.), px(5.)));
120    /// builder.line_to(point(px(20.), px(5.)));
121    /// let path = builder.build().unwrap();
122    /// assert_eq!(path.bounds.origin, point(px(0.), px(4.)));
123    /// assert_eq!(path.bounds.size, size(px(20.), px(2.)));
124    /// ```
125    pub fn stroke(width: Pixels) -> Self {
126        Self {
127            style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)),
128            ..Self::default()
129        }
130    }
131
132    /// Returns a builder that fills the interior of the path using the even-odd rule.
133    ///
134    /// An open sub-path is closed implicitly when it is filled. For the non-zero rule, pass a style to
135    /// [`crate::PathBuilder::with_style`].
136    pub fn fill() -> Self {
137        Self::default()
138    }
139
140    /// Replaces the style of this builder.
141    ///
142    /// Use this for options that [`crate::PathBuilder::stroke`] and [`crate::PathBuilder::fill`] don't set, such as the
143    /// non-zero [`FillRule`].
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// # use gpui::{FillOptions, FillRule, PathBuilder, PathStyle, point, px};
149    /// let style = PathStyle::Fill(FillOptions::default().with_fill_rule(FillRule::NonZero));
150    /// let mut builder = PathBuilder::fill().with_style(style);
151    /// builder.move_to(point(px(0.), px(0.)));
152    /// builder.line_to(point(px(10.), px(0.)));
153    /// builder.line_to(point(px(0.), px(10.)));
154    /// builder.close();
155    /// assert!(builder.build().is_ok());
156    /// ```
157    pub fn with_style(self, style: PathStyle) -> Self {
158        Self { style, ..self }
159    }
160
161    /// Sets the dash pattern of a stroked path.
162    ///
163    /// The values alternate between dash and gap lengths, in logical pixels, measured along the path.
164    /// An odd number of values is repeated, so `[5, 3, 2]` is the same as `[5, 3, 2, 5, 3, 2]`. The
165    /// pattern has no effect on fills.
166    ///
167    /// Pass a non-empty slice of finite, positive lengths. [`crate::PathBuilder::build`] returns an
168    /// error for an empty or invalid pattern, including a length too small to advance along the path.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// # use gpui::{PathBuilder, point, px};
174    /// let mut solid = PathBuilder::stroke(px(1.));
175    /// solid.move_to(point(px(0.), px(0.)));
176    /// solid.line_to(point(px(20.), px(0.)));
177    /// let solid = solid.build().unwrap();
178    ///
179    /// let mut dashed = PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(4.)]);
180    /// dashed.move_to(point(px(0.), px(0.)));
181    /// dashed.line_to(point(px(20.), px(0.)));
182    /// let dashed = dashed.build().unwrap();
183    ///
184    /// assert!(dashed.vertices.len() > solid.vertices.len());
185    /// assert_eq!(dashed.bounds.size.width, px(20.));
186    /// ```
187    pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
188        // If an odd number of values is provided, then the list of values is repeated to yield an even number of values.
189        // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
190        let array = if dash_array.len() % 2 == 1 {
191            let mut new_dash_array = dash_array.to_vec();
192            new_dash_array.extend_from_slice(dash_array);
193            new_dash_array
194        } else {
195            dash_array.to_vec()
196        };
197
198        self.dash_array = Some(array);
199        self
200    }
201
202    /// Starts a new sub-path at `to`. The current point becomes `to`.
203    #[inline]
204    pub fn move_to(&mut self, to: Point<Pixels>) {
205        self.raw.move_to(to.into());
206    }
207
208    /// Adds a straight line from the current point to `to`.
209    #[inline]
210    pub fn line_to(&mut self, to: Point<Pixels>) {
211        self.raw.line_to(to.into());
212    }
213
214    /// Adds a quadratic Bézier curve from the current point to `to`, bent toward the control point `ctrl`.
215    #[inline]
216    pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
217        self.raw.quadratic_bezier_to(ctrl.into(), to.into());
218    }
219
220    /// Adds a cubic Bézier curve from the current point to `to`, shaped by the control points
221    /// `control_a` and `control_b`.
222    #[inline]
223    pub fn cubic_bezier_to(
224        &mut self,
225        to: Point<Pixels>,
226        control_a: Point<Pixels>,
227        control_b: Point<Pixels>,
228    ) {
229        self.raw
230            .cubic_bezier_to(control_a.into(), control_b.into(), to.into());
231    }
232
233    /// Adds an elliptical arc from the current point to `to`, in absolute coordinates.
234    ///
235    /// `radii` gives the x and y radii in logical pixels. `x_rotation` is the rotation of the ellipse in
236    /// **degrees**, even though its type is [`Pixels`]. `large_arc` and `sweep` choose one of the four
237    /// possible arcs, as they do in SVG path data.
238    pub fn arc_to(
239        &mut self,
240        radii: Point<Pixels>,
241        x_rotation: Pixels,
242        large_arc: bool,
243        sweep: bool,
244        to: Point<Pixels>,
245    ) {
246        self.raw.arc_to(
247            radii.into(),
248            Angle::degrees(x_rotation.into()),
249            ArcFlags { large_arc, sweep },
250            to.into(),
251        );
252    }
253
254    /// Adds an elliptical arc like [`crate::PathBuilder::arc_to`], but `to` is relative to the current point.
255    /// `x_rotation` is in degrees, as it is for `arc_to`.
256    pub fn relative_arc_to(
257        &mut self,
258        radii: Point<Pixels>,
259        x_rotation: Pixels,
260        large_arc: bool,
261        sweep: bool,
262        to: Point<Pixels>,
263    ) {
264        self.raw.relative_arc_to(
265            radii.into(),
266            Angle::degrees(x_rotation.into()),
267            ArcFlags { large_arc, sweep },
268            to.into(),
269        );
270    }
271
272    /// Adds `points` as a sub-path. If `closed` is `true`, the last point connects back to the first.
273    pub fn add_polygon(&mut self, points: &[Point<Pixels>], closed: bool) {
274        let points = points.iter().copied().map(|p| p.into()).collect::<Vec<_>>();
275        self.raw.add_polygon(Polygon {
276            points: points.as_ref(),
277            closed,
278        });
279    }
280
281    /// Closes the current sub-path with a line back to its start.
282    #[inline]
283    pub fn close(&mut self) {
284        self.raw.close();
285    }
286
287    /// Replaces the transform that [`crate::PathBuilder::build`] applies with `transform`.
288    ///
289    /// This discards the effect of any [`crate::PathBuilder::translate`], [`crate::PathBuilder::scale`] or
290    /// [`crate::PathBuilder::rotate`] calls made before it.
291    #[inline]
292    pub fn transform(&mut self, transform: Transform) {
293        self.transform = Some(transform);
294    }
295
296    /// Moves the path by `to` when it is built.
297    ///
298    /// Each transform call applies to the path after the transform calls before it.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// # use gpui::{PathBuilder, point, px};
304    /// let mut builder = PathBuilder::fill();
305    /// builder.move_to(point(px(0.), px(0.)));
306    /// builder.line_to(point(px(10.), px(0.)));
307    /// builder.line_to(point(px(0.), px(10.)));
308    /// builder.translate(point(px(5.), px(5.)));
309    /// let path = builder.build().unwrap();
310    /// assert_eq!(path.bounds.origin, point(px(5.), px(5.)));
311    /// ```
312    #[inline]
313    pub fn translate(&mut self, to: Point<Pixels>) {
314        if let Some(transform) = self.transform {
315            self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0)));
316        } else {
317            self.transform = Some(Transform::translation(to.x.0, to.y.0))
318        }
319    }
320
321    /// Scales the path by `scale` on both axes when it is built.
322    ///
323    /// Stroke widths are not scaled, because the geometry is transformed before it is stroked.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// # use gpui::{PathBuilder, point, px};
329    /// let mut builder = PathBuilder::fill();
330    /// builder.move_to(point(px(0.), px(0.)));
331    /// builder.line_to(point(px(10.), px(0.)));
332    /// builder.line_to(point(px(0.), px(10.)));
333    /// builder.scale(2.);
334    /// let path = builder.build().unwrap();
335    /// assert_eq!(path.bounds.size.width, px(20.));
336    /// assert_eq!(path.bounds.size.height, px(20.));
337    /// ```
338    #[inline]
339    pub fn scale(&mut self, scale: f32) {
340        if let Some(transform) = self.transform {
341            self.transform = Some(transform.then_scale(scale, scale));
342        } else {
343            self.transform = Some(Transform::scale(scale, scale));
344        }
345    }
346
347    /// Rotates the path by `angle` degrees around the origin when it is built.
348    #[inline]
349    pub fn rotate(&mut self, angle: f32) {
350        let radians = angle.to_radians();
351        if let Some(transform) = self.transform {
352            self.transform = Some(transform.then_rotate(Angle::radians(radians)));
353        } else {
354            self.transform = Some(Transform::rotation(Angle::radians(radians)));
355        }
356    }
357
358    /// Tessellates the path and returns it as a [`Path`].
359    ///
360    /// The transform is applied to the geometry first. A fill uses the [`FillOptions`] of its style. A
361    /// stroke uses the [`StrokeOptions`] of its style and the dash array.
362    ///
363    /// Building is the expensive step. Build a path once, and keep it while its shape doesn't change,
364    /// rather than rebuilding it every frame.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if a point or the tolerance is NaN, if the mesh needs more vertices than a
369    /// `u16` index can address, or if the dash array is empty, has a length that isn't finite and
370    /// positive, or has a length too small to advance along the path.
371    ///
372    /// # Examples
373    ///
374    /// ```
375    /// # use gpui::{PathBuilder, point, px};
376    /// let mut builder = PathBuilder::fill();
377    /// builder.move_to(point(px(0.), px(0.)));
378    /// builder.line_to(point(px(10.), px(0.)));
379    /// builder.line_to(point(px(0.), px(10.)));
380    /// builder.close();
381    /// let path = builder.build().unwrap();
382    /// assert!(path.vertices.len() >= 3);
383    /// assert_eq!(path.vertices.len() % 3, 0);
384    /// ```
385    #[inline]
386    pub fn build(self) -> Result<Path<Pixels>, Error> {
387        let path = if let Some(transform) = self.transform {
388            self.raw.build().transformed(&transform)
389        } else {
390            self.raw.build()
391        };
392
393        match self.style {
394            PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options),
395            PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
396        }
397    }
398
399    fn tessellate_fill(
400        path: &lyon::path::Path,
401        options: &FillOptions,
402    ) -> Result<Path<Pixels>, Error> {
403        // Will contain the result of the tessellation.
404        let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
405        let mut tessellator = FillTessellator::new();
406
407        // Compute the tessellation.
408        tessellator.tessellate_path(
409            path,
410            options,
411            &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()),
412        )?;
413
414        Ok(Self::build_path(buf))
415    }
416
417    fn tessellate_stroke(
418        dash_array: Option<Vec<Pixels>>,
419        path: &lyon::path::Path,
420        options: &StrokeOptions,
421    ) -> Result<Path<Pixels>, Error> {
422        let path = if let Some(dash_array) = dash_array {
423            anyhow::ensure!(
424                !dash_array.is_empty()
425                    && dash_array
426                        .iter()
427                        .all(|length| length.0.is_finite() && length.0 > 0.),
428                "dash lengths must be finite, positive, and non-empty"
429            );
430            let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01);
431            let mut sampler = measurements
432                .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
433            let mut builder = lyon::path::Path::builder();
434
435            let total_length = sampler.length();
436            let dash_array_len = dash_array.len();
437            let mut pos = 0.;
438            let mut dash_index = 0;
439            while pos < total_length {
440                let dash_length = dash_array[dash_index % dash_array_len].0;
441                let next_pos = (pos + dash_length).min(total_length);
442                // A dash too short to move `pos` in floating point would loop forever.
443                anyhow::ensure!(
444                    next_pos > pos,
445                    "dash length is too small to advance along the path"
446                );
447                if dash_index % 2 == 0 {
448                    let start = pos / total_length;
449                    let end = next_pos / total_length;
450                    sampler.split_range(start..end, &mut builder);
451                }
452                pos = next_pos;
453                dash_index += 1;
454            }
455
456            &builder.build()
457        } else {
458            path
459        };
460
461        // Will contain the result of the tessellation.
462        let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
463        let mut tessellator = StrokeTessellator::new();
464
465        // Compute the tessellation.
466        tessellator.tessellate_path(
467            path,
468            options,
469            &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()),
470        )?;
471
472        Ok(Self::build_path(buf))
473    }
474
475    /// Converts triangles from a lyon `VertexBuffers` into a [`Path`]. Each run of three indices is one
476    /// triangle. An empty buffer gives an empty path at the origin.
477    ///
478    /// # Panics
479    ///
480    /// Panics if an index is out of range for the buffer's vertices.
481    pub fn build_path(buf: VertexBuffers<lyon::math::Point, u16>) -> Path<Pixels> {
482        if buf.vertices.is_empty() {
483            return Path::new(Point::default());
484        }
485
486        let first_point = buf.vertices[0];
487
488        let mut path = Path::new(first_point.into());
489        for i in 0..buf.indices.len() / 3 {
490            let i0 = buf.indices[i * 3] as usize;
491            let i1 = buf.indices[i * 3 + 1] as usize;
492            let i2 = buf.indices[i * 3 + 2] as usize;
493
494            let v0 = buf.vertices[i0];
495            let v1 = buf.vertices[i1];
496            let v2 = buf.vertices[i2];
497
498            path.push_triangle(
499                (v0.into(), v1.into(), v2.into()),
500                (point(0., 1.), point(0., 1.), point(0., 1.)),
501            );
502        }
503
504        path
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    fn dashed_line(dash_array: &[Pixels]) -> Result<Path<Pixels>, Error> {
513        let mut builder = PathBuilder::stroke(px(1.)).dash_array(dash_array);
514        builder.move_to(point(px(0.), px(0.)));
515        builder.line_to(point(px(10.), px(0.)));
516        builder.build()
517    }
518
519    #[test]
520    fn invalid_dash_arrays_return_errors() {
521        for dash_array in [
522            vec![],
523            vec![px(0.)],
524            vec![px(-1.)],
525            vec![px(f32::NAN)],
526            vec![px(f32::INFINITY)],
527        ] {
528            assert!(dashed_line(&dash_array).is_err(), "accepted {dash_array:?}");
529        }
530    }
531
532    #[test]
533    fn dash_lengths_must_advance_in_floating_point() {
534        assert!(dashed_line(&[px(1.), px(f32::MIN_POSITIVE)]).is_err());
535    }
536
537    #[test]
538    fn valid_dash_array_builds() {
539        assert!(dashed_line(&[px(2.), px(1.)]).is_ok());
540    }
541}