gpui: Add paint_path example (#20499)

Jason Lee created

Release Notes:

- N/A

---

```
cargo run -p gpui --example painting
```

I added this demo to verify the detailed support of Path drawing in
GPUI.

Because of, when we actually used GPUI to draw a 2D line chart, we found
that the straight line Path#line_to did not support `anti-aliasing`, and
the drawn line looked very bad.

As shown in the demo image, if we zoom in on the image, we can clearly
see that all the lines are jagged.

I read and tried to make some appropriate adjustments to the functions
in Path, but since I have no experience in the graphics field, I still
cannot achieve anti-aliasing support so far.

I don't know if I used it wrong somewhere. I checked `curve_to` and
found that the curves drawn have anti-aliasing effects, as shown in the
arc part of the figure below.

<img width="1136" alt="image"
src="https://github.com/user-attachments/assets/4dfb7603-e746-43e9-b737-cff56b56329f">

Change summary

crates/gpui/examples/painting.rs | 199 ++++++++++++++++++++++++++++++++++
1 file changed, 199 insertions(+)

Detailed changes

crates/gpui/examples/painting.rs 🔗

@@ -0,0 +1,199 @@
+use gpui::{
+    canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path,
+    Pixels, Point, Render, ViewContext, WindowOptions,
+};
+struct PaintingViewer {
+    default_lines: Vec<Path<Pixels>>,
+    lines: Vec<Vec<Point<Pixels>>>,
+    start: Point<Pixels>,
+    _painting: bool,
+}
+
+impl PaintingViewer {
+    fn new() -> Self {
+        let mut lines = vec![];
+
+        // draw a line
+        let mut path = Path::new(point(px(50.), px(180.)));
+        path.line_to(point(px(100.), px(120.)));
+        // go back to close the path
+        path.line_to(point(px(100.), px(121.)));
+        path.line_to(point(px(50.), px(181.)));
+        lines.push(path);
+
+        // draw a lightening bolt ⚡
+        let mut path = Path::new(point(px(150.), px(200.)));
+        path.line_to(point(px(200.), px(125.)));
+        path.line_to(point(px(200.), px(175.)));
+        path.line_to(point(px(250.), px(100.)));
+        lines.push(path);
+
+        // draw a ⭐
+        let mut path = Path::new(point(px(350.), px(100.)));
+        path.line_to(point(px(370.), px(160.)));
+        path.line_to(point(px(430.), px(160.)));
+        path.line_to(point(px(380.), px(200.)));
+        path.line_to(point(px(400.), px(260.)));
+        path.line_to(point(px(350.), px(220.)));
+        path.line_to(point(px(300.), px(260.)));
+        path.line_to(point(px(320.), px(200.)));
+        path.line_to(point(px(270.), px(160.)));
+        path.line_to(point(px(330.), px(160.)));
+        path.line_to(point(px(350.), px(100.)));
+        lines.push(path);
+
+        let square_bounds = Bounds {
+            origin: point(px(450.), px(100.)),
+            size: size(px(200.), px(80.)),
+        };
+        let height = square_bounds.size.height;
+        let horizontal_offset = height;
+        let vertical_offset = px(30.);
+        let mut path = Path::new(square_bounds.lower_left());
+        path.curve_to(
+            square_bounds.origin + point(horizontal_offset, vertical_offset),
+            square_bounds.origin + point(px(0.0), vertical_offset),
+        );
+        path.line_to(square_bounds.upper_right() + point(-horizontal_offset, vertical_offset));
+        path.curve_to(
+            square_bounds.lower_right(),
+            square_bounds.upper_right() + point(px(0.0), vertical_offset),
+        );
+        path.line_to(square_bounds.lower_left());
+        lines.push(path);
+
+        Self {
+            default_lines: lines.clone(),
+            lines: vec![],
+            start: point(px(0.), px(0.)),
+            _painting: false,
+        }
+    }
+
+    fn clear(&mut self, cx: &mut ViewContext<Self>) {
+        self.lines.clear();
+        cx.notify();
+    }
+}
+impl Render for PaintingViewer {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let default_lines = self.default_lines.clone();
+        let lines = self.lines.clone();
+        div()
+            .font_family(".SystemUIFont")
+            .bg(gpui::white())
+            .size_full()
+            .p_4()
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .justify_between()
+                    .items_center()
+                    .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
+                    .child(
+                        div()
+                            .id("clear")
+                            .child("Clean up")
+                            .bg(gpui::black())
+                            .text_color(gpui::white())
+                            .active(|this| this.opacity(0.8))
+                            .flex()
+                            .px_3()
+                            .py_1()
+                            .on_click(cx.listener(|this, _, cx| {
+                                this.clear(cx);
+                            })),
+                    ),
+            )
+            .child(
+                div()
+                    .size_full()
+                    .child(
+                        canvas(
+                            move |_, _| {},
+                            move |_, _, cx| {
+                                const STROKE_WIDTH: Pixels = px(2.0);
+                                for path in default_lines {
+                                    cx.paint_path(path, gpui::black());
+                                }
+                                for points in lines {
+                                    let mut path = Path::new(points[0]);
+                                    for p in points.iter().skip(1) {
+                                        path.line_to(*p);
+                                    }
+
+                                    let mut last = points.last().unwrap();
+                                    for p in points.iter().rev() {
+                                        let mut offset_x = px(0.);
+                                        if last.x == p.x {
+                                            offset_x = STROKE_WIDTH;
+                                        }
+                                        path.line_to(point(p.x + offset_x, p.y  + STROKE_WIDTH));
+                                        last = p;
+                                    }
+
+                                    cx.paint_path(path, gpui::black());
+                                }
+                            },
+                        )
+                        .size_full(),
+                    )
+                    .on_mouse_down(
+                        gpui::MouseButton::Left,
+                        cx.listener(|this, ev: &MouseDownEvent, _| {
+                            this._painting = true;
+                            this.start = ev.position;
+                            let path = vec![ev.position];
+                            this.lines.push(path);
+                        }),
+                    )
+                    .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, cx| {
+                        if !this._painting {
+                            return;
+                        }
+
+                        let is_shifted = ev.modifiers.shift;
+                        let mut pos = ev.position;
+                        // When holding shift, draw a straight line
+                        if is_shifted {
+                            let dx = pos.x - this.start.x;
+                            let dy = pos.y - this.start.y;
+                            if dx.abs() > dy.abs() {
+                                pos.y = this.start.y;
+                            } else {
+                                pos.x = this.start.x;
+                            }
+                        }
+
+                        if let Some(path) = this.lines.last_mut() {
+                            path.push(pos);
+                        }
+
+                        cx.notify();
+                    }))
+                    .on_mouse_up(
+                        gpui::MouseButton::Left,
+                        cx.listener(|this, _, _| {
+                            this._painting = false;
+                        }),
+                    ),
+            )
+    }
+}
+
+fn main() {
+    App::new().run(|cx: &mut AppContext| {
+        cx.open_window(
+            WindowOptions {
+                focus: true,
+                ..Default::default()
+            },
+            |cx| cx.new_view(|_| PaintingViewer::new()),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}