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
This commit is contained in:
Floyd Wang 2025-06-07 00:54:21 +08:00 committed by GitHub
parent 73cd6ef92c
commit ac806d982b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 84 additions and 15 deletions

View file

@ -1,13 +1,14 @@
use gpui::{ use gpui::{
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
}; };
struct PaintingViewer { struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>, default_lines: Vec<(Path<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>, lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>, start: Point<Pixels>,
dashed: bool,
_painting: bool, _painting: bool,
} }
@ -140,7 +141,7 @@ impl PaintingViewer {
.with_line_join(lyon::path::LineJoin::Bevel); .with_line_join(lyon::path::LineJoin::Bevel);
let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
builder.move_to(point(px(40.), px(320.))); builder.move_to(point(px(40.), px(320.)));
for i in 0..50 { for i in 1..50 {
builder.line_to(point( builder.line_to(point(
px(40.0 + i as f32 * 10.0), px(40.0 + i as f32 * 10.0),
px(320.0 + (i as f32 * 10.0).sin() * 40.0), px(320.0 + (i as f32 * 10.0).sin() * 40.0),
@ -153,6 +154,7 @@ impl PaintingViewer {
default_lines: lines.clone(), default_lines: lines.clone(),
lines: vec![], lines: vec![],
start: point(px(0.), px(0.)), start: point(px(0.), px(0.)),
dashed: false,
_painting: false, _painting: false,
} }
} }
@ -162,10 +164,30 @@ impl PaintingViewer {
cx.notify(); cx.notify();
} }
} }
fn button(
text: &str,
cx: &mut Context<PaintingViewer>,
on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + '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 { impl Render for PaintingViewer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone(); let default_lines = self.default_lines.clone();
let lines = self.lines.clone(); let lines = self.lines.clone();
let dashed = self.dashed;
div() div()
.font_family(".SystemUIFont") .font_family(".SystemUIFont")
.bg(gpui::white()) .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("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
.child( .child(
div() div()
.id("clear")
.child("Clean up")
.bg(gpui::black())
.text_color(gpui::white())
.active(|this| this.opacity(0.8))
.flex() .flex()
.px_3() .gap_x_2()
.py_1() .child(button(
.on_click(cx.listener(|this, _, _, cx| { if dashed { "Solid" } else { "Dashed" },
this.clear(cx); cx,
})), move |this, _| this.dashed = !dashed,
))
.child(button("Clear", cx, |this, cx| this.clear(cx))),
), ),
) )
.child( .child(
@ -202,7 +221,6 @@ impl Render for PaintingViewer {
canvas( canvas(
move |_, _, _| {}, move |_, _, _| {},
move |_, _, window, _| { move |_, _, window, _| {
for (path, color) in default_lines { for (path, color) in default_lines {
window.paint_path(path, color); window.paint_path(path, color);
} }
@ -213,6 +231,9 @@ impl Render for PaintingViewer {
} }
let mut builder = PathBuilder::stroke(px(1.)); 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() { for (i, p) in points.into_iter().enumerate() {
if i == 0 { if i == 0 {
builder.move_to(p); builder.move_to(p);

View file

@ -27,6 +27,7 @@ pub struct PathBuilder {
transform: Option<lyon::math::Transform>, transform: Option<lyon::math::Transform>,
/// PathStyle of the PathBuilder /// PathStyle of the PathBuilder
pub style: PathStyle, pub style: PathStyle,
dash_array: Option<Vec<Pixels>>,
} }
impl From<lyon::path::Builder> for PathBuilder { impl From<lyon::path::Builder> for PathBuilder {
@ -77,6 +78,7 @@ impl Default for PathBuilder {
raw: lyon::path::Path::builder().with_svg(), raw: lyon::path::Path::builder().with_svg(),
style: PathStyle::Fill(FillOptions::default()), style: PathStyle::Fill(FillOptions::default()),
transform: None, transform: None,
dash_array: None,
} }
} }
} }
@ -100,6 +102,24 @@ impl PathBuilder {
Self { style, ..self } 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. /// Move the current point to the given point.
#[inline] #[inline]
pub fn move_to(&mut self, to: Point<Pixels>) { pub fn move_to(&mut self, to: Point<Pixels>) {
@ -229,7 +249,7 @@ impl PathBuilder {
}; };
match self.style { 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), PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
} }
} }
@ -253,9 +273,37 @@ impl PathBuilder {
} }
fn tessellate_stroke( fn tessellate_stroke(
dash_array: Option<Vec<Pixels>>,
path: &lyon::path::Path, path: &lyon::path::Path,
options: &StrokeOptions, options: &StrokeOptions,
) -> Result<Path<Pixels>, Error> { ) -> Result<Path<Pixels>, 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. // Will contain the result of the tessellation.
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new(); let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new(); let mut tessellator = StrokeTessellator::new();