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}