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