diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 2e7ad19093..da22c5eb6c 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -101,6 +101,10 @@ pub enum EngineTask { BlinkTypewriterCursor, /// Change the permanent zoom to the given value Zoom(f64), + /// Task for a pen that's held down at the same position for a long time + /// We do that through a task to take into account the case of a mouse that's + /// held down at the same position without giving any new event + LongPressStatic, /// Indicates that the application is quitting. Sent to quit the handler which receives the tasks. Quit, } @@ -451,6 +455,21 @@ impl Engine { widget_flags.redraw = true; } } + EngineTask::LongPressStatic => { + println!("long press event, engine task"); + // when we are here, we know this is a long press event + widget_flags |= self.penholder.handle_long_press( + Instant::now(), + &mut EngineViewMut { + tasks_tx: self.engine_tasks_tx(), + pens_config: &mut self.pens_config, + document: &mut self.document, + store: &mut self.store, + camera: &mut self.camera, + audioplayer: &mut self.audioplayer, + }, + ); + } EngineTask::Zoom(zoom) => { widget_flags |= self.camera.zoom_temporarily_to(1.0) | self.camera.zoom_to(zoom); diff --git a/crates/rnote-engine/src/pens/brush.rs b/crates/rnote-engine/src/pens/brush.rs index d993bb898a..ed91d6ad0c 100644 --- a/crates/rnote-engine/src/pens/brush.rs +++ b/crates/rnote-engine/src/pens/brush.rs @@ -2,6 +2,7 @@ use super::pensconfig::brushconfig::BrushStyle; use super::PenBehaviour; use super::PenStyle; +use crate::engine::EngineTask; use crate::engine::{EngineView, EngineViewMut}; use crate::store::StrokeKey; use crate::strokes::BrushStroke; @@ -17,8 +18,27 @@ use rnote_compose::eventresult::{EventPropagation, EventResult}; use rnote_compose::penevent::{PenEvent, PenProgress}; use rnote_compose::penpath::{Element, Segment}; use rnote_compose::Constraints; +use std::collections::VecDeque; +use std::time::Duration; use std::time::Instant; +#[derive(Debug, Copy, Clone)] +pub struct PosTimeDict { + pub pos: na::Vector2, + distance_to_previous: f64, + time: Instant, +} + +impl Default for PosTimeDict { + fn default() -> Self { + Self { + pos: na::Vector2::new(0.0, 0.0), + distance_to_previous: 0.0, + time: Instant::now(), + } + } +} + #[derive(Debug)] enum BrushState { Idle, @@ -28,15 +48,93 @@ enum BrushState { }, } +#[derive(Debug, Default)] +pub struct LongPressDetector { + distance: f64, + total_distance: f64, + pub last_strokes: VecDeque, +} + +impl LongPressDetector { + fn clear(&mut self) { + self.last_strokes.clear(); + } + + fn total_distance(&self) -> f64 { + self.total_distance + } + + fn distance(&self) -> f64 { + self.distance + } + + fn reset(&mut self, element: Element, now: Instant) { + self.clear(); + self.last_strokes.push_front(PosTimeDict { + pos: element.pos, + distance_to_previous: 0.0, + time: now, + }); + self.distance = 0.0; + self.total_distance = 0.0; + } + + fn add_event(&mut self, element: Element, now: Instant) { + // add event to the front of the vecdeque + let latest_pos = self.last_strokes.front().unwrap().pos; + let dist_delta = latest_pos.metric_distance(&element.pos); + + self.last_strokes.push_front(PosTimeDict { + pos: element.pos, + distance_to_previous: dist_delta, + time: now, + }); + self.distance += dist_delta; + + //println!("adding {:?}", dist_delta); + + self.total_distance += dist_delta; + + while self.last_strokes.back().is_some() + && self.last_strokes.back().unwrap().time + < now - Duration::from_secs_f64(Brush::LONGPRESS_TIMEOUT) + { + // remove the last element + let back_element = self.last_strokes.pop_back().unwrap(); + self.distance -= back_element.distance_to_previous; + //println!("removing {:?}", back_element.distance_to_previous); + } + } + + pub fn get_latest_pos(&self) -> na::Vector2 { + self.last_strokes.front().unwrap().pos + } +} + #[derive(Debug)] pub struct Brush { state: BrushState, + /// handle for the separate task that makes it possible to + /// trigger long press for input with no jitter (where a long press + /// hold wouldn't trigger any new event) + longpress_handle: Option, + /// stroke key in progress when a long press occurs + pub current_stroke_key: Option, + /// save the start position for the current stroke + /// This prevents long press from happening on a point + /// We create a deadzone around the start position + pub start_position: Option, + pub long_press_detector: LongPressDetector, } impl Default for Brush { fn default() -> Self { Self { state: BrushState::Idle, + current_stroke_key: None, + longpress_handle: None, + start_position: None, + long_press_detector: LongPressDetector::default(), } } } @@ -47,6 +145,7 @@ impl PenBehaviour for Brush { } fn deinit(&mut self) -> WidgetFlags { + self.longpress_handle = None; WidgetFlags::default() } @@ -114,6 +213,18 @@ impl PenBehaviour for Brush { current_stroke_key, }; + self.start_position = Some(PosTimeDict { + pos: element.pos, + distance_to_previous: 0.0, + time: now, + }); + let tasks_tx = engine_view.tasks_tx.clone(); + self.longpress_handle = Some(crate::tasks::OneOffTaskHandle::new( + move || tasks_tx.send(EngineTask::LongPressStatic), + Duration::from_secs_f64(Self::LONGPRESS_TIMEOUT), + )); + self.long_press_detector.reset(element, now); + EventResult { handled: true, propagate: EventPropagation::Stop, @@ -153,6 +264,10 @@ impl PenBehaviour for Brush { .resize_autoexpand(engine_view.store, engine_view.camera); self.state = BrushState::Idle; + self.current_stroke_key = None; + self.start_position = None; + self.long_press_detector.clear(); + self.cancel_handle_long_press(); widget_flags |= engine_view.store.record(Instant::now()); widget_flags.store_modified = true; @@ -171,7 +286,7 @@ impl PenBehaviour for Brush { pen_event, ) => { let builder_result = - path_builder.handle_event(pen_event, now, Constraints::default()); + path_builder.handle_event(pen_event.clone(), now, Constraints::default()); let handled = builder_result.handled; let propagate = builder_result.propagate; @@ -207,6 +322,69 @@ impl PenBehaviour for Brush { ); } + // first send the event + if let Some(handle) = self.longpress_handle.as_mut() { + let _ = handle.reset_timeout(); + // only send pen down event to the detector + match pen_event { + PenEvent::Down { element, .. } => { + self.long_press_detector.add_event(element, now) + } + _ => (), + } + } else { + // recreate the handle if it was dropped + // errors from not using a refcell like the other use ? + // this happens when we sent a long_hold event and cancelled the long + // press. + // We have to restart he handle and the long press detector + let tasks_tx = engine_view.tasks_tx.clone(); + self.longpress_handle = Some(crate::tasks::OneOffTaskHandle::new( + move || tasks_tx.send(EngineTask::LongPressStatic), + Duration::from_secs_f64(Self::LONGPRESS_TIMEOUT), + )); + + match pen_event { + PenEvent::Down { element, .. } => { + self.start_position = Some(PosTimeDict { + pos: element.pos, + distance_to_previous: 0.0, + time: now, + }); + self.current_stroke_key = None; + self.long_press_detector.reset(element, now); + } + _ => { + // we are drawing only if the pen is down... + } + } + } + // then test : do we have a long press ? + let is_deadzone = self.long_press_detector.total_distance() + > 4.0 * engine_view.pens_config.brush_config.get_stroke_width(); + let is_static = self.long_press_detector.distance() + < 0.1 * engine_view.pens_config.brush_config.get_stroke_width(); + let time_delta = + now - self.start_position.unwrap_or(PosTimeDict::default()).time; + + println!("static distance {:?}", self.long_press_detector.distance()); + println!( + "deadzone : {:?}, static {:?}, {:?}", + is_deadzone, is_static, time_delta + ); + + if time_delta > Duration::from_secs_f64(Self::LONGPRESS_TIMEOUT) + && is_static + && is_deadzone + { + //save the key for potentially deleting it and replacing it with a shape + self.current_stroke_key = Some(current_stroke_key.clone()); + widget_flags.long_hold = true; + + // quit the handle. Either recognition is successful and we are right + // or we aren't and a new handle will be created on the next event + self.cancel_handle_long_press(); + } PenProgress::InProgress } BuilderProgress::Finished(segments) => { @@ -244,6 +422,7 @@ impl PenBehaviour for Brush { .resize_autoexpand(engine_view.store, engine_view.camera); self.state = BrushState::Idle; + self.cancel_handle_long_press(); widget_flags |= engine_view.store.record(Instant::now()); widget_flags.store_modified = true; @@ -311,6 +490,37 @@ impl DrawableOnDoc for Brush { impl Brush { const INPUT_OVERSHOOT: f64 = 30.0; + const LONGPRESS_TIMEOUT: f64 = 1.5; + + pub fn cancel_handle_long_press(&mut self) { + // cancel the long press handle + if let Some(handle) = self.longpress_handle.as_mut() { + let _ = handle.quit(); + } + } + + pub fn reset_long_press(&mut self, element: Element, now: Instant) { + self.start_position = None; + self.current_stroke_key = None; + self.cancel_handle_long_press(); + self.longpress_handle = None; + self.long_press_detector.reset(element, now); + } + + pub fn add_stroke_key(&mut self) -> Result<(), ()> { + // extract the content of the stroke for recognition purposes + match &mut self.state { + BrushState::Drawing { + path_builder: _, + current_stroke_key, + } => { + //save the key + self.current_stroke_key = Some(current_stroke_key.clone()); + Ok(()) + } + _ => Err(()), + } + } } fn play_marker_sound(engine_view: &mut EngineViewMut) { diff --git a/crates/rnote-engine/src/pens/mod.rs b/crates/rnote-engine/src/pens/mod.rs index 0c3c677abf..29d52b4125 100644 --- a/crates/rnote-engine/src/pens/mod.rs +++ b/crates/rnote-engine/src/pens/mod.rs @@ -18,6 +18,7 @@ pub use penbehaviour::PenBehaviour; pub use penholder::PenHolder; pub use penmode::PenMode; pub use pensconfig::PensConfig; +use rnote_compose::penpath::Element; pub use selector::Selector; pub use shaper::Shaper; pub use shortcuts::Shortcuts; @@ -52,6 +53,26 @@ impl Default for Pen { } } +impl Pen { + // intermediary function for mutability + pub fn reset_long_press(&mut self, now: Instant) -> Result<(), ()> { + match self { + Pen::Brush(brush) => { + // get the last element stored in the recognizer + brush.reset_long_press( + Element { + pos: brush.long_press_detector.get_latest_pos(), + pressure: 1.0, + }, + now, + ); + Ok(()) + } + _ => Err(()), + } + } +} + impl PenBehaviour for Pen { fn init(&mut self, engine_view: &EngineView) -> WidgetFlags { match self { diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 66952fbe25..3e8cbc8f2d 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -8,14 +8,17 @@ use super::{ use crate::camera::NudgeDirection; use crate::engine::{EngineView, EngineViewMut}; use crate::pens::shortcuts::ShortcutAction; +use crate::strokes::Stroke; use crate::widgetflags::WidgetFlags; use crate::{CloneConfig, DrawableOnDoc}; use futures::channel::oneshot; use p2d::bounding_volume::Aabb; use piet::RenderContext; +use rnote_compose::builders::ShapeBuilderType; use rnote_compose::eventresult::EventPropagation; use rnote_compose::penevent::{KeyboardKey, ModifierKey, PenEvent, PenProgress, ShortcutKey}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::time::{Duration, Instant}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -227,6 +230,77 @@ impl PenHolder { self.current_pen_mut().deinit() } + /// handle a hold press for the pen + pub fn handle_long_press( + &mut self, + now: Instant, + engine_view: &mut EngineViewMut, + ) -> WidgetFlags { + let mut widget_flags = WidgetFlags::default(); + + match &mut self.current_pen { + Pen::Brush(brush) => { + brush.add_stroke_key().unwrap(); + let stroke_key = brush.current_stroke_key.unwrap(); + + // get the path + let path = if let Some(Stroke::BrushStroke(brushstroke)) = + engine_view.store.get_stroke_ref(stroke_key) + { + Some(brushstroke.path.clone()) + } else { + None + }; + + // recognize ? + if true { + println!("recognition successful"); + + // cancel the stroke + engine_view.store.remove_stroke(stroke_key); + + // change the type to line first + engine_view.pens_config.shaper_config.builder_type = ShapeBuilderType::Line; + // switch to the shaper tool but as an override (temporary) + widget_flags |= self.change_style_override(Some(PenStyle::Shaper), engine_view); + + // need a function to add a shape directly with some stroke width ? + + // first event is the original position + let (_, wf) = self.current_pen.handle_event( + PenEvent::Down { + element: path.as_ref().unwrap().start, + modifier_keys: HashSet::new(), + }, + now, + engine_view, + ); + widget_flags |= wf; + // second event is the last position + let (_, wf) = self.current_pen.handle_event( + PenEvent::Down { + element: path.unwrap().segments.last().unwrap().end(), + modifier_keys: HashSet::new(), + }, + now, + engine_view, + ); + widget_flags |= wf; + } else { + // reset : We get the last element stored in the recognizer + match self.current_pen.reset_long_press(now) { + Ok(()) => (), + Err(()) => { + tracing::debug!("called `reset_long_press` on an incompatible pen mode") + } + } + } + } + _ => {} + } + return widget_flags; + } + /// Handle a pen event. pub fn handle_pen_event( &mut self, @@ -245,14 +319,19 @@ impl PenHolder { let (mut event_result, wf) = self .current_pen .handle_event(event.clone(), now, engine_view); + 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; } + if widget_flags.long_hold { + widget_flags |= self.handle_long_press(now, engine_view); + } + // Always redraw after handling a pen event widget_flags.redraw = true; diff --git a/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs b/crates/rnote-engine/src/pens/pensconfig/brushconfig.rs index a9e868b737..82e9ac6c45 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/widgetflags.rs b/crates/rnote-engine/src/widgetflags.rs index d348063a9c..fbcc594fd6 100644 --- a/crates/rnote-engine/src/widgetflags.rs +++ b/crates/rnote-engine/src/widgetflags.rs @@ -26,6 +26,10 @@ pub struct WidgetFlags { /// Meaning, when enabled instead of key events, text events are then emitted /// for regular unicode text. Used when writing text with the typewriter. pub enable_text_preprocessing: Option, + /// long press event to take into account + /// This triggers actions on the engine further up (additional events) + /// to take it into consideration + pub long_hold: bool, } impl Default for WidgetFlags { @@ -42,6 +46,7 @@ impl Default for WidgetFlags { hide_undo: None, hide_redo: None, enable_text_preprocessing: None, + long_hold: false, } } } @@ -74,5 +79,6 @@ impl std::ops::BitOrAssign for WidgetFlags { if rhs.enable_text_preprocessing.is_some() { self.enable_text_preprocessing = rhs.enable_text_preprocessing; } + self.long_hold |= rhs.long_hold; } }