x11: Implement Drag and Drop (#17491)

Closes #16225

Release Notes:

- x11: Implemented Drag and Drop.
This commit is contained in:
Fernando Tagawa 2024-09-09 18:27:45 -03:00 committed by GitHub
parent 12dde17608
commit 59be07ad90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 226 additions and 5 deletions

View file

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

View file

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

View file

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