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
This commit is contained in:
apricotbucket28 2024-04-22 20:20:24 -03:00 committed by GitHub
parent 029eb67043
commit ae3c641bbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 270 additions and 13 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -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;

View File

@ -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));
}
}
_ => {}
}
}
}