x11: Implement Drag and Drop (#17491)

Fernando Tagawa created

Closes #16225

Release Notes:

- x11: Implemented Drag and Drop.

Change summary

crates/gpui/src/platform/linux/x11/client.rs | 210 +++++++++++++++++++++
crates/gpui/src/platform/linux/x11/window.rs |  17 +
typos.toml                                   |   4 
3 files changed, 226 insertions(+), 5 deletions(-)

Detailed changes

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<Pixels>,
+}
+
 pub struct X11ClientState {
     pub(crate) loop_handle: LoopHandle<'static, X11Client>,
     pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -142,6 +157,7 @@ pub struct X11ClientState {
     pub(crate) common: LinuxCommon,
     pub(crate) clipboard: x11_clipboard::Clipboard,
     pub(crate) clipboard_item: Option<ClipboardItem>,
+    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();
+}

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,

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