diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index f127d312d2..0909d09f25 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,3 +1,4 @@ +use core::str; use std::cell::RefCell; use std::collections::HashSet; use std::ops::Deref; @@ -9,6 +10,8 @@ use calloop::generic::{FdWrapper, Generic}; use calloop::{EventLoop, LoopHandle, RegistrationToken}; use collections::HashMap; +use http_client::Url; +use smallvec::SmallVec; use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; @@ -17,9 +20,13 @@ use x11rb::errors::ConnectionError; use x11rb::protocol::randr::ConnectionExt as _; use x11rb::protocol::xinput::ConnectionExt; use x11rb::protocol::xkb::ConnectionExt as _; -use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyPressEvent}; +use x11rb::protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, ConnectionExt as _, + EventMask, KeyPressEvent, +}; use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event}; use x11rb::resource_manager::Database; +use x11rb::wrapper::ConnectionExt as _; use x11rb::xcb_ffi::XCBConnection; use xim::{x11rb::X11rbClient, Client}; use xim::{AttributeName, InputStyle}; @@ -30,8 +37,8 @@ use crate::platform::linux::LinuxClient; use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, - DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay, - PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, + PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; use super::{button_of_key, modifiers_from_state, pressed_button_from_mask}; @@ -101,6 +108,14 @@ struct XKBStateNotiy { locked_layout: LayoutIndex, } +#[derive(Debug, Default)] +pub struct Xdnd { + other_window: xproto::Window, + drag_type: u32, + retrieved: bool, + position: Point, +} + pub struct X11ClientState { pub(crate) loop_handle: LoopHandle<'static, X11Client>, pub(crate) event_loop: Option>, @@ -142,6 +157,7 @@ pub struct X11ClientState { pub(crate) common: LinuxCommon, pub(crate) clipboard: x11_clipboard::Clipboard, pub(crate) clipboard_item: Option, + pub(crate) xdnd_state: Xdnd, } #[derive(Clone)] @@ -423,6 +439,7 @@ impl X11Client { clipboard, clipboard_item: None, + xdnd_state: Xdnd::default(), }))) } @@ -611,7 +628,7 @@ impl X11Client { match event { Event::ClientMessage(event) => { let window = self.get_window(event.window)?; - let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32(); + let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); let mut state = self.0.borrow_mut(); if atom == state.atoms.WM_DELETE_WINDOW { @@ -627,6 +644,106 @@ impl X11Client { hi: arg3 as i32, }) } + + if event.type_ == state.atoms.XdndEnter { + state.xdnd_state.other_window = atom; + if (arg1 & 0x1) == 0x1 { + state.xdnd_state.drag_type = xdnd_get_supported_atom( + &state.xcb_connection, + &state.atoms, + state.xdnd_state.other_window, + ); + } else { + if let Some(atom) = [arg2, arg3, arg4] + .into_iter() + .find(|atom| xdnd_is_atom_supported(*atom, &state.atoms)) + { + state.xdnd_state.drag_type = atom; + } + } + } else if event.type_ == state.atoms.XdndLeave { + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { + position: state.xdnd_state.position, + })); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {})); + state.xdnd_state = Xdnd::default(); + } else if event.type_ == state.atoms.XdndPosition { + if let Ok(pos) = state + .xcb_connection + .query_pointer(event.window) + .unwrap() + .reply() + { + state.xdnd_state.position = + Point::new(Pixels(pos.win_x as f32), Pixels(pos.win_y as f32)); + } + if !state.xdnd_state.retrieved { + state + .xcb_connection + .convert_selection( + event.window, + state.atoms.XdndSelection, + state.xdnd_state.drag_type, + state.atoms.XDND_DATA, + arg3, + ) + .unwrap(); + } + xdnd_send_status( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + arg4, + ); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { + position: state.xdnd_state.position, + })); + } else if event.type_ == state.atoms.XdndDrop { + xdnd_send_finished( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + ); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Submit { + position: state.xdnd_state.position, + })); + state.xdnd_state = Xdnd::default(); + } + } + Event::SelectionNotify(event) => { + let window = self.get_window(event.requestor)?; + let mut state = self.0.borrow_mut(); + let property = state.xcb_connection.get_property( + false, + event.requestor, + state.atoms.XDND_DATA, + AtomEnum::ANY, + 0, + 1024, + ); + if property.as_ref().log_err().is_none() { + return Some(()); + } + if let Ok(reply) = property.unwrap().reply() { + match str::from_utf8(&reply.value) { + Ok(file_list) => { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| url.to_file_path().log_err()) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: crate::ExternalPaths(paths), + }); + window.handle_input(input); + state.xdnd_state.retrieved = true; + } + Err(_) => {} + } + } } Event::ConfigureNotify(event) => { let bounds = Bounds { @@ -1179,6 +1296,16 @@ impl LinuxClient for X11Client { state.scale_factor, state.common.appearance, )?; + state + .xcb_connection + .change_property32( + xproto::PropMode::REPLACE, + x_window, + state.atoms.XdndAware, + state.atoms.XA_ATOM, + &[5], + ) + .unwrap(); let screen_resources = state .xcb_connection @@ -1540,3 +1667,78 @@ fn check_gtk_frame_extents_supported( supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS) } + +fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { + return atom == atoms.TEXT + || atom == atoms.STRING + || atom == atoms.UTF8_STRING + || atom == atoms.TEXT_PLAIN + || atom == atoms.TEXT_PLAIN_UTF8 + || atom == atoms.TextUriList; +} + +fn xdnd_get_supported_atom( + xcb_connection: &XCBConnection, + supported_atoms: &XcbAtoms, + target: xproto::Window, +) -> u32 { + let property = xcb_connection + .get_property( + false, + target, + supported_atoms.XdndTypeList, + AtomEnum::ANY, + 0, + 1024, + ) + .unwrap(); + if let Ok(reply) = property.reply() { + if let Some(atoms) = reply.value32() { + for atom in atoms { + if xdnd_is_atom_supported(atom, &supported_atoms) { + return atom; + } + } + } + } + return 0; +} + +fn xdnd_send_finished( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndFinished, + data: ClientMessageData::from([source, 1, atoms.XdndActionCopy, 0, 0]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + xcb_connection + .send_event(false, target, EventMask::default(), message) + .unwrap(); +} + +fn xdnd_send_status( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, + action: u32, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndStatus, + data: ClientMessageData::from([source, 1, 0, 0, action]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + xcb_connection + .send_event(false, target, EventMask::default(), message) + .unwrap(); +} diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index f1aa10f311..b0cf82d605 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -32,7 +32,24 @@ use std::{ use super::{X11Display, XINPUT_MASTER_DEVICE}; x11rb::atom_manager! { pub XcbAtoms: AtomsCookie { + XA_ATOM, + XdndAware, + XdndStatus, + XdndEnter, + XdndLeave, + XdndPosition, + XdndSelection, + XdndDrop, + XdndFinished, + XdndTypeList, + XdndActionCopy, + TextUriList: b"text/uri-list", UTF8_STRING, + TEXT, + STRING, + TEXT_PLAIN_UTF8: b"text/plain;charset=utf-8", + TEXT_PLAIN: b"text/plain", + XDND_DATA, WM_PROTOCOLS, WM_DELETE_WINDOW, WM_CHANGE_STATE, diff --git a/typos.toml b/typos.toml index 2bbb4907a7..1b5c82b906 100644 --- a/typos.toml +++ b/typos.toml @@ -56,6 +56,8 @@ extend-ignore-re = [ "rename = \"sesssion_id\"", "doas", # ProtoLS crate with tree-sitter Protobuf grammar. - "protols" + "protols", + # x11rb SelectionNotifyEvent struct field + "requestor" ] check-filename = true