Rework #1326: a short drag the toolkit counts as a click keeps its caret.

The late-focus case in the original change has no reproducible path
(#1339 already covers focus on the press frame), so the button-down
defer is dropped. The real issue is the clicked_focus branch: a very
short drag inside one character produces no text selection, egui still
reports a click, and select-all on release replaced the drag. The press
position is now remembered and a release more than three points away
keeps the caret; a plain tap still selects all.

Also fixes the test placement bug: the new test sat between the #[test]
attribute and keyboard_focus_keeps_selecting_the_entire_numeric_field,
so that test no longer ran. The regression test fails on the fork-point
widgets.rs and passes with this change.
This commit is contained in:
Vikram Bhaskaran
2026-10-16 01:36:23 +07:00
committed by bflatastic
parent 8354dbcd74
commit 173c88d2a1
2 changed files with 47 additions and 25 deletions
+26 -9
View File
@@ -397,24 +397,41 @@ 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]
/// Focus that arrives while the button is still down, without a press event on that frame,
/// keeps the caret until release.
#[test]
fn focus_a_frame_after_the_press_does_not_select_all_until_release() {
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();
f.frame(vec![Event::PointerMoved(at)], &draw);
assert_eq!(f.selection(), pressed, "holding the button does not select the whole field");
assert!(pressed.is_some_and(|(a, b)| a == b));
f.frame(vec![pointer_button(at, false)], &draw);
assert_eq!(f.selection(), Some((0, field_text(0).len())));
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 {
let mut f = Field::new();
+21 -16
View File
@@ -311,36 +311,41 @@ 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, down) = ui.input(|i| {
(
i.pointer.button_pressed(egui::PointerButton::Primary),
i.pointer.button_released(egui::PointerButton::Primary),
i.pointer.button_down(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())
});
// Focus can arrive the frame after the press, when `button_pressed` is already false
// and the button is still down. Defer select-all until release so a drag keeps its caret.
let defer = pressed || (down && resp.hovered());
if resp.gained_focus() {
ui.data_mut(|d| {
if defer {
if pressed {
d.insert_temp(pending, true);
if let Some(p) = at_press {
d.insert_temp(press_pos, p);
}
} else {
d.remove_temp::<bool>(pending);
d.remove_temp::<egui::Pos2>(press_pos);
}
});
}
let clicked_focus = released && ui.data_mut(|d| d.remove_temp::<bool>(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::<egui::Pos2>(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::<bool>(pending));
ui.data_mut(|d| {
d.remove_temp::<bool>(pending);
d.remove_temp::<egui::Pos2>(press_pos);
});
}
if resp.gained_focus() && defer {
return;
}
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) {