From f330760c1c9510fef722d99c4a109ae64f348566 Mon Sep 17 00:00:00 2001 From: Enrico Lumetti Date: Mon, 1 Mar 2021 23:19:41 +0100 Subject: [PATCH] Add support to stylus eraser --- src/lib.rs | 2 + src/main.rs | 115 +++++++++++++++++++++++++++++++------------ src/tool.rs | 3 +- src/widget/canvas.rs | 54 +++++++++++++++++--- 4 files changed, 134 insertions(+), 40 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8376545..222ad17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,8 @@ pub mod commands { use druid::Selector; pub const UPDATE_TOOL: Selector = Selector::new("stiletto_update_tool"); + pub const PUSH_ERASER: Selector<()> = Selector::new("push_eraser_context"); + pub const POP_ERASER: Selector<()> = Selector::new("pop_eraser_context"); } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/main.rs b/src/main.rs index 91e1be8..287e5f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,9 @@ use log::{info, warn}; use druid::commands; use druid::im::vector; use druid::widget::prelude::*; -use druid::widget::{Align, Button, CrossAxisAlignment, Flex, List, SizedBox, WidgetExt}; +use druid::widget::{ + Align, Button, Controller, CrossAxisAlignment, Flex, List, SizedBox, WidgetExt, +}; use druid::{ AppDelegate, AppLauncher, Color, Command, Data, DelegateCtx, Env, FileDialogOptions, FileSpec, Handled, Lens, Target, WindowDesc, @@ -70,6 +72,7 @@ pub fn main() { }, ], current_tool: 0, + eraser_tool_id: 2, current_file_path: None, }; AppLauncher::with_window(window) @@ -84,10 +87,24 @@ struct StilettoState { canvas: CanvasState, tool_icons: Vector, current_tool: usize, + eraser_tool_id: usize, #[data(same_fn = "PartialEq::eq")] current_file_path: Option, } +impl StilettoState { + pub fn set_tool(&mut self, new_tool: usize) { + let old_tool = self.current_tool; + info!("Changing active tool to: tool #{}", new_tool); + 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.tool_icons.get(new_tool).unwrap().tool_params.clone(), + )); + } +} + fn build_ui() -> impl Widget { let history_buttons = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) @@ -107,17 +124,19 @@ fn build_ui() -> impl Widget { let save_buttons = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(Button::new("Save").on_click(move |ctx, data: &mut StilettoState, _| { - if data.current_file_path.is_some() { - ctx.submit_command(Command::new(commands::SAVE_FILE, (), Target::Auto)); - } else { - ctx.submit_command(Command::new( - druid::commands::SHOW_SAVE_PANEL, - save_dialog_options_clone.clone(), - Target::Auto, - )) - } - })) + .with_child( + Button::new("Save").on_click(move |ctx, data: &mut StilettoState, _| { + if data.current_file_path.is_some() { + ctx.submit_command(Command::new(commands::SAVE_FILE, (), Target::Auto)); + } else { + ctx.submit_command(Command::new( + druid::commands::SHOW_SAVE_PANEL, + save_dialog_options_clone.clone(), + Target::Auto, + )) + } + }), + ) .with_child(Button::new("Save As").on_click(move |ctx, _, _| { ctx.submit_command(Command::new( druid::commands::SHOW_SAVE_PANEL, @@ -141,7 +160,7 @@ fn build_ui() -> impl Widget { .padding((8.0, 0.0)) .on_click(|ctx, tool_state, _| { tool_state.selected = true; - ctx.submit_command(stiletto::commands::UPDATE_TOOL); + ctx.submit_notification(stiletto::commands::UPDATE_TOOL); }) }) .horizontal() @@ -163,13 +182,60 @@ fn build_ui() -> impl Widget { .cross_axis_alignment(CrossAxisAlignment::Center) .must_fill_main_axis(true) .with_child(SizedBox::new(Align::left(toolbar)).height(50.0)) - .with_flex_child((CanvasWidget {}).lens(StilettoState::canvas), 1.0) + .with_flex_child((CanvasWidget).lens(StilettoState::canvas), 1.0) + .controller(ToolSwitcher::new()) +} + +struct ToolSwitcher { + saved_tool: usize, +} + +impl ToolSwitcher { + pub fn new() -> Self { + ToolSwitcher { saved_tool: 0 } + } +} + +impl> Controller for ToolSwitcher { + fn event( + &mut self, + child: &mut W, + ctx: &mut EventCtx, + event: &Event, + data: &mut StilettoState, + env: &Env, + ) { + match event { + Event::Notification(cmd) => { + let new_tool = if cmd.get(stiletto::commands::PUSH_ERASER).is_some() { + self.saved_tool = data.current_tool; + Some(data.eraser_tool_id) + } else if cmd.get(stiletto::commands::POP_ERASER).is_some() { + Some(self.saved_tool) + } else if cmd.get(stiletto::commands::UPDATE_TOOL).is_some() { + let old_tool = data.current_tool; + data.tool_icons + .iter() + .enumerate() + .position(|(pos, x)| pos != old_tool && x.selected) + } else { + None + }; + + if let Some(new_tool) = new_tool { + data.set_tool(new_tool); + ctx.set_handled(); + } + } + _ => {} + } + child.event(ctx, event, data, env); + } } struct Delegate; impl AppDelegate for Delegate { - fn command( &mut self, _ctx: &mut DelegateCtx, @@ -181,7 +247,8 @@ impl AppDelegate for Delegate { use std::fs::File; if cmd.get(commands::SAVE_FILE_AS).is_some() || cmd.get(commands::SAVE_FILE).is_some() { - let mut path_buf = cmd.get(commands::SAVE_FILE_AS) + let mut path_buf = cmd + .get(commands::SAVE_FILE_AS) .map(|file_info| file_info.path().to_path_buf()) .or_else(|| data.current_file_path.as_ref().cloned()) .unwrap(); @@ -220,22 +287,6 @@ impl AppDelegate for Delegate { } return Handled::Yes; } - if cmd.get(stiletto::commands::UPDATE_TOOL).is_some() { - let old_tool = data.current_tool; - if let Some(new_tool) = data - .tool_icons - .iter() - .enumerate() - .position(|(pos, x)| pos != old_tool && x.selected) - { - info!("Changing active tool to: tool #{}", new_tool); - data.tool_icons.get_mut(old_tool).unwrap().selected = false; - data.current_tool = new_tool; - data.canvas.set_tool_ctx(CanvasToolCtx::new( - data.tool_icons.get(new_tool).unwrap().tool_params.clone(), - )); - } - } Handled::No } } diff --git a/src/tool.rs b/src/tool.rs index dec5a63..47fec7b 100644 --- a/src/tool.rs +++ b/src/tool.rs @@ -91,8 +91,7 @@ impl CanvasToolCtx { self.state = CanvasToolState::Erasing; } (CanvasToolState::Erasing, Event::MouseMove(mouse_event)) => { - let eraser_rect = - druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0)); + let eraser_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0)); let old_elements = vcanvas.get().elements(); let mut new_elements = old_elements.clone(); new_elements.retain(|elem| { diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs index ece8ac0..bb64c03 100644 --- a/src/widget/canvas.rs +++ b/src/widget/canvas.rs @@ -22,19 +22,21 @@ use crate::tool::CanvasToolCtx; use crate::DocumentSnapshot; use druid::widget::prelude::*; -use druid::{Color, Data, Env, Event}; +use druid::{Color, Data, Env, Event, PointerType}; #[derive(Clone, Data)] pub struct CanvasState { - pub versioned_canvas: VersionedCanvas, - pub tool_ctx: CanvasToolCtx, + versioned_canvas: VersionedCanvas, + tool_ctx: CanvasToolCtx, + temporary_erasing: bool, } impl CanvasState { pub fn new(tool_ctx: CanvasToolCtx) -> Self { CanvasState { versioned_canvas: VersionedCanvas::new(Canvas::new()), - tool_ctx, + tool_ctx: tool_ctx, + temporary_erasing: true, } } pub fn perform_undo(&mut self) { @@ -72,14 +74,54 @@ impl CanvasState { pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) { self.tool_ctx = ctx; } + + pub fn get_tool_ctx(&self) -> &CanvasToolCtx { + &self.tool_ctx + } + + pub fn get_tool_ctx_mut(&mut self) -> &mut CanvasToolCtx { + &mut self.tool_ctx + } + + pub fn handle_event(&mut self, mut ctx: &mut EventCtx, event: &Event, env: &Env) { + self.tool_ctx + .handle_event(ctx, event, &mut self.versioned_canvas, env); + } } pub struct CanvasWidget; impl Widget for CanvasWidget { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut CanvasState, env: &Env) { - data.tool_ctx - .handle_event(ctx, event, &mut data.versioned_canvas, env); + ctx.request_focus(); + let mut toggle_eraser_event = false; + let mut enable_temporary_erasing = false; + match event { + Event::MouseDown(mouse_event) => { + toggle_eraser_event = true; + enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser; + } + Event::MouseMove(mouse_event) => { + toggle_eraser_event = true; + enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser; + } + Event::MouseUp(mouse_event) => { + toggle_eraser_event = true; + enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser; + } + _ => {} + } + // TODO: the first eraser toggle is not handled + if toggle_eraser_event && data.temporary_erasing != enable_temporary_erasing { + if enable_temporary_erasing { + ctx.submit_notification(crate::commands::PUSH_ERASER); + } else { + ctx.submit_notification(crate::commands::POP_ERASER); + } + data.temporary_erasing = enable_temporary_erasing; + } else { + data.handle_event(ctx, event, env); + } } fn lifecycle(