diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 298d8abfbff..89fb880810b 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -39,6 +39,14 @@ impl TextCursorState { pub fn set_char_range(&mut self, ccursor_range: Option) { self.ccursor_range = ccursor_range; } + + /// Clamp the cursors to be in bounds of the galley. + pub fn ensure_in_bounds(&mut self, galley: &Galley) { + if let Some(range) = self.ccursor_range.as_mut() { + range.primary = galley.clamp_cursor(&range.primary); + range.secondary = galley.clamp_cursor(&range.secondary); + } + } } impl TextCursorState { diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index ca201edda02..8737a6a1a0c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -549,6 +549,16 @@ impl TextEdit<'_> { }); let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); + // At this point we don't know if the saved cursor still applies to the current galley since + // it's possible the app changed the TextBuffer (e.g. when submitting a chat input) so here + // we detect if the galley changed from the last known one and clamp the cursor to the new one + if let Some(last_galley) = state.last_galley.as_ref() { + if last_galley.upgrade().as_ref() != Some(&galley) { + state.cursor.ensure_in_bounds(&galley); + state.last_galley = Some(Arc::downgrade(&galley)); + } + } + // On touch screens (e.g. mobile in `eframe` web), should // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)? // Since currently copying selected text in not supported on `eframe` web, @@ -799,6 +809,7 @@ impl TextEdit<'_> { ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_)))); } + state.last_galley = Some(Arc::downgrade(&galley)); state.clone().store(ui.ctx(), id); if response.changed() { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 0051ea8e7ef..de05eb86979 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; use crate::mutex::Mutex; @@ -57,6 +57,10 @@ pub struct TextEditState { /// Used to pause the cursor animation when typing. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) last_interaction_time: f64, + + /// The last galley that was used to render the text. When this changes, we need to ensure the cursor is still in bounds. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) last_galley: Option>, } impl TextEditState { diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 6b786342675..f7e11911b73 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,