Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wayland: support basic Drag&Drop #2429

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ And please only add new entries to the top of this list, right below the `# Unre
- On Windows, added `WindowExtWindows::set_undecorated_shadow` and `WindowBuilderExtWindows::with_undecorated_shadow` to draw the drop shadow behind a borderless window.
- On Windows, fixed default window features (ie snap, animations, shake, etc.) when decorations are disabled.
- On macOS, add support for two-finger touchpad magnification and rotation gestures with new events `WindowEvent::TouchpadMagnify` and `WindowEvent::TouchpadRotate`.
- On Wayland, Drag & Drop is now supported.

# 0.27.2 (2022-8-12)

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ targets = [
[features]
default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"]
wayland = ["wayland-client", "wayland-protocols", "sctk"]
wayland = ["wayland-client", "wayland-protocols", "sctk", "percent-encoding"]
wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"]
wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"]
wayland-csd-adwaita-notitle = ["sctk-adwaita"]
Expand Down
2 changes: 1 addition & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Legend:
|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❌ |
|Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ |
|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |✔️ |
|Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ |
|Drag & Drop |▢[#720] |▢[#720] |▢[#720] |▢[#720] |**N/A**|**N/A**|❓ |
|Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ |
|Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ |
|Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ |
Expand Down
8 changes: 8 additions & 0 deletions src/platform_impl/linux/wayland/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use sctk::reexports::client::protocol::wl_shell::WlShell;
use sctk::reexports::client::protocol::wl_subcompositor::WlSubcompositor;
use sctk::reexports::client::{Attached, DispatchData};
use sctk::reexports::client::protocol::wl_shm::WlShm;
use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager;
use sctk::reexports::protocols::xdg_shell::client::xdg_wm_base::XdgWmBase;
use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1;
use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1;
Expand Down Expand Up @@ -61,6 +62,7 @@ sctk::environment!(WinitEnv,
ZwpPointerConstraintsV1 => pointer_constraints,
ZwpTextInputManagerV3 => text_input_manager,
XdgActivationV1 => xdg_activation,
WlDataDeviceManager => data_device_manager,
],
multis = [
WlSeat => seats,
Expand Down Expand Up @@ -91,6 +93,8 @@ pub struct WinitEnv {
decoration_manager: SimpleGlobal<ZxdgDecorationManagerV1>,

xdg_activation: SimpleGlobal<XdgActivationV1>,

data_device_manager: SimpleGlobal<WlDataDeviceManager>,
}

impl WinitEnv {
Expand Down Expand Up @@ -125,6 +129,9 @@ impl WinitEnv {
// Surface activation.
let xdg_activation = SimpleGlobal::new();

// Data device manager.
let data_device_manager = SimpleGlobal::new();

Self {
seats,
outputs,
Expand All @@ -137,6 +144,7 @@ impl WinitEnv {
pointer_constraints,
text_input_manager,
xdg_activation,
data_device_manager,
}
}
}
Expand Down
185 changes: 185 additions & 0 deletions src/platform_impl/linux/wayland/seat/dnd/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use std::io::{self, BufRead, Read};
use std::os::unix::prelude::{AsRawFd, RawFd};
use std::path::PathBuf;
use std::str;

use percent_encoding::percent_decode_str;
use sctk::data_device::{DataOffer, DndEvent, ReadPipe};
use sctk::reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction};

use crate::dpi::PhysicalPosition;
use crate::event::WindowEvent;
use crate::platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId};

use super::DndInner;

const MIME_TYPE: &str = "text/uri-list";

pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: &mut WinitState) {
match event {
DndEvent::Enter {
offer: Some(offer),
surface,
x,
y,
..
} => {
let window_id = make_wid(&surface);
inner.window_id = Some(window_id);
offer.accept(Some(MIME_TYPE.into()));
let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of these is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback emits the HoveredFile event though?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah. Sure. I'm not sure why anyone would need it. but ok.

if !paths.is_empty() {
winit_state.event_sink.push_window_event(
WindowEvent::CursorEntered {
device_id: crate::event::DeviceId(
crate::platform_impl::DeviceId::Wayland(DeviceId),
),
},
window_id,
);
winit_state.event_sink.push_window_event(
WindowEvent::CursorMoved {
device_id: crate::event::DeviceId(
Comment on lines +32 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto.

crate::platform_impl::DeviceId::Wayland(DeviceId),
),
position: PhysicalPosition::new(x, y),
modifiers: Default::default(),
},
window_id,
);

for path in paths {
winit_state
.event_sink
.push_window_event(WindowEvent::HoveredFile(path), window_id);
}
}
});
}
DndEvent::Drop { offer: Some(offer) } => {
if let Some(window_id) = inner.window_id {
inner.window_id = None;

let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| {
for path in paths {
winit_state
.event_sink
.push_window_event(WindowEvent::DroppedFile(path), window_id);
}
});
}
}
DndEvent::Leave => {
if let Some(window_id) = inner.window_id {
inner.window_id = None;

winit_state
.event_sink
.push_window_event(WindowEvent::HoveredFileCancelled, window_id);
winit_state.event_sink.push_window_event(
WindowEvent::CursorLeft {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure how it's related, I'm pretty sure wl_pointer will send the right events anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to happen at least on KDE and GNOME. Only when no file is being dragged over.

device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(
DeviceId,
)),
},
window_id,
);
}
}
DndEvent::Motion { x, y, .. } => {
if let Some(window_id) = inner.window_id {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why it generates that event, could you elaborate?

winit_state.event_sink.push_window_event(
WindowEvent::CursorMoved {
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(
DeviceId,
)),
position: PhysicalPosition::new(x, y),
modifiers: Default::default(),
},
window_id,
);
}
}
_ => {}
}
}

fn parse_offer(
loop_handle: &LoopHandle<'static, WinitState>,
offer: &DataOffer,
mut callback: impl FnMut(Vec<PathBuf>, &mut WinitState) + 'static,
) -> io::Result<()> {
let can_accept = offer.with_mime_types(|types| types.iter().any(|s| s == MIME_TYPE));
if can_accept {
let pipe = offer.receive(MIME_TYPE.into())?;
SludgePhD marked this conversation as resolved.
Show resolved Hide resolved
read_pipe_nonblocking(pipe, loop_handle, move |bytes, winit_state| {
// Format: https://www.iana.org/assignments/media-types/text/uri-list
let mut paths = Vec::new();
for line in bytes.lines() {
let line = match line {
Ok(line) => line,
Err(_) => continue,
};

if line.starts_with('#') {
continue;
}

let decoded = match percent_decode_str(&line).decode_utf8() {
Ok(decoded) => decoded,
Err(_) => continue,
};
if let Some(path) = decoded.strip_prefix("file://") {
paths.push(PathBuf::from(path));
}
}
callback(paths, winit_state);
})?;
}
Ok(())
}

fn read_pipe_nonblocking(
pipe: ReadPipe,
loop_handle: &LoopHandle<'static, WinitState>,
mut callback: impl FnMut(Vec<u8>, &mut WinitState) + 'static,
) -> io::Result<()> {
unsafe {
make_fd_nonblocking(pipe.as_raw_fd())?;
}

let mut content = Vec::<u8>::with_capacity(u16::MAX as usize);
let mut reader_buffer = [0; u16::MAX as usize];
let reader = Generic::new(pipe, Interest::READ, Mode::Level);

let _ = loop_handle.insert_source(reader, move |_, file, winit_state| {
let action = loop {
match file.read(&mut reader_buffer) {
Ok(0) => {
let data = std::mem::take(&mut content);
callback(data, winit_state);
break PostAction::Remove;
}
Ok(n) => content.extend_from_slice(&reader_buffer[..n]),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break PostAction::Continue,
Err(_) => break PostAction::Remove,
}
};

Ok(action)
});
Ok(())
}

unsafe fn make_fd_nonblocking(raw_fd: RawFd) -> io::Result<()> {
let flags = libc::fcntl(raw_fd, libc::F_GETFL);
if flags < 0 {
return Err(io::Error::from_raw_os_error(flags));
}
let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
if result < 0 {
return Err(io::Error::from_raw_os_error(result));
}

Ok(())
}
38 changes: 38 additions & 0 deletions src/platform_impl/linux/wayland/seat/dnd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use sctk::{data_device::DataDevice, reexports::calloop::LoopHandle};
use wayland_client::protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat};
use wayland_client::Attached;

use crate::platform_impl::{wayland::event_loop::WinitState, WindowId};

mod handlers;

pub(crate) struct Dnd {
_data_device: DataDevice,
}

impl Dnd {
pub fn new(
seat: &Attached<WlSeat>,
manager: &WlDataDeviceManager,
loop_handle: LoopHandle<'static, WinitState>,
) -> Self {
let mut inner = DndInner {
loop_handle,
window_id: None,
};
let data_device =
DataDevice::init_for_seat(manager, seat, move |event, mut dispatch_data| {
let winit_state = dispatch_data.get::<WinitState>().unwrap();
handlers::handle_dnd(event, &mut inner, winit_state);
});
Self {
_data_device: data_device,
}
}
}

struct DndInner {
loop_handle: LoopHandle<'static, WinitState>,
/// Window ID of the currently hovered window.
window_id: Option<WindowId>,
}
26 changes: 26 additions & 0 deletions src/platform_impl/linux/wayland/seat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_rela
use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1;
use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;

use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::Attached;

Expand All @@ -19,11 +20,13 @@ use super::env::WinitEnv;
use super::event_loop::WinitState;
use crate::event::ModifiersState;

mod dnd;
mod keyboard;
pub mod pointer;
pub mod text_input;
mod touch;

use dnd::Dnd;
use keyboard::Keyboard;
use pointer::Pointers;
use text_input::TextInput;
Expand All @@ -43,12 +46,14 @@ impl SeatManager {
let relative_pointer_manager = env.get_global::<ZwpRelativePointerManagerV1>();
let pointer_constraints = env.get_global::<ZwpPointerConstraintsV1>();
let text_input_manager = env.get_global::<ZwpTextInputManagerV3>();
let data_device_manager = env.get_global::<WlDataDeviceManager>();

let mut inner = SeatManagerInner::new(
theme_manager,
relative_pointer_manager,
pointer_constraints,
text_input_manager,
data_device_manager,
loop_handle,
);

Expand Down Expand Up @@ -89,6 +94,9 @@ struct SeatManagerInner {
/// Text input manager.
text_input_manager: Option<Attached<ZwpTextInputManagerV3>>,

/// Data device manager (for Drag and Drop).
data_device_manager: Option<Attached<WlDataDeviceManager>>,

/// A theme manager.
theme_manager: ThemeManager,
}
Expand All @@ -99,6 +107,7 @@ impl SeatManagerInner {
relative_pointer_manager: Option<Attached<ZwpRelativePointerManagerV1>>,
pointer_constraints: Option<Attached<ZwpPointerConstraintsV1>>,
text_input_manager: Option<Attached<ZwpTextInputManagerV3>>,
data_device_manager: Option<Attached<WlDataDeviceManager>>,
loop_handle: LoopHandle<'static, WinitState>,
) -> Self {
Self {
Expand All @@ -107,6 +116,7 @@ impl SeatManagerInner {
relative_pointer_manager,
pointer_constraints,
text_input_manager,
data_device_manager,
theme_manager,
}
}
Expand Down Expand Up @@ -168,6 +178,18 @@ impl SeatManagerInner {
seat_info.text_input = Some(TextInput::new(seat, text_input_manager));
}
}

if let Some(data_device_manager) = self.data_device_manager.as_ref() {
if seat_data.defunct {
seat_info.dnd = None;
} else {
seat_info.dnd = Some(Dnd::new(
seat,
data_device_manager,
self.loop_handle.clone(),
));
}
}
}
}

Expand All @@ -188,6 +210,9 @@ struct SeatInfo {
/// Text input handling aka IME.
text_input: Option<TextInput>,

/// Drag and Drop handler.
dnd: Option<Dnd>,

/// The current state of modifiers observed in keyboard handler.
///
/// We keep modifiers state on a seat, since it's being used by pointer events as well.
Expand All @@ -202,6 +227,7 @@ impl SeatInfo {
pointer: None,
touch: None,
text_input: None,
dnd: None,
modifiers_state: Rc::new(RefCell::new(ModifiersState::default())),
}
}
Expand Down