@@ -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();
+}