painting.rs

  1use gpui::{
  2    Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
  3    PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
  4    div, linear_color_stop, linear_gradient, point, prelude::*, px, quad, rgb, size,
  5};
  6
  7struct PaintingViewer {
  8    default_lines: Vec<(Path<Pixels>, Background)>,
  9    background_quads: Vec<(Bounds<Pixels>, Background)>,
 10    lines: Vec<Vec<Point<Pixels>>>,
 11    start: Point<Pixels>,
 12    dashed: bool,
 13    _painting: bool,
 14}
 15
 16impl PaintingViewer {
 17    fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
 18        let mut lines = vec![];
 19
 20        // Black squares beneath transparent paths.
 21        let background_quads = vec![
 22            (
 23                Bounds {
 24                    origin: point(px(70.), px(70.)),
 25                    size: size(px(40.), px(40.)),
 26                },
 27                gpui::black().into(),
 28            ),
 29            (
 30                Bounds {
 31                    origin: point(px(170.), px(70.)),
 32                    size: size(px(40.), px(40.)),
 33                },
 34                gpui::black().into(),
 35            ),
 36            (
 37                Bounds {
 38                    origin: point(px(270.), px(70.)),
 39                    size: size(px(40.), px(40.)),
 40                },
 41                gpui::black().into(),
 42            ),
 43            (
 44                Bounds {
 45                    origin: point(px(370.), px(70.)),
 46                    size: size(px(40.), px(40.)),
 47                },
 48                gpui::black().into(),
 49            ),
 50            (
 51                Bounds {
 52                    origin: point(px(450.), px(50.)),
 53                    size: size(px(80.), px(80.)),
 54                },
 55                gpui::black().into(),
 56            ),
 57        ];
 58
 59        // 50% opaque red path that extends across black quad.
 60        let mut builder = PathBuilder::fill();
 61        builder.move_to(point(px(50.), px(50.)));
 62        builder.line_to(point(px(130.), px(50.)));
 63        builder.line_to(point(px(130.), px(130.)));
 64        builder.line_to(point(px(50.), px(130.)));
 65        builder.close();
 66        let path = builder.build().unwrap();
 67        let mut red = rgb(0xFF0000);
 68        red.a = 0.5;
 69        lines.push((path, red.into()));
 70
 71        // 50% opaque blue path that extends across black quad.
 72        let mut builder = PathBuilder::fill();
 73        builder.move_to(point(px(150.), px(50.)));
 74        builder.line_to(point(px(230.), px(50.)));
 75        builder.line_to(point(px(230.), px(130.)));
 76        builder.line_to(point(px(150.), px(130.)));
 77        builder.close();
 78        let path = builder.build().unwrap();
 79        let mut blue = rgb(0x0000FF);
 80        blue.a = 0.5;
 81        lines.push((path, blue.into()));
 82
 83        // 50% opaque green path that extends across black quad.
 84        let mut builder = PathBuilder::fill();
 85        builder.move_to(point(px(250.), px(50.)));
 86        builder.line_to(point(px(330.), px(50.)));
 87        builder.line_to(point(px(330.), px(130.)));
 88        builder.line_to(point(px(250.), px(130.)));
 89        builder.close();
 90        let path = builder.build().unwrap();
 91        let mut green = rgb(0x00FF00);
 92        green.a = 0.5;
 93        lines.push((path, green.into()));
 94
 95        // 50% opaque black path that extends across black quad.
 96        let mut builder = PathBuilder::fill();
 97        builder.move_to(point(px(350.), px(50.)));
 98        builder.line_to(point(px(430.), px(50.)));
 99        builder.line_to(point(px(430.), px(130.)));
100        builder.line_to(point(px(350.), px(130.)));
101        builder.close();
102        let path = builder.build().unwrap();
103        let mut black = rgb(0x000000);
104        black.a = 0.5;
105        lines.push((path, black.into()));
106
107        // Two 50% opaque red circles overlapping - center should be darker red
108        let mut builder = PathBuilder::fill();
109        let center = point(px(530.), px(85.));
110        let radius = px(30.);
111        builder.move_to(point(center.x + radius, center.y));
112        builder.arc_to(
113            point(radius, radius),
114            px(0.),
115            false,
116            false,
117            point(center.x - radius, center.y),
118        );
119        builder.arc_to(
120            point(radius, radius),
121            px(0.),
122            false,
123            false,
124            point(center.x + radius, center.y),
125        );
126        builder.close();
127        let path = builder.build().unwrap();
128        let mut red1 = rgb(0xFF0000);
129        red1.a = 0.5;
130        lines.push((path, red1.into()));
131
132        let mut builder = PathBuilder::fill();
133        let center = point(px(570.), px(85.));
134        let radius = px(30.);
135        builder.move_to(point(center.x + radius, center.y));
136        builder.arc_to(
137            point(radius, radius),
138            px(0.),
139            false,
140            false,
141            point(center.x - radius, center.y),
142        );
143        builder.arc_to(
144            point(radius, radius),
145            px(0.),
146            false,
147            false,
148            point(center.x + radius, center.y),
149        );
150        builder.close();
151        let path = builder.build().unwrap();
152        let mut red2 = rgb(0xFF0000);
153        red2.a = 0.5;
154        lines.push((path, red2.into()));
155
156        // draw a Rust logo
157        let mut builder = lyon::path::Path::svg_builder();
158        lyon::extra::rust_logo::build_logo_path(&mut builder);
159        // move down the Path
160        let mut builder: PathBuilder = builder.into();
161        builder.translate(point(px(10.), px(200.)));
162        builder.scale(0.9);
163        let path = builder.build().unwrap();
164        lines.push((path, gpui::black().into()));
165
166        // draw a lightening bolt ⚡
167        let mut builder = PathBuilder::fill();
168        builder.add_polygon(
169            &[
170                point(px(150.), px(300.)),
171                point(px(200.), px(225.)),
172                point(px(200.), px(275.)),
173                point(px(250.), px(200.)),
174            ],
175            false,
176        );
177        let path = builder.build().unwrap();
178        lines.push((path, rgb(0x1d4ed8).into()));
179
180        // draw a ⭐
181        let mut builder = PathBuilder::fill();
182        builder.move_to(point(px(350.), px(200.)));
183        builder.line_to(point(px(370.), px(260.)));
184        builder.line_to(point(px(430.), px(260.)));
185        builder.line_to(point(px(380.), px(300.)));
186        builder.line_to(point(px(400.), px(360.)));
187        builder.line_to(point(px(350.), px(320.)));
188        builder.line_to(point(px(300.), px(360.)));
189        builder.line_to(point(px(320.), px(300.)));
190        builder.line_to(point(px(270.), px(260.)));
191        builder.line_to(point(px(330.), px(260.)));
192        builder.line_to(point(px(350.), px(200.)));
193        let path = builder.build().unwrap();
194        lines.push((
195            path,
196            linear_gradient(
197                180.,
198                linear_color_stop(rgb(0xFACC15), 0.7),
199                linear_color_stop(rgb(0xD56D0C), 1.),
200            )
201            .color_space(ColorSpace::Oklab),
202        ));
203
204        // draw linear gradient
205        let square_bounds = Bounds {
206            origin: point(px(450.), px(200.)),
207            size: size(px(200.), px(80.)),
208        };
209        let height = square_bounds.size.height;
210        let horizontal_offset = height;
211        let vertical_offset = px(30.);
212        let mut builder = PathBuilder::fill();
213        builder.move_to(square_bounds.bottom_left());
214        builder.curve_to(
215            square_bounds.origin + point(horizontal_offset, vertical_offset),
216            square_bounds.origin + point(px(0.0), vertical_offset),
217        );
218        builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
219        builder.curve_to(
220            square_bounds.bottom_right(),
221            square_bounds.top_right() + point(px(0.0), vertical_offset),
222        );
223        builder.line_to(square_bounds.bottom_left());
224        let path = builder.build().unwrap();
225        lines.push((
226            path,
227            linear_gradient(
228                180.,
229                linear_color_stop(gpui::blue(), 0.4),
230                linear_color_stop(gpui::red(), 1.),
231            ),
232        ));
233
234        // draw a pie chart
235        let center = point(px(96.), px(96.));
236        let pie_center = point(px(775.), px(255.));
237        let segments = [
238            (
239                point(px(871.), px(255.)),
240                point(px(747.), px(163.)),
241                rgb(0x1374e9),
242            ),
243            (
244                point(px(747.), px(163.)),
245                point(px(679.), px(263.)),
246                rgb(0xe13527),
247            ),
248            (
249                point(px(679.), px(263.)),
250                point(px(754.), px(349.)),
251                rgb(0x0751ce),
252            ),
253            (
254                point(px(754.), px(349.)),
255                point(px(854.), px(310.)),
256                rgb(0x209742),
257            ),
258            (
259                point(px(854.), px(310.)),
260                point(px(871.), px(255.)),
261                rgb(0xfbc10a),
262            ),
263        ];
264
265        for (start, end, color) in segments {
266            let mut builder = PathBuilder::fill();
267            builder.move_to(start);
268            builder.arc_to(center, px(0.), false, false, end);
269            builder.line_to(pie_center);
270            builder.close();
271            let path = builder.build().unwrap();
272            lines.push((path, color.into()));
273        }
274
275        // draw a wave
276        let options = StrokeOptions::default()
277            .with_line_width(1.)
278            .with_line_join(lyon::path::LineJoin::Bevel);
279        let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
280        builder.move_to(point(px(40.), px(420.)));
281        for i in 1..50 {
282            builder.line_to(point(
283                px(40.0 + i as f32 * 10.0),
284                px(420.0 + (i as f32 * 10.0).sin() * 40.0),
285            ));
286        }
287        let path = builder.build().unwrap();
288        lines.push((path, gpui::green().into()));
289
290        Self {
291            default_lines: lines.clone(),
292            background_quads,
293            lines: vec![],
294            start: point(px(0.), px(0.)),
295            dashed: false,
296            _painting: false,
297        }
298    }
299
300    fn clear(&mut self, cx: &mut Context<Self>) {
301        self.lines.clear();
302        cx.notify();
303    }
304}
305
306fn button(
307    text: &str,
308    cx: &mut Context<PaintingViewer>,
309    on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
310) -> impl IntoElement {
311    div()
312        .id(SharedString::from(text.to_string()))
313        .child(text.to_string())
314        .bg(gpui::black())
315        .text_color(gpui::white())
316        .active(|this| this.opacity(0.8))
317        .flex()
318        .px_3()
319        .py_1()
320        .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
321}
322
323impl Render for PaintingViewer {
324    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
325        let default_lines = self.default_lines.clone();
326        let background_quads = self.background_quads.clone();
327        let lines = self.lines.clone();
328        let dashed = self.dashed;
329
330        div()
331            .bg(gpui::white())
332            .size_full()
333            .p_4()
334            .flex()
335            .flex_col()
336            .child(
337                div()
338                    .flex()
339                    .gap_2()
340                    .justify_between()
341                    .items_center()
342                    .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
343                    .child(
344                        div()
345                            .flex()
346                            .gap_x_2()
347                            .child(button(
348                                if dashed { "Solid" } else { "Dashed" },
349                                cx,
350                                move |this, _| this.dashed = !dashed,
351                            ))
352                            .child(button("Clear", cx, |this, cx| this.clear(cx))),
353                    ),
354            )
355            .child(
356                div()
357                    .size_full()
358                    .child(
359                        canvas(
360                            move |_, _, _| {},
361                            move |_, _, window, _| {
362                                // First draw background quads
363                                for (bounds, color) in background_quads.iter() {
364                                    window.paint_quad(quad(
365                                        *bounds,
366                                        px(0.),
367                                        *color,
368                                        px(0.),
369                                        gpui::transparent_black(),
370                                        Default::default(),
371                                    ));
372                                }
373
374                                // Then draw the default paths on top
375                                for (path, color) in default_lines {
376                                    window.paint_path(path, color);
377                                }
378
379                                for points in lines {
380                                    if points.len() < 2 {
381                                        continue;
382                                    }
383
384                                    let mut builder = PathBuilder::stroke(px(1.));
385                                    if dashed {
386                                        builder = builder.dash_array(&[px(4.), px(2.)]);
387                                    }
388                                    for (i, p) in points.into_iter().enumerate() {
389                                        if i == 0 {
390                                            builder.move_to(p);
391                                        } else {
392                                            builder.line_to(p);
393                                        }
394                                    }
395
396                                    if let Ok(path) = builder.build() {
397                                        window.paint_path(path, gpui::black());
398                                    }
399                                }
400                            },
401                        )
402                        .size_full(),
403                    )
404                    .on_mouse_down(
405                        gpui::MouseButton::Left,
406                        cx.listener(|this, ev: &MouseDownEvent, _, _| {
407                            this._painting = true;
408                            this.start = ev.position;
409                            let path = vec![ev.position];
410                            this.lines.push(path);
411                        }),
412                    )
413                    .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
414                        if !this._painting {
415                            return;
416                        }
417
418                        let is_shifted = ev.modifiers.shift;
419                        let mut pos = ev.position;
420                        // When holding shift, draw a straight line
421                        if is_shifted {
422                            let dx = pos.x - this.start.x;
423                            let dy = pos.y - this.start.y;
424                            if dx.abs() > dy.abs() {
425                                pos.y = this.start.y;
426                            } else {
427                                pos.x = this.start.x;
428                            }
429                        }
430
431                        if let Some(path) = this.lines.last_mut() {
432                            path.push(pos);
433                        }
434
435                        cx.notify();
436                    }))
437                    .on_mouse_up(
438                        gpui::MouseButton::Left,
439                        cx.listener(|this, _, _, _| {
440                            this._painting = false;
441                        }),
442                    ),
443            )
444    }
445}
446
447fn main() {
448    Application::new().run(|cx| {
449        cx.open_window(
450            WindowOptions {
451                focus: true,
452                ..Default::default()
453            },
454            |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
455        )
456        .unwrap();
457        cx.on_window_closed(|cx| {
458            cx.quit();
459        })
460        .detach();
461        cx.activate(true);
462    });
463}