Repository: flxzt/rnote (language: rust)

Issue:
Double click to select word and sentences in the text?
**Please submit only one feature request in one issue!**  
If it is UX / UI related, don't post how a single aspect of the UI should be changed, rather in which use-case you felt the UI is lacking or should be improved.

**Is your feature request related to a problem?**
When selecting the texts typed in the note, it is not possible to use double click/triple click to select the entire word or all texts in a sentence, like other programs in Windows. 

**Describe the solution you'd like**  
Double clicking/triple clicking would select the word or sentence where the cursor is located. 

**Describe alternatives you've considered**  
Holding down the left mouse button and move the mouse, but this is not as convenient.

**Additional context**  
Thanks for making this note-taking tool! It would be much more easy to type with quick selection implemented.



Discussion / hints:

Fix patch:
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<f64>),
     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));


Test patch: