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:
parent
73cd6ef92c
commit
ac806d982b
2 changed files with 84 additions and 15 deletions
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue