From ac806d982be93759031c561f335c8d09ab96626a Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Sat, 7 Jun 2025 00:54:21 +0800 Subject: [PATCH] gpui: Introduce dash array support for `PathBuilder` (#31678) A simple way to draw dashed lines. https://github.com/user-attachments/assets/2105d7b2-42d0-4d73-bb29-83a4a6bd7029 Release Notes: - N/A --- crates/gpui/examples/painting.rs | 49 ++++++++++++++++++++++--------- crates/gpui/src/path_builder.rs | 50 +++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 22a3ad070f..ff4b64cbda 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,13 +1,14 @@ use gpui::{ Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, - PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, - linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, + PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas, + div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, }; struct PaintingViewer { default_lines: Vec<(Path, Background)>, lines: Vec>>, start: Point, + dashed: bool, _painting: bool, } @@ -140,7 +141,7 @@ impl PaintingViewer { .with_line_join(lyon::path::LineJoin::Bevel); let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); builder.move_to(point(px(40.), px(320.))); - for i in 0..50 { + for i in 1..50 { builder.line_to(point( px(40.0 + i as f32 * 10.0), px(320.0 + (i as f32 * 10.0).sin() * 40.0), @@ -153,6 +154,7 @@ impl PaintingViewer { default_lines: lines.clone(), lines: vec![], start: point(px(0.), px(0.)), + dashed: false, _painting: false, } } @@ -162,10 +164,30 @@ impl PaintingViewer { cx.notify(); } } + +fn button( + text: &str, + cx: &mut Context, + on_click: impl Fn(&mut PaintingViewer, &mut Context) + 'static, +) -> impl IntoElement { + div() + .id(SharedString::from(text.to_string())) + .child(text.to_string()) + .bg(gpui::black()) + .text_color(gpui::white()) + .active(|this| this.opacity(0.8)) + .flex() + .px_3() + .py_1() + .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) +} + impl Render for PaintingViewer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let default_lines = self.default_lines.clone(); let lines = self.lines.clone(); + let dashed = self.dashed; + div() .font_family(".SystemUIFont") .bg(gpui::white()) @@ -182,17 +204,14 @@ impl Render for PaintingViewer { .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") .child( div() - .id("clear") - .child("Clean up") - .bg(gpui::black()) - .text_color(gpui::white()) - .active(|this| this.opacity(0.8)) .flex() - .px_3() - .py_1() - .on_click(cx.listener(|this, _, _, cx| { - this.clear(cx); - })), + .gap_x_2() + .child(button( + if dashed { "Solid" } else { "Dashed" }, + cx, + move |this, _| this.dashed = !dashed, + )) + .child(button("Clear", cx, |this, cx| this.clear(cx))), ), ) .child( @@ -202,7 +221,6 @@ impl Render for PaintingViewer { canvas( move |_, _, _| {}, move |_, _, window, _| { - for (path, color) in default_lines { window.paint_path(path, color); } @@ -213,6 +231,9 @@ impl Render for PaintingViewer { } let mut builder = PathBuilder::stroke(px(1.)); + if dashed { + builder = builder.dash_array(&[px(4.), px(2.)]); + } for (i, p) in points.into_iter().enumerate() { if i == 0 { builder.move_to(p); diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index bf8d2d65bb..6c8cfddd52 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -27,6 +27,7 @@ pub struct PathBuilder { transform: Option, /// PathStyle of the PathBuilder pub style: PathStyle, + dash_array: Option>, } impl From for PathBuilder { @@ -77,6 +78,7 @@ impl Default for PathBuilder { raw: lyon::path::Path::builder().with_svg(), style: PathStyle::Fill(FillOptions::default()), transform: None, + dash_array: None, } } } @@ -100,6 +102,24 @@ impl PathBuilder { Self { style, ..self } } + /// Sets the dash array of the [`PathBuilder`]. + /// + /// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray) + pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self { + // If an odd number of values is provided, then the list of values is repeated to yield an even number of values. + // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2. + let array = if dash_array.len() % 2 == 1 { + let mut new_dash_array = dash_array.to_vec(); + new_dash_array.extend_from_slice(dash_array); + new_dash_array + } else { + dash_array.to_vec() + }; + + self.dash_array = Some(array); + self + } + /// Move the current point to the given point. #[inline] pub fn move_to(&mut self, to: Point) { @@ -229,7 +249,7 @@ impl PathBuilder { }; match self.style { - PathStyle::Stroke(options) => Self::tessellate_stroke(&path, &options), + PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options), PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), } } @@ -253,9 +273,37 @@ impl PathBuilder { } fn tessellate_stroke( + dash_array: Option>, path: &lyon::path::Path, options: &StrokeOptions, ) -> Result, Error> { + let path = if let Some(dash_array) = dash_array { + let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01); + let mut sampler = measurements + .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); + let mut builder = lyon::path::Path::builder(); + + let total_length = sampler.length(); + let dash_array_len = dash_array.len(); + let mut pos = 0.; + let mut dash_index = 0; + while pos < total_length { + let dash_length = dash_array[dash_index % dash_array_len].0; + let next_pos = (pos + dash_length).min(total_length); + if dash_index % 2 == 0 { + let start = pos / total_length; + let end = next_pos / total_length; + sampler.split_range(start..end, &mut builder); + } + pos = next_pos; + dash_index += 1; + } + + &builder.build() + } else { + path + }; + // Will contain the result of the tessellation. let mut buf: VertexBuffers = VertexBuffers::new(); let mut tessellator = StrokeTessellator::new();