1use gpui::{
2 Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
3 PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, bounds,
4 canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
5};
6
7struct PaintingViewer {
8 default_lines: Vec<(Path<Pixels>, Background)>,
9 lines: Vec<Vec<Point<Pixels>>>,
10 start: Point<Pixels>,
11 dashed: bool,
12 _painting: bool,
13}
14
15impl PaintingViewer {
16 fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
17 let mut lines = vec![];
18
19 // draw a Rust logo
20 let mut builder = lyon::path::Path::svg_builder();
21 lyon::extra::rust_logo::build_logo_path(&mut builder);
22 // move down the Path
23 let mut builder: PathBuilder = builder.into();
24 builder.translate(point(px(10.), px(100.)));
25 builder.scale(0.9);
26 let path = builder.build().unwrap();
27 lines.push((path, gpui::black().into()));
28
29 // draw a lightening bolt ⚡
30 let mut builder = PathBuilder::fill();
31 builder.add_polygon(
32 &[
33 point(px(150.), px(200.)),
34 point(px(200.), px(125.)),
35 point(px(200.), px(175.)),
36 point(px(250.), px(100.)),
37 ],
38 false,
39 );
40 let path = builder.build().unwrap();
41 lines.push((path, rgb(0x1d4ed8).into()));
42
43 // draw a ⭐
44 let mut builder = PathBuilder::fill();
45 builder.move_to(point(px(350.), px(100.)));
46 builder.line_to(point(px(370.), px(160.)));
47 builder.line_to(point(px(430.), px(160.)));
48 builder.line_to(point(px(380.), px(200.)));
49 builder.line_to(point(px(400.), px(260.)));
50 builder.line_to(point(px(350.), px(220.)));
51 builder.line_to(point(px(300.), px(260.)));
52 builder.line_to(point(px(320.), px(200.)));
53 builder.line_to(point(px(270.), px(160.)));
54 builder.line_to(point(px(330.), px(160.)));
55 builder.line_to(point(px(350.), px(100.)));
56 let path = builder.build().unwrap();
57 lines.push((
58 path,
59 linear_gradient(
60 180.,
61 linear_color_stop(rgb(0xFACC15), 0.7),
62 linear_color_stop(rgb(0xD56D0C), 1.),
63 )
64 .color_space(ColorSpace::Oklab),
65 ));
66
67 // draw linear gradient
68 let square_bounds = Bounds {
69 origin: point(px(450.), px(100.)),
70 size: size(px(200.), px(80.)),
71 };
72 let height = square_bounds.size.height;
73 let horizontal_offset = height;
74 let vertical_offset = px(30.);
75 let mut builder = PathBuilder::fill();
76 builder.move_to(square_bounds.bottom_left());
77 builder.curve_to(
78 square_bounds.origin + point(horizontal_offset, vertical_offset),
79 square_bounds.origin + point(px(0.0), vertical_offset),
80 );
81 builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
82 builder.curve_to(
83 square_bounds.bottom_right(),
84 square_bounds.top_right() + point(px(0.0), vertical_offset),
85 );
86 builder.line_to(square_bounds.bottom_left());
87 let path = builder.build().unwrap();
88 lines.push((
89 path,
90 linear_gradient(
91 180.,
92 linear_color_stop(gpui::blue(), 0.4),
93 linear_color_stop(gpui::red(), 1.),
94 ),
95 ));
96
97 // draw a pie chart
98 let center = point(px(96.), px(96.));
99 let pie_center = point(px(775.), px(155.));
100 let segments = [
101 (
102 point(px(871.), px(155.)),
103 point(px(747.), px(63.)),
104 rgb(0x1374e9),
105 ),
106 (
107 point(px(747.), px(63.)),
108 point(px(679.), px(163.)),
109 rgb(0xe13527),
110 ),
111 (
112 point(px(679.), px(163.)),
113 point(px(754.), px(249.)),
114 rgb(0x0751ce),
115 ),
116 (
117 point(px(754.), px(249.)),
118 point(px(854.), px(210.)),
119 rgb(0x209742),
120 ),
121 (
122 point(px(854.), px(210.)),
123 point(px(871.), px(155.)),
124 rgb(0xfbc10a),
125 ),
126 ];
127
128 for (start, end, color) in segments {
129 let mut builder = PathBuilder::fill();
130 builder.move_to(start);
131 builder.arc_to(center, px(0.), false, false, end);
132 builder.line_to(pie_center);
133 builder.close();
134 let path = builder.build().unwrap();
135 lines.push((path, color.into()));
136 }
137
138 // draw a wave
139 let options = StrokeOptions::default()
140 .with_line_width(1.)
141 .with_line_join(lyon::path::LineJoin::Bevel);
142 let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
143 builder.move_to(point(px(40.), px(320.)));
144 for i in 1..50 {
145 builder.line_to(point(
146 px(40.0 + i as f32 * 10.0),
147 px(320.0 + (i as f32 * 10.0).sin() * 40.0),
148 ));
149 }
150 let path = builder.build().unwrap();
151 lines.push((path, gpui::green().into()));
152
153 // draw the indicators (aligned and unaligned versions)
154 let aligned_indicator = breakpoint_indicator_path(
155 bounds(point(px(50.), px(250.)), size(px(60.), px(16.))),
156 1.0,
157 false,
158 );
159 lines.push((aligned_indicator, rgb(0x1e88e5).into()));
160
161 Self {
162 default_lines: lines.clone(),
163 lines: vec![],
164 start: point(px(0.), px(0.)),
165 dashed: false,
166 _painting: false,
167 }
168 }
169
170 fn clear(&mut self, cx: &mut Context<Self>) {
171 self.lines.clear();
172 cx.notify();
173 }
174}
175
176fn button(
177 text: &str,
178 cx: &mut Context<PaintingViewer>,
179 on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
180) -> impl IntoElement {
181 div()
182 .id(SharedString::from(text.to_string()))
183 .child(text.to_string())
184 .bg(gpui::black())
185 .text_color(gpui::white())
186 .active(|this| this.opacity(0.8))
187 .flex()
188 .px_3()
189 .py_1()
190 .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
191}
192
193impl Render for PaintingViewer {
194 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
195 let default_lines = self.default_lines.clone();
196 let lines = self.lines.clone();
197 let dashed = self.dashed;
198
199 div()
200 .font_family(".SystemUIFont")
201 .bg(gpui::white())
202 .size_full()
203 .p_4()
204 .flex()
205 .flex_col()
206 .child(
207 div()
208 .flex()
209 .gap_2()
210 .justify_between()
211 .items_center()
212 .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
213 .child(
214 div()
215 .flex()
216 .gap_x_2()
217 .child(button(
218 if dashed { "Solid" } else { "Dashed" },
219 cx,
220 move |this, _| this.dashed = !dashed,
221 ))
222 .child(button("Clear", cx, |this, cx| this.clear(cx))),
223 ),
224 )
225 .child(
226 div()
227 .size_full()
228 .child(
229 canvas(
230 move |_, _, _| {},
231 move |_, _, window, _| {
232 for (path, color) in default_lines {
233 window.paint_path(path, color);
234 }
235
236 for points in lines {
237 if points.len() < 2 {
238 continue;
239 }
240
241 let mut builder = PathBuilder::stroke(px(1.));
242 if dashed {
243 builder = builder.dash_array(&[px(4.), px(2.)]);
244 }
245 for (i, p) in points.into_iter().enumerate() {
246 if i == 0 {
247 builder.move_to(p);
248 } else {
249 builder.line_to(p);
250 }
251 }
252
253 if let Ok(path) = builder.build() {
254 window.paint_path(path, gpui::black());
255 }
256 }
257 },
258 )
259 .size_full(),
260 )
261 .on_mouse_down(
262 gpui::MouseButton::Left,
263 cx.listener(|this, ev: &MouseDownEvent, _, _| {
264 this._painting = true;
265 this.start = ev.position;
266 let path = vec![ev.position];
267 this.lines.push(path);
268 }),
269 )
270 .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
271 if !this._painting {
272 return;
273 }
274
275 let is_shifted = ev.modifiers.shift;
276 let mut pos = ev.position;
277 // When holding shift, draw a straight line
278 if is_shifted {
279 let dx = pos.x - this.start.x;
280 let dy = pos.y - this.start.y;
281 if dx.abs() > dy.abs() {
282 pos.y = this.start.y;
283 } else {
284 pos.x = this.start.x;
285 }
286 }
287
288 if let Some(path) = this.lines.last_mut() {
289 path.push(pos);
290 }
291
292 cx.notify();
293 }))
294 .on_mouse_up(
295 gpui::MouseButton::Left,
296 cx.listener(|this, _, _, _| {
297 this._painting = false;
298 }),
299 ),
300 )
301 }
302}
303
304fn main() {
305 Application::new().run(|cx| {
306 cx.open_window(
307 WindowOptions {
308 focus: true,
309 ..Default::default()
310 },
311 |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
312 )
313 .unwrap();
314 cx.activate(true);
315 });
316}
317
318/// Draw the path for the breakpoint indicator.
319///
320/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
321/// wide to draw without graphical issues, so it will ignore narrower width.
322fn breakpoint_indicator_path(bounds: Bounds<Pixels>, scale: f32, stroke: bool) -> Path<Pixels> {
323 static MIN_WIDTH: f32 = 31.;
324
325 // Apply user scale to the minimum width
326 let min_width = MIN_WIDTH * scale;
327
328 let width = if bounds.size.width.0 < min_width {
329 px(min_width)
330 } else {
331 bounds.size.width
332 };
333 let height = bounds.size.height;
334
335 // Position the indicator on the canvas
336 let base_x = bounds.origin.x;
337 let base_y = bounds.origin.y;
338
339 // Calculate the scaling factor for the height (SVG is 15px tall), incorporating user scale
340 let scale_factor = (height / px(15.0)) * scale;
341
342 // Calculate how much width to allocate to the stretchable middle section
343 // SVG has 32px of fixed elements (corners), so the rest is for the middle
344 let fixed_width = px(32.0) * scale_factor;
345 let middle_width = width - fixed_width;
346
347 // Helper function to round to nearest quarter pixel
348 let round_to_quarter = |value: Pixels| -> Pixels {
349 let value_f32: f32 = value.into();
350 px((value_f32 * 4.0).round() / 4.0)
351 };
352
353 // Create a new path - either fill or stroke based on the flag
354 let mut builder = if stroke {
355 // For stroke, we need to set appropriate line width and options
356 let stroke_width = px(1.0 * scale); // Apply scale to stroke width
357 let options = StrokeOptions::default().with_line_width(stroke_width.0);
358
359 PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
360 } else {
361 // For fill, use the original implementation
362 PathBuilder::fill()
363 };
364
365 // Upper half of the shape - Based on the provided SVG
366 // Start at bottom left (0, 8)
367 let start_x = round_to_quarter(base_x);
368 let start_y = round_to_quarter(base_y + px(7.5) * scale_factor);
369 builder.move_to(point(start_x, start_y));
370
371 // Vertical line to (0, 5)
372 let vert_y = round_to_quarter(base_y + px(5.0) * scale_factor);
373 builder.line_to(point(start_x, vert_y));
374
375 // Curve to (5, 0) - using cubic Bezier
376 let curve1_end_x = round_to_quarter(base_x + px(5.0) * scale_factor);
377 let curve1_end_y = round_to_quarter(base_y);
378 let curve1_ctrl1_x = round_to_quarter(base_x);
379 let curve1_ctrl1_y = round_to_quarter(base_y + px(1.5) * scale_factor);
380 let curve1_ctrl2_x = round_to_quarter(base_x + px(1.5) * scale_factor);
381 let curve1_ctrl2_y = round_to_quarter(base_y);
382 builder.cubic_bezier_to(
383 point(curve1_end_x, curve1_end_y),
384 point(curve1_ctrl1_x, curve1_ctrl1_y),
385 point(curve1_ctrl2_x, curve1_ctrl2_y),
386 );
387
388 // Horizontal line through the middle section to (37, 0)
389 let middle_end_x = round_to_quarter(base_x + px(5.0) * scale_factor + middle_width);
390 builder.line_to(point(middle_end_x, curve1_end_y));
391
392 // Horizontal line to (41, 0)
393 let right_section_x =
394 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(4.0) * scale_factor);
395 builder.line_to(point(right_section_x, curve1_end_y));
396
397 // Curve to (50, 7.5) - using cubic Bezier
398 let curve2_end_x =
399 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
400 let curve2_end_y = round_to_quarter(base_y + px(7.5) * scale_factor);
401 let curve2_ctrl1_x =
402 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
403 let curve2_ctrl1_y = round_to_quarter(base_y);
404 let curve2_ctrl2_x =
405 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
406 let curve2_ctrl2_y = round_to_quarter(base_y + px(6.0) * scale_factor);
407 builder.cubic_bezier_to(
408 point(curve2_end_x, curve2_end_y),
409 point(curve2_ctrl1_x, curve2_ctrl1_y),
410 point(curve2_ctrl2_x, curve2_ctrl2_y),
411 );
412
413 // Lower half of the shape - mirrored vertically
414 // Curve from (50, 7.5) to (41, 15)
415 let curve3_end_y = round_to_quarter(base_y + px(15.0) * scale_factor);
416 let curve3_ctrl1_x =
417 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
418 let curve3_ctrl1_y = round_to_quarter(base_y + px(9.0) * scale_factor);
419 let curve3_ctrl2_x =
420 round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
421 let curve3_ctrl2_y = round_to_quarter(base_y + px(15.0) * scale_factor);
422 builder.cubic_bezier_to(
423 point(right_section_x, curve3_end_y),
424 point(curve3_ctrl1_x, curve3_ctrl1_y),
425 point(curve3_ctrl2_x, curve3_ctrl2_y),
426 );
427
428 // Horizontal line to (37, 15)
429 builder.line_to(point(middle_end_x, curve3_end_y));
430
431 // Horizontal line through the middle section to (5, 15)
432 builder.line_to(point(curve1_end_x, curve3_end_y));
433
434 // Curve to (0, 10)
435 let curve4_end_y = round_to_quarter(base_y + px(10.0) * scale_factor);
436 let curve4_ctrl1_x = round_to_quarter(base_x + px(1.5) * scale_factor);
437 let curve4_ctrl1_y = round_to_quarter(base_y + px(15.0) * scale_factor);
438 let curve4_ctrl2_x = round_to_quarter(base_x);
439 let curve4_ctrl2_y = round_to_quarter(base_y + px(13.5) * scale_factor);
440 builder.cubic_bezier_to(
441 point(start_x, curve4_end_y),
442 point(curve4_ctrl1_x, curve4_ctrl1_y),
443 point(curve4_ctrl2_x, curve4_ctrl2_y),
444 );
445
446 // Close the path
447 builder.line_to(point(start_x, start_y));
448
449 builder.build().unwrap()
450}