diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..097cf4c --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,88 @@ +#[derive(Clone, druid::Data)] +pub struct Path { + pub kurbo_path: druid::kurbo::BezPath, +} + +#[derive(Clone, druid::Data)] +pub enum CanvasElement { + Freehand { path: Path, thickness: f64 }, +} + +impl CanvasElement { + pub fn bounding_box(&self) -> druid::Rect { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::kurbo::Shape; + path.kurbo_path + .bounding_box() + .inflate(*thickness, *thickness) + } + } + } + pub fn draw(&self, ctx: &mut druid::PaintCtx) { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::RenderContext; + let stroke_color = druid::Color::rgb8(0, 128, 0); + ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); + } + } + } + + pub fn get_path_mut(&mut self) -> Option<&mut Path> { + match self { + CanvasElement::Freehand { path, .. } => Some(path), + } + } +} + +// O(1) complexity +fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool { + match segment { + druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0) || rect.contains(line.p1), + + druid::kurbo::PathSeg::Quad(quad) => { + rect.contains(quad.p0) || rect.contains(quad.p1) || rect.contains(quad.p2) + } + + druid::kurbo::PathSeg::Cubic(cubic) => { + rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) + } + } +} + +// A canvas contains all elements to be drawn +#[derive(Clone, druid::Data)] +pub struct Canvas { + pub elements: im::Vector, +} + +impl Canvas { + pub fn new() -> Canvas { + Canvas { + elements: im::vector![], + } + } + // O(n) complexity, where n is the number of segments belonging to paths + // that intersect the eraser rectangle + pub fn erase(&mut self, eraser_rect: druid::Rect) { + for elem in self.elements.iter_mut() { + if elem.bounding_box().intersect(eraser_rect).area() > 0.0 { + match elem { + CanvasElement::Freehand { path, thickness: _ } => { + // Remove segments intersecting the eraser + let new_segments = (*path) + .kurbo_path + .segments() + .filter(|path| !segment_intersects_rect(path, eraser_rect)); + path.kurbo_path = druid::kurbo::BezPath::from_path_segments(new_segments); + } + } + } + } + } + + pub fn push_back(&mut self, element: CanvasElement) { + self.elements.push_back(element); + } +} diff --git a/src/canvas_tools.rs b/src/canvas_tools.rs new file mode 100644 index 0000000..ba26292 --- /dev/null +++ b/src/canvas_tools.rs @@ -0,0 +1,118 @@ +use crate::canvas::*; +use crate::versioned_canvas::VersionedCanvas; + +use druid::kurbo::BezPath; +use druid::{Data, Event, Rect}; + +// Tools that can be used to interact with the canvas +#[derive(Clone, Data)] +pub enum CanvasTool { + Pen { + current_element: Option, + }, + Eraser { + eraser_rect: Option, + }, +} + +impl CanvasTool { + pub fn new_pen() -> CanvasTool { + CanvasTool::Pen { + current_element: None, + } + } + + pub fn new_eraser() -> CanvasTool { + CanvasTool::Eraser { eraser_rect: None } + } + + pub fn handle_event(&mut self, event: &Event, canvas: &mut VersionedCanvas) { + match self { + CanvasTool::Pen { current_element } => { + CanvasTool::pen_handle_event(current_element, event, canvas) + } + CanvasTool::Eraser { eraser_rect } => { + CanvasTool::eraser_handle_event(eraser_rect, event, canvas) + } + } + } + + fn pen_handle_event( + current_element: &mut Option, + event: &Event, + canvas: &mut VersionedCanvas, + ) { + match event { + Event::MouseDown(mouse_event) => { + let mut kurbo_path = BezPath::new(); + kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); + *current_element = Some(CanvasElement::Freehand { + path: Path { kurbo_path }, + thickness: 2.0, + }); + } + Event::MouseMove(mouse_event) => { + if current_element.is_some() { + if let Some(current_element) = current_element.as_mut() { + current_element + .get_path_mut() + .unwrap() + .kurbo_path + .line_to((mouse_event.pos.x, mouse_event.pos.y)); + } + } + } + Event::MouseUp(_) => { + if current_element.is_some() { + if let Some(current_element) = current_element.take() { + canvas.update(move |canvas: &Canvas| -> Canvas { + let mut new_canvas = canvas.clone(); + new_canvas.push_back(current_element); + return new_canvas; + }); + } + } + } + _ => {} + } + } + + fn eraser_handle_event( + eraser_rect: &mut Option, + event: &Event, + canvas: &mut VersionedCanvas, + ) { + match event { + Event::MouseDown(mouse_event) => { + let rect = + druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); + *eraser_rect = Some(rect); + // Create a new undo version each time the mouse is down + canvas.update(|canvas: &Canvas| -> Canvas { + let mut new_canvas = canvas.clone(); + new_canvas.erase(rect); + return new_canvas; + }); + } + Event::MouseMove(mouse_event) => { + if eraser_rect.is_some() { + let rect = druid::Rect::from_center_size( + mouse_event.pos, + druid::Size::new(10.0, 10.0), + ); + *eraser_rect = Some(rect); + // We don't want too many levels of undoing + // So we make irreversible changes as long as + // the mouse is pressed. + canvas.irreversible_update(|canvas: &mut Canvas| { + canvas.erase(rect); + }); + } + } + Event::MouseUp(_) => { + *eraser_rect = None; + } + _ => {} + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 383c121..44639cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,160 +1,3 @@ -#[derive(Clone, druid::Data)] -pub struct Path { - pub kurbo_path: druid::kurbo::BezPath, -} - -#[derive(Clone, druid::Data)] -pub enum CanvasElement { - Freehand { path: Path, thickness: f64 }, -} - -impl CanvasElement { - pub fn bounding_box(&self) -> druid::Rect { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::kurbo::Shape; - path.kurbo_path - .bounding_box() - .inflate(*thickness, *thickness) - } - } - } - pub fn draw(&self, ctx: &mut druid::PaintCtx) { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::RenderContext; - let stroke_color = druid::Color::rgb8(0, 128, 0); - ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); - } - } - } - - pub fn get_path_mut(&mut self) -> Option<&mut Path> { - match self { - CanvasElement::Freehand { path, .. } => Some(path), - } - } -} - -// O(1) complexity -fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool { - - match segment { - druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0) - || rect.contains(line.p1), - - druid::kurbo::PathSeg::Quad(quad) => rect.contains(quad.p0) - || rect.contains(quad.p1) - || rect.contains(quad.p2), - - druid::kurbo::PathSeg::Cubic(cubic) => rect.contains(cubic.p0) - || rect.contains(cubic.p1) - || rect.contains(cubic.p2), - } -} - -// A canvas contains all elements to be drawn -#[derive(Clone, druid::Data)] -pub struct Canvas { - pub elements: im::Vector, -} - -impl Canvas { - pub fn new() -> Canvas { - Canvas { - elements: im::vector![], - } - } - // O(n) complexity, where n is the number of segments belonging to paths - // that intersect the eraser rectangle - pub fn erase(&mut self, eraser_rect: druid::Rect) { - for elem in self.elements.iter_mut() { - if elem.bounding_box().intersect(eraser_rect).area() > 0.0 { - match elem { - CanvasElement::Freehand{path, thickness: _} => { - // Remove segments intersecting the eraser - let new_segments = (*path).kurbo_path.segments().filter(|path| ! segment_intersects_rect(path, eraser_rect)); - path.kurbo_path = druid::kurbo::BezPath::from_path_segments(new_segments); - } - } - } - } - } - - pub fn push_back(&mut self, element: CanvasElement) { - self.elements.push_back(element); - } -} - -#[derive(Clone, druid::Data)] -pub struct VersionedCanvas { - // We internally guarantee that this vector - // is never empty - versions: im::Vector, - curr_version: usize, -} - -impl VersionedCanvas { - pub fn new(canvas: Canvas) -> VersionedCanvas { - VersionedCanvas { - versions: im::vector![canvas], - curr_version: 0, - } - } - - // Get current canvas version - pub fn get(&self) -> &Canvas { - self.versions.get(self.curr_version).unwrap() - } - - fn has_newer_versions(&self) -> bool { - self.curr_version + 1 < self.versions.len() - } - - fn has_older_versions(&self) -> bool { - self.curr_version > 0 - } - - pub fn version(&self) -> usize { - self.curr_version - } - - pub fn undo(&mut self) { - if self.has_older_versions() { - self.curr_version = self.curr_version - 1; - } - } - - pub fn redo(&mut self) { - if self.has_newer_versions() { - self.curr_version = self.curr_version + 1; - } - } - - pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> Canvas) { - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } - let new_version = update_fn(self.get()); - self.versions.push_back(new_version); - self.curr_version = self.curr_version + 1; - } - - // Do inplace update, which will be irreversible - pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } - - update_fn(self.versions.back_mut().unwrap()); - } -} - +pub mod canvas; +pub mod canvas_tools; +pub mod versioned_canvas; diff --git a/src/main.rs b/src/main.rs index 607a727..876a7ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,74 +14,23 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use druid::kurbo::{BezPath, Rect}; use druid::widget::prelude::*; -use druid::{AppLauncher, Color, Data, Lens, Event, LocalizedString, WidgetExt, WindowDesc}; +use druid::{ + AppLauncher, Color, Data, Event, Lens, LensExt, LocalizedString, WidgetExt, WindowDesc, +}; -use stiletto::{CanvasElement, VersionedCanvas, Canvas}; +use stiletto::canvas::Canvas; +use stiletto::canvas_tools::CanvasTool; +use stiletto::versioned_canvas::VersionedCanvas; -#[derive(Clone, Data, PartialEq)] -enum CanvasToolType { - Pen, - Eraser -} - -// Tools that can be used to interact with the canvas -#[derive(Clone, Data)] -enum CanvasTool { - Pen { current_element: Option }, - Eraser { eraser_rect: Option }, -} - -impl CanvasTool { - fn new_pen() -> CanvasTool { - CanvasTool::Pen { current_element: None } - } - fn new_eraser() -> CanvasTool { - CanvasTool::Eraser { eraser_rect: None } - } - - fn tool_type(&self) -> CanvasToolType { - match self { - CanvasTool::Pen { current_element: _ } => CanvasToolType::Pen, - CanvasTool::Eraser { eraser_rect: _ } => CanvasToolType::Eraser, - } - } -} - -struct CanvasToolLens; - -impl Lens for CanvasToolLens { - fn with R>(&self, data: &CanvasData, f: F) -> R { - f(&data.current_tool.tool_type()) - } - - fn with_mut R>(&self, data: &mut CanvasData, f: F) -> R { - let mut curr_type = data.current_tool.tool_type(); - let result = f(&mut curr_type); - - match curr_type { - CanvasToolType::Pen => data.current_tool = CanvasTool::new_pen(), - CanvasToolType::Eraser => data.current_tool = CanvasTool::new_eraser(), - } - - result - } -} - -#[derive(Clone, Data)] +#[derive(Clone, Data, Lens)] struct CanvasData { + #[lens(name = "current_tool_lens")] current_tool: CanvasTool, elements: VersionedCanvas, } impl CanvasData { - /*fn is_drawing(&self) -> bool { - self.current_element.is_some() - }*/ - - const CURRENT_TOOL_LENS: CanvasToolLens = CanvasToolLens; - fn perform_undo(&mut self) { self.elements.undo(); } @@ -91,85 +40,15 @@ impl CanvasData { } fn handle_tool_event(&mut self, event: &Event) { - match &mut self.current_tool { - CanvasTool::Pen { current_element } => { - match event { - Event::MouseDown(mouse_event) => { - let mut kurbo_path = BezPath::new(); - kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); - *current_element = Some(CanvasElement::Freehand { - path: stiletto::Path { kurbo_path }, - thickness: 2.0, - }); - } - Event::MouseMove(mouse_event) => { - if current_element.is_some() { - if let Some(current_element) = current_element.as_mut() { - current_element - .get_path_mut() - .unwrap() - .kurbo_path - .line_to((mouse_event.pos.x, mouse_event.pos.y)); - } - } - } - Event::MouseUp(_) => { - if current_element.is_some() { - if let Some(current_element) = current_element.take() { - - self.elements.update(move |canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.push_back(current_element); - return new_canvas; - }); - - } - } - } - _ => {} - } - }, - CanvasTool::Eraser { eraser_rect } => { - match event { - Event::MouseDown(mouse_event) => { - let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); - *eraser_rect = Some(rect); - // Create a new undo version each time the mouse is down - self.elements.update(|canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.erase(rect); - return new_canvas; - }); - } - Event::MouseMove(mouse_event) => { - if eraser_rect.is_some() { - let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); - *eraser_rect = Some(rect); - // We don't want too many levels of undoing - // So we make irreversible changes as long as - // the mouse is pressed. - self.elements.irreversible_update(|canvas: &mut Canvas| { - canvas.erase(rect); - }); - } - } - Event::MouseUp(_) => { - *eraser_rect = None; - } - _ => {} - } - }, - } + self.current_tool.handle_event(event, &mut self.elements); } } - struct CanvasWidget; impl Widget for CanvasWidget { fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { data.handle_tool_event(event); - } fn lifecycle( @@ -188,12 +67,19 @@ impl Widget for CanvasWidget { data: &CanvasData, _env: &Env, ) { - + // TODO: Polish a bit this logic match (&old_data.current_tool, &data.current_tool) { - (CanvasTool::Pen{ current_element: old_element }, CanvasTool::Pen{ current_element: new_element }) => { + ( + CanvasTool::Pen { + current_element: old_element, + }, + CanvasTool::Pen { + current_element: new_element, + }, + ) => { // the current_element is moved to the elements array, no need to repaint if old_element.is_some() && !new_element.is_some() { - return + return; } if new_element.is_some() { @@ -204,7 +90,7 @@ impl Widget for CanvasWidget { ctx.request_paint(); } } - (CanvasTool::Eraser{ eraser_rect: _ }, CanvasTool::Eraser{ eraser_rect }) => { + (CanvasTool::Eraser { eraser_rect: _ }, CanvasTool::Eraser { eraser_rect }) => { // We just stopped erasing, no need to repaint if let Some(rect) = eraser_rect { ctx.request_paint_rect(*rect); @@ -261,18 +147,49 @@ impl Widget for CanvasWidget { } } -fn build_ui() -> impl Widget { - use druid::widget::{Align, Button, RadioGroup, CrossAxisAlignment, Flex, SizedBox}; +// Enum containing an entry for each canvas tool +// it is used to implement the radio group for selecting the tool +#[derive(Clone, Data, PartialEq)] +enum ToolType { + Pen, + Eraser, +} - let radio_group = RadioGroup::new(vec![("Pen", CanvasToolType::Pen), ("Eraser", CanvasToolType::Eraser)]); - +// Make a CanvasTool of a given type +fn update_canvas_tool_from_type(tool: &mut CanvasTool, new_type: ToolType) { + match new_type { + ToolType::Pen => *tool = CanvasTool::new_pen(), + ToolType::Eraser => *tool = CanvasTool::new_eraser(), + } +} + +// Get a ToolType from a CanvasTool +fn canvas_tool_type(tool: &CanvasTool) -> ToolType { + match tool { + CanvasTool::Pen { .. } => ToolType::Pen, + CanvasTool::Eraser { .. } => ToolType::Eraser, + } +} + +fn build_ui() -> impl Widget { + use druid::widget::{Align, Button, CrossAxisAlignment, Flex, RadioGroup, SizedBox}; + + let radio_group = RadioGroup::new(vec![("Pen", ToolType::Pen), ("Eraser", ToolType::Eraser)]) + .lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type)); + // ^ We use a lens to make the radiogroup act only on the tool portion of data. + // Actually we must also perform a conversion, between ToolType and CanvasTool, + // that's why we perform a map on the lens. let toolbar = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) .with_spacer(30.0) - .with_child(Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo())) - .with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo())) - .with_child(radio_group.lens(CanvasData::CURRENT_TOOL_LENS)); + .with_child( + Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo()), + ) + .with_child( + Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()), + ) + .with_child(radio_group); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) diff --git a/src/versioned_canvas.rs b/src/versioned_canvas.rs new file mode 100644 index 0000000..e549baa --- /dev/null +++ b/src/versioned_canvas.rs @@ -0,0 +1,73 @@ +use crate::canvas::Canvas; + +#[derive(Clone, druid::Data)] +pub struct VersionedCanvas { + // We internally guarantee that this vector + // is never empty + versions: im::Vector, + curr_version: usize, +} + +impl VersionedCanvas { + pub fn new(canvas: Canvas) -> VersionedCanvas { + VersionedCanvas { + versions: im::vector![canvas], + curr_version: 0, + } + } + + // Get current canvas version + pub fn get(&self) -> &Canvas { + self.versions.get(self.curr_version).unwrap() + } + + fn has_newer_versions(&self) -> bool { + self.curr_version + 1 < self.versions.len() + } + + fn has_older_versions(&self) -> bool { + self.curr_version > 0 + } + + pub fn version(&self) -> usize { + self.curr_version + } + + pub fn undo(&mut self) { + if self.has_older_versions() { + self.curr_version = self.curr_version - 1; + } + } + + pub fn redo(&mut self) { + if self.has_newer_versions() { + self.curr_version = self.curr_version + 1; + } + } + + pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> Canvas) { + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + let new_version = update_fn(self.get()); + self.versions.push_back(new_version); + self.curr_version = self.curr_version + 1; + } + + // Do inplace update, which will be irreversible + pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + + update_fn(self.versions.back_mut().unwrap()); + } +}