Add OS file drop event handler

Kirill Bulatov and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/gpui2/src/app.rs                  |  2 
crates/gpui2/src/interactive.rs          | 41 +++++++-----
crates/gpui2/src/platform/mac/events.rs  |  4 
crates/gpui2/src/platform/mac/window.rs  | 61 +++++++++++--------
crates/gpui2/src/window.rs               | 80 ++++++++++++++++---------
crates/gpui2_macros/src/style_helpers.rs | 13 +++
crates/ui2/src/color.rs                  |  2 
crates/ui2/src/components/panes.rs       | 17 ++++
crates/ui2/src/components/workspace.rs   |  2 
9 files changed, 139 insertions(+), 83 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -821,7 +821,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
 }
 
 pub(crate) struct AnyDrag {
-    pub drag_handle_view: AnyView,
+    pub drag_handle_view: Option<AnyView>,
     pub cursor_offset: Point<Pixels>,
     pub state: AnyBox,
     pub state_type: TypeId,

crates/gpui2/src/interactive.rs 🔗

@@ -374,10 +374,12 @@ pub trait StatefulInteractive: StatelessInteractive {
             Some(Arc::new(move |view_state, cursor_offset, cx| {
                 let drag = listener(view_state, cx);
                 let view_handle = cx.handle().upgrade().unwrap();
-                let drag_handle_view = view(view_handle, move |view_state, cx| {
-                    (drag.render_drag_handle)(view_state, cx)
-                })
-                .into_any();
+                let drag_handle_view = Some(
+                    view(view_handle, move |view_state, cx| {
+                        (drag.render_drag_handle)(view_state, cx)
+                    })
+                    .into_any(),
+                );
                 AnyDrag {
                     drag_handle_view,
                     cursor_offset,
@@ -780,11 +782,7 @@ impl GroupBounds {
     }
 
     pub fn pop(name: &SharedString, cx: &mut AppContext) {
-        cx.default_global::<Self>()
-            .0
-            .get_mut(name)
-            .unwrap()
-            .pop();
+        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
     }
 }
 
@@ -1035,16 +1033,21 @@ impl Deref for MouseExitEvent {
 }
 
 #[derive(Debug, Clone, Default)]
+pub struct DroppedFiles(pub(crate) SmallVec<[PathBuf; 2]>);
+
+#[derive(Debug, Clone)]
 pub enum FileDropEvent {
-    #[default]
-    End,
+    Entered {
+        position: Point<Pixels>,
+        files: DroppedFiles,
+    },
     Pending {
         position: Point<Pixels>,
     },
     Submit {
         position: Point<Pixels>,
-        paths: Vec<PathBuf>,
     },
+    Exited,
 }
 
 #[derive(Clone, Debug)]
@@ -1054,7 +1057,7 @@ pub enum InputEvent {
     ModifiersChanged(ModifiersChangedEvent),
     MouseDown(MouseDownEvent),
     MouseUp(MouseUpEvent),
-    MouseMoved(MouseMoveEvent),
+    MouseMove(MouseMoveEvent),
     MouseExited(MouseExitEvent),
     ScrollWheel(ScrollWheelEvent),
     FileDrop(FileDropEvent),
@@ -1068,12 +1071,14 @@ impl InputEvent {
             InputEvent::ModifiersChanged { .. } => None,
             InputEvent::MouseDown(event) => Some(event.position),
             InputEvent::MouseUp(event) => Some(event.position),
-            InputEvent::MouseMoved(event) => Some(event.position),
+            InputEvent::MouseMove(event) => Some(event.position),
             InputEvent::MouseExited(event) => Some(event.position),
             InputEvent::ScrollWheel(event) => Some(event.position),
-            InputEvent::FileDrop(FileDropEvent::End) => None,
+            InputEvent::FileDrop(FileDropEvent::Exited) => None,
             InputEvent::FileDrop(
-                FileDropEvent::Pending { position } | FileDropEvent::Submit { position, .. },
+                FileDropEvent::Entered { position, .. }
+                | FileDropEvent::Pending { position, .. }
+                | FileDropEvent::Submit { position, .. },
             ) => Some(*position),
         }
     }
@@ -1085,7 +1090,7 @@ impl InputEvent {
             InputEvent::ModifiersChanged { .. } => None,
             InputEvent::MouseDown(event) => Some(event),
             InputEvent::MouseUp(event) => Some(event),
-            InputEvent::MouseMoved(event) => Some(event),
+            InputEvent::MouseMove(event) => Some(event),
             InputEvent::MouseExited(event) => Some(event),
             InputEvent::ScrollWheel(event) => Some(event),
             InputEvent::FileDrop(event) => Some(event),
@@ -1099,7 +1104,7 @@ impl InputEvent {
             InputEvent::ModifiersChanged(event) => Some(event),
             InputEvent::MouseDown(_) => None,
             InputEvent::MouseUp(_) => None,
-            InputEvent::MouseMoved(_) => None,
+            InputEvent::MouseMove(_) => None,
             InputEvent::MouseExited(_) => None,
             InputEvent::ScrollWheel(_) => None,
             InputEvent::FileDrop(_) => None,

crates/gpui2/src/platform/mac/events.rs 🔗

@@ -202,7 +202,7 @@ impl InputEvent {
                 };
 
                 window_height.map(|window_height| {
-                    Self::MouseMoved(MouseMoveEvent {
+                    Self::MouseMove(MouseMoveEvent {
                         pressed_button: Some(pressed_button),
                         position: point(
                             px(native_event.locationInWindow().x as f32),
@@ -213,7 +213,7 @@ impl InputEvent {
                 })
             }
             NSEventType::NSMouseMoved => window_height.map(|window_height| {
-                Self::MouseMoved(MouseMoveEvent {
+                Self::MouseMove(MouseMoveEvent {
                     position: point(
                         px(native_event.locationInWindow().x as f32),
                         window_height - px(native_event.locationInWindow().y as f32),

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -1,10 +1,10 @@
 use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
 use crate::{
-    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, Executor, FileDropEvent,
-    GlobalPixels, InputEvent, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
-    PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Scene, Size, Timer,
-    WindowAppearance, WindowBounds, WindowKind, WindowOptions, WindowPromptLevel,
+    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, DroppedFiles, Executor,
+    FileDropEvent, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, Modifiers,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
+    PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Scene, Size,
+    Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, WindowPromptLevel,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -31,6 +31,7 @@ use objc::{
     sel, sel_impl,
 };
 use parking_lot::Mutex;
+use smallvec::SmallVec;
 use std::{
     any::Any,
     cell::{Cell, RefCell},
@@ -1177,7 +1178,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         };
 
         match &event {
-            InputEvent::MouseMoved(
+            InputEvent::MouseMove(
                 event @ MouseMoveEvent {
                     pressed_button: Some(_),
                     ..
@@ -1194,7 +1195,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     .detach();
             }
 
-            InputEvent::MouseMoved(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
+            InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
 
             InputEvent::MouseUp(MouseUpEvent {
                 button: MouseButton::Left,
@@ -1633,11 +1634,14 @@ extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
 
 extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
     let window_state = unsafe { get_window_state(this) };
-    let position = drag_event_position(&window_state, dragging_info);
-    if send_new_event(
-        &window_state,
-        InputEvent::FileDrop(FileDropEvent::Pending { position }),
-    ) {
+    if send_new_event(&window_state, {
+        let position = drag_event_position(&window_state, dragging_info);
+        let paths = external_paths_from_event(dragging_info);
+        InputEvent::FileDrop(FileDropEvent::Entered {
+            position,
+            files: paths,
+        })
+    }) {
         NSDragOperationCopy
     } else {
         NSDragOperationNone
@@ -1659,26 +1663,17 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
 
 extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
-    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::End));
+    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
 }
 
 extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
-    let mut paths = Vec::new();
-    let pb: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
-    let filenames = unsafe { NSPasteboard::propertyListForType(pb, NSFilenamesPboardType) };
-    for file in unsafe { filenames.iter() } {
-        let path = unsafe {
-            let f = NSString::UTF8String(file);
-            CStr::from_ptr(f).to_string_lossy().into_owned()
-        };
-        paths.push(PathBuf::from(path))
-    }
+    let files = external_paths_from_event(dragging_info);
 
     let window_state = unsafe { get_window_state(this) };
     let position = drag_event_position(&window_state, dragging_info);
     if send_new_event(
         &window_state,
-        InputEvent::FileDrop(FileDropEvent::Submit { position, paths }),
+        InputEvent::FileDrop(FileDropEvent::Submit { position }),
     ) {
         YES
     } else {
@@ -1686,9 +1681,23 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -
     }
 }
 
+fn external_paths_from_event(dragging_info: *mut Object) -> DroppedFiles {
+    let mut paths = SmallVec::new();
+    let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
+    let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) };
+    for file in unsafe { filenames.iter() } {
+        let path = unsafe {
+            let f = NSString::UTF8String(file);
+            CStr::from_ptr(f).to_string_lossy().into_owned()
+        };
+        paths.push(PathBuf::from(path))
+    }
+    DroppedFiles(paths)
+}
+
 extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
-    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::End));
+    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
 }
 
 async fn synthetic_drag(
@@ -1703,7 +1712,7 @@ async fn synthetic_drag(
             if lock.synthetic_drag_counter == drag_id {
                 if let Some(mut callback) = lock.event_callback.take() {
                     drop(lock);
-                    callback(InputEvent::MouseMoved(event.clone()));
+                    callback(InputEvent::MouseMove(event.clone()));
                     window_state.lock().event_callback = Some(callback);
                 }
             } else {

crates/gpui2/src/window.rs 🔗

@@ -1,13 +1,14 @@
 use crate::{
-    px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds,
-    BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, Element,
-    EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Handle,
-    Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId,
-    MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, MouseUpEvent, Path, Pixels,
-    PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams,
-    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
-    Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
-    WindowOptions, SUBPIXEL_VARIANTS,
+    px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
+    Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, DroppedFiles,
+    Edges, Effect, Element, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId,
+    GlobalElementId, GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch,
+    KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, Modifiers, MonochromeSprite,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
+    PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams,
+    RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription,
+    TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle, WindowOptions,
+    SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use collections::HashMap;
@@ -816,7 +817,9 @@ impl<'a, 'w> WindowContext<'a, 'w> {
                     cx.with_element_offset(Some(offset), |cx| {
                         let available_space =
                             size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                        draw_any_view(&mut active_drag.drag_handle_view, available_space, cx);
+                        if let Some(drag_handle_view) = &mut active_drag.drag_handle_view {
+                            draw_any_view(drag_handle_view, available_space, cx);
+                        }
                         cx.active_drag = Some(active_drag);
                     });
                 });
@@ -889,27 +892,48 @@ impl<'a, 'w> WindowContext<'a, 'w> {
     }
 
     fn dispatch_event(&mut self, event: InputEvent) -> bool {
-        if let Some(any_mouse_event) = event.mouse_event() {
-            if let Some(MouseMoveEvent { position, .. }) = any_mouse_event.downcast_ref() {
-                self.window.mouse_position = *position;
+        let event = match event {
+            InputEvent::MouseMove(mouse_move) => {
+                self.window.mouse_position = mouse_move.position;
+                InputEvent::MouseMove(mouse_move)
             }
-
-            match any_mouse_event.downcast_ref() {
-                Some(FileDropEvent::Pending { position }) => {
-                    dbg!("FileDropEvent::Pending", position);
-                    return true;
-                }
-                Some(FileDropEvent::Submit { position, paths }) => {
-                    dbg!("FileDropEvent::Submit", position, paths);
-                    return true;
-                }
-                Some(FileDropEvent::End) => {
-                    self.active_drag = None;
-                    return true;
+            InputEvent::FileDrop(file_drop) => match file_drop {
+                FileDropEvent::Entered { position, files } => {
+                    self.active_drag.get_or_insert_with(|| AnyDrag {
+                        drag_handle_view: None,
+                        cursor_offset: position,
+                        state: Box::new(files),
+                        state_type: TypeId::of::<DroppedFiles>(),
+                    });
+                    InputEvent::MouseDown(MouseDownEvent {
+                        position,
+                        button: MouseButton::Left,
+                        click_count: 1,
+                        modifiers: Modifiers::default(),
+                    })
                 }
-                _ => {}
-            }
+                FileDropEvent::Pending { position } => InputEvent::MouseMove(MouseMoveEvent {
+                    position,
+                    pressed_button: Some(MouseButton::Left),
+                    modifiers: Modifiers::default(),
+                }),
+                FileDropEvent::Submit { position } => InputEvent::MouseUp(MouseUpEvent {
+                    button: MouseButton::Left,
+                    position,
+                    modifiers: Modifiers::default(),
+                    click_count: 1,
+                }),
+                FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent {
+                    button: MouseButton::Left,
+                    position: Point::default(),
+                    modifiers: Modifiers::default(),
+                    click_count: 1,
+                }),
+            },
+            _ => event,
+        };
 
+        if let Some(any_mouse_event) = event.mouse_event() {
             // Handlers may set this to false by calling `stop_propagation`
             self.app.propagate_event = true;
             self.window.default_prevented = false;

crates/gpui2_macros/src/style_helpers.rs 🔗

@@ -305,7 +305,18 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)>
             vec![quote! { padding.right }],
             "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
         ),
-        ("top", true, vec![quote! { inset.top }], "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",),
+        (
+            "inset",
+            true,
+            vec![quote! { inset.top }, quote! { inset.right }, quote! { inset.bottom }, quote! { inset.left }],
+            "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "top",
+            true,
+            vec![quote! { inset.top }],
+            "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
         (
             "bottom",
             true,

crates/ui2/src/color.rs 🔗

@@ -159,8 +159,6 @@ pub struct ThemeColor {
 
 impl std::fmt::Debug for ThemeColor {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        dbg!("ThemeColor debug");
-
         f.debug_struct("ThemeColor")
             .field("transparent", &self.transparent.to_rgb().to_hex())
             .field(

crates/ui2/src/components/panes.rs 🔗

@@ -1,6 +1,6 @@
 use std::marker::PhantomData;
 
-use gpui2::{hsla, AnyElement, ElementId, Hsla, Length, Size};
+use gpui2::{hsla, red, AnyElement, DroppedFiles, ElementId, Hsla, Length, Size};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -50,8 +50,19 @@ impl<S: 'static + Send + Sync> Pane<S> {
             .bg(self.fill)
             .w(self.size.width)
             .h(self.size.height)
-            .overflow_y_scroll()
-            .children(self.children.drain(..))
+            .relative()
+            .children(cx.stack(0, |_| self.children.drain(..)))
+            .child(cx.stack(1, |_| {
+                // TODO kb! Figure out why we can't we see the red background when we drag a file over this div.
+                div()
+                    .id("drag-target")
+                    .drag_over::<DroppedFiles>(|d| d.bg(red()))
+                    .on_drop(|_, files: DroppedFiles, _| {
+                        dbg!("dropped files!", files);
+                    })
+                    .absolute()
+                    .inset_0()
+            }))
     }
 }
 

crates/ui2/src/components/workspace.rs 🔗

@@ -179,8 +179,6 @@ impl Workspace {
 
         let color = ThemeColor::new(cx);
 
-        dbg!(color);
-
         // HACK: This should happen inside of `debug_toggle_user_settings`, but
         // we don't have `cx.global::<FakeSettings>()` in event handlers at the moment.
         // Need to talk with Nathan/Antonio about this.