diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 6a3c661753..0a3d54252c 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -845,6 +845,18 @@ impl Engine { widget_flags } + pub fn text_select_closest_word(&mut self) { + if let Pen::Typewriter(typewriter) = self.penholder.current_pen_mut() { + typewriter.select_closest_word(&mut engine_view_mut!(self)) + } + } + + pub fn text_select_closest_line(&mut self) { + if let Pen::Typewriter(typewriter) = self.penholder.current_pen_mut() { + typewriter.select_closest_line(&mut engine_view_mut!(self)) + } + } + pub fn text_selection_toggle_attribute( &mut self, text_attribute: TextAttribute, diff --git a/crates/rnote-engine/src/pens/typewriter/mod.rs b/crates/rnote-engine/src/pens/typewriter/mod.rs index f4bd028e7e..9eb7207b78 100644 --- a/crates/rnote-engine/src/pens/typewriter/mod.rs +++ b/crates/rnote-engine/src/pens/typewriter/mod.rs @@ -24,12 +24,27 @@ use std::time::{Duration, Instant}; use tracing::error; use unicode_segmentation::GraphemeCursor; +#[derive(Debug, Clone)] +pub(super) enum SelectionMode { + /// Select individual characters. + Caret, + /// Select whole words. + /// + /// The values represent the start and end of the initially selected word. + Word(usize, usize), + /// Select whole lines. + /// + /// The values represent the start and end of the initially selected line. + Line(usize, usize), +} + #[derive(Debug, Clone)] pub(super) enum ModifyState { Up, Hover(na::Vector2), Selecting { selection_cursor: GraphemeCursor, + mode: SelectionMode, /// Whether selecting is finished. /// /// If true, the state will get reset on the next click. diff --git a/crates/rnote-engine/src/pens/typewriter/penevents.rs b/crates/rnote-engine/src/pens/typewriter/penevents.rs index 3fc6de8b4f..379f6ebea0 100644 --- a/crates/rnote-engine/src/pens/typewriter/penevents.rs +++ b/crates/rnote-engine/src/pens/typewriter/penevents.rs @@ -1,5 +1,5 @@ // Imports -use super::{ModifyState, Typewriter, TypewriterState}; +use super::{ModifyState, SelectionMode, Typewriter, TypewriterState}; use crate::engine::EngineViewMut; use crate::pens::PenBehaviour; use crate::strokes::{Stroke, TextStroke}; @@ -150,6 +150,7 @@ impl Typewriter { self.state = TypewriterState::Modifying { modify_state: ModifyState::Selecting { selection_cursor: cursor.clone(), + mode: SelectionMode::Caret, finished: false, }, stroke_key: *stroke_key, @@ -176,7 +177,11 @@ impl Typewriter { progress, } } - ModifyState::Selecting { finished, .. } => { + ModifyState::Selecting { + selection_cursor, + mode, + finished, + } => { let mut progress = PenProgress::InProgress; if let Some(typewriter_bounds) = typewriter_bounds { @@ -214,8 +219,48 @@ impl Typewriter { if let Ok(new_cursor) = textstroke.get_cursor_for_global_coord(element.pos) { + let previous_cursor_position = cursor.cur_cursor(); *cursor = new_cursor; - self.reset_blink(); + + match mode { + SelectionMode::Word(start, end) => { + let mouse_position = cursor.cur_cursor(); + + if mouse_position <= *start { + selection_cursor.set_cursor(*end); + textstroke + .move_cursor_word_boundary_back(cursor); + } else if mouse_position >= *end { + selection_cursor.set_cursor(*start); + textstroke + .move_cursor_word_boundary_forward( + cursor, + ); + } else { + selection_cursor.set_cursor(*start); + cursor.set_cursor(*end); + } + } + SelectionMode::Line(start, end) => { + let mouse_position = cursor.cur_cursor(); + + if mouse_position < *start { + selection_cursor.set_cursor(*end); + textstroke.move_cursor_line_start(cursor); + } else if mouse_position > *end { + selection_cursor.set_cursor(*start); + textstroke.move_cursor_line_end(cursor); + } else { + selection_cursor.set_cursor(*start); + cursor.set_cursor(*end); + } + } + SelectionMode::Caret => {} + } + + if previous_cursor_position != cursor.cur_cursor() { + self.reset_blink(); + } } } } @@ -590,6 +635,7 @@ impl Typewriter { textstroke.text.len(), true, ), + mode: SelectionMode::Caret, finished: true, }; } else { @@ -665,6 +711,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, } } else { @@ -693,6 +740,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -717,6 +765,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -736,6 +785,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -759,6 +809,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -787,6 +838,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -821,6 +873,7 @@ impl Typewriter { ModifyState::Selecting { selection_cursor, finished, + .. } => { super::play_sound(Some(keyboard_key), engine_view.audioplayer); @@ -1150,6 +1203,7 @@ impl Typewriter { ModifyState::Selecting { selection_cursor, finished, + .. } => { super::play_sound(None, engine_view.audioplayer); @@ -1229,4 +1283,64 @@ impl Typewriter { (event_result, widget_flags) } + + pub fn select_closest_word(&mut self, engine_view: &mut EngineViewMut) { + match &mut self.state { + TypewriterState::Modifying { + modify_state, + stroke_key, + cursor, + pen_down: _, + } => { + if let Some(Stroke::TextStroke(ref mut textstroke)) = + engine_view.store.get_stroke_mut(*stroke_key) + { + textstroke.move_cursor_word_boundary_forward(cursor); + + let mut selection_cursor = cursor.clone(); + textstroke.move_cursor_word_boundary_back(&mut selection_cursor); + + *modify_state = ModifyState::Selecting { + mode: SelectionMode::Word( + selection_cursor.cur_cursor(), + cursor.cur_cursor(), + ), + selection_cursor, + finished: false, + }; + } + } + _ => {} + } + } + + pub fn select_closest_line(&mut self, engine_view: &mut EngineViewMut) { + match &mut self.state { + TypewriterState::Modifying { + modify_state, + stroke_key, + cursor, + pen_down: _, + } => { + if let Some(Stroke::TextStroke(ref mut textstroke)) = + engine_view.store.get_stroke_mut(*stroke_key) + { + textstroke.move_cursor_line_end(cursor); + + let mut selection_cursor = cursor.clone(); + textstroke.move_cursor_line_start(&mut selection_cursor); + + *modify_state = ModifyState::Selecting { + mode: SelectionMode::Line( + selection_cursor.cur_cursor(), + cursor.cur_cursor(), + ), + selection_cursor, + finished: false, + }; + } + } + _ => {} + } + } } diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index 83816854d5..09d1e910ad 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -840,6 +840,22 @@ impl TextStroke { current_char_index } + fn get_prev_word_boundary_index(&self, current_char_index: usize) -> usize { + for (start_index, word) in self.text.unicode_word_indices().rev() { + let end_index = start_index + word.len(); + + if end_index < current_char_index { + return end_index; + } + + if start_index < current_char_index { + return start_index; + } + } + + current_char_index + } + fn get_next_word_end_index(&self, current_char_index: usize) -> usize { for (start_index, word) in self.text.unicode_word_indices() { let end_index = start_index + word.len(); @@ -852,6 +868,22 @@ impl TextStroke { current_char_index } + fn get_next_word_boundary_index(&self, current_char_index: usize) -> usize { + for (start_index, word) in self.text.unicode_word_indices() { + if start_index >= current_char_index { + return start_index; + } + + let end_index = start_index + word.len(); + + if end_index >= current_char_index { + return end_index; + } + } + + current_char_index + } + pub fn move_cursor_back(&self, cursor: &mut GraphemeCursor) { // Cant fail, we are providing the entire text cursor.prev_boundary(&self.text, 0).unwrap(); @@ -866,10 +898,18 @@ impl TextStroke { cursor.set_cursor(self.get_prev_word_start_index(cursor.cur_cursor())); } + pub fn move_cursor_word_boundary_back(&self, cursor: &mut GraphemeCursor) { + cursor.set_cursor(self.get_prev_word_boundary_index(cursor.cur_cursor())); + } + pub fn move_cursor_word_forward(&self, cursor: &mut GraphemeCursor) { cursor.set_cursor(self.get_next_word_end_index(cursor.cur_cursor())); } + pub fn move_cursor_word_boundary_forward(&self, cursor: &mut GraphemeCursor) { + cursor.set_cursor(self.get_next_word_boundary_index(cursor.cur_cursor())); + } + pub fn move_cursor_text_start(&self, cursor: &mut GraphemeCursor) { cursor.set_cursor(0); } diff --git a/crates/rnote-ui/src/canvas/input.rs b/crates/rnote-ui/src/canvas/input.rs index 8775ec852b..897fe2e6db 100644 --- a/crates/rnote-ui/src/canvas/input.rs +++ b/crates/rnote-ui/src/canvas/input.rs @@ -300,7 +300,7 @@ fn trace_gdk_event(event: &gdk::Event) { } /// Returns true if input should be rejected -fn reject_pointer_input(event: &gdk::Event, touch_drawing: bool) -> bool { +pub(crate) fn reject_pointer_input(event: &gdk::Event, touch_drawing: bool) -> bool { if touch_drawing { if event.device().unwrap().num_touches() > 1 { return true; diff --git a/crates/rnote-ui/src/canvas/mod.rs b/crates/rnote-ui/src/canvas/mod.rs index 11bee39b2b..a6f6054b1e 100644 --- a/crates/rnote-ui/src/canvas/mod.rs +++ b/crates/rnote-ui/src/canvas/mod.rs @@ -6,6 +6,7 @@ mod widgetflagsboxed; // Re-exports pub(crate) use canvaslayout::RnCanvasLayout; +pub(crate) use input::reject_pointer_input; pub(crate) use widgetflagsboxed::WidgetFlagsBoxed; // Imports diff --git a/crates/rnote-ui/src/canvaswrapper.rs b/crates/rnote-ui/src/canvaswrapper.rs index e94ad1bdba..3a3532f325 100644 --- a/crates/rnote-ui/src/canvaswrapper.rs +++ b/crates/rnote-ui/src/canvaswrapper.rs @@ -1,9 +1,9 @@ // Imports -use crate::{RnAppWindow, RnCanvas, RnContextMenu}; +use crate::{canvas::reject_pointer_input, RnAppWindow, RnCanvas, RnContextMenu}; use gtk4::{ gdk, glib, glib::clone, graphene, prelude::*, subclass::prelude::*, CompositeTemplate, CornerType, EventControllerMotion, EventControllerScroll, EventControllerScrollFlags, - EventSequenceState, GestureDrag, GestureLongPress, GestureZoom, PropagationPhase, + EventSequenceState, GestureClick, GestureDrag, GestureLongPress, GestureZoom, PropagationPhase, ScrolledWindow, Widget, }; use once_cell::sync::Lazy; @@ -39,6 +39,7 @@ mod imp { pub(crate) pointer_motion_controller: EventControllerMotion, pub(crate) canvas_drag_gesture: GestureDrag, pub(crate) canvas_zoom_gesture: GestureZoom, + pub(crate) canvas_multi_press_gesture: GestureClick, pub(crate) canvas_zoom_scroll_controller: EventControllerScroll, pub(crate) canvas_mouse_drag_middle_gesture: GestureDrag, pub(crate) canvas_alt_drag_gesture: GestureDrag, @@ -75,6 +76,13 @@ mod imp { .propagation_phase(PropagationPhase::Capture) .build(); + let canvas_multi_press_gesture = GestureClick::builder() + .name("canvas_multi_press_gesture") + .button(gdk::BUTTON_PRIMARY) + .exclusive(true) + .propagation_phase(PropagationPhase::Capture) + .build(); + let canvas_zoom_scroll_controller = EventControllerScroll::builder() .name("canvas_zoom_scroll_controller") .propagation_phase(PropagationPhase::Bubble) @@ -130,6 +138,7 @@ mod imp { pointer_motion_controller, canvas_drag_gesture, canvas_zoom_gesture, + canvas_multi_press_gesture, canvas_zoom_scroll_controller, canvas_mouse_drag_middle_gesture, canvas_alt_drag_gesture, @@ -171,6 +180,8 @@ mod imp { .add_controller(self.canvas_drag_gesture.clone()); self.scroller .add_controller(self.canvas_zoom_gesture.clone()); + self.scroller + .add_controller(self.canvas_multi_press_gesture.clone()); self.scroller .add_controller(self.canvas_zoom_scroll_controller.clone()); self.scroller @@ -642,6 +653,40 @@ mod imp { )); } + // Double press to select word, triple press to select line + { + self.canvas_multi_press_gesture.connect_pressed(clone!( + #[weak(rename_to=canvaswrapper)] + obj, + move |signal, n_press, _, _| { + // cycle through 0, 1, 2 - single, double, triple press + let action = (n_press - 1) % 3; + + if action <= 0 { + // Single press or invalid press count + return; + } + + let canvas = canvaswrapper.canvas(); + + if signal.current_event().is_none_or(|event| { + reject_pointer_input(&event, canvas.touch_drawing()) + }) { + // Reject certain kinds of input (same behavior as canvas) + return; + } + + match action { + // Double press + 1 => canvas.engine_mut().text_select_closest_word(), + // Triple press + 2 => canvas.engine_mut().text_select_closest_line(), + _ => unreachable!(), + } + } + )); + } + // Zoom with alt + shift + drag { let zoom_begin = Rc::new(Cell::new(1_f64));