Back to Blog

Drawing App in Rust egui — Painter, Strokes & Undo | Learn egui Ep30

Celest KimCelest Kim

Video: Drawing App in Rust egui — Painter, Strokes & Undo | Learn egui Ep30 by Taught by Celeste AI - AI Coding Coach

Watch full page →

Drawing App in Rust egui — Painter, Strokes & Undo

This tutorial demonstrates how to build a freehand drawing application in Rust using the egui GUI framework. You'll learn to create a custom drawing canvas, handle mouse input for drawing strokes, render lines with configurable color and width, and implement undo and clear functionality with stroke history management.

Code

use egui::{Color32, Painter, Pos2, Rect, Response, Sense, Stroke, Vec2};
use egui::{CentralPanel, ColorEdit, Slider, TopBottomPanel};
use egui::PointerButton;

struct StrokeData {
  points: Vec,
  color: Color32,
  width: f32,
}

struct DrawingApp {
  strokes: Vec<StrokeData>,
  current_stroke: Option<StrokeData>,
  stroke_color: Color32,
  stroke_width: f32,
  is_drawing: bool,
}

impl DrawingApp {
  fn ui(&mut self, ctx: &egui::Context) {
    // Toolbar panel with color picker and width slider
    TopBottomPanel::top("toolbar").show(ctx, |ui| {
      ui.horizontal(|ui| {
        ui.label("Brush color:");
        ui.color_edit_button_rgb(&mut self.stroke_color);
        ui.label("Brush width:");
        ui.add(Slider::new(&mut self.stroke_width, 1.0..=20.0));
        if ui.add_enabled(!self.strokes.is_empty(), egui::Button::new("Undo")).clicked() {
          self.strokes.pop();
        }
        if ui.add_enabled(!self.strokes.is_empty(), egui::Button::new("Clear")).clicked() {
          self.strokes.clear();
        }
      });
    });

    // Central canvas panel
    CentralPanel::default().show(ctx, |ui| {
      // Allocate painter with desired size
      let (rect, response) = ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());

      let painter = ui.painter();

      // Draw dark background and border
      painter.rect_filled(rect, 0.0, Color32::from_rgb(30, 30, 30));
      painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Color32::WHITE));

      // Handle input for drawing
      if response.dragged_by(PointerButton::Primary) {
        if let Some(pos) = response.interact_pointer_pos() {
          if !self.is_drawing {
            // Start a new stroke
            self.current_stroke = Some(StrokeData {
              points: vec![pos],
              color: self.stroke_color,
              width: self.stroke_width,
            });
            self.is_drawing = true;
          } else if let Some(stroke) = &mut self.current_stroke {
            // Continue current stroke
            stroke.points.push(pos);
          }
          ctx.request_repaint(); // Keep repainting while drawing
        }
      } else if self.is_drawing {
        // Finish stroke on mouse release
        if let Some(stroke) = self.current_stroke.take() {
          self.strokes.push(stroke);
        }
        self.is_drawing = false;
      }

      // Draw all completed strokes
      for stroke in &self.strokes {
        for points in stroke.points.windows(2) {
          painter.line_segment(
            [points[0], points[1]],
            Stroke::new(stroke.width, stroke.color),
          );
        }
      }

      // Draw current stroke in progress
      if let Some(stroke) = &self.current_stroke {
        for points in stroke.points.windows(2) {
          painter.line_segment(
            [points[0], points[1]],
            Stroke::new(stroke.width, stroke.color),
          );
        }
      }
    });
  }
}

Key Points

  • Use allocate_exact_size with Sense::click_and_drag() to create a custom canvas that detects mouse drag input.
  • Track drawing state with an is_drawing flag and store points in a Vec<Pos2> for each stroke.
  • Render strokes by iterating over adjacent point pairs using points.windows(2) and drawing line segments with configurable color and width.
  • Implement undo by popping the last stroke from the history vector and clear by emptying the stroke list.
  • Use egui widgets like color_edit_button_rgb and Slider in a toolbar panel to let users customize brush color and size.