From 54fded362dfc82f0ea188a110fc950d9adbb2f13 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:53:00 -0400 Subject: [PATCH] Clamp text cursor positions in the same places where we used to (#7081) Closes #7077. This fixes the problem shown in #7077 where clearing a `TextEdit` wouldn't reset its cursor position. I've fixed that by adding back the `TextCursorState::range` method, which clamps the selection range to that of the passed `Galley`, and calling it in the same places where it was called before #5785. (/cc @juancampa) * [x] I have followed the instructions in the PR template --- .../src/text_selection/label_text_selection.rs | 8 ++++---- .../egui/src/text_selection/text_cursor_state.rs | 14 ++++++++++++-- crates/egui/src/widgets/text_edit/builder.rs | 8 ++++---- crates/epaint/src/text/text_layout_types.rs | 4 ++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 9ce8fbd5..a315c235 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -530,7 +530,7 @@ impl LabelSelectionState { let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley); - let old_range = cursor_state.char_range(); + let old_range = cursor_state.range(galley); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { if response.contains_pointer() { @@ -544,7 +544,7 @@ impl LabelSelectionState { } } - if let Some(mut cursor_range) = cursor_state.char_range() { + if let Some(mut cursor_range) = cursor_state.range(galley) { let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size()); self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect); @@ -562,7 +562,7 @@ impl LabelSelectionState { } // Look for changes due to keyboard and/or mouse interaction: - let new_range = cursor_state.char_range(); + let new_range = cursor_state.range(galley); let selection_changed = old_range != new_range; if let (true, Some(range)) = (selection_changed, new_range) { @@ -632,7 +632,7 @@ impl LabelSelectionState { } } - let cursor_range = cursor_state.char_range(); + let cursor_range = cursor_state.range(galley); let mut new_vertex_indices = vec![]; diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 298d8abf..d2158c6b 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -35,6 +35,16 @@ impl TextCursorState { self.ccursor_range } + /// The currently selected range of characters, clamped within the character + /// range of the given [`Galley`]. + pub fn range(&self, galley: &Galley) -> Option { + self.ccursor_range.map(|mut range| { + range.primary = galley.clamp_cursor(&range.primary); + range.secondary = galley.clamp_cursor(&range.secondary); + range + }) + } + /// Sets the currently selected range of characters. pub fn set_char_range(&mut self, ccursor_range: Option) { self.ccursor_range = ccursor_range; @@ -69,7 +79,7 @@ impl TextCursorState { if response.hovered() && ui.input(|i| i.pointer.any_pressed()) { // The start of a drag (or a click). if ui.input(|i| i.modifiers.shift) { - if let Some(mut cursor_range) = self.char_range() { + if let Some(mut cursor_range) = self.range(galley) { cursor_range.primary = cursor_at_pointer; self.set_char_range(Some(cursor_range)); } else { @@ -81,7 +91,7 @@ impl TextCursorState { true } else if is_being_dragged { // Drag to select text: - if let Some(mut cursor_range) = self.char_range() { + if let Some(mut cursor_range) = self.range(galley) { cursor_range.primary = cursor_at_pointer; self.set_char_range(Some(cursor_range)); } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 29f4b2cb..e21c512a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -617,7 +617,7 @@ impl TextEdit<'_> { } let mut cursor_range = None; - let prev_cursor_range = state.cursor.char_range(); + let prev_cursor_range = state.cursor.range(&galley); if interactive && ui.memory(|mem| mem.has_focus(id)) { ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); @@ -720,7 +720,7 @@ impl TextEdit<'_> { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus { - if let Some(cursor_range) = state.cursor.char_range() { + if let Some(cursor_range) = state.cursor.range(&galley) { // Add text selection rectangles to the galley: paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } @@ -742,7 +742,7 @@ impl TextEdit<'_> { painter.galley(galley_pos, galley.clone(), text_color); if has_focus { - if let Some(cursor_range) = state.cursor.char_range() { + if let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) .translate(galley_pos.to_vec2()); @@ -898,7 +898,7 @@ fn events( ) -> (bool, CCursorRange) { let os = ui.ctx().os(); - let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range); + let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); // We feed state to the undoer both before and after handling input // so that the undoer creates automatic saves even when there are no events for a while. diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 6b786342..f7e11911 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1104,6 +1104,10 @@ impl Galley { } } + pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor { + self.cursor_from_layout(self.layout_from_cursor(*cursor)) + } + pub fn cursor_up_one_row( &self, cursor: &CCursor,