From 3c9d943ff21c3d7b8fb40b1f5cb1a85539bd7205 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Mon, 2 Nov 2026 00:42:22 -0500 Subject: [PATCH] Field scrub: the field always senses drags; Alt as the drag begins makes it a scrub until the button is let go Co-Authored-By: Claude Fable 5.1 --- crates/ui-egui/src/field_tests.rs | 13 +++++++++++++ crates/ui-egui/src/widgets.rs | 16 +++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/field_tests.rs b/crates/ui-egui/src/field_tests.rs index 91fbae2b2..4e3dfb35d 100644 --- a/crates/ui-egui/src/field_tests.rs +++ b/crates/ui-egui/src/field_tests.rs @@ -1216,4 +1216,17 @@ fn alt_held_over_a_field_scrubs_it() { drag(&mut u, at, 25.0, Modifiers::NONE); assert_eq!(geom(&u, a).0, 35.0, "without Alt the field doesn't scrub"); assert_eq!(undo_steps(&u), steps + 1); + // Alt and the press in the same frame still scrub, and Alt let go before the button still + // ends the scrub with its undo step. + let (at, _) = field(&mut u, "X"); + let press = |pressed: bool, m: Modifiers, p: Pos2| Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: m }; + u.frame_with(vec![Event::PointerMoved(at), press(true, alt, at)], alt); + for k in 1..=10 { + u.frame_with(vec![Event::PointerMoved(pos2(at.x + 2.0 * k as f32, at.y))], alt); + } + let to = pos2(at.x + 20.0, at.y); + u.frame_with(vec![Event::PointerMoved(to), press(false, Modifiers::NONE, to)], Modifiers::NONE); + u.frame(vec![]); + assert_eq!(geom(&u, a).0, 55.0, "twenty more"); + assert_eq!(undo_steps(&u), steps + 2, "committed when the button went up, without Alt"); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 4ed92eb0d..7db22abc0 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -268,10 +268,16 @@ fn number_field_impl( let mut editing: Option = ui.data(|d| d.get_temp(editing_id)); // Alt held over the field scrubs it too, as the label always does: "hover the input field // itself and hold down the modifier key" (Adjust alignment, rotation, position, and - // dimensions). The drag sense stays on until a scrub begun that way is let go. + // dimensions). The field always senses drags (egui assigns a press by the frame before it, so + // a sense that waits for Alt would miss a press that arrives with it); Alt as the drag begins + // is what makes it a scrub, and that holds until the button is let go, Alt released or not. let field_id = id.with("field"); - let alt_scrub = editing.is_none() && (ui.input(|i| i.modifiers.alt) || ui.ctx().is_being_dragged(field_id)); - let field_resp = ui.interact(edit_rect, field_id, if alt_scrub { Sense::CLICK | Sense::DRAG } else { Sense::click() }); + let field_resp = ui.interact(edit_rect, field_id, Sense::click_and_drag()); + let alt_now = ui.input(|i| i.modifiers.alt); + let scrubbing_id = id.with("altscrub"); + let alt_scrub = + editing.is_none() && if field_resp.drag_started() { alt_now } else { ui.data(|d| d.get_temp::(scrubbing_id)).unwrap_or(false) }; + ui.data_mut(|d| d.insert_temp(scrubbing_id, alt_scrub && field_resp.dragged() && !field_resp.drag_stopped())); a11y::number(&field_resp, lead.name(), value, suffix, empty.text()); a11y::ring_around(ui, rect, &field_resp); // The label scrubs with the pointer; the keyboard stops at the value only. @@ -297,7 +303,7 @@ fn number_field_impl( } Lead::None | Lead::Named(_) => {} } - if lead_resp.hovered() || lead_resp.dragged() || (alt_scrub && (field_resp.hovered() || field_resp.dragged())) { + if lead_resp.hovered() || lead_resp.dragged() || (alt_now && editing.is_none() && field_resp.hovered()) || (alt_scrub && field_resp.dragged()) { ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal); } // On the field, Alt is what makes the drag a scrub, so it is not the fine rate there. @@ -349,7 +355,7 @@ fn number_field_impl( } // Typing on a field the keyboard reached (Tab) edits it: the editor takes the typed text. let typed_now = editing.is_none() && on_field && ui.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(_)))); - let started = editing.is_none() && ((field_resp.clicked() && !alt_scrub) || typed_now); + let started = editing.is_none() && ((field_resp.clicked() && !alt_now) || typed_now); // The text the field would commit to no effect: what it showed when the edit began, or the // value the arrows last stepped it to. let kept_id = id.with("kept");