Skip to main content

gpui_x/
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/// Style of the PathBuilder
17pub enum PathStyle {
18    /// Stroke style
19    Stroke(StrokeOptions),
20    /// Fill style
21    Fill(FillOptions),
22}
23
24/// A [`Path`] builder.
25pub struct PathBuilder {
26    raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>,
27    transform: Option<lyon::math::Transform>,
28    /// PathStyle of the PathBuilder
29    pub style: PathStyle,
30    dash_array: Option<Vec<Pixels>>,
31}
32
33impl From<lyon::path::Builder> for PathBuilder {
34    fn from(builder: lyon::path::Builder) -> Self {
35        Self {
36            raw: builder.with_svg(),
37            ..Default::default()
38        }
39    }
40}
41
42impl From<lyon::path::builder::WithSvg<lyon::path::BuilderImpl>> for PathBuilder {
43    fn from(raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>) -> Self {
44        Self {
45            raw,
46            ..Default::default()
47        }
48    }
49}
50
51impl From<lyon::math::Point> for Point<Pixels> {
52    fn from(p: lyon::math::Point) -> Self {
53        point(px(p.x), px(p.y))
54    }
55}
56
57impl From<Point<Pixels>> for lyon::math::Point {
58    fn from(p: Point<Pixels>) -> Self {
59        lyon::math::point(p.x.0, p.y.0)
60    }
61}
62
63impl From<Point<Pixels>> for Vector {
64    fn from(p: Point<Pixels>) -> Self {
65        vector(p.x.0, p.y.0)
66    }
67}
68
69impl From<Point<Pixels>> for Point2D<f32, Pixels> {
70    fn from(p: Point<Pixels>) -> Self {
71        Point2D::new(p.x.0, p.y.0)
72    }
73}
74
75impl Default for PathBuilder {
76    fn default() -> Self {
77        Self {
78            raw: lyon::path::Path::builder().with_svg(),
79            style: PathStyle::Fill(FillOptions::default()),
80            transform: None,
81            dash_array: None,
82        }
83    }
84}
85
86impl PathBuilder {
87    /// Creates a new [`PathBuilder`] to build a Stroke path.
88    pub fn stroke(width: Pixels) -> Self {
89        Self {
90            style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)),
91            ..Self::default()
92        }
93    }
94
95    /// Creates a new [`PathBuilder`] to build a Stroke path with round caps
96    /// and round joins. lyon defaults to Butt/Miter; rounded strokes match
97    /// UI elements drawn with circular dots (lane curves, arrowheads).
98    pub fn stroke_rounded(width: Pixels) -> Self {
99        Self {
100            style: PathStyle::Stroke(
101                StrokeOptions::default()
102                    .with_line_width(width.0)
103                    .with_line_cap(lyon::tessellation::LineCap::Round)
104                    .with_line_join(lyon::tessellation::LineJoin::Round),
105            ),
106            ..Self::default()
107        }
108    }
109
110    /// Creates a new [`PathBuilder`] to build a Fill path.
111    pub fn fill() -> Self {
112        Self::default()
113    }
114
115    /// Sets the style of the [`PathBuilder`].
116    pub fn with_style(self, style: PathStyle) -> Self {
117        Self { style, ..self }
118    }
119
120    /// Sets the dash array of the [`PathBuilder`].
121    ///
122    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray)
123    pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
124        // If an odd number of values is provided, then the list of values is repeated to yield an even number of values.
125        // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
126        let array = if dash_array.len() % 2 == 1 {
127            let mut new_dash_array = dash_array.to_vec();
128            new_dash_array.extend_from_slice(dash_array);
129            new_dash_array
130        } else {
131            dash_array.to_vec()
132        };
133
134        self.dash_array = Some(array);
135        self
136    }
137
138    /// Move the current point to the given point.
139    #[inline]
140    pub fn move_to(&mut self, to: Point<Pixels>) {
141        self.raw.move_to(to.into());
142    }
143
144    /// Draw a straight line from the current point to the given point.
145    #[inline]
146    pub fn line_to(&mut self, to: Point<Pixels>) {
147        self.raw.line_to(to.into());
148    }
149
150    /// Draw a curve from the current point to the given point, using the given control point.
151    #[inline]
152    pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
153        self.raw.quadratic_bezier_to(ctrl.into(), to.into());
154    }
155
156    /// Adds a cubic Bézier to the [`Path`] given its two control points
157    /// and its end point.
158    #[inline]
159    pub fn cubic_bezier_to(
160        &mut self,
161        to: Point<Pixels>,
162        control_a: Point<Pixels>,
163        control_b: Point<Pixels>,
164    ) {
165        self.raw
166            .cubic_bezier_to(control_a.into(), control_b.into(), to.into());
167    }
168
169    /// Adds an elliptical arc.
170    pub fn arc_to(
171        &mut self,
172        radii: Point<Pixels>,
173        x_rotation: Pixels,
174        large_arc: bool,
175        sweep: bool,
176        to: Point<Pixels>,
177    ) {
178        self.raw.arc_to(
179            radii.into(),
180            Angle::degrees(x_rotation.into()),
181            ArcFlags { large_arc, sweep },
182            to.into(),
183        );
184    }
185
186    /// Equivalent to `arc_to` in relative coordinates.
187    pub fn relative_arc_to(
188        &mut self,
189        radii: Point<Pixels>,
190        x_rotation: Pixels,
191        large_arc: bool,
192        sweep: bool,
193        to: Point<Pixels>,
194    ) {
195        self.raw.relative_arc_to(
196            radii.into(),
197            Angle::degrees(x_rotation.into()),
198            ArcFlags { large_arc, sweep },
199            to.into(),
200        );
201    }
202
203    /// Adds a polygon.
204    pub fn add_polygon(&mut self, points: &[Point<Pixels>], closed: bool) {
205        let points = points.iter().copied().map(|p| p.into()).collect::<Vec<_>>();
206        self.raw.add_polygon(Polygon {
207            points: points.as_ref(),
208            closed,
209        });
210    }
211
212    /// Close the current sub-path.
213    #[inline]
214    pub fn close(&mut self) {
215        self.raw.close();
216    }
217
218    /// Applies a transform to the path.
219    #[inline]
220    pub fn transform(&mut self, transform: Transform) {
221        self.transform = Some(transform);
222    }
223
224    /// Applies a translation to the path.
225    #[inline]
226    pub fn translate(&mut self, to: Point<Pixels>) {
227        if let Some(transform) = self.transform {
228            self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0)));
229        } else {
230            self.transform = Some(Transform::translation(to.x.0, to.y.0))
231        }
232    }
233
234    /// Applies a scale to the path.
235    #[inline]
236    pub fn scale(&mut self, scale: f32) {
237        if let Some(transform) = self.transform {
238            self.transform = Some(transform.then_scale(scale, scale));
239        } else {
240            self.transform = Some(Transform::scale(scale, scale));
241        }
242    }
243
244    /// Applies a rotation to the path.
245    ///
246    /// The `angle` is in degrees value in the range 0.0 to 360.0.
247    #[inline]
248    pub fn rotate(&mut self, angle: f32) {
249        let radians = angle.to_radians();
250        if let Some(transform) = self.transform {
251            self.transform = Some(transform.then_rotate(Angle::radians(radians)));
252        } else {
253            self.transform = Some(Transform::rotation(Angle::radians(radians)));
254        }
255    }
256
257    /// Builds into a [`Path`].
258    #[inline]
259    pub fn build(self) -> Result<Path<Pixels>, Error> {
260        let path = if let Some(transform) = self.transform {
261            self.raw.build().transformed(&transform)
262        } else {
263            self.raw.build()
264        };
265
266        match self.style {
267            PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options),
268            PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
269        }
270    }
271
272    fn tessellate_fill(
273        path: &lyon::path::Path,
274        options: &FillOptions,
275    ) -> Result<Path<Pixels>, Error> {
276        // Will contain the result of the tessellation.
277        let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
278        let mut tessellator = FillTessellator::new();
279
280        // Compute the tessellation.
281        tessellator.tessellate_path(
282            path,
283            options,
284            &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()),
285        )?;
286
287        Ok(Self::build_path(buf))
288    }
289
290    fn tessellate_stroke(
291        dash_array: Option<Vec<Pixels>>,
292        path: &lyon::path::Path,
293        options: &StrokeOptions,
294    ) -> Result<Path<Pixels>, Error> {
295        let path = if let Some(dash_array) = dash_array {
296            let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01);
297            let mut sampler = measurements
298                .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
299            let mut builder = lyon::path::Path::builder();
300
301            let total_length = sampler.length();
302            let dash_array_len = dash_array.len();
303            let mut pos = 0.;
304            let mut dash_index = 0;
305            while pos < total_length {
306                let dash_length = dash_array[dash_index % dash_array_len].0;
307                let next_pos = (pos + dash_length).min(total_length);
308                if dash_index % 2 == 0 {
309                    let start = pos / total_length;
310                    let end = next_pos / total_length;
311                    sampler.split_range(start..end, &mut builder);
312                }
313                pos = next_pos;
314                dash_index += 1;
315            }
316
317            &builder.build()
318        } else {
319            path
320        };
321
322        // Will contain the result of the tessellation.
323        let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
324        let mut tessellator = StrokeTessellator::new();
325
326        // Compute the tessellation.
327        tessellator.tessellate_path(
328            path,
329            options,
330            &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()),
331        )?;
332
333        Ok(Self::build_path(buf))
334    }
335
336    /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`].
337    pub fn build_path(buf: VertexBuffers<lyon::math::Point, u16>) -> Path<Pixels> {
338        if buf.vertices.is_empty() {
339            return Path::new(Point::default());
340        }
341
342        let first_point = buf.vertices[0];
343
344        let mut path = Path::new(first_point.into());
345        for i in 0..buf.indices.len() / 3 {
346            let i0 = buf.indices[i * 3] as usize;
347            let i1 = buf.indices[i * 3 + 1] as usize;
348            let i2 = buf.indices[i * 3 + 2] as usize;
349
350            let v0 = buf.vertices[i0];
351            let v1 = buf.vertices[i1];
352            let v2 = buf.vertices[i2];
353
354            path.push_triangle(
355                (v0.into(), v1.into(), v2.into()),
356                (point(0., 1.), point(0., 1.), point(0., 1.)),
357            );
358        }
359
360        path
361    }
362}