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}