diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 936c293fc2..0c278b2621 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -33,10 +33,11 @@ use futures::channel::{mpsc, oneshot}; use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::eventresult::EventPropagation; use rnote_compose::ext::AabbExt; -use rnote_compose::penevent::{PenEvent, ShortcutKey}; +use rnote_compose::penevent::{KeyboardKey, PenEvent, ShortcutKey}; use rnote_compose::{Color, SplitOrder}; use serde::{Deserialize, Serialize}; use snapshot::Snapshotable; +use std::collections::HashSet; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Instant; @@ -779,13 +780,39 @@ impl Engine { | self.update_rendering_current_viewport() } - pub fn trash_selection(&mut self) -> WidgetFlags { + pub fn cancel_selection_temporary_pen(&self) -> bool { let selection_keys = self.store.selection_keys_as_rendered(); - self.store.set_trashed_keys(&selection_keys, true); - self.current_pen_update_state() - | self.doc_resize_autoexpand() - | self.record(Instant::now()) - | self.update_rendering_current_viewport() + !selection_keys.is_empty() + && self + .penholder + .pen_mode_state() + .take_style_override() + .is_some() + } + + pub fn trash_selection(&mut self) -> WidgetFlags { + // check if we have a selector as a temporary tool and need to change the pen + let cancel_selection = self.cancel_selection_temporary_pen(); + if cancel_selection { + // we trigger a delete press event as this reset the pen back to its + // original mode and deletes the content + let (_, widget_flags) = self.handle_pen_event( + rnote_compose::PenEvent::KeyPressed { + keyboard_key: KeyboardKey::Delete, + modifier_keys: HashSet::new(), + }, + None, + Instant::now(), + ); + widget_flags + } else { + let selection_keys = self.store.selection_keys_as_rendered(); + self.store.set_trashed_keys(&selection_keys, true); + self.current_pen_update_state() + | self.doc_resize_autoexpand() + | self.record(Instant::now()) + | self.update_rendering_current_viewport() + } } pub fn nothing_selected(&self) -> bool { diff --git a/crates/rnote-engine/src/pens/brush.rs b/crates/rnote-engine/src/pens/brush.rs index 5979de0dd2..88b1c269b3 100644 --- a/crates/rnote-engine/src/pens/brush.rs +++ b/crates/rnote-engine/src/pens/brush.rs @@ -7,6 +7,7 @@ use crate::store::StrokeKey; use crate::strokes::BrushStroke; use crate::strokes::Stroke; use crate::{DrawableOnDoc, WidgetFlags}; +use kurbo::Shape; use p2d::bounding_volume::{Aabb, BoundingVolume}; use piet::RenderContext; use rnote_compose::Constraints; @@ -17,7 +18,9 @@ use rnote_compose::builders::{ use rnote_compose::eventresult::{EventPropagation, EventResult}; use rnote_compose::penevent::{PenEvent, PenProgress}; use rnote_compose::penpath::{Element, Segment}; +use rnote_compose::shapes::Shapeable; use std::time::Instant; +use tracing::debug; #[derive(Debug)] enum BrushState { @@ -41,6 +44,14 @@ impl Default for Brush { } } +impl Brush { + /// Threshold for the stroke length over which we consider + /// that strokes that are left by pressing the pen down + /// when cancelling a selection should be kept. + /// Smaller ratio are deleted. Zoom ratio is taken into account + const LENGTH_PX_THRESHOLD: f64 = 25.0; +} + impl PenBehaviour for Brush { fn init(&mut self, _engine_view: &EngineView) -> WidgetFlags { WidgetFlags::default() @@ -63,6 +74,7 @@ impl PenBehaviour for Brush { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + _temporary_tool: bool, ) -> (EventResult, WidgetFlags) { let mut widget_flags = WidgetFlags::default(); @@ -235,6 +247,33 @@ impl PenBehaviour for Brush { ); } + // remove strokes that follow a selection cancellation if they are large + // hence we can write after selecting strokes but we won't leave tiny spots + // behind + if engine_view.store.get_cancelled_state() { + let current_stroke_width = engine_view + .config + .pens_config + .brush_config + .get_stroke_width(); + let length_px = engine_view + .store + .get_stroke_ref(*current_stroke_key) + .unwrap() + .outline_path() + .perimeter(current_stroke_width); + let threshold = Self::LENGTH_PX_THRESHOLD / engine_view.camera.zoom(); + debug!( + "perimeter {:?} threshold {:?}, zoom {:?}", + length_px, + threshold, + engine_view.camera.zoom() + ); + if length_px < threshold { + engine_view.store.remove_stroke(*current_stroke_key); + } + } + // Finish up the last stroke engine_view .store diff --git a/crates/rnote-engine/src/pens/eraser.rs b/crates/rnote-engine/src/pens/eraser.rs index d944aa8cc9..950c565836 100644 --- a/crates/rnote-engine/src/pens/eraser.rs +++ b/crates/rnote-engine/src/pens/eraser.rs @@ -55,13 +55,17 @@ impl PenBehaviour for Eraser { event: PenEvent, _now: Instant, engine_view: &mut EngineViewMut, + _temporary_tool: bool, ) -> (EventResult, WidgetFlags) { let mut widget_flags = WidgetFlags::default(); let event_result = match (&mut self.state, event) { (EraserState::Up | EraserState::Proximity { .. }, PenEvent::Down { element, .. }) => { - widget_flags |= erase(element, engine_view); - self.state = EraserState::Down(element); + if !engine_view.store.get_cancelled_state() { + widget_flags |= erase(element, engine_view); + self.state = EraserState::Down(element); + // this means we need one more up/down event here to activate the eraser after a selection cancellation + } EventResult { handled: true, propagate: EventPropagation::Stop, diff --git a/crates/rnote-engine/src/pens/mod.rs b/crates/rnote-engine/src/pens/mod.rs index 5b8c870ee8..aaa1338632 100644 --- a/crates/rnote-engine/src/pens/mod.rs +++ b/crates/rnote-engine/src/pens/mod.rs @@ -102,14 +102,19 @@ impl PenBehaviour for Pen { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + temporary_tool: bool, ) -> (EventResult, WidgetFlags) { match self { - Pen::Brush(brush) => brush.handle_event(event, now, engine_view), - Pen::Shaper(shaper) => shaper.handle_event(event, now, engine_view), - Pen::Typewriter(typewriter) => typewriter.handle_event(event, now, engine_view), - Pen::Eraser(eraser) => eraser.handle_event(event, now, engine_view), - Pen::Selector(selector) => selector.handle_event(event, now, engine_view), - Pen::Tools(tools) => tools.handle_event(event, now, engine_view), + Pen::Brush(brush) => brush.handle_event(event, now, engine_view, temporary_tool), + Pen::Shaper(shaper) => shaper.handle_event(event, now, engine_view, temporary_tool), + Pen::Typewriter(typewriter) => { + typewriter.handle_event(event, now, engine_view, temporary_tool) + } + Pen::Eraser(eraser) => eraser.handle_event(event, now, engine_view, temporary_tool), + Pen::Selector(selector) => { + selector.handle_event(event, now, engine_view, temporary_tool) + } + Pen::Tools(tools) => tools.handle_event(event, now, engine_view, temporary_tool), } } diff --git a/crates/rnote-engine/src/pens/penbehaviour.rs b/crates/rnote-engine/src/pens/penbehaviour.rs index 93a435bd00..4c9fed4322 100644 --- a/crates/rnote-engine/src/pens/penbehaviour.rs +++ b/crates/rnote-engine/src/pens/penbehaviour.rs @@ -30,6 +30,7 @@ pub trait PenBehaviour: DrawableOnDoc { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + temporary_tool: bool, ) -> (EventResult, WidgetFlags); /// Handle a requested animation frame. diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index c4fff50c45..49eebb954d 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -45,6 +45,15 @@ pub struct PenHolder { toggle_pen_style: Option, #[serde(skip)] prev_shortcut_key: Option, + + /// indicates if we toggled a shortcut key + /// in temporary mode without changing + /// `PenMode` in the current sequence. + /// Used to tweak the behavior of tools + /// on exit so that we don't exit the tool + /// with the shortcut key pressed + #[serde(skip)] + temporary_style: bool, } impl Default for PenHolder { @@ -57,6 +66,7 @@ impl Default for PenHolder { progress: PenProgress::Idle, toggle_pen_style: None, prev_shortcut_key: None, + temporary_style: false, } } } @@ -110,6 +120,7 @@ impl PenHolder { // When the style is changed externally, the toggle mode / internal states are reset self.toggle_pen_style = None; self.prev_shortcut_key = None; + self.temporary_style = false; widget_flags } @@ -147,6 +158,7 @@ impl PenHolder { if self.pen_mode_state.pen_mode() != new_pen_mode { self.pen_mode_state.set_pen_mode(new_pen_mode); + self.temporary_style = false; widget_flags |= self.reinstall_pen_current_style(engine_view); widget_flags.refresh_ui = true; } @@ -163,7 +175,7 @@ impl PenHolder { // first cancel the current pen let (_, mut widget_flags) = self.current_pen - .handle_event(PenEvent::Cancel, Instant::now(), engine_view); + .handle_event(PenEvent::Cancel, Instant::now(), engine_view, false); // then reinstall a new pen instance let mut new_pen = new_pen(self.current_pen_style_w_override(&engine_view.as_im())); @@ -193,17 +205,25 @@ impl PenHolder { widget_flags |= self.change_pen_mode(pen_mode, engine_view); } + if matches!(event, PenEvent::Cancel | PenEvent::Up { .. }) { + self.temporary_style = false; + } + // Handle the event with the current pen - let (mut event_result, wf) = self - .current_pen - .handle_event(event.clone(), now, engine_view); + let (mut event_result, wf) = + self.current_pen + .handle_event(event.clone(), now, engine_view, self.temporary_style); widget_flags |= wf | self.handle_pen_progress(event_result.progress, engine_view); if !event_result.handled { - let (propagate, wf) = self.handle_pen_event_global(event, now, engine_view); + let (propagate, wf) = self.handle_pen_event_global(event.clone(), now, engine_view); event_result.propagate |= propagate; widget_flags |= wf; } + // reset the cancelled state as we just handled a pen tool (and not a selection tool) event + if matches!(event, PenEvent::Up { .. }) { + engine_view.store.set_cancelled_state(false); + } // Always redraw after handling a pen event. // @@ -238,6 +258,13 @@ impl PenHolder { match action { ShortcutAction::ChangePenStyle { style, mode } => match mode { ShortcutMode::Temporary => { + if self + .pen_mode_state + .current_style_w_override(&engine_view.config.pens_config) + == style + { + self.temporary_style = true; + }; widget_flags |= self.change_style_override(Some(style), engine_view); } ShortcutMode::Permanent => { diff --git a/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs b/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs index 514efaddbf..f60c104c1a 100644 --- a/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs +++ b/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs @@ -148,4 +148,12 @@ impl BrushConfig { } } } + + pub(crate) fn get_stroke_width(&self) -> f64 { + match &self.style { + BrushStyle::Marker => self.marker_options.stroke_width, + BrushStyle::Solid => self.solid_options.stroke_width, + BrushStyle::Textured => self.textured_options.stroke_width, + } + } } diff --git a/crates/rnote-engine/src/pens/selector/mod.rs b/crates/rnote-engine/src/pens/selector/mod.rs index b1035b86f6..f0db1af69c 100644 --- a/crates/rnote-engine/src/pens/selector/mod.rs +++ b/crates/rnote-engine/src/pens/selector/mod.rs @@ -133,12 +133,15 @@ impl PenBehaviour for Selector { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + temporary_tool: bool, ) -> (EventResult, WidgetFlags) { match event { PenEvent::Down { element, modifier_keys, - } => self.handle_pen_event_down(element, modifier_keys, now, engine_view), + } => { + self.handle_pen_event_down(element, modifier_keys, now, engine_view, temporary_tool) + } PenEvent::Up { element, modifier_keys, diff --git a/crates/rnote-engine/src/pens/selector/penevents.rs b/crates/rnote-engine/src/pens/selector/penevents.rs index d4880c8212..5efa95859c 100644 --- a/crates/rnote-engine/src/pens/selector/penevents.rs +++ b/crates/rnote-engine/src/pens/selector/penevents.rs @@ -22,6 +22,7 @@ impl Selector { modifier_keys: HashSet, _now: Instant, engine_view: &mut EngineViewMut, + temporary_tool: bool, ) -> (EventResult, WidgetFlags) { let mut widget_flags = WidgetFlags::default(); self.pos = Some(element.pos); @@ -198,10 +199,24 @@ impl Selector { }; } else { // when clicking outside the selection bounds, reset + tracing::debug!("cancelling the selection"); engine_view.store.set_selected_keys(selection, false); - self.state = SelectorState::Idle; - progress = PenProgress::Finished; + if temporary_tool { + // we had a selection active then for the next sequence + // we activated the temporary mode for the selector and + // clicked outside the selection. We expect to be able + // to do another selection + self.state = SelectorState::Selecting { path: Vec::new() }; + progress = PenProgress::InProgress; + } else { + self.state = SelectorState::Idle; + progress = PenProgress::Finished; + + // This event is in the same sequence than the one that + // cancelled the selection. We thus set the variable to true here + engine_view.store.set_cancelled_state(true); + } } } ModifyState::Translate { diff --git a/crates/rnote-engine/src/pens/shaper.rs b/crates/rnote-engine/src/pens/shaper.rs index ef9bac90a3..9a98512239 100644 --- a/crates/rnote-engine/src/pens/shaper.rs +++ b/crates/rnote-engine/src/pens/shaper.rs @@ -62,26 +62,31 @@ impl PenBehaviour for Shaper { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + _temporary_tool: bool, ) -> (EventResult, WidgetFlags) { let mut widget_flags = WidgetFlags::default(); let event_result = match (&mut self.state, event) { (ShaperState::Idle, PenEvent::Down { mut element, .. }) => { - engine_view - .config - .pens_config - .shaper_config - .new_style_seeds(); - element.pos = engine_view - .document - .snap_position(element.pos, engine_view.config); + if !engine_view.store.get_cancelled_state() { + // here we need an additional up/down event after a selection + // cancellation + engine_view + .config + .pens_config + .shaper_config + .new_style_seeds(); + element.pos = engine_view + .document + .snap_position(element.pos, engine_view.config); - self.state = ShaperState::BuildShape { - builder: new_builder( - engine_view.config.pens_config.shaper_config.builder_type, - element, - now, - ), + self.state = ShaperState::BuildShape { + builder: new_builder( + engine_view.config.pens_config.shaper_config.builder_type, + element, + now, + ), + }; }; EventResult { @@ -177,17 +182,19 @@ impl PenBehaviour for Shaper { .gen_style_for_current_options(); let shapes_emitted = !shapes.is_empty(); - for shape in shapes { - let key = engine_view.store.insert_stroke( - Stroke::ShapeStroke(ShapeStroke::new(shape, style.clone())), - None, - ); - style.advance_seed(); - engine_view.store.regenerate_rendering_for_stroke( - key, - engine_view.camera.viewport(), - engine_view.camera.image_scale(), - ); + if shapes_emitted { + for shape in shapes { + let key = engine_view.store.insert_stroke( + Stroke::ShapeStroke(ShapeStroke::new(shape, style.clone())), + None, + ); + style.advance_seed(); + engine_view.store.regenerate_rendering_for_stroke( + key, + engine_view.camera.viewport(), + engine_view.camera.image_scale(), + ); + } } self.state = ShaperState::Idle; diff --git a/crates/rnote-engine/src/pens/tools/mod.rs b/crates/rnote-engine/src/pens/tools/mod.rs index a55ecf9c30..f37a8653bf 100644 --- a/crates/rnote-engine/src/pens/tools/mod.rs +++ b/crates/rnote-engine/src/pens/tools/mod.rs @@ -64,6 +64,7 @@ impl PenBehaviour for Tools { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + _temporary_tool: bool, ) -> (EventResult, WidgetFlags) { match engine_view.config.pens_config.tools_config.style { ToolStyle::VerticalSpace => { diff --git a/crates/rnote-engine/src/pens/typewriter/mod.rs b/crates/rnote-engine/src/pens/typewriter/mod.rs index 306936d1a3..ccccdc57c0 100644 --- a/crates/rnote-engine/src/pens/typewriter/mod.rs +++ b/crates/rnote-engine/src/pens/typewriter/mod.rs @@ -14,6 +14,7 @@ use futures::channel::oneshot; use p2d::bounding_volume::{Aabb, BoundingVolume}; use piet::RenderContext; use rnote_compose::EventResult; +use rnote_compose::eventresult::EventPropagation; use rnote_compose::ext::{AabbExt, Vector2Ext}; use rnote_compose::penevent::{KeyboardKey, PenEvent, PenProgress, PenState}; use rnote_compose::shapes::Shapeable; @@ -401,12 +402,26 @@ impl PenBehaviour for Typewriter { event: PenEvent, now: Instant, engine_view: &mut EngineViewMut, + _temporary_tool: bool, ) -> (EventResult, WidgetFlags) { let (event_result, widget_flags) = match event { PenEvent::Down { element, modifier_keys, - } => self.handle_pen_event_down(element, modifier_keys, now, engine_view), + } => { + if !engine_view.store.get_cancelled_state() { + self.handle_pen_event_down(element, modifier_keys, now, engine_view) + } else { + ( + EventResult { + handled: true, + propagate: EventPropagation::Stop, + progress: PenProgress::InProgress, + }, + WidgetFlags::default(), + ) + } + } PenEvent::Up { element, modifier_keys, diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index e73fc2f7ef..be67c36a97 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -91,6 +91,12 @@ pub struct StrokeStore { /// The index of the current live document in the history stack. #[serde(skip)] live_index: usize, + /// Boolean that indicates the pen event is one from the same + /// event sequence than the one that cancelled the selection + /// Events that cancel a selection should set this to true + /// and this should be set back to false on a pen up event + #[serde(skip)] + canceled_state: bool, /// An rtree backed by the slotmap store, for faster spatial queries. /// /// Needs to be updated with `update_with_key()` when strokes changed their geometry or position! @@ -110,6 +116,7 @@ impl Default for StrokeStore { // Start off with state in the history history: VecDeque::from(vec![HistoryEntry::default()]), live_index: 0, + canceled_state: false, key_tree: KeyTree::default(), @@ -368,4 +375,14 @@ impl StrokeStore { widget_flags } + + /// set the active state for the cancelled selection + pub fn set_cancelled_state(&mut self, state: bool) { + self.canceled_state = state + } + + /// get the active state for the cancelled selection + pub fn get_cancelled_state(&self) -> bool { + self.canceled_state + } } diff --git a/crates/rnote-ui/src/appwindow/actions.rs b/crates/rnote-ui/src/appwindow/actions.rs index 740ca2245e..b221f5352e 100644 --- a/crates/rnote-ui/src/appwindow/actions.rs +++ b/crates/rnote-ui/src/appwindow/actions.rs @@ -9,11 +9,12 @@ use gtk4::{ }; use p2d::bounding_volume::BoundingVolume; use rnote_compose::SplitOrder; -use rnote_compose::penevent::ShortcutKey; +use rnote_compose::penevent::{KeyboardKey, ShortcutKey}; use rnote_engine::engine::StrokeContent; use rnote_engine::ext::GraphenePointExt; use rnote_engine::strokes::resize::{ImageSizeOption, Resize}; use rnote_engine::{Camera, Engine}; +use std::collections::HashSet; use std::path::PathBuf; use std::time::Instant; use tracing::{debug, error}; @@ -407,7 +408,23 @@ impl RnAppWindow { let Some(canvas) = appwindow.active_tab_canvas() else { return; }; - let widget_flags = canvas.engine_mut().trash_selection(); + // check if we have a selector as a temporary tool and need to change the pen + let cancel_selection = canvas.engine_ref().cancel_selection_temporary_pen(); + let widget_flags = if cancel_selection { + // trigger an event for a KeyboardPress::Delete : this both deletes the selection + // and resets the pen back to its previous mode + let (_, widget_flags) = canvas.engine_mut().handle_pen_event( + rnote_compose::PenEvent::KeyPressed { + keyboard_key: KeyboardKey::Delete, + modifier_keys: HashSet::new(), + }, + None, + Instant::now(), + ); + widget_flags + } else { + canvas.engine_mut().trash_selection() + }; appwindow.handle_widget_flags(widget_flags, &canvas); } ));