← Examples

Custom Drawing

This example demonstrates custom drawing in GPUI using: canvas element, pathbuilder, window.paint_* methods, interactive drawing.

Run

cargo run --example custom_drawing

Category

learn

Source

View on GitHub →

Source Code

//! Custom Drawing Example
//!
//! This example demonstrates custom drawing in GPUI using:
//!
//! 1. `canvas` element - For direct painting control
//! 2. `PathBuilder` - Creating custom vector shapes
//! 3. `window.paint_*` methods - Drawing quads, paths, and more
//! 4. Interactive drawing - Responding to mouse events

use gpui::{
    App, Application, Bounds, Colors, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent,
    MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds,
    WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size,
};

#[path = "../prelude.rs"]
mod example_prelude;

// Example 1: Basic Canvas Drawing
//
// The `canvas` element provides two callbacks:
// - prepaint: Called during layout to prepare drawing state
// - paint: Called during paint to actually draw

fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement {
    let error = colors.error;
    let success = colors.success;
    let accent = colors.accent;

    canvas(
        move |_bounds, _window, _cx| {},
        move |bounds, _prepaint_state, window, _cx| {
            // Draw a filled rectangle
            window.paint_quad(fill(
                Bounds {
                    origin: point(bounds.origin.x + px(10.), bounds.origin.y + px(10.)),
                    size: size(px(60.), px(40.)),
                },
                error,
            ));

            // Draw another rectangle
            window.paint_quad(fill(
                Bounds {
                    origin: point(bounds.origin.x + px(80.), bounds.origin.y + px(10.)),
                    size: size(px(60.), px(40.)),
                },
                success,
            ));

            // Draw a third rectangle
            window.paint_quad(fill(
                Bounds {
                    origin: point(bounds.origin.x + px(150.), bounds.origin.y + px(10.)),
                    size: size(px(60.), px(40.)),
                },
                accent,
            ));
        },
    )
    .size_full()
}

// Example 2: Custom Paths with PathBuilder
//
// PathBuilder lets you create complex vector shapes:
// - move_to: Start a new subpath
// - line_to: Draw a straight line
// - curve_to: Draw a bezier curve
// - close: Close the current subpath

fn create_star(center: Point<Pixels>, outer_radius: f32, inner_radius: f32) -> Path<Pixels> {
    let mut builder = PathBuilder::fill();
    let points = 5;

    for i in 0..points * 2 {
        let angle =
            std::f32::consts::PI / 2.0 - (i as f32) * std::f32::consts::PI / (points as f32);
        let radius = if i % 2 == 0 {
            outer_radius
        } else {
            inner_radius
        };

        let x = center.x + px(angle.cos() * radius);
        let y = center.y - px(angle.sin() * radius);

        if i == 0 {
            builder.move_to(point(x, y));
        } else {
            builder.line_to(point(x, y));
        }
    }

    builder.close();
    builder.build().unwrap()
}

fn create_triangle(p1: Point<Pixels>, p2: Point<Pixels>, p3: Point<Pixels>) -> Path<Pixels> {
    let mut builder = PathBuilder::fill();
    builder.move_to(p1);
    builder.line_to(p2);
    builder.line_to(p3);
    builder.close();
    builder.build().unwrap()
}

fn custom_paths_canvas(colors: &Colors) -> impl IntoElement {
    let warning = colors.warning;
    let accent = colors.accent;

    canvas(
        move |_bounds, _window, _cx| {},
        move |bounds, _, window, _cx| {
            let center_y = bounds.origin.y + bounds.size.height / 2.0;

            // Draw a star
            let star_center = point(bounds.origin.x + px(50.), center_y);
            let star = create_star(star_center, 30., 15.);
            window.paint_path(star, warning);

            // Draw a triangle
            let tri_base_x = bounds.origin.x + px(120.);
            let triangle = create_triangle(
                point(tri_base_x + px(30.), center_y - px(25.)),
                point(tri_base_x, center_y + px(25.)),
                point(tri_base_x + px(60.), center_y + px(25.)),
            );
            window.paint_path(triangle, rgb(0x8b5cf6)); // Purple

            // Draw a custom shape (arrow)
            let arrow_x = bounds.origin.x + px(200.);
            let mut arrow_builder = PathBuilder::fill();
            arrow_builder.move_to(point(arrow_x, center_y));
            arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(20.)));
            arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(10.)));
            arrow_builder.line_to(point(arrow_x + px(50.), center_y - px(10.)));
            arrow_builder.line_to(point(arrow_x + px(50.), center_y + px(10.)));
            arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(10.)));
            arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(20.)));
            arrow_builder.close();
            let arrow = arrow_builder.build().unwrap();
            window.paint_path(arrow, accent);
        },
    )
    .size_full()
}

// Example 3: Interactive Drawing
//
// Combine canvas with mouse events for interactive drawing

struct DrawingCanvas {
    lines: Vec<Vec<Point<Pixels>>>,
    current_line: Vec<Point<Pixels>>,
    is_drawing: bool,
    color_index: usize,
}

impl DrawingCanvas {
    fn new() -> Self {
        Self {
            lines: Vec::new(),
            current_line: Vec::new(),
            is_drawing: false,
            color_index: 0,
        }
    }

    fn get_colors(colors: &Colors) -> Vec<Rgba> {
        vec![
            colors.error,
            colors.success,
            colors.accent,
            colors.warning,
            rgb(0x8b5cf6), // Purple
            rgb(0x06b6d4), // Cyan
        ]
    }

    fn current_color(&self, colors: &Colors) -> Rgba {
        let palette = Self::get_colors(colors);
        palette[self.color_index % palette.len()]
    }

    fn next_color(&mut self, colors: &Colors) {
        let palette = Self::get_colors(colors);
        self.color_index = (self.color_index + 1) % palette.len();
    }

    fn on_mouse_down(
        &mut self,
        event: &MouseDownEvent,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if event.button == MouseButton::Left {
            self.is_drawing = true;
            self.current_line = vec![event.position];
            cx.notify();
        }
    }

    fn on_mouse_move(
        &mut self,
        event: &MouseMoveEvent,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if self.is_drawing {
            self.current_line.push(event.position);
            cx.notify();
        }
    }

    fn on_mouse_up(&mut self, _event: &MouseUpEvent, window: &mut Window, cx: &mut Context<Self>) {
        if self.is_drawing && self.current_line.len() > 1 {
            self.lines.push(std::mem::take(&mut self.current_line));
            let colors = Colors::for_appearance(window);
            self.next_color(&colors);
        }
        self.is_drawing = false;
        self.current_line.clear();
        cx.notify();
    }

    fn clear(&mut self, cx: &mut Context<Self>) {
        self.lines.clear();
        self.current_line.clear();
        self.color_index = 0;
        cx.notify();
    }

    fn draw_line(window: &mut Window, points: &[Point<Pixels>], color: Rgba) {
        if points.len() < 2 {
            return;
        }

        for pair in points.windows(2) {
            let start = pair[0];
            let end = pair[1];

            let dx = end.x - start.x;
            let dy = end.y - start.y;

            let dx_f = f32::from(dx);
            let dy_f = f32::from(dy);
            let len = (dx_f * dx_f + dy_f * dy_f).sqrt();

            if len < 0.1 {
                continue;
            }

            let thickness = 3.0_f32;
            let px_offset = px(-dy_f / len * thickness / 2.0);
            let py_offset = px(dx_f / len * thickness / 2.0);

            let mut builder = PathBuilder::fill();
            builder.move_to(point(start.x + px_offset, start.y + py_offset));
            builder.line_to(point(end.x + px_offset, end.y + py_offset));
            builder.line_to(point(end.x - px_offset, end.y - py_offset));
            builder.line_to(point(start.x - px_offset, start.y - py_offset));
            builder.close();

            if let Ok(path) = builder.build() {
                window.paint_path(path, color);
            }
        }
    }
}

impl Render for DrawingCanvas {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);
        let lines = self.lines.clone();
        let current_line = self.current_line.clone();
        let current_color = self.current_color(&colors);
        let palette = Self::get_colors(&colors);

        let surface = colors.surface;
        let border = colors.border;
        let error = colors.error;
        let error_hover = colors.error_hover;
        let text = colors.selected_text;
        let text_muted = colors.text_muted;

        div()
            .flex()
            .flex_col()
            .gap_2()
            .child(
                div()
                    .id("drawing-area")
                    .h_48()
                    .rounded_lg()
                    .bg(surface)
                    .border_1()
                    .border_color(border)
                    .cursor_crosshair()
                    .overflow_hidden()
                    .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
                    .on_mouse_move(cx.listener(Self::on_mouse_move))
                    .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
                    .child(
                        canvas(
                            move |_, _, _| {},
                            move |_bounds, _, window, _cx| {
                                for (i, line) in lines.iter().enumerate() {
                                    let color = palette[i % palette.len()];
                                    DrawingCanvas::draw_line(window, line, color);
                                }

                                if !current_line.is_empty() {
                                    DrawingCanvas::draw_line(window, &current_line, current_color);
                                }
                            },
                        )
                        .size_full(),
                    ),
            )
            .child(
                div()
                    .flex()
                    .gap_2()
                    .child(
                        div()
                            .id("clear-btn")
                            .px_3()
                            .py_1()
                            .rounded_md()
                            .bg(error)
                            .text_sm()
                            .text_color(text)
                            .cursor_pointer()
                            .hover(move |s| s.bg(error_hover))
                            .child("Clear")
                            .on_click(cx.listener(|this, _, _, cx| {
                                this.clear(cx);
                            })),
                    )
                    .child(
                        div()
                            .text_xs()
                            .text_color(text_muted)
                            .child("Click and drag to draw"),
                    ),
            )
    }
}

// Main Application View

struct CustomDrawingExample {
    drawing_canvas: gpui::Entity<DrawingCanvas>,
}

impl CustomDrawingExample {
    fn new(cx: &mut Context<Self>) -> Self {
        Self {
            drawing_canvas: cx.new(|_| DrawingCanvas::new()),
        }
    }
}

impl Render for CustomDrawingExample {
    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .id("main")
            .size_full()
            .p_6()
            .bg(colors.background)
            .overflow_scroll()
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_6()
                    .max_w(px(500.))
                    .child(
                        div()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .child(
                                div()
                                    .text_xl()
                                    .font_weight(gpui::FontWeight::BOLD)
                                    .text_color(colors.text)
                                    .child("Custom Drawing"),
                            )
                            .child(
                                div()
                                    .text_sm()
                                    .text_color(colors.text_muted)
                                    .child("Canvas element, paths, and interactive painting"),
                            ),
                    )
                    .child(section(
                        &colors,
                        "1. Basic Shapes (paint_quad)",
                        "Use window.paint_quad() to draw filled rectangles",
                        basic_shapes_canvas(&colors),
                        px(70.),
                    ))
                    .child(section(
                        &colors,
                        "2. Custom Paths (PathBuilder)",
                        "Create complex shapes with PathBuilder and paint_path()",
                        custom_paths_canvas(&colors),
                        px(80.),
                    ))
                    .child(section(
                        &colors,
                        "3. Interactive Drawing",
                        "Combine canvas with mouse events for drawing",
                        self.drawing_canvas.clone(),
                        px(240.),
                    )),
            )
    }
}

fn section(
    colors: &Colors,
    title: &'static str,
    description: &'static str,
    content: impl IntoElement,
    height: Pixels,
) -> impl IntoElement {
    let surface: Hsla = colors.surface.into();

    div()
        .flex()
        .flex_col()
        .gap_3()
        .p_4()
        .rounded_lg()
        .bg(surface.opacity(0.5))
        .border_1()
        .border_color(colors.border)
        .child(
            div()
                .flex()
                .flex_col()
                .gap_1()
                .child(
                    div()
                        .text_sm()
                        .font_weight(gpui::FontWeight::SEMIBOLD)
                        .text_color(colors.text)
                        .child(title),
                )
                .child(
                    div()
                        .text_xs()
                        .text_color(colors.text_muted)
                        .child(description),
                ),
        )
        .child(div().h(height).child(content))
}

fn main() {
    Application::new().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_, cx| cx.new(|cx| CustomDrawingExample::new(cx)),
        )
        .expect("Failed to open window");

        example_prelude::init_example(cx, "Custom Drawing");
    });
}