gpui: Restore file drags when re-entering the source window on macOS (#61940)
Follow-up to #58161, which handed file drags to AppKit once the pointer left the window but left the handoff one-way. Re-entering the source window and dropping was not possible. GPUI now suspends the original drag when handing it to the platform and restores it when the drag re-enters the source window, so drop targets receive the original typed payload instead of the synthetic external-paths payload. No release notes this hasn't shipped yet. Release Notes: - N/A
This commit is contained in:
1 parent
4c1f737b8c
commit
a8b25c3131
4 files changed
+287
-28
No files matched your search
@@ -674,6 +674,18 @@ impl GpuiMode {
|
||||
}
|
||||
}
|
||||
|
||||
struct PlatformOwnedDrag {
|
||||
source_window: WindowId,
|
||||
state: PlatformOwnedDragState,
|
||||
}
|
||||
|
||||
enum PlatformOwnedDragState {
|
||||
Suspended(AnyDrag),
|
||||
// A source-window drop consumes `active_drag` before AppKit ends the dragging session, so this
|
||||
// marker can outlive the active drag and is cleaned up by `FileDropEvent::Ended`.
|
||||
RestoredInSourceWindow,
|
||||
}
|
||||
|
||||
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
|
||||
/// Other [Context] derefs to this type.
|
||||
/// You need a reference to an `App` to access the state of a [Entity].
|
||||
@@ -684,6 +696,7 @@ pub struct App {
|
||||
|
||||
pub(crate) actions: Rc<ActionRegistry>,
|
||||
pub(crate) active_drag: Option<AnyDrag>,
|
||||
platform_owned_drag: Option<PlatformOwnedDrag>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
pub(crate) entities: EntityMap,
|
||||
@@ -798,6 +811,7 @@ impl App {
|
||||
flushing_effects: false,
|
||||
pending_updates: 0,
|
||||
active_drag: None,
|
||||
platform_owned_drag: None,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
||||
@@ -1799,6 +1813,7 @@ impl App {
|
||||
cx.window_update_stack.pop();
|
||||
|
||||
if window.removed {
|
||||
cx.end_platform_drag(id);
|
||||
cx.window_handles.remove(&id);
|
||||
cx.windows.remove(id);
|
||||
if let Some(tracked) = cx.tracked_entities.remove(&id) {
|
||||
@@ -2440,6 +2455,12 @@ impl App {
|
||||
pub fn stop_active_drag(&mut self, window: &mut Window) -> bool {
|
||||
if self.active_drag.is_some() {
|
||||
self.active_drag = None;
|
||||
if self.platform_owned_drag.as_ref().is_some_and(|drag| {
|
||||
drag.source_window == window.window_handle().window_id()
|
||||
&& matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow)
|
||||
}) {
|
||||
self.platform_owned_drag = None;
|
||||
}
|
||||
window.refresh();
|
||||
true
|
||||
} else {
|
||||
@@ -2447,6 +2468,63 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hand_active_drag_to_platform(&mut self, source_window: WindowId) -> bool {
|
||||
let Some(drag) = self.active_drag.take() else {
|
||||
return false;
|
||||
};
|
||||
self.platform_owned_drag = Some(PlatformOwnedDrag {
|
||||
source_window,
|
||||
state: PlatformOwnedDragState::Suspended(drag),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn restore_platform_drag(&mut self, source_window: WindowId) -> bool {
|
||||
let Some(platform_drag) = self
|
||||
.platform_owned_drag
|
||||
.as_mut()
|
||||
.filter(|drag| drag.source_window == source_window)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let state = std::mem::replace(
|
||||
&mut platform_drag.state,
|
||||
PlatformOwnedDragState::RestoredInSourceWindow,
|
||||
);
|
||||
let PlatformOwnedDragState::Suspended(drag) = state else {
|
||||
return false;
|
||||
};
|
||||
self.active_drag = Some(drag);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn hand_restored_drag_to_platform(&mut self, source_window: WindowId) -> bool {
|
||||
let Some(platform_drag) = self.platform_owned_drag.as_mut().filter(|drag| {
|
||||
drag.source_window == source_window
|
||||
&& matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
let Some(drag) = self.active_drag.take() else {
|
||||
return false;
|
||||
};
|
||||
platform_drag.state = PlatformOwnedDragState::Suspended(drag);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn end_platform_drag(&mut self, source_window: WindowId) -> bool {
|
||||
if !self
|
||||
.platform_owned_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.source_window == source_window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.platform_owned_drag = None;
|
||||
self.active_drag = None;
|
||||
true
|
||||
}
|
||||
|
||||
/// Sets the cursor style for the currently active drag operation.
|
||||
pub fn set_active_drag_cursor_style(
|
||||
&mut self,
|
||||
|
||||
@@ -745,6 +745,8 @@ pub enum FileDropEvent {
|
||||
},
|
||||
/// The user has stopped dragging the files over the window.
|
||||
Exited,
|
||||
/// The platform-owned drag session has ended.
|
||||
Ended,
|
||||
}
|
||||
|
||||
impl Sealed for FileDropEvent {}
|
||||
|
||||
+189
-10
@@ -4956,7 +4956,8 @@ impl Window {
|
||||
PlatformInput::FileDrop(file_drop) => match file_drop {
|
||||
FileDropEvent::Entered { position, paths } => {
|
||||
self.mouse_position = position;
|
||||
if cx.active_drag.is_none() {
|
||||
let source_window = self.handle.window_id();
|
||||
if !cx.restore_platform_drag(source_window) && cx.active_drag.is_none() {
|
||||
cx.active_drag = Some(AnyDrag {
|
||||
value: Arc::new(paths.clone()),
|
||||
view: cx.new(|_| paths).into(),
|
||||
@@ -4990,9 +4991,17 @@ impl Window {
|
||||
})
|
||||
}
|
||||
FileDropEvent::Exited => {
|
||||
cx.active_drag.take();
|
||||
if !cx.hand_restored_drag_to_platform(self.handle.window_id()) {
|
||||
cx.active_drag.take();
|
||||
}
|
||||
self.refresh();
|
||||
PlatformInput::FileDrop(FileDropEvent::Exited)
|
||||
}
|
||||
FileDropEvent::Ended => {
|
||||
cx.end_platform_drag(self.handle.window_id());
|
||||
self.refresh();
|
||||
PlatformInput::FileDrop(FileDropEvent::Ended)
|
||||
}
|
||||
},
|
||||
PlatformInput::Touch(touch) => PlatformInput::Touch(touch),
|
||||
PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event,
|
||||
@@ -5047,8 +5056,10 @@ impl Window {
|
||||
let Some(payload) = payload_source(self, cx) else {
|
||||
return;
|
||||
};
|
||||
if self.platform_window.start_external_drag(&payload) {
|
||||
cx.stop_active_drag(self);
|
||||
if self.platform_window.start_external_drag(&payload)
|
||||
&& cx.hand_active_drag_to_platform(self.handle.window_id())
|
||||
{
|
||||
self.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6741,10 +6752,11 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
AnyWindowHandle, AppContext as _, Bounds, Context, DragMoveEvent, Empty,
|
||||
ExternalDragPayload, FileDragPaths, FocusHandle, InputEvent as _, InteractiveElement as _,
|
||||
IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Point,
|
||||
Render, StatefulInteractiveElement as _, Styled, TestAppContext, Window, WindowAppearance,
|
||||
WindowOptions, canvas, div, point, px, size,
|
||||
ExternalDragPayload, ExternalPaths, FileDragPaths, FileDropEvent, FocusHandle,
|
||||
InputEvent as _, InteractiveElement as _, IntoElement, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, ParentElement, Pixels, Point, Render, StatefulInteractiveElement as _,
|
||||
Styled, TestAppContext, Window, WindowAppearance, WindowOptions, canvas, div, point, px,
|
||||
size,
|
||||
};
|
||||
|
||||
struct EmptyView;
|
||||
@@ -6898,6 +6910,7 @@ mod tests {
|
||||
struct FileDragView {
|
||||
path: PathBuf,
|
||||
observed_drag_moves: Rc<RefCell<Vec<Point<Pixels>>>>,
|
||||
observed_drops: Rc<RefCell<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl Render for FileDragView {
|
||||
@@ -6918,24 +6931,32 @@ mod tests {
|
||||
observed_drag_moves.borrow_mut().push(event.event.position);
|
||||
}
|
||||
})
|
||||
.on_drop({
|
||||
let observed_drops = self.observed_drops.clone();
|
||||
move |path: &PathBuf, _, _| observed_drops.borrow_mut().push(path.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn file_drag_is_promoted_once_after_leaving_the_viewport(cx: &mut TestAppContext) {
|
||||
fn file_drag_is_promoted_once_and_restored_in_source_window(cx: &mut TestAppContext) {
|
||||
struct Drag {
|
||||
window: AnyWindowHandle,
|
||||
observed_drag_moves: Rc<RefCell<Vec<Point<Pixels>>>>,
|
||||
observed_drops: Rc<RefCell<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
fn start_drag(cx: &mut TestAppContext, path: PathBuf, platform_result: bool) -> Drag {
|
||||
let observed_drag_moves = Rc::new(RefCell::new(Vec::new()));
|
||||
let observed_drops = Rc::new(RefCell::new(Vec::new()));
|
||||
let window: AnyWindowHandle = cx
|
||||
.add_window({
|
||||
let observed_drag_moves = observed_drag_moves.clone();
|
||||
let observed_drops = observed_drops.clone();
|
||||
move |_, _| FileDragView {
|
||||
path,
|
||||
observed_drag_moves,
|
||||
observed_drops,
|
||||
}
|
||||
})
|
||||
.into();
|
||||
@@ -6975,6 +6996,7 @@ mod tests {
|
||||
Drag {
|
||||
window,
|
||||
observed_drag_moves,
|
||||
observed_drops,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6999,7 +7021,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
cx.test_window(successful.window).external_drag_files(),
|
||||
[(successful_path, true)]
|
||||
[(successful_path.clone(), true)]
|
||||
);
|
||||
// Views must still see the move that leaves the window, otherwise they never learn to tear
|
||||
// down the drag state they built up while the pointer was inside.
|
||||
@@ -7008,6 +7030,163 @@ mod tests {
|
||||
Some(&outside_position)
|
||||
);
|
||||
|
||||
let destination: AnyWindowHandle = cx.add_window(|_, _| EmptyView).into();
|
||||
let reentry_position = point(px(30.), px(30.));
|
||||
let external_paths = || ExternalPaths([successful_path.clone()].into_iter().collect());
|
||||
let update_result = cx.update_window(destination, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Entered {
|
||||
position: reentry_position,
|
||||
paths: external_paths(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(
|
||||
cx.active_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.value.downcast_ref::<ExternalPaths>().is_some())
|
||||
);
|
||||
window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx);
|
||||
assert!(cx.active_drag.is_none());
|
||||
});
|
||||
assert!(
|
||||
update_result.is_ok(),
|
||||
"failed to handle drag in destination window: {update_result:?}"
|
||||
);
|
||||
|
||||
let update_result = cx.update_window(successful.window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Entered {
|
||||
position: reentry_position,
|
||||
paths: external_paths(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(
|
||||
cx.active_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.value.downcast_ref::<PathBuf>().is_some())
|
||||
);
|
||||
assert_eq!(
|
||||
successful.observed_drag_moves.borrow().last(),
|
||||
Some(&reentry_position)
|
||||
);
|
||||
|
||||
window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx);
|
||||
assert!(cx.active_drag.is_none());
|
||||
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Entered {
|
||||
position: reentry_position,
|
||||
paths: external_paths(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(
|
||||
cx.active_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.value.downcast_ref::<PathBuf>().is_some())
|
||||
);
|
||||
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Submit {
|
||||
position: reentry_position,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
successful.observed_drops.borrow().as_slice(),
|
||||
std::slice::from_ref(&successful_path)
|
||||
);
|
||||
assert!(cx.active_drag.is_none());
|
||||
|
||||
window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx);
|
||||
assert!(cx.active_drag.is_none());
|
||||
window.dispatch_event(FileDropEvent::Ended.to_platform_input(), cx);
|
||||
assert!(cx.active_drag.is_none());
|
||||
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Entered {
|
||||
position: reentry_position,
|
||||
paths: external_paths(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(
|
||||
cx.active_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.value.downcast_ref::<ExternalPaths>().is_some())
|
||||
);
|
||||
window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx);
|
||||
});
|
||||
assert!(
|
||||
update_result.is_ok(),
|
||||
"failed to restore drag in source window: {update_result:?}"
|
||||
);
|
||||
|
||||
let cancelled_path = PathBuf::from("/tmp/cancelled-drag");
|
||||
let cancelled = start_drag(cx, cancelled_path.clone(), true);
|
||||
let update_result = cx.update_window(cancelled.window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
MouseMoveEvent {
|
||||
position: outside_position,
|
||||
pressed_button: Some(MouseButton::Left),
|
||||
modifiers: Default::default(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(cx.active_drag.is_none());
|
||||
|
||||
window.dispatch_event(
|
||||
FileDropEvent::Entered {
|
||||
position: reentry_position,
|
||||
paths: ExternalPaths([cancelled_path].into_iter().collect()),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(
|
||||
cx.active_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.value.downcast_ref::<PathBuf>().is_some())
|
||||
);
|
||||
assert!(cx.stop_active_drag(window));
|
||||
assert!(cx.active_drag.is_none());
|
||||
});
|
||||
assert!(
|
||||
update_result.is_ok(),
|
||||
"failed to cancel restored drag: {update_result:?}"
|
||||
);
|
||||
assert!(!cx.update(|cx| cx.end_platform_drag(cancelled.window.window_id())));
|
||||
|
||||
let removed_path = PathBuf::from("/tmp/removed-window-drag");
|
||||
let removed = start_drag(cx, removed_path, true);
|
||||
let removed_window_id = removed.window.window_id();
|
||||
let update_result = cx.update_window(removed.window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
MouseMoveEvent {
|
||||
position: outside_position,
|
||||
pressed_button: Some(MouseButton::Left),
|
||||
modifiers: Default::default(),
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
assert!(cx.active_drag.is_none());
|
||||
window.remove_window();
|
||||
});
|
||||
assert!(
|
||||
update_result.is_ok(),
|
||||
"failed to remove drag source window: {update_result:?}"
|
||||
);
|
||||
assert!(!cx.update(|cx| cx.end_platform_drag(removed_window_id)));
|
||||
|
||||
let failed_path = PathBuf::from("/tmp/failed-drag");
|
||||
let failed = start_drag(cx, failed_path.clone(), false);
|
||||
let update_result = cx.update_window(failed.window, |_, window, cx| {
|
||||
|
||||
@@ -3070,45 +3070,42 @@ fn is_drag_from_this_window(this: &Object, dragging_info: id) -> bool {
|
||||
}
|
||||
|
||||
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||||
if is_drag_from_this_window(this, dragging_info) {
|
||||
return NSDragOperationNone;
|
||||
}
|
||||
let is_source_window = is_drag_from_this_window(this, dragging_info);
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
let paths = external_paths_from_event(dragging_info);
|
||||
if let Some(event) = paths.map(|paths| FileDropEvent::Entered { position, paths })
|
||||
&& send_file_drop_event(window_state, event)
|
||||
{
|
||||
if is_source_window {
|
||||
return NSDragOperationMove;
|
||||
}
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
NSDragOperationNone
|
||||
}
|
||||
|
||||
extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||||
if is_drag_from_this_window(this, dragging_info) {
|
||||
return NSDragOperationNone;
|
||||
}
|
||||
let is_source_window = is_drag_from_this_window(this, dragging_info);
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
if send_file_drop_event(window_state, FileDropEvent::Pending { position }) {
|
||||
NSDragOperationCopy
|
||||
if is_source_window {
|
||||
NSDragOperationMove
|
||||
} else {
|
||||
NSDragOperationCopy
|
||||
}
|
||||
} else {
|
||||
NSDragOperationNone
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dragging_exited(this: &Object, _: Sel, dragging_info: id) {
|
||||
if is_drag_from_this_window(this, dragging_info) {
|
||||
return;
|
||||
}
|
||||
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
send_file_drop_event(window_state, FileDropEvent::Exited);
|
||||
}
|
||||
|
||||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||||
if is_drag_from_this_window(this, dragging_info) {
|
||||
return NO;
|
||||
}
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
send_file_drop_event(window_state, FileDropEvent::Submit { position }).to_objc()
|
||||
@@ -3166,9 +3163,12 @@ extern "C" fn dragging_session_ended(
|
||||
// SAFETY: AppKit invokes this selector on the GPUIWindow instance registered in build_classes,
|
||||
// which always has WINDOW_STATE_IVAR initialized to the owning MacWindowState.
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.lock();
|
||||
lock.synthetic_drag_counter += 1;
|
||||
lock.last_left_mouse_down_event = None;
|
||||
{
|
||||
let mut lock = window_state.lock();
|
||||
lock.synthetic_drag_counter += 1;
|
||||
lock.last_left_mouse_down_event = None;
|
||||
}
|
||||
send_file_drop_event(window_state, FileDropEvent::Ended);
|
||||
}
|
||||
|
||||
async fn synthetic_drag(
|
||||
@@ -3202,7 +3202,7 @@ fn send_file_drop_event(
|
||||
) -> bool {
|
||||
let external_files_dragged = match file_drop_event {
|
||||
FileDropEvent::Entered { .. } => Some(true),
|
||||
FileDropEvent::Exited => Some(false),
|
||||
FileDropEvent::Exited | FileDropEvent::Ended => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user