From 2d1352d6703facbe9057d12182cb86066c6bbded Mon Sep 17 00:00:00 2001 From: Kneemund Date: Tue, 29 Oct 2024 01:18:41 +0100 Subject: [PATCH 1/5] feat: typewriter multiclick selection --- crates/rnote-engine/src/engine/mod.rs | 26 +++++++++ .../src/pens/typewriter/penevents.rs | 54 +++++++++++++++++++ crates/rnote-engine/src/strokes/textstroke.rs | 12 +++-- crates/rnote-ui/src/canvaswrapper.rs | 40 +++++++++++++- 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 2e7ad19093..690efec3e6 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -929,6 +929,32 @@ 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 EngineViewMut { + tasks_tx: self.tasks_tx.clone(), + pens_config: &mut self.pens_config, + document: &mut self.document, + store: &mut self.store, + camera: &mut self.camera, + audioplayer: &mut self.audioplayer, + }) + } + } + + pub fn text_select_closest_line(&mut self) { + if let Pen::Typewriter(typewriter) = self.penholder.current_pen_mut() { + typewriter.select_closest_line(&mut EngineViewMut { + tasks_tx: self.tasks_tx.clone(), + pens_config: &mut self.pens_config, + document: &mut self.document, + store: &mut self.store, + camera: &mut self.camera, + audioplayer: &mut self.audioplayer, + }) + } + } + pub fn text_selection_toggle_attribute( &mut self, text_attribute: TextAttribute, diff --git a/crates/rnote-engine/src/pens/typewriter/penevents.rs b/crates/rnote-engine/src/pens/typewriter/penevents.rs index 3fc6de8b4f..f28976af5e 100644 --- a/crates/rnote-engine/src/pens/typewriter/penevents.rs +++ b/crates/rnote-engine/src/pens/typewriter/penevents.rs @@ -1229,4 +1229,58 @@ 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_forward(cursor); + + *modify_state = ModifyState::Selecting { + selection_cursor: GraphemeCursor::new( + textstroke.get_prev_word_start_index(cursor.cur_cursor()), + textstroke.text.len(), + true, + ), + finished: true, + }; + } + } + _ => {} + } + } + + 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_start(cursor); + + *modify_state = ModifyState::Selecting { + selection_cursor: GraphemeCursor::new( + textstroke.get_line_end_index(cursor), + textstroke.text.len(), + true, + ), + finished: true, + }; + } + } + _ => {} + } + } } diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index 83816854d5..ffb0ac4c28 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -830,7 +830,7 @@ impl TextStroke { selection_cursor.set_cursor(0); } - fn get_prev_word_start_index(&self, current_char_index: usize) -> usize { + pub fn get_prev_word_start_index(&self, current_char_index: usize) -> usize { for (start_index, _) in self.text.unicode_word_indices().rev() { if start_index < current_char_index { return start_index; @@ -892,7 +892,7 @@ impl TextStroke { } } - pub fn move_cursor_line_end(&self, cursor: &mut GraphemeCursor) { + pub fn get_line_end_index(&self, cursor: &GraphemeCursor) -> usize { if let (Ok(lines), Ok(hittest_position)) = ( self.text_style .lines(&mut piet_cairo::CairoText::new(), self.text.clone()), @@ -926,8 +926,14 @@ impl TextStroke { offset -= 1; } - cursor.set_cursor(offset); + return offset; } + + 0 + } + + pub fn move_cursor_line_end(&self, cursor: &mut GraphemeCursor) { + cursor.set_cursor(self.get_line_end_index(cursor)); } pub fn move_cursor_line_down(&self, cursor: &mut GraphemeCursor) { diff --git a/crates/rnote-ui/src/canvaswrapper.rs b/crates/rnote-ui/src/canvaswrapper.rs index a860f336f8..1a8fe24e27 100644 --- a/crates/rnote-ui/src/canvaswrapper.rs +++ b/crates/rnote-ui/src/canvaswrapper.rs @@ -3,7 +3,7 @@ use crate::{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 @@ -638,6 +649,33 @@ 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, _, _| { + let action = (n_press - 1) % 3; + + if action == 1 { + canvaswrapper + .canvas() + .engine_mut() + .text_select_closest_word(); + } else if action == 2 { + canvaswrapper + .canvas() + .engine_mut() + .text_select_closest_line(); + } else { + return; + } + + signal.set_state(EventSequenceState::Claimed); + } + )); + } + // Zoom with alt + shift + drag { let zoom_begin = Rc::new(Cell::new(1_f64)); From 23085bd03b2fcbec48bf6954a40c2bb8057ce2b4 Mon Sep 17 00:00:00 2001 From: Kneemund Date: Tue, 29 Oct 2024 19:09:44 +0100 Subject: [PATCH 2/5] feat: drag multiclick selection --- .../rnote-engine/src/pens/typewriter/mod.rs | 15 ++++ .../src/pens/typewriter/penevents.rs | 79 ++++++++++++++++--- crates/rnote-engine/src/strokes/textstroke.rs | 10 +-- crates/rnote-ui/src/canvas/input.rs | 2 +- crates/rnote-ui/src/canvas/mod.rs | 1 + crates/rnote-ui/src/canvaswrapper.rs | 33 +++++--- 6 files changed, 105 insertions(+), 35 deletions(-) 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 f28976af5e..8d2ecce626 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 { @@ -215,6 +220,39 @@ impl Typewriter { textstroke.get_cursor_for_global_coord(element.pos) { *cursor = new_cursor; + + 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_back(cursor); + } else if mouse_position > *end { + selection_cursor.set_cursor(*start); + textstroke.move_cursor_word_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 => {} + } + self.reset_blink(); } } @@ -590,6 +628,7 @@ impl Typewriter { textstroke.text.len(), true, ), + mode: SelectionMode::Caret, finished: true, }; } else { @@ -665,6 +704,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, } } else { @@ -693,6 +733,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -717,6 +758,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -736,6 +778,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -759,6 +802,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -787,6 +831,7 @@ impl Typewriter { *modify_state = ModifyState::Selecting { selection_cursor: old_cursor, + mode: SelectionMode::Caret, finished: false, }; } else { @@ -821,6 +866,7 @@ impl Typewriter { ModifyState::Selecting { selection_cursor, finished, + .. } => { super::play_sound(Some(keyboard_key), engine_view.audioplayer); @@ -1150,6 +1196,7 @@ impl Typewriter { ModifyState::Selecting { selection_cursor, finished, + .. } => { super::play_sound(None, engine_view.audioplayer); @@ -1243,13 +1290,16 @@ impl Typewriter { { textstroke.move_cursor_word_forward(cursor); + let mut selection_cursor = cursor.clone(); + textstroke.move_cursor_word_back(&mut selection_cursor); + *modify_state = ModifyState::Selecting { - selection_cursor: GraphemeCursor::new( - textstroke.get_prev_word_start_index(cursor.cur_cursor()), - textstroke.text.len(), - true, + mode: SelectionMode::Word( + selection_cursor.cur_cursor(), + cursor.cur_cursor(), ), - finished: true, + selection_cursor, + finished: false, }; } } @@ -1268,15 +1318,18 @@ impl Typewriter { if let Some(Stroke::TextStroke(ref mut textstroke)) = engine_view.store.get_stroke_mut(*stroke_key) { - textstroke.move_cursor_line_start(cursor); + textstroke.move_cursor_line_end(cursor); + + let mut selection_cursor = cursor.clone(); + textstroke.move_cursor_line_start(&mut selection_cursor); *modify_state = ModifyState::Selecting { - selection_cursor: GraphemeCursor::new( - textstroke.get_line_end_index(cursor), - textstroke.text.len(), - true, + mode: SelectionMode::Line( + selection_cursor.cur_cursor(), + cursor.cur_cursor(), ), - finished: true, + selection_cursor, + finished: false, }; } } diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index ffb0ac4c28..f805ff443e 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -892,7 +892,7 @@ impl TextStroke { } } - pub fn get_line_end_index(&self, cursor: &GraphemeCursor) -> usize { + pub fn move_cursor_line_end(&self, cursor: &mut GraphemeCursor) { if let (Ok(lines), Ok(hittest_position)) = ( self.text_style .lines(&mut piet_cairo::CairoText::new(), self.text.clone()), @@ -926,14 +926,8 @@ impl TextStroke { offset -= 1; } - return offset; + cursor.set_cursor(offset); } - - 0 - } - - pub fn move_cursor_line_end(&self, cursor: &mut GraphemeCursor) { - cursor.set_cursor(self.get_line_end_index(cursor)); } pub fn move_cursor_line_down(&self, cursor: &mut GraphemeCursor) { diff --git a/crates/rnote-ui/src/canvas/input.rs b/crates/rnote-ui/src/canvas/input.rs index e0f0967d80..0d404aeb3b 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 2f6f947e01..1a8d1992e8 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 1a8fe24e27..114454a4fd 100644 --- a/crates/rnote-ui/src/canvaswrapper.rs +++ b/crates/rnote-ui/src/canvaswrapper.rs @@ -1,5 +1,5 @@ // 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, @@ -655,23 +655,30 @@ mod imp { #[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 == 1 { - canvaswrapper - .canvas() - .engine_mut() - .text_select_closest_word(); - } else if action == 2 { - canvaswrapper - .canvas() - .engine_mut() - .text_select_closest_line(); - } else { + if action <= 0 { + // Single press or invalid press count return; } - signal.set_state(EventSequenceState::Claimed); + 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!(), + } } )); } From 65ebf890922d92413ca2839fb38fc57ccbcab38d Mon Sep 17 00:00:00 2001 From: Kneemund Date: Tue, 29 Oct 2024 19:46:51 +0100 Subject: [PATCH 3/5] fix: revert visibility change --- crates/rnote-engine/src/strokes/textstroke.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index f805ff443e..83816854d5 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -830,7 +830,7 @@ impl TextStroke { selection_cursor.set_cursor(0); } - pub fn get_prev_word_start_index(&self, current_char_index: usize) -> usize { + fn get_prev_word_start_index(&self, current_char_index: usize) -> usize { for (start_index, _) in self.text.unicode_word_indices().rev() { if start_index < current_char_index { return start_index; From bcdc42c4518f584eac25139e7bb66ad5b8eafd60 Mon Sep 17 00:00:00 2001 From: Kneemund Date: Sun, 10 Nov 2024 01:04:50 +0100 Subject: [PATCH 4/5] fix: reset blink only if position changed --- crates/rnote-engine/src/pens/typewriter/penevents.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/pens/typewriter/penevents.rs b/crates/rnote-engine/src/pens/typewriter/penevents.rs index 8d2ecce626..5ed299e223 100644 --- a/crates/rnote-engine/src/pens/typewriter/penevents.rs +++ b/crates/rnote-engine/src/pens/typewriter/penevents.rs @@ -219,6 +219,7 @@ 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; match mode { @@ -253,7 +254,9 @@ impl Typewriter { SelectionMode::Caret => {} } - self.reset_blink(); + if previous_cursor_position != cursor.cur_cursor() { + self.reset_blink(); + } } } } From 959383cef69b07289e04a43f154db379f226d269 Mon Sep 17 00:00:00 2001 From: Kneemund Date: Sun, 10 Nov 2024 01:05:23 +0100 Subject: [PATCH 5/5] improv: all word boundaries for word selection --- .../src/pens/typewriter/penevents.rs | 16 +++++--- crates/rnote-engine/src/strokes/textstroke.rs | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/crates/rnote-engine/src/pens/typewriter/penevents.rs b/crates/rnote-engine/src/pens/typewriter/penevents.rs index 5ed299e223..379f6ebea0 100644 --- a/crates/rnote-engine/src/pens/typewriter/penevents.rs +++ b/crates/rnote-engine/src/pens/typewriter/penevents.rs @@ -226,12 +226,16 @@ impl Typewriter { SelectionMode::Word(start, end) => { let mouse_position = cursor.cur_cursor(); - if mouse_position < *start { + if mouse_position <= *start { selection_cursor.set_cursor(*end); - textstroke.move_cursor_word_back(cursor); - } else if mouse_position > *end { + textstroke + .move_cursor_word_boundary_back(cursor); + } else if mouse_position >= *end { selection_cursor.set_cursor(*start); - textstroke.move_cursor_word_forward(cursor); + textstroke + .move_cursor_word_boundary_forward( + cursor, + ); } else { selection_cursor.set_cursor(*start); cursor.set_cursor(*end); @@ -1291,10 +1295,10 @@ impl Typewriter { if let Some(Stroke::TextStroke(ref mut textstroke)) = engine_view.store.get_stroke_mut(*stroke_key) { - textstroke.move_cursor_word_forward(cursor); + textstroke.move_cursor_word_boundary_forward(cursor); let mut selection_cursor = cursor.clone(); - textstroke.move_cursor_word_back(&mut selection_cursor); + textstroke.move_cursor_word_boundary_back(&mut selection_cursor); *modify_state = ModifyState::Selecting { mode: SelectionMode::Word( 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); }