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