diff --git a/src/canvas.rs b/src/canvas.rs index b33abe3..0729189 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::vec::Vec; - use im::{vector, Vector}; use serde::de::{Deserializer, SeqAccess, Visitor}; @@ -128,12 +126,26 @@ impl CanvasElement { } } } + + pub fn draw_selected(&self, ctx: &mut druid::PaintCtx) { + match self { + CanvasElement::Freehand { + path, + thickness, + .. + } => { + use druid::RenderContext; + ctx.stroke(&path.kurbo_path, &druid::Color::rgb(10.0, 0.0, 255.0), *thickness * 1.4); + } + } + } } #[derive(Clone, druid::Data)] pub struct Canvas { elements: Vector, content_size: druid::Size, + selected_elements: Vector, } impl Canvas { @@ -141,6 +153,7 @@ impl Canvas { Canvas { elements: vector![], content_size: druid::Size::new(0.0, 0.0), + selected_elements: vector![], } } @@ -165,25 +178,43 @@ impl Canvas { &self.elements } - /// Find all CanvasElement that intersect with rect - pub fn find_intersections(&self, rect: druid::Rect) -> Vec { - let mut found_elements = Vec::::new(); - + pub fn has_elements_interesecting(&self, rect: druid::Rect) -> bool { for (i, elem) in self.elements.iter().enumerate() { // Check if the element intersects the eraser rect if elem.bounding_box().intersect(rect).area() > 0.0 { if elem.intersects_rect(rect) { - found_elements.push(i); + return true; } } } + false + } + /// Find all CanvasElement that intersect with rect + pub fn find_elements_intersecting(&self, rect: druid::Rect) -> Vector { + let found_elements = self.elements.iter().enumerate() + .filter_map(|(i, elem)| { + if elem.bounding_box().intersect(rect).area() > 0.0 { + if elem.intersects_rect(rect) { + return Some(i); + } + } + None + }).collect::>(); found_elements } pub fn content_size(&self) -> druid::Size { self.content_size } + + pub fn selected_elements(&self) -> &Vector { + &self.selected_elements + } + + pub fn selected_elements_mut(&mut self) -> &mut Vector { + &mut self.selected_elements + } } impl Serialize for Path { diff --git a/src/history.rs b/src/history.rs index 7acc021..5eea309 100644 --- a/src/history.rs +++ b/src/history.rs @@ -14,27 +14,26 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use super::canvas::Canvas; use im::Vector; #[derive(Clone, druid::Data)] -pub struct VersionedCanvas { +pub struct Versioned { // We internally guarantee that this vector // is never empty - versions: Vector, + versions: Vector, curr_version: usize, } -impl VersionedCanvas { - pub fn new(canvas: Canvas) -> VersionedCanvas { - VersionedCanvas { - versions: im::vector![canvas], +impl Versioned { + pub fn new(first_version: T) -> Versioned { + Versioned { + versions: im::vector![first_version], curr_version: 0, } } - // Get current canvas version - pub fn get(&self) -> &Canvas { + // Get current version + pub fn get(&self) -> &T { self.versions.get(self.curr_version).unwrap() } @@ -58,10 +57,10 @@ impl VersionedCanvas { } } - pub fn update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // Make a new copy of the current canvas version, + pub fn update(&mut self, update_fn: impl FnOnce(&mut T)) { + // Make a new copy of the current version, // so that we can safely modify it without losing - // the previous canvas version + // the previous version let mut new_version = self.get().clone(); update_fn(&mut new_version); @@ -77,8 +76,8 @@ impl VersionedCanvas { } // Do inplace update, which will be irreversible - pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // Do the update directly on the current canvas version + pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut T)) { + // Do the update directly on the current version update_fn(self.versions.back_mut().unwrap()); // This is a linear history, diff --git a/src/lib.rs b/src/lib.rs index 0b239c2..245b865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,11 @@ pub mod tool; pub mod widget; pub mod migration; +pub mod colors { + //use druid::Color; + //pub const SELECTED_STROKE: Color = Color::rgb(10.0, 0.0, 255.0); +} + pub mod commands { use druid::Selector; @@ -46,3 +51,5 @@ impl DocumentSnapshot { serde_bare::to_writer(writer, &self) } } + +pub type VersionedCanvas = history::Versioned; diff --git a/src/main.rs b/src/main.rs index 8d2edf5..d4121f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,10 @@ pub fn main() { tool_params: CanvasToolParams::Eraser, selected: false, }, + CanvasToolIconState { + tool_params: CanvasToolParams::RectangularSelection, + selected: false, + }, ], current_tool: 0, eraser_tool_id: 2, @@ -100,9 +104,9 @@ impl StilettoState { self.tool_icons.get_mut(old_tool).unwrap().selected = false; self.tool_icons.get_mut(new_tool).unwrap().selected = true; self.current_tool = new_tool; - self.canvas.set_tool_ctx(CanvasToolCtx::new( + self.canvas.change_tool( self.tool_icons.get(new_tool).unwrap().tool_params.clone(), - )); + ); } } @@ -116,6 +120,18 @@ fn build_ui() -> impl Widget { |_ctx: &mut EventCtx, data: &mut StilettoState, _env: &Env| data.canvas.perform_redo(), )); + let selection_buttons = Flex::row() + .cross_axis_alignment(CrossAxisAlignment::Center) + .with_child(Button::new("Copy").on_click( + |_ctx: &mut EventCtx, data: &mut StilettoState, _env: &Env| {}, + )) + .with_child(Button::new("Cut").on_click( + |_ctx: &mut EventCtx, data: &mut StilettoState, _env: &Env| {}, + )) + .with_child(Button::new("Delete").on_click( + |_ctx: &mut EventCtx, data: &mut StilettoState, _env: &Env| data.canvas.delete_selected(), + )); + let stlt = FileSpec::new("Stiletto notebook", &["stlt"]); let save_dialog_options = FileDialogOptions::new() .allowed_types(vec![stlt]) @@ -174,6 +190,8 @@ fn build_ui() -> impl Widget { .with_spacer(30.0) .with_flex_child(Align::left(history_buttons), 1.0) .with_spacer(10.0) + .with_flex_child(Align::left(selection_buttons), 1.0) + .with_spacer(10.0) .with_flex_child(Align::left(tool_buttons), 2.0) .with_spacer(20.0) .with_flex_child(Align::right(save_buttons), 1.0) diff --git a/src/tool.rs b/src/tool.rs index 2c57785..8639cc0 100644 --- a/src/tool.rs +++ b/src/tool.rs @@ -20,11 +20,13 @@ use druid::{Color, Data}; pub enum CanvasToolParams { Pen { thickness: f64, color: Color }, Eraser, + RectangularSelection, } #[derive(Clone, PartialEq)] pub enum CanvasToolType { Pen, Eraser, + RectangularSelection, } diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs index f29cce4..e3216b5 100644 --- a/src/widget/canvas.rs +++ b/src/widget/canvas.rs @@ -16,13 +16,13 @@ use im::Vector; -use super::tool_ctx::{CanvasToolCtx}; -use crate::canvas::Canvas; -use crate::history::VersionedCanvas; -use crate::DocumentSnapshot; +use super::tool_ctx::{CanvasToolCtx, CanvasToolState}; +use crate::canvas::{Canvas, CanvasElement}; +use crate::tool::CanvasToolParams; +use crate::{DocumentSnapshot, VersionedCanvas}; use druid::widget::prelude::*; -use druid::{Color, Data, Env, Event, PointerType}; +use druid::{Color, Data, Env, Event, PointerType, Selector}; #[derive(Clone, Data)] pub struct CanvasState { @@ -42,12 +42,14 @@ impl CanvasState { pub fn perform_undo(&mut self) { //if !self.is_drawing() { self.versioned_canvas.undo(); + self.set_ctx_selection_state(); //} } pub fn perform_redo(&mut self) { //if !self.is_drawing() { self.versioned_canvas.redo(); + self.set_ctx_selection_state(); //} } @@ -71,6 +73,44 @@ impl CanvasState { ))); } + fn set_ctx_selection_state(&mut self) { + if !self.versioned_canvas.get().selected_elements().is_empty() { + self.tool_ctx.set_state(CanvasToolState::ActiveSelection); + } + } + + pub fn delete_selected(&mut self) { + let selected_elements = self.versioned_canvas.get().selected_elements(); + if !selected_elements.is_empty() { + if !selected_elements.is_empty() { + let elements = self.versioned_canvas.get().elements(); + // TODO(enrico): is this memory efficient? + // what is the difference between this and + // im::Vector::retain? + let new_elements = elements.iter().enumerate() + .filter_map(|(i, elem)| { + if !selected_elements.contains(&i) { + Some(elem) + } else { + None + } + }) + .cloned() + .collect::>(); + + self.versioned_canvas.update(move |canvas: &mut Canvas| { + *canvas = Canvas::new_with_elements(new_elements); + }); + } + } + } + + + /// Changes tool while mantaining the current CanvasToolCtx + pub fn change_tool(&mut self, tool_params: CanvasToolParams) { + self.tool_ctx.set_params(tool_params); + } + pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) { self.tool_ctx = ctx; } @@ -91,12 +131,28 @@ impl CanvasState { pub struct CanvasWidget; +impl CanvasWidget { + pub const IS_OVER_SELECTION: Selector = Selector::new("is_over_selection"); +} + impl Widget for CanvasWidget { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut CanvasState, env: &Env) { ctx.request_focus(); let mut toggle_eraser_event = false; let mut enable_temporary_erasing = false; match event { + Event::Notification(cmd) => { + cmd.get(CanvasWidget::IS_OVER_SELECTION).map(|is_over_sel| { + if *is_over_sel { + use druid::Cursor; + ctx.set_cursor(&Cursor::OpenHand); + } else { + // TODO: this is not correct. Must check if had modified cursor before + ctx.clear_cursor(); + } + }); + ctx.set_handled(); + } Event::MouseDown(mouse_event) => { toggle_eraser_event = true; enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser; @@ -178,11 +234,20 @@ impl Widget for CanvasWidget { let rect = size.to_rect(); ctx.fill(rect, &Color::WHITE); - for element in data.versioned_canvas.get().elements().iter() { - element.draw(ctx); + let canvas = data.versioned_canvas.get(); + let selected = canvas.selected_elements(); + + for (pos, element) in canvas.elements().iter().enumerate() { + if data.tool_ctx.has_active_selection() && selected.contains(&pos) { + element.draw_selected(ctx); + } else { + element.draw(ctx); + } } if data.tool_ctx.needs_repaint() { data.tool_ctx.paint(ctx, env); } + + } } diff --git a/src/widget/tool_ctx.rs b/src/widget/tool_ctx.rs index 2dc95f1..88b9abf 100644 --- a/src/widget/tool_ctx.rs +++ b/src/widget/tool_ctx.rs @@ -16,10 +16,13 @@ use crate::tool::{CanvasToolParams, CanvasToolType}; use crate::canvas::{Canvas, CanvasElement}; -use crate::history::VersionedCanvas; +use crate::widget::CanvasWidget; +use crate::VersionedCanvas; use druid::kurbo::BezPath; -use druid::{Data, Env, Event, EventCtx, MouseButton, MouseEvent, PaintCtx}; +use druid::{Color, Data, Env, Event, EventCtx, MouseButton, MouseEvent, PaintCtx, RenderContext}; + +use im::{vector, Vector}; #[derive(Clone, Data)] pub enum CanvasToolState { @@ -29,6 +32,12 @@ pub enum CanvasToolState { current_path: CanvasElement, }, Erasing, + SelectingRect { + origin: druid::Point, + current_rect: druid::Rect, + }, + DraggingSelection, + ActiveSelection, } #[derive(Clone, Data)] @@ -42,6 +51,7 @@ impl CanvasToolParams { match self { CanvasToolParams::Pen { .. } => CanvasToolType::Pen, CanvasToolParams::Eraser => CanvasToolType::Eraser, + CanvasToolParams::RectangularSelection => CanvasToolType::RectangularSelection, } } } @@ -51,6 +61,14 @@ fn pressed(mouse_event: &MouseEvent) -> bool { || mouse_event.button == MouseButton::Left } +fn is_over_active_selection(mouse_event: &MouseEvent, vcanvas: &VersionedCanvas) -> bool { + let mouse_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0)); + // TODO(enrico): this can be faster + let intersecting_elements = vcanvas.get().find_elements_intersecting(mouse_rect); + let selected_elements = vcanvas.get().selected_elements(); + false +} + impl CanvasToolCtx { pub fn new(params: CanvasToolParams) -> Self { CanvasToolCtx { @@ -59,9 +77,22 @@ impl CanvasToolCtx { } } + pub fn set_params(&mut self, params: CanvasToolParams) { + self.initial_params = params; + // TODO(enrico) is this necessary? + match self.state { + CanvasToolState::Idle | CanvasToolState::ActiveSelection => {} + _ => { self.state = CanvasToolState::Idle; } + } + } + + pub fn set_state(&mut self, state: CanvasToolState) { + self.state = state; + } + pub fn handle_event( &mut self, - ctx: &EventCtx, + mut ctx: &mut EventCtx, event: &Event, mut vcanvas: &mut VersionedCanvas, env: &Env, @@ -69,6 +100,7 @@ impl CanvasToolCtx { match self.initial_params.tool_type() { CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env), CanvasToolType::Eraser => self.handle_erase_event(&ctx, &event, &mut vcanvas, &env), + CanvasToolType::RectangularSelection => self.handle_rectangular_select_event(&mut ctx, &event, &mut vcanvas, &env), } } @@ -80,6 +112,7 @@ impl CanvasToolCtx { _env: &Env, ) { match (&mut self.state, event) { + (CanvasToolState::ActiveSelection, Event::MouseDown(mouse_event)) | (CanvasToolState::Idle, Event::MouseDown(mouse_event)) if pressed(mouse_event) => { self.state = CanvasToolState::Erasing; } @@ -117,6 +150,7 @@ impl CanvasToolCtx { _env: &Env, ) { match (&mut self.state, event) { + (CanvasToolState::ActiveSelection, Event::MouseDown(mouse_event)) | (CanvasToolState::Idle, Event::MouseDown(mouse_event)) if pressed(mouse_event) => { let mut kurbo_path = BezPath::new(); kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); @@ -137,7 +171,7 @@ impl CanvasToolCtx { .. }, Event::MouseMove(mouse_event), - ) => if pressed(mouse_event) { + ) if pressed(mouse_event) => { if let CanvasElement::Freehand { ref mut path, .. } = current_path { path.kurbo_path .line_to((mouse_event.pos.x, mouse_event.pos.y)); @@ -158,6 +192,7 @@ impl CanvasToolCtx { thickness, stroke_color, }); + *canvas.selected_elements_mut() = vector![]; } } }); @@ -166,13 +201,78 @@ impl CanvasToolCtx { } } + pub fn handle_rectangular_select_event( + &mut self, + ctx: &mut EventCtx, + event: &Event, + vcanvas: &mut VersionedCanvas, + _env: &Env, + ) { + match (&mut self.state, event) { + (CanvasToolState::ActiveSelection, Event::MouseMove(mouse_event)) if !pressed(mouse_event) => { + ctx.submit_notification(CanvasWidget::IS_OVER_SELECTION.with(is_over_active_selection(mouse_event, vcanvas))); + } + (CanvasToolState::ActiveSelection, Event::MouseDown(mouse_event)) if pressed(mouse_event) + && is_over_active_selection(mouse_event, vcanvas) => { + } + (CanvasToolState::DraggingSelection, Event::MouseMove(mouse_event)) if pressed(mouse_event) => { + } + (CanvasToolState::DraggingSelection, Event::MouseUp(mouse_event)) if pressed(mouse_event) => { + } + + (CanvasToolState::ActiveSelection, Event::MouseDown(mouse_event)) | + (CanvasToolState::Idle, Event::MouseDown(mouse_event)) if pressed(mouse_event) => { + self.state = CanvasToolState::SelectingRect { + origin: mouse_event.pos, + current_rect: druid::Rect::from_origin_size(mouse_event.pos, (0.0, 0.0)) + }; + } + + (CanvasToolState::SelectingRect{ ref mut current_rect, ref origin }, Event::MouseMove(mouse_event)) if pressed(mouse_event) => { + *current_rect = druid::Rect::from_origin_size(*origin, (0.0, 0.0)) + .union_pt(mouse_event.pos); + } + + (CanvasToolState::SelectingRect{ ref mut current_rect, ref origin }, Event::MouseUp(mouse_event)) if pressed(mouse_event) => { + *current_rect = druid::Rect::from_origin_size(*origin, (0.0, 0.0)) + .union_pt(mouse_event.pos); + let elements = vcanvas.get().elements(); + let selected_elements_idx = elements.iter().enumerate() + .filter_map(|(i, elem)| { + let bbox = elem.bounding_box(); + let p0 = druid::Point::new(bbox.x0, bbox.y0); + let p1 = druid::Point::new(bbox.x1, bbox.y1); + if current_rect.contains(p0) && current_rect.contains(p1) { + return Some(i); + } + None + }).collect::>(); + if !elements.is_empty() { + vcanvas.update(move |canvas: &mut Canvas| { + *canvas.selected_elements_mut() = selected_elements_idx; + }); + } + self.state = CanvasToolState::ActiveSelection; + } + _ => {} + } + } + pub fn needs_repaint(&self) -> bool { true } + pub fn has_active_selection(&self) -> bool { + match self.state { + CanvasToolState::ActiveSelection => true, + _ => false, + } + } + pub fn paint(&self, ctx: &mut PaintCtx, _env: &Env) { match &self.state { CanvasToolState::DrawingFreehand { current_path, .. } => current_path.draw(ctx), + CanvasToolState::SelectingRect { current_rect, .. } => ctx.stroke(current_rect, &Color::BLACK, 0.5), _ => {} } } diff --git a/src/widget/tool_icon.rs b/src/widget/tool_icon.rs index 4bcfb36..8910ea7 100644 --- a/src/widget/tool_icon.rs +++ b/src/widget/tool_icon.rs @@ -34,6 +34,9 @@ impl CanvasToolIconState { CanvasToolParams::Eraser => include_str!("../../icons/eraser.svg") .parse::() .unwrap(), + CanvasToolParams::RectangularSelection => include_str!("../../icons/eraser.svg") + .parse::() + .unwrap(), } } }