diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 68ec86a50..84a60cb8c 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -198,17 +198,22 @@ pub(crate) fn interact( // When the mouse first is pressed, it could be either, // so we postpone the decision until we know. // - // …unless a click is no longer possible at all: a click has to be - // released on the widget, and `hits.click` tells us whether the - // pointer is still somewhere a release would land on this widget. - // Note that this is not the same as being inside `interact_rect`: - // the hit-test also picks up widgets within `interact_radius`, and - // lets a widget on top take the hit. + // …unless the pointer has left the widget: a click has to be + // released on the widget, so once the pointer is outside there is + // nothing left to wait for. // // Deciding here means a thin drag handle (narrower than // `max_click_dist`) doesn't spend the decision window as neither // hovered nor dragged, which would make its highlight blink out. - let could_still_be_clicked = hits.click.is_some_and(|hit| hit.id == widget.id); + // The hit-test picks up widgets within `interact_radius`, so + // `hits.click` can name such a handle even when the pointer is a + // few points outside it. + // + // A widget on top might "steal" the click hit, but then the pointer is still inside + // us, and pressing that button must not start a drag. So we check both. + let pointer_is_inside = hits.contains_pointer.iter().any(|w| w.id == widget.id); + let could_still_be_clicked = + pointer_is_inside || hits.click.is_some_and(|hit| hit.id == widget.id); input.pointer.is_decidedly_dragging() || !could_still_be_clicked } else { // This widget is just sensitive to drags, so we can mark it as dragged right away: diff --git a/tests/egui_tests/tests/test_click_or_drag.rs b/tests/egui_tests/tests/test_click_or_drag.rs index 58be44247..c3cc010ec 100644 --- a/tests/egui_tests/tests/test_click_or_drag.rs +++ b/tests/egui_tests/tests/test_click_or_drag.rs @@ -185,3 +185,50 @@ fn click_inside_a_widget_still_clicks() { "press and release without moving should be a click" ); } + +/// A button inside a draggable row takes the click hit, because it is on top. +/// The pointer is still inside the row though, so the press must stay undecided — +/// otherwise the row starts dragging the moment the user touches the button. +#[test] +fn press_on_a_button_inside_a_draggable_row_stays_undecided() { + let button_id = Id::new("button"); + let button_size = Vec2::new(50.0, 20.0); + + let mut harness = Harness::builder() + .with_step_dt(1.0 / 60.0) + .with_size(Vec2::new(300.0, 200.0)) + .build_ui(move |ui| { + let row_rect = ui.max_rect(); + ui.interact(row_rect, widget_id(), Sense::click_and_drag()); + + // Allocated after the row, so it ends up _on top_ of it. + let button_rect = Rect::from_min_size(row_rect.min, button_size); + ui.interact(button_rect, button_id, Sense::click()); + }); + harness.step(); + + let grab = Rect::from_min_size(widget_rect(&harness).min, button_size).center(); + press_at(&mut harness, grab); + + let (_hovered, dragged) = widget_state(&harness); + assert!( + !dragged, + "pressing a button inside the row must not start dragging the row" + ); + + harness.event(egui::Event::PointerButton { + pos: grab, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + + assert!( + harness + .ctx + .read_response(button_id) + .is_some_and(|r| r.clicked()), + "the button should have been clicked" + ); +}