From ae3c641bbee2029fb4588d008e45ddb783593622 Mon Sep 17 00:00:00 2001 From: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:20:24 -0300 Subject: [PATCH] wayland: File drag and drop (#10817) Implements file drag and drop on Wayland https://github.com/zed-industries/zed/assets/71973804/febcfbfe-3a23-4593-8dd3-e85254e58eb5 Release Notes: - N/A --- Cargo.lock | 12 + crates/gpui/Cargo.toml | 1 + crates/gpui/src/platform/linux/platform.rs | 17 ++ .../gpui/src/platform/linux/wayland/client.rs | 253 +++++++++++++++++- 4 files changed, 270 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8d019f3ac..52942e16b6 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 471e0a7347..f49ab6e571 100644 --- a/crates/gpui/Cargo.toml +++ b/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"] } diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 6bf70ad46d..8ecb639335 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/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, b: Point) -> bo diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE } +pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result { + 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; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 1eb438795f..8f23b22921 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/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, pub compositor: wl_compositor::WlCompositor, + pub data_device_manager: Option, pub wm_base: xdg_wm_base::XdgWmBase, pub shm: wl_shm::WlShm, pub viewporter: Option, @@ -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, + data_device: Option, // Surface to Window mapping windows: HashMap, // Output to scale mapping output_scales: HashMap, keymap_state: Option, + drag: DragState, click: ClickState, repeat: KeyRepeat, modifiers: Modifiers, @@ -124,6 +145,12 @@ pub(crate) struct WaylandClientState { common: LinuxCommon, } +pub struct DragState { + data_offer: Option, + window: Option, + position: Point, +} + pub struct ClickState { last_click: Instant, last_location: Point, @@ -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>); +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::(&conn).unwrap(); let qh = event_queue.handle(); - let mut outputs = HashMap::default(); + let mut seat: Option = None; + let mut outputs = HashMap::default(); globals.contents().with_list(|list| { for global in list { match &global.interface[..] { "wl_seat" => { - globals.registry().bind::( + // TODO: multi-seat support + seat = Some(globals.registry().bind::( global.name, wl_seat_version(global.version), &qh, (), - ); + )); } "wl_output" => { let output = globals.registry().bind::( @@ -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::::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 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 for WaylandClientStatePtr { impl Dispatch for WaylandClientStatePtr { fn event( - _: &mut Self, + this: &mut Self, wm_base: &xdg_wm_base::XdgWmBase, event: ::Event, _: &(), @@ -607,6 +657,9 @@ impl Dispatch for WaylandClientStatePtr { _: &QueueHandle, ) { 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 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 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 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 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 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 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 window.handle_toplevel_decoration_event(event); } } + +const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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 for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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)); + } + } + _ => {} + } + } +}