From f4f60b6ac68441db4a8633235c53445f1ee3e332 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:16:04 +0200 Subject: [PATCH 1/2] Fix numeric fields reverting a typed value when clicking away instead of pressing Enter (#1637) --- crates/ui-egui/src/tests_numfields.rs | 38 +++++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 13 +++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/tests_numfields.rs b/crates/ui-egui/src/tests_numfields.rs index 7c1cc917..ad516f65 100644 --- a/crates/ui-egui/src/tests_numfields.rs +++ b/crates/ui-egui/src/tests_numfields.rs @@ -408,3 +408,41 @@ fn keyboard_focus_keeps_selecting_the_entire_numeric_field() { assert_eq!(f.selection(), Some((0, field_text(mode).chars().count())), "mode {mode}"); } } + +/// #1637: text typed into a field is committed when the focus leaves it by a click on empty space +/// or by a press on a field drawn before it (another channel of the Color panel), whether the +/// window reports keyboard focus or not, as Enter commits it. Escape still drops it. +#[test] +fn typed_text_is_committed_when_the_focus_moves_away() { + for focused in [true, false] { + for onto_field_above in [false, true] { + let mut f = Field::new(); + f.focused = focused; + let above = Cell::new(Rect::NOTHING); + let draw = |ui: &mut egui::Ui| { + widgets::range_field(ui, "above", 50.0, 0.0..=1000.0, "%", 0, 80.0); + above.set(ui.min_rect()); + widgets::range_field(ui, "f", 100.0, 0.0..=1000.0, "%", 0, 80.0) + }; + f.frame(vec![], &draw); + let field = Pos2::new(above.get().center().x, (above.get().bottom() + f.rect.get().bottom()) / 2.0); + let away = if onto_field_above { above.get().center() } else { Pos2::new(field.x, 180.0) }; + let case = format!("window focus {focused}, onto the field above {onto_field_above}"); + f.frame(vec![Event::PointerMoved(field), pointer_button(field, true)], &draw); + f.frame(vec![pointer_button(field, false)], &draw); + f.frame(vec![Event::Text("42".into())], &draw); + let committed = [ + f.frame(vec![Event::PointerMoved(away), pointer_button(away, true)], &draw), + f.frame(vec![pointer_button(away, false)], &draw), + f.frame(vec![], &draw), + ]; + assert_eq!(committed.iter().flatten().copied().collect::>(), [42.0], "{case}"); + + f.frame(vec![Event::PointerMoved(field), pointer_button(field, true)], &draw); + f.frame(vec![pointer_button(field, false)], &draw); + f.frame(vec![Event::Text("7".into())], &draw); + let escaped = [f.frame(vec![key(Key::Escape, Modifiers::NONE)], &draw), f.frame(vec![], &draw)]; + assert_eq!(escaped, [None, None], "{case}"); + } + } +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 7019fdcb..d8fd2d1e 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -354,7 +354,12 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize let shown_id = id.with("shown"); let followed = ui.data_mut(|d| d.get_temp::(shown_id)).is_none_or(|before| before == shown); ui.data_mut(|d| d.insert_temp(shown_id, shown.to_string())); - let typed = if editing && followed { ui.data_mut(|d| d.get_temp::(id)) } else { None }; + // A press elsewhere can end the focus outside this field's own drawing (a field drawn before it + // takes it, the canvas ends text input), so `editing` is already false on the frame it reports + // `lost_focus`: the text typed is still taken then, to be committed (#1637). Escape drops it. + let focus_id = id.with("had-focus"); + let had_focus = ui.data_mut(|d| d.get_temp::(focus_id)).unwrap_or(false) && !ui.input(|i| i.key_pressed(egui::Key::Escape)); + let typed = if (editing || had_focus) && followed { ui.data_mut(|d| d.get_temp::(id)) } else { None }; let mut buf: String = typed.unwrap_or_else(|| shown.to_string()); // The box is exactly as tall as the other controls of a row (26 points a line), its text // centred in it: a frame around the text came out a point taller and stood out next to @@ -385,7 +390,11 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize (rect, resp) }) .inner; - ui.data_mut(|d| d.insert_temp(id, buf.clone())); + let focused = ui.memory(|m| m.has_focus(id)); + ui.data_mut(|d| { + d.insert_temp(id, buf.clone()); + d.insert_temp(focus_id, focused); + }); (buf, resp, rect) } From d775549258a813c4407a672cdbd9060ec42c2136 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 14 Oct 2026 02:30:28 +0700 Subject: [PATCH 2/2] A press outside a field commits what was typed before the press acts --- crates/ui-egui/src/tests_numfields.rs | 32 +++++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 9 ++++++++ 2 files changed, 41 insertions(+) diff --git a/crates/ui-egui/src/tests_numfields.rs b/crates/ui-egui/src/tests_numfields.rs index ad516f65..98d8d4b4 100644 --- a/crates/ui-egui/src/tests_numfields.rs +++ b/crates/ui-egui/src/tests_numfields.rs @@ -446,3 +446,35 @@ fn typed_text_is_committed_when_the_focus_moves_away() { } } } + +/// Text typed into a field is committed in the frame of a press elsewhere, before what is drawn +/// after the field (the canvas) acts on the press: a click that selects another object must not +/// get the typed value, even when both objects show the same one. +#[test] +fn a_press_on_the_canvas_commits_to_the_object_being_edited() { + let mut f = Field::new(); + // The object the panel shows; both have the value 100. + let selected = Cell::new(1); + let committed = Cell::new(None); + let draw = |ui: &mut egui::Ui| { + let got = widgets::range_field(ui, "f", 100.0, 0.0..=1000.0, "%", 0, 80.0); + if let Some(v) = got { + committed.set(Some((selected.get(), v))); + } + // The canvas: a press ends text input and selects the other object. + if ui.input(|i| i.pointer.primary_pressed() && i.pointer.interact_pos().is_some_and(|p| p.y > 100.0)) { + ui.memory_mut(|m| m.stop_text_input()); + selected.set(2); + } + got + }; + f.frame(vec![], &draw); + let (field, canvas) = (f.rect.get().center(), Pos2::new(200.0, 180.0)); + f.frame(vec![Event::PointerMoved(field), pointer_button(field, true)], &draw); + f.frame(vec![pointer_button(field, false)], &draw); + f.frame(vec![Event::Text("42".into())], &draw); + f.frame(vec![Event::PointerMoved(canvas), pointer_button(canvas, true)], &draw); + f.frame(vec![pointer_button(canvas, false)], &draw); + f.frame(vec![], &draw); + assert_eq!((committed.get(), selected.get()), (Some((1, 42.0)), 2)); +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d8fd2d1e..5ce5723b 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -348,6 +348,15 @@ fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) { fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize) -> (String, Response, Rect) { let t = Tokens::get(ui.ctx()); let editing = ui.memory(|m| m.has_focus(id)); + // A press outside the field ends its editing in this frame, so the text typed is committed + // before the press acts: the canvas, drawn after the panels, may select another object, and a + // frame later the text would be committed to that one. + if editing + && let Some(at) = ui.input(|i| i.pointer.primary_pressed().then(|| i.pointer.interact_pos()).flatten()) + && ui.ctx().read_response(id).is_some_and(|r| !r.rect.contains(at)) + { + ui.memory_mut(|m| m.surrender_focus(id)); + } // When the value changes under a focused field (the slider beside it, a preview, another // field's edit), its text follows: the text typed for the old value would otherwise go back in // on the next Enter, Tab or arrow key (#1457).