Drawing App in Rust egui — Painter, Strokes & Undo | Learn egui Ep30
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_sizewithSense::click_and_drag()to create a custom canvas that detects mouse drag input. - Track drawing state with an
is_drawingflag and store points in aVec<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_rgbandSliderin a toolbar panel to let users customize brush color and size.