use gpui::{ Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, }; struct PaintingViewer { default_lines: Vec<(Path, Background)>, lines: Vec>>, start: Point, _painting: bool, } impl PaintingViewer { fn new(_window: &mut Window, _cx: &mut Context) -> Self { let mut lines = vec![]; // draw a Rust logo let mut builder = lyon::path::Path::svg_builder(); lyon::extra::rust_logo::build_logo_path(&mut builder); // move down the Path let mut builder: PathBuilder = builder.into(); builder.translate(point(px(10.), px(100.))); builder.scale(0.9); let path = builder.build().unwrap(); lines.push((path, gpui::black().into())); // draw a lightening bolt ⚡ let mut builder = PathBuilder::fill(); builder.add_polygon( &[ point(px(150.), px(200.)), point(px(200.), px(125.)), point(px(200.), px(175.)), point(px(250.), px(100.)), ], false, ); let path = builder.build().unwrap(); lines.push((path, rgb(0x1d4ed8).into())); // draw a ⭐ let mut builder = PathBuilder::fill(); builder.move_to(point(px(350.), px(100.))); builder.line_to(point(px(370.), px(160.))); builder.line_to(point(px(430.), px(160.))); builder.line_to(point(px(380.), px(200.))); builder.line_to(point(px(400.), px(260.))); builder.line_to(point(px(350.), px(220.))); builder.line_to(point(px(300.), px(260.))); builder.line_to(point(px(320.), px(200.))); builder.line_to(point(px(270.), px(160.))); builder.line_to(point(px(330.), px(160.))); builder.line_to(point(px(350.), px(100.))); let path = builder.build().unwrap(); lines.push(( path, linear_gradient( 180., linear_color_stop(rgb(0xFACC15), 0.7), linear_color_stop(rgb(0xD56D0C), 1.), ) .color_space(ColorSpace::Oklab), )); // draw linear gradient let square_bounds = Bounds { origin: point(px(450.), px(100.)), size: size(px(200.), px(80.)), }; let height = square_bounds.size.height; let horizontal_offset = height; let vertical_offset = px(30.); let mut builder = PathBuilder::fill(); builder.move_to(square_bounds.bottom_left()); builder.curve_to( square_bounds.origin + point(horizontal_offset, vertical_offset), square_bounds.origin + point(px(0.0), vertical_offset), ); builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); builder.curve_to( square_bounds.bottom_right(), square_bounds.top_right() + point(px(0.0), vertical_offset), ); builder.line_to(square_bounds.bottom_left()); let path = builder.build().unwrap(); lines.push(( path, linear_gradient( 180., linear_color_stop(gpui::blue(), 0.4), linear_color_stop(gpui::red(), 1.), ), )); // draw a pie chart let center = point(px(96.), px(96.)); let pie_center = point(px(775.), px(155.)); let segments = [ ( point(px(871.), px(155.)), point(px(747.), px(63.)), rgb(0x1374e9), ), ( point(px(747.), px(63.)), point(px(679.), px(163.)), rgb(0xe13527), ), ( point(px(679.), px(163.)), point(px(754.), px(249.)), rgb(0x0751ce), ), ( point(px(754.), px(249.)), point(px(854.), px(210.)), rgb(0x209742), ), ( point(px(854.), px(210.)), point(px(871.), px(155.)), rgb(0xfbc10a), ), ]; for (start, end, color) in segments { let mut builder = PathBuilder::fill(); builder.move_to(start); builder.arc_to(center, px(0.), false, false, end); builder.line_to(pie_center); builder.close(); let path = builder.build().unwrap(); lines.push((path, color.into())); } // draw a wave let options = StrokeOptions::default() .with_line_width(1.) .with_line_join(lyon::path::LineJoin::Bevel); let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); builder.move_to(point(px(40.), px(320.))); for i in 0..50 { builder.line_to(point( px(40.0 + i as f32 * 10.0), px(320.0 + (i as f32 * 10.0).sin() * 40.0), )); } let path = builder.build().unwrap(); lines.push((path, gpui::green().into())); Self { default_lines: lines.clone(), lines: vec![], start: point(px(0.), px(0.)), _painting: false, } } fn clear(&mut self, cx: &mut Context) { self.lines.clear(); cx.notify(); } } impl Render for PaintingViewer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let default_lines = self.default_lines.clone(); let lines = self.lines.clone(); div() .font_family(".SystemUIFont") .bg(gpui::white()) .size_full() .p_4() .flex() .flex_col() .child( div() .flex() .gap_2() .justify_between() .items_center() .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") .child( div() .id("clear") .child("Clean up") .bg(gpui::black()) .text_color(gpui::white()) .active(|this| this.opacity(0.8)) .flex() .px_3() .py_1() .on_click(cx.listener(|this, _, _, cx| { this.clear(cx); })), ), ) .child( div() .size_full() .child( canvas( move |_, _, _| {}, move |_, _, window, _| { for (path, color) in default_lines { window.paint_path(path, color); } for points in lines { if points.len() < 2 { continue; } let mut builder = PathBuilder::stroke(px(1.)); for (i, p) in points.into_iter().enumerate() { if i == 0 { builder.move_to(p); } else { builder.line_to(p); } } if let Ok(path) = builder.build() { window.paint_path(path, gpui::black()); } } }, ) .size_full(), ) .on_mouse_down( gpui::MouseButton::Left, cx.listener(|this, ev: &MouseDownEvent, _, _| { this._painting = true; this.start = ev.position; let path = vec![ev.position]; this.lines.push(path); }), ) .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| { if !this._painting { return; } let is_shifted = ev.modifiers.shift; let mut pos = ev.position; // When holding shift, draw a straight line if is_shifted { let dx = pos.x - this.start.x; let dy = pos.y - this.start.y; if dx.abs() > dy.abs() { pos.y = this.start.y; } else { pos.x = this.start.x; } } if let Some(path) = this.lines.last_mut() { path.push(pos); } cx.notify(); })) .on_mouse_up( gpui::MouseButton::Left, cx.listener(|this, _, _, _| { this._painting = false; }), ), ) } } fn main() { Application::new().run(|cx| { cx.open_window( WindowOptions { focus: true, ..Default::default() }, |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), ) .unwrap(); cx.activate(true); }); }