Skip to content

Commit 3b6a09b

Browse files
committed
Ensure TextEdit cursor is never out of bounds (upstreamed at: emilk#7077)
When rendering a TextArea we don't know if the saved cursor applies to the current galley since it's possible the app changed the TextBuffer (e.g. when submitting a chat input) So we now detect if the galley changed from the last known one and clamp the cursor to ensure it's not out of bounds. This fixes an issue where backspace can suddenly stop working Repro: - Render TextArea with long-ish text (say 20 chars) - Without losing focus, clear the text - Write something short (say 5 chars) - Result: backspace doesn't work because the cursor position is wrong
1 parent 3fb4e3f commit 3b6a09b

File tree

4 files changed

+28
-1
lines changed

4 files changed

+28
-1
lines changed

crates/egui/src/text_selection/text_cursor_state.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ impl TextCursorState {
3939
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
4040
self.ccursor_range = ccursor_range;
4141
}
42+
43+
/// Clamp the cursors to be in bounds of the galley.
44+
pub fn ensure_in_bounds(&mut self, galley: &Galley) {
45+
if let Some(range) = self.ccursor_range.as_mut() {
46+
range.primary = galley.clamp_cursor(&range.primary);
47+
range.secondary = galley.clamp_cursor(&range.secondary);
48+
}
49+
}
4250
}
4351

4452
impl TextCursorState {

crates/egui/src/widgets/text_edit/builder.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,16 @@ impl TextEdit<'_> {
553553
});
554554
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
555555

556+
// At this point we don't know if the saved cursor still applies to the current galley since
557+
// it's possible the app changed the TextBuffer (e.g. when submitting a chat input) so here
558+
// we detect if the galley changed from the last known one and clamp the cursor to the new one
559+
if let Some(last_galley) = state.last_galley.as_ref() {
560+
if last_galley.upgrade().as_ref() != Some(&galley) {
561+
state.cursor.ensure_in_bounds(&galley);
562+
state.last_galley = Some(Arc::downgrade(&galley));
563+
}
564+
}
565+
556566
// On touch screens (e.g. mobile in `eframe` web), should
557567
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
558568
// Since currently copying selected text in not supported on `eframe` web,
@@ -803,6 +813,7 @@ impl TextEdit<'_> {
803813
ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
804814
}
805815

816+
state.last_galley = Some(Arc::downgrade(&galley));
806817
state.clone().store(ui.ctx(), id);
807818

808819
if response.changed() {

crates/egui/src/widgets/text_edit/state.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::Arc;
1+
use std::sync::{Arc, Weak};
22

33
use crate::mutex::Mutex;
44

@@ -57,6 +57,10 @@ pub struct TextEditState {
5757
/// Used to pause the cursor animation when typing.
5858
#[cfg_attr(feature = "serde", serde(skip))]
5959
pub(crate) last_interaction_time: f64,
60+
61+
/// The last galley that was used to render the text. When this changes, we need to ensure the cursor is still in bounds.
62+
#[cfg_attr(feature = "serde", serde(skip))]
63+
pub(crate) last_galley: Option<Weak<epaint::Galley>>,
6064
}
6165

6266
impl TextEditState {

crates/epaint/src/text/text_layout_types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,10 @@ impl Galley {
11051105
}
11061106
}
11071107

1108+
pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1109+
self.cursor_from_layout(self.layout_from_cursor(*cursor))
1110+
}
1111+
11081112
pub fn cursor_up_one_row(
11091113
&self,
11101114
cursor: &CCursor,

0 commit comments

Comments
 (0)