painting.rs

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