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