diff --git a/internal/core/input.rs b/internal/core/input.rs index 3da92dbca02..f38be36965b 100644 --- a/internal/core/input.rs +++ b/internal/core/input.rs @@ -994,6 +994,10 @@ pub struct MouseInputState { /// When this is Some, it means we are in the middle of a drag-drop operation and it contains the dragged data. /// The `position` field has no signification pub(crate) drag_data: Option, + /// The DropArea that accepted the most recent DragMove, if any. On release we use + /// this to decide whether to deliver a Drop — matching OS DnD pipelines, where a + /// target that didn't previously accept never receives a drop. + pub(crate) drop_target: Option, delayed: Option<(crate::timers::Timer, MouseEvent)>, delayed_exit_items: Vec, pub(crate) cursor: MouseCursor, @@ -1169,6 +1173,7 @@ pub fn process_mouse_input( ) -> MouseInputState { let mut result = MouseInputState { drag_data: mouse_input_state.drag_data.clone(), + drop_target: mouse_input_state.drop_target.clone(), cursor: mouse_input_state.cursor, ..Default::default() }; @@ -1180,6 +1185,12 @@ pub fn process_mouse_input( mouse_input_state.top_item().as_ref(), false, ); + if matches!(mouse_event, MouseEvent::DragMove(_)) { + // Remember the accepting DropArea (or forget if none did) so the subsequent + // Release knows whether to deliver a Drop. + result.drop_target = + r.has_aborted().then(|| result.item_stack.last().map(|(w, _)| w.clone())).flatten(); + } if mouse_input_state.delayed.is_some() && (!r.has_aborted() || Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last()) diff --git a/internal/core/window.rs b/internal/core/window.rs index f25f0328a2f..a0471c07d51 100644 --- a/internal/core/window.rs +++ b/internal/core/window.rs @@ -627,9 +627,18 @@ impl WindowInner { if let Some(mut drop_event) = mouse_input_state.drag_data.clone() { match &event { MouseEvent::Released { position, button: PointerEventButton::Left, .. } => { - drop_event.position = crate::lengths::logical_position_to_api(*position); - event = MouseEvent::Drop(drop_event); mouse_input_state.drag_data = None; + if mouse_input_state.drop_target.take().is_some() { + drop_event.position = crate::lengths::logical_position_to_api(*position); + event = MouseEvent::Drop(drop_event); + } else { + // No DropArea accepted the most recent DragMove. Tear the drag + // down via Exit instead of converting to Drop so a non-accepting + // DropArea under the cursor doesn't fire `dropped`, and so the + // underlying Release doesn't reach hit-tested items as a + // spurious click. + event = MouseEvent::Exit; + } } MouseEvent::Moved { position, .. } => { drop_event.position = crate::lengths::logical_position_to_api(*position); @@ -638,6 +647,7 @@ impl WindowInner { } MouseEvent::Exit => { mouse_input_state.drag_data = None; + mouse_input_state.drop_target = None; } _ => {} } diff --git a/tests/cases/elements/dragarea_droparea.slint b/tests/cases/elements/dragarea_droparea.slint index 6b791447f83..af580e30df9 100644 --- a/tests/cases/elements/dragarea_droparea.slint +++ b/tests/cases/elements/dragarea_droparea.slint @@ -240,6 +240,25 @@ assert_eq!(instance.get_result(), ""); assert_eq!(instance.get_contains_drag(), false); assert_eq!(slint_testing::access_testing_window(instance.window(), |window| window.mouse_cursor()), MouseCursor::Default); +// When `can-drop` returns false, releasing over the DropArea must not invoke `dropped`. +instance.set_use_image(false); +instance.set_result("".into()); +instance.on_can_drop(|_, _| false); + +instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(20.0, 25.0), button: PointerEventButton::Left }); +slint_testing::mock_elapsed_time(20); +instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(21.0, 40.0) }); +slint_testing::mock_elapsed_time(20); +instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(22.0, 120.0) }); +slint_testing::mock_elapsed_time(20); +assert_eq!(instance.get_contains_drag(), false); +assert_eq!(slint_testing::access_testing_window(instance.window(), |window| window.mouse_cursor()), MouseCursor::NoDrop); + +instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(22.0, 120.0), button: PointerEventButton::Left }); +slint_testing::mock_elapsed_time(20); +assert_eq!(instance.get_result(), ""); +assert_eq!(instance.get_contains_drag(), false); + ``` */