Merge pull request #1787 from MarekSpodBiedry/issue-1637

Fix numeric fields reverting a typed value when clicking away instead of pressing Enter (#1637)
This commit is contained in:
BFlat 2026-10-14 03:53:08 +07:00 committed by GitHub
commit bb8897a2eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 90 additions and 2 deletions

View File

@ -408,3 +408,73 @@ 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::<Vec<_>>(), [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}");
}
}
}
/// 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));
}

View File

@ -348,13 +348,27 @@ 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).
let shown_id = id.with("shown");
let followed = ui.data_mut(|d| d.get_temp::<String>(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::<String>(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::<bool>(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::<String>(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 +399,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)
}