painting.rs

  1use gpui::{
  2    Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
  3    PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowBounds,
  4    WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb,
  5    size,
  6};
  7
  8const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
  9const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0);
 10
 11struct PaintingViewer {
 12    default_lines: Vec<(Path<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        // draw a Rust logo
 24        let mut builder = lyon::path::Path::svg_builder();
 25        lyon::extra::rust_logo::build_logo_path(&mut builder);
 26        // move down the Path
 27        let mut builder: PathBuilder = builder.into();
 28        builder.translate(point(px(10.), px(100.)));
 29        builder.scale(0.9);
 30        let path = builder.build().unwrap();
 31        lines.push((path, gpui::black().into()));
 32
 33        // draw a lightening bolt ⚡
 34        let mut builder = PathBuilder::fill();
 35        builder.add_polygon(
 36            &[
 37                point(px(150.), px(200.)),
 38                point(px(200.), px(125.)),
 39                point(px(200.), px(175.)),
 40                point(px(250.), px(100.)),
 41            ],
 42            false,
 43        );
 44        let path = builder.build().unwrap();
 45        lines.push((path, rgb(0x1d4ed8).into()));
 46
 47        // draw a ⭐
 48        let mut builder = PathBuilder::fill();
 49        builder.move_to(point(px(350.), px(100.)));
 50        builder.line_to(point(px(370.), px(160.)));
 51        builder.line_to(point(px(430.), px(160.)));
 52        builder.line_to(point(px(380.), px(200.)));
 53        builder.line_to(point(px(400.), px(260.)));
 54        builder.line_to(point(px(350.), px(220.)));
 55        builder.line_to(point(px(300.), px(260.)));
 56        builder.line_to(point(px(320.), px(200.)));
 57        builder.line_to(point(px(270.), px(160.)));
 58        builder.line_to(point(px(330.), px(160.)));
 59        builder.line_to(point(px(350.), px(100.)));
 60        let path = builder.build().unwrap();
 61        lines.push((
 62            path,
 63            linear_gradient(
 64                180.,
 65                linear_color_stop(rgb(0xFACC15), 0.7),
 66                linear_color_stop(rgb(0xD56D0C), 1.),
 67            )
 68            .color_space(ColorSpace::Oklab),
 69        ));
 70
 71        // draw linear gradient
 72        let square_bounds = Bounds {
 73            origin: point(px(450.), px(100.)),
 74            size: size(px(200.), px(80.)),
 75        };
 76        let height = square_bounds.size.height;
 77        let horizontal_offset = height;
 78        let vertical_offset = px(30.);
 79        let mut builder = PathBuilder::fill();
 80        builder.move_to(square_bounds.bottom_left());
 81        builder.curve_to(
 82            square_bounds.origin + point(horizontal_offset, vertical_offset),
 83            square_bounds.origin + point(px(0.0), vertical_offset),
 84        );
 85        builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
 86        builder.curve_to(
 87            square_bounds.bottom_right(),
 88            square_bounds.top_right() + point(px(0.0), vertical_offset),
 89        );
 90        builder.line_to(square_bounds.bottom_left());
 91        let path = builder.build().unwrap();
 92        lines.push((
 93            path,
 94            linear_gradient(
 95                180.,
 96                linear_color_stop(gpui::blue(), 0.4),
 97                linear_color_stop(gpui::red(), 1.),
 98            ),
 99        ));
100
101        // draw a pie chart
102        let center = point(px(96.), px(96.));
103        let pie_center = point(px(775.), px(155.));
104        let segments = [
105            (
106                point(px(871.), px(155.)),
107                point(px(747.), px(63.)),
108                rgb(0x1374e9),
109            ),
110            (
111                point(px(747.), px(63.)),
112                point(px(679.), px(163.)),
113                rgb(0xe13527),
114            ),
115            (
116                point(px(679.), px(163.)),
117                point(px(754.), px(249.)),
118                rgb(0x0751ce),
119            ),
120            (
121                point(px(754.), px(249.)),
122                point(px(854.), px(210.)),
123                rgb(0x209742),
124            ),
125            (
126                point(px(854.), px(210.)),
127                point(px(871.), px(155.)),
128                rgb(0xfbc10a),
129            ),
130        ];
131
132        for (start, end, color) in segments {
133            let mut builder = PathBuilder::fill();
134            builder.move_to(start);
135            builder.arc_to(center, px(0.), false, false, end);
136            builder.line_to(pie_center);
137            builder.close();
138            let path = builder.build().unwrap();
139            lines.push((path, color.into()));
140        }
141
142        // draw a wave
143        let options = StrokeOptions::default()
144            .with_line_width(1.)
145            .with_line_join(lyon::path::LineJoin::Bevel);
146        let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
147        builder.move_to(point(px(40.), px(320.)));
148        for i in 1..50 {
149            builder.line_to(point(
150                px(40.0 + i as f32 * 10.0),
151                px(320.0 + (i as f32 * 10.0).sin() * 40.0),
152            ));
153        }
154
155        Self {
156            default_lines: lines.clone(),
157            lines: vec![],
158            start: point(px(0.), px(0.)),
159            dashed: false,
160            _painting: false,
161        }
162    }
163
164    fn clear(&mut self, cx: &mut Context<Self>) {
165        self.lines.clear();
166        cx.notify();
167    }
168}
169
170fn button(
171    text: &str,
172    cx: &mut Context<PaintingViewer>,
173    on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
174) -> impl IntoElement {
175    div()
176        .id(SharedString::from(text.to_string()))
177        .child(text.to_string())
178        .bg(gpui::black())
179        .text_color(gpui::white())
180        .active(|this| this.opacity(0.8))
181        .flex()
182        .px_3()
183        .py_1()
184        .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
185}
186
187impl Render for PaintingViewer {
188    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
189        window.request_animation_frame();
190
191        let default_lines = self.default_lines.clone();
192        let lines = self.lines.clone();
193        let window_size = window.bounds().size;
194        let scale = window_size.width / DEFAULT_WINDOW_WIDTH;
195        let dashed = self.dashed;
196
197        div()
198            .font_family(".SystemUIFont")
199            .bg(gpui::white())
200            .size_full()
201            .p_4()
202            .flex()
203            .flex_col()
204            .child(
205                div()
206                    .flex()
207                    .gap_2()
208                    .justify_between()
209                    .items_center()
210                    .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
211                    .child(
212                        div()
213                            .flex()
214                            .gap_x_2()
215                            .child(button(
216                                if dashed { "Solid" } else { "Dashed" },
217                                cx,
218                                move |this, _| this.dashed = !dashed,
219                            ))
220                            .child(button("Clear", cx, |this, cx| this.clear(cx))),
221                    ),
222            )
223            .child(
224                div()
225                    .size_full()
226                    .child(
227                        canvas(
228                            move |_, _, _| {},
229                            move |_, _, window, _| {
230                                for (path, color) in default_lines {
231                                    window.paint_path(path.clone().scale(scale), color);
232                                }
233
234                                for points in lines {
235                                    if points.len() < 2 {
236                                        continue;
237                                    }
238
239                                    let mut builder = PathBuilder::stroke(px(1.));
240                                    if dashed {
241                                        builder = builder.dash_array(&[px(4.), px(2.)]);
242                                    }
243                                    for (i, p) in points.into_iter().enumerate() {
244                                        if i == 0 {
245                                            builder.move_to(p);
246                                        } else {
247                                            builder.line_to(p);
248                                        }
249                                    }
250
251                                    if let Ok(path) = builder.build() {
252                                        window.paint_path(path, gpui::black());
253                                    }
254                                }
255                            },
256                        )
257                        .size_full(),
258                    )
259                    .on_mouse_down(
260                        gpui::MouseButton::Left,
261                        cx.listener(|this, ev: &MouseDownEvent, _, _| {
262                            this._painting = true;
263                            this.start = ev.position;
264                            let path = vec![ev.position];
265                            this.lines.push(path);
266                        }),
267                    )
268                    .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
269                        if !this._painting {
270                            return;
271                        }
272
273                        let is_shifted = ev.modifiers.shift;
274                        let mut pos = ev.position;
275                        // When holding shift, draw a straight line
276                        if is_shifted {
277                            let dx = pos.x - this.start.x;
278                            let dy = pos.y - this.start.y;
279                            if dx.abs() > dy.abs() {
280                                pos.y = this.start.y;
281                            } else {
282                                pos.x = this.start.x;
283                            }
284                        }
285
286                        if let Some(path) = this.lines.last_mut() {
287                            path.push(pos);
288                        }
289
290                        cx.notify();
291                    }))
292                    .on_mouse_up(
293                        gpui::MouseButton::Left,
294                        cx.listener(|this, _, _, _| {
295                            this._painting = false;
296                        }),
297                    ),
298            )
299    }
300}
301
302fn main() {
303    Application::new().run(|cx| {
304        cx.open_window(
305            WindowOptions {
306                focus: true,
307                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
308                    None,
309                    size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
310                    cx,
311                ))),
312                ..Default::default()
313            },
314            |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
315        )
316        .unwrap();
317        cx.activate(true);
318    });
319}