From 3672e0ae8496b1c873d57404281b29b253b51eaa Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 02:41:21 +0700 Subject: [PATCH] Fix #1457: a focused field follows its value when a slider or preview changes it --- crates/ui-egui/src/tests_numfields.rs | 36 +++++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 9 ++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/tests_numfields.rs b/crates/ui-egui/src/tests_numfields.rs index 9415532f..7c1cc917 100644 --- a/crates/ui-egui/src/tests_numfields.rs +++ b/crates/ui-egui/src/tests_numfields.rs @@ -9,6 +9,42 @@ use vectorcraft_doc::Unit; use crate::widgets; +/// #1457: a focused field follows its value when that changes from outside (the slider beside +/// it), so the arrow keys step from the value shown and losing focus doesn't put the old one back. +#[test] +fn a_focused_field_follows_a_value_changed_from_outside() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut time = 0.0; + let mut frame = |value: f64, events: Vec| { + time += 0.05; + let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(400.0, 200.0))); + let mut got = (None, Rect::NOTHING); + let mut out = ctx.run_ui(egui::RawInput { events, time: Some(time), screen_rect, focused: true, ..Default::default() }, |ui| { + got.0 = widgets::range_field(ui, "f", value, 0.0..=1000.0, "%", 2, 80.0); + got.1 = ui.min_rect(); + }); + out.textures_delta.clear(); + got + }; + let key = |key| Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }; + let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }; + // Click into the field showing 100 %. + let at = frame(100.0, vec![]).1.center(); + frame(100.0, vec![Event::PointerMoved(at), button(at, true)]); + frame(100.0, vec![button(at, false)]); + assert_eq!(frame(100.0, vec![]).0, None); + // The value changes under the focused field (a slider dragged to 142): ↓ steps from 142, not + // from the 100 the field held when it took the focus. + frame(142.0, vec![]); + assert_eq!(frame(142.0, vec![key(Key::ArrowDown)]).0, Some(141.0)); + // And changed again, losing focus leaves the value alone. + frame(50.0, vec![]); + let away = at + vec2(0.0, 150.0); + frame(50.0, vec![Event::PointerMoved(away), button(away, true)]); + assert_eq!(frame(50.0, vec![button(away, false)]).0, None); +} + /// Headless frames of one field: its rect and id as last drawn. struct Field { ctx: egui::Context, diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index c0a94c6e..06987064 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -348,7 +348,14 @@ 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)); - let mut buf: String = if editing { ui.data_mut(|d| d.get_temp::(id)).unwrap_or_else(|| shown.to_string()) } else { shown.to_string() }; + // 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). + 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 }; + 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 // spinners and dropdowns (#900).