wayland: File drag and drop (#10817)

apricotbucket28 created

Implements file drag and drop on Wayland


https://github.com/zed-industries/zed/assets/71973804/febcfbfe-3a23-4593-8dd3-e85254e58eb5


Release Notes:

- N/A

Change summary

Cargo.lock                                       |  12 
crates/gpui/Cargo.toml                           |   1 
crates/gpui/src/platform/linux/platform.rs       |  17 +
crates/gpui/src/platform/linux/wayland/client.rs | 253 +++++++++++++++++
4 files changed, 270 insertions(+), 13 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3799,6 +3799,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "filedescriptor"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
+dependencies = [
+ "libc",
+ "thiserror",
+ "winapi",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.22"
@@ -4482,6 +4493,7 @@ dependencies = [
  "derive_more",
  "env_logger",
  "etagere",
+ "filedescriptor",
  "flume",
  "font-kit",
  "foreign-types 0.5.0",

crates/gpui/Cargo.toml 🔗

@@ -115,6 +115,7 @@ wayland-protocols = { version = "0.31.2", features = [
 ] }
 oo7 = "0.3.0"
 open = "5.1.2"
+filedescriptor = "0.8.2"
 x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] }
 xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
 

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -3,7 +3,10 @@
 use std::any::{type_name, Any};
 use std::cell::{self, RefCell};
 use std::env;
+use std::fs::File;
+use std::io::Read;
 use std::ops::{Deref, DerefMut};
+use std::os::fd::{AsRawFd, FromRawFd};
 use std::panic::Location;
 use std::{
     path::{Path, PathBuf},
@@ -19,6 +22,7 @@ use async_task::Runnable;
 use calloop::channel::Channel;
 use calloop::{EventLoop, LoopHandle, LoopSignal};
 use copypasta::ClipboardProvider;
+use filedescriptor::FileDescriptor;
 use flume::{Receiver, Sender};
 use futures::channel::oneshot;
 use parking_lot::Mutex;
@@ -484,6 +488,19 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
     diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
 }
 
+pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
+    let mut file = File::from_raw_fd(fd.as_raw_fd());
+
+    let mut buffer = String::new();
+    file.read_to_string(&mut buffer)?;
+
+    // Normalize the text to unix line endings, otherwise
+    // copying from eg: firefox inserts a lot of blank
+    // lines, and that is super annoying.
+    let result = buffer.replace("\r\n", "\n");
+    Ok(result)
+}
+
 impl Keystroke {
     pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
         let mut modifiers = modifiers;

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -1,5 +1,9 @@
+use core::hash;
 use std::cell::{RefCell, RefMut};
+use std::os::fd::{AsRawFd, BorrowedFd};
+use std::path::PathBuf;
 use std::rc::{Rc, Weak};
+use std::sync::Arc;
 use std::time::{Duration, Instant};
 
 use async_task::Runnable;
@@ -9,13 +13,19 @@ use calloop_wayland_source::WaylandSource;
 use collections::HashMap;
 use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
 use copypasta::ClipboardProvider;
+use filedescriptor::Pipe;
+use smallvec::SmallVec;
 use util::ResultExt;
 use wayland_backend::client::ObjectId;
 use wayland_backend::protocol::WEnum;
+use wayland_client::event_created_child;
 use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents};
 use wayland_client::protocol::wl_callback::{self, WlCallback};
-use wayland_client::protocol::wl_output;
+use wayland_client::protocol::wl_data_device_manager::DndAction;
 use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource};
+use wayland_client::protocol::{
+    wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output,
+};
 use wayland_client::{
     delegate_noop,
     protocol::{
@@ -35,14 +45,14 @@ use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_ba
 use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
 use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
 
-use super::super::DOUBLE_CLICK_INTERVAL;
+use super::super::{read_fd, DOUBLE_CLICK_INTERVAL};
 use super::window::{WaylandWindowState, WaylandWindowStatePtr};
 use crate::platform::linux::is_within_click_distance;
 use crate::platform::linux::wayland::cursor::Cursor;
 use crate::platform::linux::wayland::window::WaylandWindow;
 use crate::platform::linux::LinuxClient;
 use crate::platform::PlatformWindow;
-use crate::{point, px, ForegroundExecutor, MouseExitEvent};
+use crate::{point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent};
 use crate::{
     AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
@@ -58,6 +68,7 @@ const MIN_KEYCODE: u32 = 8;
 pub struct Globals {
     pub qh: QueueHandle<WaylandClientStatePtr>,
     pub compositor: wl_compositor::WlCompositor,
+    pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
     pub wm_base: xdg_wm_base::XdgWmBase,
     pub shm: wl_shm::WlShm,
     pub viewporter: Option<wp_viewporter::WpViewporter>,
@@ -82,6 +93,13 @@ impl Globals {
                     (),
                 )
                 .unwrap(),
+            data_device_manager: globals
+                .bind(
+                    &qh,
+                    WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION,
+                    (),
+                )
+                .ok(),
             shm: globals.bind(&qh, 1..=1, ()).unwrap(),
             wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
             viewporter: globals.bind(&qh, 1..=1, ()).ok(),
@@ -94,13 +112,16 @@ impl Globals {
 }
 
 pub(crate) struct WaylandClientState {
+    serial: u32,
     globals: Globals,
     wl_pointer: Option<wl_pointer::WlPointer>,
+    data_device: Option<wl_data_device::WlDataDevice>,
     // Surface to Window mapping
     windows: HashMap<ObjectId, WaylandWindowStatePtr>,
     // Output to scale mapping
     output_scales: HashMap<ObjectId, i32>,
     keymap_state: Option<xkb::State>,
+    drag: DragState,
     click: ClickState,
     repeat: KeyRepeat,
     modifiers: Modifiers,
@@ -124,6 +145,12 @@ pub(crate) struct WaylandClientState {
     common: LinuxCommon,
 }
 
+pub struct DragState {
+    data_offer: Option<wl_data_offer::WlDataOffer>,
+    window: Option<WaylandWindowStatePtr>,
+    position: Point<Pixels>,
+}
+
 pub struct ClickState {
     last_click: Instant,
     last_location: Point<Pixels>,
@@ -167,6 +194,12 @@ impl WaylandClientStatePtr {
             // Drop the clipboard to prevent a seg fault after we've closed all Wayland connections.
             state.clipboard = None;
             state.primary = None;
+            if let Some(wl_pointer) = &state.wl_pointer {
+                wl_pointer.release();
+            }
+            if let Some(data_device) = &state.data_device {
+                data_device.release();
+            }
             state.common.signal.stop();
         }
     }
@@ -175,6 +208,7 @@ impl WaylandClientStatePtr {
 #[derive(Clone)]
 pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
 
+const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
 const WL_OUTPUT_VERSION: u32 = 2;
 
 fn wl_seat_version(version: u32) -> u32 {
@@ -199,18 +233,20 @@ impl WaylandClient {
         let (globals, mut event_queue) =
             registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
         let qh = event_queue.handle();
-        let mut outputs = HashMap::default();
 
+        let mut seat: Option<wl_seat::WlSeat> = None;
+        let mut outputs = HashMap::default();
         globals.contents().with_list(|list| {
             for global in list {
                 match &global.interface[..] {
                     "wl_seat" => {
-                        globals.registry().bind::<wl_seat::WlSeat, _, _>(
+                        // TODO: multi-seat support
+                        seat = Some(globals.registry().bind::<wl_seat::WlSeat, _, _>(
                             global.name,
                             wl_seat_version(global.version),
                             &qh,
                             (),
-                        );
+                        ));
                     }
                     "wl_output" => {
                         let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
@@ -227,34 +263,47 @@ impl WaylandClient {
         });
 
         let display = conn.backend().display_ptr() as *mut std::ffi::c_void;
-        let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
 
         let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
 
         let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
 
         let handle = event_loop.handle();
-
         handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
             if let calloop::channel::Event::Msg(runnable) = event {
                 runnable.run();
             }
         });
 
-        let globals = Globals::new(globals, common.foreground_executor.clone(), qh);
+        let seat = seat.unwrap();
+        let globals = Globals::new(globals, common.foreground_executor.clone(), qh.clone());
+
+        let data_device = globals
+            .data_device_manager
+            .as_ref()
+            .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ()));
+
+        let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
 
         let cursor = Cursor::new(&conn, &globals, 24);
 
         let mut state = Rc::new(RefCell::new(WaylandClientState {
+            serial: 0,
             globals,
             wl_pointer: None,
+            data_device,
             output_scales: outputs,
             windows: HashMap::default(),
             common,
             keymap_state: None,
+            drag: DragState {
+                data_offer: None,
+                window: None,
+                position: Point::default(),
+            },
             click: ClickState {
                 last_click: Instant::now(),
-                last_location: Point::new(px(0.0), px(0.0)),
+                last_location: Point::default(),
                 current_count: 0,
             },
             repeat: KeyRepeat {
@@ -467,6 +516,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
 }
 
 delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
+delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager);
 delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
 delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
 delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
@@ -599,7 +649,7 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
 
 impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
     fn event(
-        _: &mut Self,
+        this: &mut Self,
         wm_base: &xdg_wm_base::XdgWmBase,
         event: <xdg_wm_base::XdgWmBase as Proxy>::Event,
         _: &(),
@@ -607,6 +657,9 @@ impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
         _: &QueueHandle<Self>,
     ) {
         if let xdg_wm_base::Event::Ping { serial } = event {
+            let client = this.get_client();
+            let mut state = client.borrow_mut();
+            state.serial = serial;
             wm_base.pong(serial);
         }
     }
@@ -678,7 +731,10 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                 };
                 state.keymap_state = Some(xkb::State::new(&keymap));
             }
-            wl_keyboard::Event::Enter { surface, .. } => {
+            wl_keyboard::Event::Enter {
+                serial, surface, ..
+            } => {
+                state.serial = serial;
                 state.keyboard_focused_window = get_window(&mut state, &surface.id());
 
                 if let Some(window) = state.keyboard_focused_window.clone() {
@@ -686,7 +742,10 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                     window.set_focused(true);
                 }
             }
-            wl_keyboard::Event::Leave { surface, .. } => {
+            wl_keyboard::Event::Leave {
+                serial, surface, ..
+            } => {
+                state.serial = serial;
                 let keyboard_focused_window = get_window(&mut state, &surface.id());
                 state.keyboard_focused_window = None;
 
@@ -696,12 +755,14 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                 }
             }
             wl_keyboard::Event::Modifiers {
+                serial,
                 mods_depressed,
                 mods_latched,
                 mods_locked,
                 group,
                 ..
             } => {
+                state.serial = serial;
                 let focused_window = state.keyboard_focused_window.clone();
                 let Some(focused_window) = focused_window else {
                     return;
@@ -721,8 +782,11 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
             wl_keyboard::Event::Key {
                 key,
                 state: WEnum::Value(key_state),
+                serial,
                 ..
             } => {
+                state.serial = serial;
+
                 let focused_window = state.keyboard_focused_window.clone();
                 let Some(focused_window) = focused_window else {
                     return;
@@ -833,6 +897,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 surface_y,
                 ..
             } => {
+                state.serial = serial;
                 state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
 
                 if let Some(window) = get_window(&mut state, &surface.id()) {
@@ -885,10 +950,12 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 }
             }
             wl_pointer::Event::Button {
+                serial,
                 button,
                 state: WEnum::Value(button_state),
                 ..
             } => {
+                state.serial = serial;
                 let button = linux_button_to_gpui(button);
                 let Some(button) = button else { return };
                 if state.mouse_focused_window.is_none() {
@@ -1123,3 +1190,163 @@ impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
         window.handle_toplevel_decoration_event(event);
     }
 }
+
+const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
+
+impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
+    fn event(
+        this: &mut Self,
+        _: &wl_data_device::WlDataDevice,
+        event: wl_data_device::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        let client = this.get_client();
+        let mut state = client.borrow_mut();
+
+        match event {
+            wl_data_device::Event::Enter {
+                serial,
+                surface,
+                x,
+                y,
+                id: data_offer,
+            } => {
+                state.serial = serial;
+                if let Some(data_offer) = data_offer {
+                    let Some(drag_window) = get_window(&mut state, &surface.id()) else {
+                        return;
+                    };
+
+                    const ACTIONS: DndAction = DndAction::Copy;
+                    data_offer.set_actions(ACTIONS, ACTIONS);
+
+                    let pipe = Pipe::new().unwrap();
+                    data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe {
+                        BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
+                    });
+                    let fd = pipe.read;
+                    drop(pipe.write);
+
+                    let read_task = state
+                        .common
+                        .background_executor
+                        .spawn(async { unsafe { read_fd(fd) } });
+
+                    let this = this.clone();
+                    state
+                        .common
+                        .foreground_executor
+                        .spawn(async move {
+                            let file_list = match read_task.await {
+                                Ok(list) => list,
+                                Err(err) => {
+                                    log::error!("error reading drag and drop pipe: {err:?}");
+                                    return;
+                                }
+                            };
+
+                            let paths: SmallVec<[_; 2]> = file_list
+                                .lines()
+                                .map(|path| PathBuf::from(path.replace("file://", "")))
+                                .collect();
+                            let position = Point::new(x.into(), y.into());
+
+                            // Prevent dropping text from other programs.
+                            if paths.is_empty() {
+                                data_offer.finish();
+                                data_offer.destroy();
+                                return;
+                            }
+
+                            let input = PlatformInput::FileDrop(FileDropEvent::Entered {
+                                position,
+                                paths: crate::ExternalPaths(paths),
+                            });
+
+                            let client = this.get_client();
+                            let mut state = client.borrow_mut();
+                            state.drag.data_offer = Some(data_offer);
+                            state.drag.window = Some(drag_window.clone());
+                            state.drag.position = position;
+
+                            drop(state);
+                            drag_window.handle_input(input);
+                        })
+                        .detach();
+                }
+            }
+            wl_data_device::Event::Motion { x, y, .. } => {
+                let Some(drag_window) = state.drag.window.clone() else {
+                    return;
+                };
+                let position = Point::new(x.into(), y.into());
+                state.drag.position = position;
+
+                let input = PlatformInput::FileDrop(FileDropEvent::Pending { position });
+                drop(state);
+                drag_window.handle_input(input);
+            }
+            wl_data_device::Event::Leave => {
+                let Some(drag_window) = state.drag.window.clone() else {
+                    return;
+                };
+                let data_offer = state.drag.data_offer.clone().unwrap();
+                data_offer.destroy();
+
+                state.drag.data_offer = None;
+                state.drag.window = None;
+
+                let input = PlatformInput::FileDrop(FileDropEvent::Exited {});
+                drop(state);
+                drag_window.handle_input(input);
+            }
+            wl_data_device::Event::Drop => {
+                let Some(drag_window) = state.drag.window.clone() else {
+                    return;
+                };
+                let data_offer = state.drag.data_offer.clone().unwrap();
+                data_offer.finish();
+                data_offer.destroy();
+
+                state.drag.data_offer = None;
+                state.drag.window = None;
+
+                let input = PlatformInput::FileDrop(FileDropEvent::Submit {
+                    position: state.drag.position,
+                });
+                drop(state);
+                drag_window.handle_input(input);
+            }
+            _ => {}
+        }
+    }
+
+    event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [
+        wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()),
+    ]);
+}
+
+impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr {
+    fn event(
+        this: &mut Self,
+        data_offer: &wl_data_offer::WlDataOffer,
+        event: wl_data_offer::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        let client = this.get_client();
+        let mut state = client.borrow_mut();
+
+        match event {
+            wl_data_offer::Event::Offer { mime_type } => {
+                if mime_type == FILE_LIST_MIME_TYPE {
+                    data_offer.accept(state.serial, Some(mime_type));
+                }
+            }
+            _ => {}
+        }
+    }
+}