painting.rs

  1use gpui::{
  2    Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
  3    PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, bounds,
  4    canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
  5};
  6
  7struct PaintingViewer {
  8    default_lines: Vec<(Path<Pixels>, Background)>,
  9    lines: Vec<Vec<Point<Pixels>>>,
 10    start: Point<Pixels>,
 11    dashed: bool,
 12    _painting: bool,
 13}
 14
 15impl PaintingViewer {
 16    fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
 17        let mut lines = vec![];
 18
 19        // draw a Rust logo
 20        let mut builder = lyon::path::Path::svg_builder();
 21        lyon::extra::rust_logo::build_logo_path(&mut builder);
 22        // move down the Path
 23        let mut builder: PathBuilder = builder.into();
 24        builder.translate(point(px(10.), px(100.)));
 25        builder.scale(0.9);
 26        let path = builder.build().unwrap();
 27        lines.push((path, gpui::black().into()));
 28
 29        // draw a lightening bolt ⚡
 30        let mut builder = PathBuilder::fill();
 31        builder.add_polygon(
 32            &[
 33                point(px(150.), px(200.)),
 34                point(px(200.), px(125.)),
 35                point(px(200.), px(175.)),
 36                point(px(250.), px(100.)),
 37            ],
 38            false,
 39        );
 40        let path = builder.build().unwrap();
 41        lines.push((path, rgb(0x1d4ed8).into()));
 42
 43        // draw a ⭐
 44        let mut builder = PathBuilder::fill();
 45        builder.move_to(point(px(350.), px(100.)));
 46        builder.line_to(point(px(370.), px(160.)));
 47        builder.line_to(point(px(430.), px(160.)));
 48        builder.line_to(point(px(380.), px(200.)));
 49        builder.line_to(point(px(400.), px(260.)));
 50        builder.line_to(point(px(350.), px(220.)));
 51        builder.line_to(point(px(300.), px(260.)));
 52        builder.line_to(point(px(320.), px(200.)));
 53        builder.line_to(point(px(270.), px(160.)));
 54        builder.line_to(point(px(330.), px(160.)));
 55        builder.line_to(point(px(350.), px(100.)));
 56        let path = builder.build().unwrap();
 57        lines.push((
 58            path,
 59            linear_gradient(
 60                180.,
 61                linear_color_stop(rgb(0xFACC15), 0.7),
 62                linear_color_stop(rgb(0xD56D0C), 1.),
 63            )
 64            .color_space(ColorSpace::Oklab),
 65        ));
 66
 67        // draw linear gradient
 68        let square_bounds = Bounds {
 69            origin: point(px(450.), px(100.)),
 70            size: size(px(200.), px(80.)),
 71        };
 72        let height = square_bounds.size.height;
 73        let horizontal_offset = height;
 74        let vertical_offset = px(30.);
 75        let mut builder = PathBuilder::fill();
 76        builder.move_to(square_bounds.bottom_left());
 77        builder.curve_to(
 78            square_bounds.origin + point(horizontal_offset, vertical_offset),
 79            square_bounds.origin + point(px(0.0), vertical_offset),
 80        );
 81        builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
 82        builder.curve_to(
 83            square_bounds.bottom_right(),
 84            square_bounds.top_right() + point(px(0.0), vertical_offset),
 85        );
 86        builder.line_to(square_bounds.bottom_left());
 87        let path = builder.build().unwrap();
 88        lines.push((
 89            path,
 90            linear_gradient(
 91                180.,
 92                linear_color_stop(gpui::blue(), 0.4),
 93                linear_color_stop(gpui::red(), 1.),
 94            ),
 95        ));
 96
 97        // draw a pie chart
 98        let center = point(px(96.), px(96.));
 99        let pie_center = point(px(775.), px(155.));
100        let segments = [
101            (
102                point(px(871.), px(155.)),
103                point(px(747.), px(63.)),
104                rgb(0x1374e9),
105            ),
106            (
107                point(px(747.), px(63.)),
108                point(px(679.), px(163.)),
109                rgb(0xe13527),
110            ),
111            (
112                point(px(679.), px(163.)),
113                point(px(754.), px(249.)),
114                rgb(0x0751ce),
115            ),
116            (
117                point(px(754.), px(249.)),
118                point(px(854.), px(210.)),
119                rgb(0x209742),
120            ),
121            (
122                point(px(854.), px(210.)),
123                point(px(871.), px(155.)),
124                rgb(0xfbc10a),
125            ),
126        ];
127
128        for (start, end, color) in segments {
129            let mut builder = PathBuilder::fill();
130            builder.move_to(start);
131            builder.arc_to(center, px(0.), false, false, end);
132            builder.line_to(pie_center);
133            builder.close();
134            let path = builder.build().unwrap();
135            lines.push((path, color.into()));
136        }
137
138        // draw a wave
139        let options = StrokeOptions::default()
140            .with_line_width(1.)
141            .with_line_join(lyon::path::LineJoin::Bevel);
142        let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
143        builder.move_to(point(px(40.), px(320.)));
144        for i in 1..50 {
145            builder.line_to(point(
146                px(40.0 + i as f32 * 10.0),
147                px(320.0 + (i as f32 * 10.0).sin() * 40.0),
148            ));
149        }
150        let path = builder.build().unwrap();
151        lines.push((path, gpui::green().into()));
152
153        // draw the indicators (aligned and unaligned versions)
154        let aligned_indicator = breakpoint_indicator_path(
155            bounds(point(px(50.), px(250.)), size(px(60.), px(16.))),
156            1.0,
157            false,
158        );
159        lines.push((aligned_indicator, rgb(0x1e88e5).into()));
160
161        Self {
162            default_lines: lines.clone(),
163            lines: vec![],
164            start: point(px(0.), px(0.)),
165            dashed: false,
166            _painting: false,
167        }
168    }
169
170    fn clear(&mut self, cx: &mut Context<Self>) {
171        self.lines.clear();
172        cx.notify();
173    }
174}
175
176fn button(
177    text: &str,
178    cx: &mut Context<PaintingViewer>,
179    on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
180) -> impl IntoElement {
181    div()
182        .id(SharedString::from(text.to_string()))
183        .child(text.to_string())
184        .bg(gpui::black())
185        .text_color(gpui::white())
186        .active(|this| this.opacity(0.8))
187        .flex()
188        .px_3()
189        .py_1()
190        .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
191}
192
193impl Render for PaintingViewer {
194    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
195        let default_lines = self.default_lines.clone();
196        let lines = self.lines.clone();
197        let dashed = self.dashed;
198
199        div()
200            .font_family(".SystemUIFont")
201            .bg(gpui::white())
202            .size_full()
203            .p_4()
204            .flex()
205            .flex_col()
206            .child(
207                div()
208                    .flex()
209                    .gap_2()
210                    .justify_between()
211                    .items_center()
212                    .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
213                    .child(
214                        div()
215                            .flex()
216                            .gap_x_2()
217                            .child(button(
218                                if dashed { "Solid" } else { "Dashed" },
219                                cx,
220                                move |this, _| this.dashed = !dashed,
221                            ))
222                            .child(button("Clear", cx, |this, cx| this.clear(cx))),
223                    ),
224            )
225            .child(
226                div()
227                    .size_full()
228                    .child(
229                        canvas(
230                            move |_, _, _| {},
231                            move |_, _, window, _| {
232                                for (path, color) in default_lines {
233                                    window.paint_path(path, color);
234                                }
235
236                                for points in lines {
237                                    if points.len() < 2 {
238                                        continue;
239                                    }
240
241                                    let mut builder = PathBuilder::stroke(px(1.));
242                                    if dashed {
243                                        builder = builder.dash_array(&[px(4.), px(2.)]);
244                                    }
245                                    for (i, p) in points.into_iter().enumerate() {
246                                        if i == 0 {
247                                            builder.move_to(p);
248                                        } else {
249                                            builder.line_to(p);
250                                        }
251                                    }
252
253                                    if let Ok(path) = builder.build() {
254                                        window.paint_path(path, gpui::black());
255                                    }
256                                }
257                            },
258                        )
259                        .size_full(),
260                    )
261                    .on_mouse_down(
262                        gpui::MouseButton::Left,
263                        cx.listener(|this, ev: &MouseDownEvent, _, _| {
264                            this._painting = true;
265                            this.start = ev.position;
266                            let path = vec![ev.position];
267                            this.lines.push(path);
268                        }),
269                    )
270                    .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
271                        if !this._painting {
272                            return;
273                        }
274
275                        let is_shifted = ev.modifiers.shift;
276                        let mut pos = ev.position;
277                        // When holding shift, draw a straight line
278                        if is_shifted {
279                            let dx = pos.x - this.start.x;
280                            let dy = pos.y - this.start.y;
281                            if dx.abs() > dy.abs() {
282                                pos.y = this.start.y;
283                            } else {
284                                pos.x = this.start.x;
285                            }
286                        }
287
288                        if let Some(path) = this.lines.last_mut() {
289                            path.push(pos);
290                        }
291
292                        cx.notify();
293                    }))
294                    .on_mouse_up(
295                        gpui::MouseButton::Left,
296                        cx.listener(|this, _, _, _| {
297                            this._painting = false;
298                        }),
299                    ),
300            )
301    }
302}
303
304fn main() {
305    Application::new().run(|cx| {
306        cx.open_window(
307            WindowOptions {
308                focus: true,
309                ..Default::default()
310            },
311            |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
312        )
313        .unwrap();
314        cx.activate(true);
315    });
316}
317
318/// Draw the path for the breakpoint indicator.
319///
320/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
321/// wide to draw without graphical issues, so it will ignore narrower width.
322fn breakpoint_indicator_path(bounds: Bounds<Pixels>, scale: f32, stroke: bool) -> Path<Pixels> {
323    static MIN_WIDTH: f32 = 31.;
324
325    // Apply user scale to the minimum width
326    let min_width = MIN_WIDTH * scale;
327
328    let width = if bounds.size.width.0 < min_width {
329        px(min_width)
330    } else {
331        bounds.size.width
332    };
333    let height = bounds.size.height;
334
335    // Position the indicator on the canvas
336    let base_x = bounds.origin.x;
337    let base_y = bounds.origin.y;
338
339    // Calculate the scaling factor for the height (SVG is 15px tall), incorporating user scale
340    let scale_factor = (height / px(15.0)) * scale;
341
342    // Calculate how much width to allocate to the stretchable middle section
343    // SVG has 32px of fixed elements (corners), so the rest is for the middle
344    let fixed_width = px(32.0) * scale_factor;
345    let middle_width = width - fixed_width;
346
347    // Helper function to round to nearest quarter pixel
348    let round_to_quarter = |value: Pixels| -> Pixels {
349        let value_f32: f32 = value.into();
350        px((value_f32 * 4.0).round() / 4.0)
351    };
352
353    // Create a new path - either fill or stroke based on the flag
354    let mut builder = if stroke {
355        // For stroke, we need to set appropriate line width and options
356        let stroke_width = px(1.0 * scale); // Apply scale to stroke width
357        let options = StrokeOptions::default().with_line_width(stroke_width.0);
358
359        PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
360    } else {
361        // For fill, use the original implementation
362        PathBuilder::fill()
363    };
364
365    // Upper half of the shape - Based on the provided SVG
366    // Start at bottom left (0, 8)
367    let start_x = round_to_quarter(base_x);
368    let start_y = round_to_quarter(base_y + px(7.5) * scale_factor);
369    builder.move_to(point(start_x, start_y));
370
371    // Vertical line to (0, 5)
372    let vert_y = round_to_quarter(base_y + px(5.0) * scale_factor);
373    builder.line_to(point(start_x, vert_y));
374
375    // Curve to (5, 0) - using cubic Bezier
376    let curve1_end_x = round_to_quarter(base_x + px(5.0) * scale_factor);
377    let curve1_end_y = round_to_quarter(base_y);
378    let curve1_ctrl1_x = round_to_quarter(base_x);
379    let curve1_ctrl1_y = round_to_quarter(base_y + px(1.5) * scale_factor);
380    let curve1_ctrl2_x = round_to_quarter(base_x + px(1.5) * scale_factor);
381    let curve1_ctrl2_y = round_to_quarter(base_y);
382    builder.cubic_bezier_to(
383        point(curve1_end_x, curve1_end_y),
384        point(curve1_ctrl1_x, curve1_ctrl1_y),
385        point(curve1_ctrl2_x, curve1_ctrl2_y),
386    );
387
388    // Horizontal line through the middle section to (37, 0)
389    let middle_end_x = round_to_quarter(base_x + px(5.0) * scale_factor + middle_width);
390    builder.line_to(point(middle_end_x, curve1_end_y));
391
392    // Horizontal line to (41, 0)
393    let right_section_x =
394        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(4.0) * scale_factor);
395    builder.line_to(point(right_section_x, curve1_end_y));
396
397    // Curve to (50, 7.5) - using cubic Bezier
398    let curve2_end_x =
399        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
400    let curve2_end_y = round_to_quarter(base_y + px(7.5) * scale_factor);
401    let curve2_ctrl1_x =
402        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
403    let curve2_ctrl1_y = round_to_quarter(base_y);
404    let curve2_ctrl2_x =
405        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
406    let curve2_ctrl2_y = round_to_quarter(base_y + px(6.0) * scale_factor);
407    builder.cubic_bezier_to(
408        point(curve2_end_x, curve2_end_y),
409        point(curve2_ctrl1_x, curve2_ctrl1_y),
410        point(curve2_ctrl2_x, curve2_ctrl2_y),
411    );
412
413    // Lower half of the shape - mirrored vertically
414    // Curve from (50, 7.5) to (41, 15)
415    let curve3_end_y = round_to_quarter(base_y + px(15.0) * scale_factor);
416    let curve3_ctrl1_x =
417        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
418    let curve3_ctrl1_y = round_to_quarter(base_y + px(9.0) * scale_factor);
419    let curve3_ctrl2_x =
420        round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
421    let curve3_ctrl2_y = round_to_quarter(base_y + px(15.0) * scale_factor);
422    builder.cubic_bezier_to(
423        point(right_section_x, curve3_end_y),
424        point(curve3_ctrl1_x, curve3_ctrl1_y),
425        point(curve3_ctrl2_x, curve3_ctrl2_y),
426    );
427
428    // Horizontal line to (37, 15)
429    builder.line_to(point(middle_end_x, curve3_end_y));
430
431    // Horizontal line through the middle section to (5, 15)
432    builder.line_to(point(curve1_end_x, curve3_end_y));
433
434    // Curve to (0, 10)
435    let curve4_end_y = round_to_quarter(base_y + px(10.0) * scale_factor);
436    let curve4_ctrl1_x = round_to_quarter(base_x + px(1.5) * scale_factor);
437    let curve4_ctrl1_y = round_to_quarter(base_y + px(15.0) * scale_factor);
438    let curve4_ctrl2_x = round_to_quarter(base_x);
439    let curve4_ctrl2_y = round_to_quarter(base_y + px(13.5) * scale_factor);
440    builder.cubic_bezier_to(
441        point(start_x, curve4_end_y),
442        point(curve4_ctrl1_x, curve4_ctrl1_y),
443        point(curve4_ctrl2_x, curve4_ctrl2_y),
444    );
445
446    // Close the path
447    builder.line_to(point(start_x, start_y));
448
449    builder.build().unwrap()
450}