Skip to content

Commit a0b5855

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 725b7fc commit a0b5855

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
@@ -38,6 +38,14 @@ impl TextCursorState {
3838
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
3939
self.ccursor_range = ccursor_range;
4040
}
41+
42+
/// Clamp the cursors to be in bounds of the galley.
43+
pub fn ensure_in_bounds(&mut self, galley: &Galley) {
44+
if let Some(range) = self.ccursor_range.as_mut() {
45+
range.primary = galley.clamp_cursor(&range.primary);
46+
range.secondary = galley.clamp_cursor(&range.secondary);
47+
}
48+
}
4149
}
4250

4351
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
@@ -1082,6 +1082,10 @@ impl Galley {
10821082
}
10831083
}
10841084

1085+
pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1086+
self.cursor_from_layout(self.layout_from_cursor(*cursor))
1087+
}
1088+
10851089
pub fn cursor_up_one_row(
10861090
&self,
10871091
cursor: &CCursor,

0 commit comments

Comments
 (0)