@@ -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<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
+ 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<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 {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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);
@@ -27,6 +27,7 @@ pub struct PathBuilder {
transform: Option<lyon::math::Transform>,
/// PathStyle of the PathBuilder
pub style: PathStyle,
+ dash_array: Option<Vec<Pixels>>,
}
impl From<lyon::path::Builder> 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<Pixels>) {
@@ -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<Vec<Pixels>>,
path: &lyon::path::Path,
options: &StrokeOptions,
) -> 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.
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();