painting.rs

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