diff --git a/crates/ui-egui/src/tests_numfields.rs b/crates/ui-egui/src/tests_numfields.rs index 98d8d4b4e..6358958e4 100644 --- a/crates/ui-egui/src/tests_numfields.rs +++ b/crates/ui-egui/src/tests_numfields.rs @@ -397,6 +397,40 @@ fn a_new_numeric_click_selects_all_on_release_without_moving_its_press_caret() { assert_eq!(f.num(vec![enter()]), Some(7.0)); } +/// #1326: a short drag that the toolkit still counts as a click must keep its caret on +/// release instead of selecting the whole value. +#[test] +fn a_short_drag_released_as_a_click_keeps_its_caret() { + let mut f = Field::new(); + let draw = |ui: &mut egui::Ui| numeric_fixture(ui, 0); + f.frame(vec![], &draw); + let at = f.text_point(field_text(0), 2, 7.0); + f.frame(vec![Event::PointerMoved(at), pointer_button(at, true)], &draw); + let pressed = f.selection(); + assert!(pressed.is_some_and(|(a, b)| a == b), "the press places a caret"); + // Moved a few points: inside one character, so egui reports a click, not a text selection. + let nudge = egui::vec2(3.4, 0.0); + f.frame(vec![Event::PointerMoved(at + nudge)], &draw); + f.frame(vec![pointer_button(at + nudge, false)], &draw); + assert_eq!(f.selection(), pressed, "the short drag keeps its caret instead of selecting all"); + assert_ne!(f.selection(), Some((0, field_text(0).chars().count())), "a clicked short drag must not select all (#1326)"); + f.frame(vec![Event::Text("7".into())], &draw); + assert_eq!(f.frame(vec![enter()], &draw), Some(1273456.0), "typing lands where the drag left the caret"); +} + +/// A tap (no movement) on a fresh numeric field still selects all on release. +#[test] +fn a_tap_without_movement_still_selects_all_on_release() { + let mut f = Field::new(); + let draw = |ui: &mut egui::Ui| numeric_fixture(ui, 0); + f.frame(vec![], &draw); + let at = f.text_point(field_text(0), 2, 7.0); + f.frame(vec![Event::PointerMoved(at), pointer_button(at, true)], &draw); + f.frame(vec![], &draw); + f.frame(vec![pointer_button(at, false)], &draw); + assert_eq!(f.selection(), Some((0, field_text(0).chars().count())), "a plain tap selects all"); +} + #[test] fn keyboard_focus_keeps_selecting_the_entire_numeric_field() { for mode in 0..4 { diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index e03c4a968..c84ec2e81 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -311,25 +311,43 @@ fn select_all(ui: &Ui, id: egui::Id, text: &str) { } /// Numeric fields select the whole text on keyboard focus, a completed focus click or a -/// double-click. A pointer press keeps its caret so dragging selects from the pressed position. +/// double-click. A pointer press keeps its caret so dragging selects from the pressed position, +/// and a short drag the toolkit still counts as a click keeps it too (#1326): only a tap +/// releases into select-all. fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) { let pending = resp.id.with("select-all-on-focus-click"); - let (pressed, released) = - ui.input(|i| (i.pointer.button_pressed(egui::PointerButton::Primary), i.pointer.button_released(egui::PointerButton::Primary))); + let press_pos = resp.id.with("select-all-on-focus-press"); + let (pressed, released, at_press) = ui.input(|i| { + (i.pointer.button_pressed(egui::PointerButton::Primary), i.pointer.button_released(egui::PointerButton::Primary), i.pointer.interact_pos()) + }); if resp.gained_focus() { ui.data_mut(|d| { if pressed { d.insert_temp(pending, true); + if let Some(p) = at_press { + d.insert_temp(press_pos, p); + } } else { d.remove_temp::(pending); + d.remove_temp::(press_pos); } }); } let clicked_focus = released && ui.data_mut(|d| d.remove_temp::(pending)).unwrap_or(false) && resp.clicked_by(egui::PointerButton::Primary); + // A press released a few points away is a short drag, not a tap. egui still reports a + // click, and a release inside one character leaves no text selection, so select-all on + // release would replace the drag the user just made (#1326). + let dragged = clicked_focus + && ui + .data_mut(|d| d.remove_temp::(press_pos)) + .is_some_and(|p0| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p1| (p1 - p0).length() > 3.0)); if resp.lost_focus() { - ui.data_mut(|d| d.remove_temp::(pending)); + ui.data_mut(|d| { + d.remove_temp::(pending); + d.remove_temp::(press_pos); + }); } - if !(resp.gained_focus() && !pressed || resp.double_clicked() || clicked_focus) { + if !(resp.gained_focus() && !pressed || resp.double_clicked() || clicked_focus && !dragged) { return; } if let Some(st) = egui::TextEdit::load_state(ui.ctx(), resp.id) {