Skip to content

Commit

Permalink
wayland: register drag & drop events
Browse files Browse the repository at this point in the history
  • Loading branch information
PeakKS committed Nov 27, 2024
1 parent fc6cf89 commit 2d5072e
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 4 deletions.
12 changes: 9 additions & 3 deletions examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,18 @@ impl ApplicationHandler for Application {
WindowEvent::DoubleTapGesture { .. } => {
info!("Smart zoom");
},
WindowEvent::HoveredFile(path) => {
info!("File hover initiated: {path:?}");
},
WindowEvent::HoveredFileCancelled => {
info!("File hover canceled");
},
WindowEvent::DroppedFile(path) => {
info!("File dropped: {path:?}");
},
WindowEvent::TouchpadPressure { .. }
| WindowEvent::HoveredFileCancelled
| WindowEvent::KeyboardInput { .. }
| WindowEvent::PointerEntered { .. }
| WindowEvent::DroppedFile(_)
| WindowEvent::HoveredFile(_)
| WindowEvent::Destroyed
| WindowEvent::Moved(_) => (),
}
Expand Down
329 changes: 329 additions & 0 deletions src/platform_impl/linux/wayland/seat/data_device/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
use std::io::{BufRead, BufReader};
use std::os::fd::AsFd;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use calloop::{LoopHandle, PostAction, RegistrationToken};
use rustix::pipe::{pipe_with, PipeFlags};
use sctk::data_device_manager::data_source::DataSourceData;
use sctk::data_device_manager::ReadPipe;
use sctk::globals::GlobalData;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_data_device::{Event as WlDataDeviceEvent, WlDataDevice};
use sctk::reexports::client::protocol::wl_data_device_manager::{DndAction, WlDataDeviceManager};
use sctk::reexports::client::protocol::wl_data_offer::{Event as WlDataOfferEvent, WlDataOffer};
use sctk::reexports::client::protocol::wl_data_source::WlDataSource;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::{
delegate_dispatch, event_created_child, Connection, Dispatch, Proxy, QueueHandle,
};
use tracing::warn;

use crate::event::WindowEvent;
use crate::platform_impl::wayland;
use crate::platform_impl::wayland::state::WinitState;
use crate::window::WindowId;

pub struct DataDeviceManager {
manager: WlDataDeviceManager,
}

impl DataDeviceManager {
pub fn new(
globals: &GlobalList,
queue_handle: &QueueHandle<WinitState>,
) -> Result<Self, BindError> {
let manager = globals.bind(queue_handle, 1..=3, GlobalData)?;
Ok(Self { manager })
}

pub fn init_seat_data_device(&self, seat: &WlSeat, queue_handle: &QueueHandle<WinitState>) {
let data = DataDeviceData { seat: seat.clone() };
self.manager.get_data_device(seat, queue_handle, data);
}
}

impl Dispatch<WlDataDeviceManager, GlobalData, WinitState> for DataDeviceState {
fn event(
_state: &mut WinitState,
_proxy: &WlDataDeviceManager,
_event: <WlDataDeviceManager as Proxy>::Event,
_data: &GlobalData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
}
}

struct DataDeviceData {
seat: WlSeat,
}

impl Dispatch<WlDataDevice, DataDeviceData, WinitState> for DataDeviceState {
event_created_child!(WinitState, WlDataDevice, [
0 => (WlDataOffer, Default::default())
]);

fn event(
state: &mut WinitState,
wl_data_device: &WlDataDevice,
event: <WlDataDevice as Proxy>::Event,
data: &DataDeviceData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
let seat_state = match state.seats.get_mut(&data.seat.id()) {
Some(seat_state) => seat_state,
None => {
warn!("Received data device event {event:?} without seat");
return;
},
};

let data_device_state = match seat_state.data_device_state.as_mut() {
Some(data_device_state) => data_device_state,
None => {
warn!("Received data device event {event:?} without data device state");
return;
},
};

match event {
WlDataDeviceEvent::Enter { serial, surface, x: _, y: _, id } => {
let Some(wl_data_offer) = id else {
warn!("Received data device enter event without id");
return;
};

let Some(data_offer) = wl_data_offer.data::<DataOfferData>() else {
warn!("Received data device enter event without data offer");
return;
};

let Some(mime_type) = (match data_offer.mime_types.lock() {
Ok(mime_types) => mime_types
.iter()
.find(|&mime| mime == FILE_TRANSFER_MIME_TYPE)
.map(|mime| mime.clone()),
Err(e) => {
warn!("Failed to lock MIME type vector: {e}");
None
},
}) else {
warn!("Data deviced entered with no valid MIME type");
return;
};

wl_data_offer.set_actions(DndAction::Copy, DndAction::Copy);
wl_data_offer.accept(serial, Some(mime_type.clone()));

let (read_fd, write_fd) = match pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK)
{
Ok((read_fd, write_fd)) => (read_fd, write_fd),
Err(e) => {
warn!("Failed to create pipe to read data offer from: {e}");
return;
},
};

wl_data_offer.receive(mime_type, write_fd.as_fd());
let read_pipe = ReadPipe::from(read_fd);

if let Some(token) = data_device_state.read_token.take() {
warn!("Cancelling previous data device enter read");
data_device_state.loop_handle.remove(token);
}

let window_id = wayland::make_wid(&surface);
data_device_state.hovered_window = Some(window_id);
data_device_state.offer = Some(wl_data_offer);
data_device_state.read_buffer.clear();

let wl_data_device = wl_data_device.clone();
data_device_state.read_token = data_device_state
.loop_handle
.insert_source(read_pipe, move |_, f, state| {
state.dispatched_events = true;
let data = wl_data_device.data::<DataDeviceData>().unwrap();

let seat_state = match state.seats.get_mut(&data.seat.id()) {
Some(seat_state) => seat_state,
None => return PostAction::Remove,
};

let data_device_state = match seat_state.data_device_state.as_mut() {
Some(data_device_state) => data_device_state,
None => return PostAction::Remove,
};

let f: &mut std::fs::File = unsafe { f.get_mut() };
let mut reader = BufReader::new(f);

let consumed = match reader.fill_buf() {
Ok(buf) => {
if buf.is_empty() {
if let Some(hovered) = data_device_state.hovered_window {
data_device_state.with_file_paths(|paths| {
for path in paths {
state.events_sink.push_window_event(
WindowEvent::HoveredFile(path),
hovered,
);
}
});
} else {
warn!("No window specified to push HoveredFile to");
}
return PostAction::Remove;
} else {
data_device_state.read_buffer.extend_from_slice(buf);
}
buf.len()
},
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::Interrupted
| std::io::ErrorKind::WouldBlock
) =>
{
return PostAction::Continue;
},
Err(_) => {
return PostAction::Remove;
},
};
reader.consume(consumed);

PostAction::Continue
})
.ok();
},
WlDataDeviceEvent::Motion { time: _, x: _, y: _ } => {},
WlDataDeviceEvent::Drop => {
if let Some(hovered_window) = data_device_state.hovered_window {
data_device_state.with_file_paths(|paths| {
for path in paths {
state
.events_sink
.push_window_event(WindowEvent::DroppedFile(path), hovered_window);
}
});
data_device_state.finish_offer();
}
},
WlDataDeviceEvent::Leave => {
if let Some(hovered_window) = data_device_state.hovered_window {
state
.events_sink
.push_window_event(WindowEvent::HoveredFileCancelled, hovered_window);
data_device_state.finish_offer();
}
},
WlDataDeviceEvent::DataOffer { id: _ } => {},
WlDataDeviceEvent::Selection { id: _ } => {},
_ => unreachable!(),
}
}
}

#[derive(Default)]
struct DataOfferData {
pub mime_types: Arc<Mutex<Vec<String>>>,
}

impl Dispatch<WlDataOffer, DataOfferData, WinitState> for DataDeviceState {
fn event(
_state: &mut WinitState,
_wl_data_offer: &WlDataOffer,
event: <WlDataOffer as Proxy>::Event,
data: &DataOfferData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
match event {
WlDataOfferEvent::SourceActions { source_actions: _ } => {},
WlDataOfferEvent::Action { dnd_action: _ } => {},
WlDataOfferEvent::Offer { mime_type } => match data.mime_types.lock() {
Ok(ref mut mime_types) => {
mime_types.push(mime_type);
},
Err(e) => {
warn!("Failed to lock data offer mime types: {e}");
},
},
_ => unreachable!(),
}
}
}

impl Dispatch<WlDataSource, DataSourceData, WinitState> for DataDeviceState {
fn event(
_state: &mut WinitState,
_proxy: &WlDataSource,
_event: <WlDataSource as Proxy>::Event,
_data: &DataSourceData,
_conn: &Connection,
_qhandle: &QueueHandle<WinitState>,
) {
}
}

#[derive(Debug)]
pub struct DataDeviceState {
pub loop_handle: LoopHandle<'static, WinitState>,
pub hovered_window: Option<WindowId>,
pub offer: Option<WlDataOffer>,
pub read_token: Option<RegistrationToken>,
pub read_buffer: Vec<u8>,
}

impl DataDeviceState {
pub fn new(loop_handle: LoopHandle<'static, WinitState>) -> Self {
Self {
loop_handle,
hovered_window: None,
offer: None,
read_token: None,
read_buffer: Vec::new(),
}
}

fn with_file_paths(&mut self, mut callback: impl FnMut(Vec<PathBuf>)) {
let mut paths = Vec::new();
if let Ok(uri_list) = String::from_utf8(self.read_buffer.clone()) {
for line in uri_list.lines() {
if line.starts_with("#") {
continue;
}

if let Some(file) = line.strip_prefix("file://") {
paths.push(PathBuf::from(file));
} else {
warn!("Non-comment line in URI list missing prefix: '{line}'");
}
}
} else {
warn!("Failed to parse URI list from hovered file");
}
callback(paths);
}

fn finish_offer(&mut self) {
self.hovered_window = None;
if let Some(offer) = self.offer.take() {
offer.finish();
offer.destroy();
}
if let Some(token) = self.read_token.take() {
self.loop_handle.remove(token);
}
}
}

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

delegate_dispatch!(WinitState: [WlDataDeviceManager: GlobalData] => DataDeviceState);
delegate_dispatch!(WinitState: [WlDataDevice: DataDeviceData] => DataDeviceState);
delegate_dispatch!(WinitState: [WlDataOffer: DataOfferData] => DataDeviceState);
delegate_dispatch!(WinitState: [WlDataSource: DataSourceData] => DataDeviceState);
11 changes: 11 additions & 0 deletions src/platform_impl/linux/wayland/seat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ use crate::event::WindowEvent;
use crate::keyboard::ModifiersState;
use crate::platform_impl::wayland::state::WinitState;

pub mod data_device;
mod keyboard;
mod pointer;
mod text_input;
mod touch;

use data_device::DataDeviceState;
use keyboard::{KeyboardData, KeyboardState};
pub use pointer::relative_pointer::RelativePointerState;
pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt};
Expand Down Expand Up @@ -52,6 +54,8 @@ pub struct WinitSeatState {
/// The keyboard bound on the seat.
keyboard_state: Option<KeyboardState>,

data_device_state: Option<DataDeviceState>,

/// The current modifiers state on the seat.
modifiers: ModifiersState,

Expand Down Expand Up @@ -85,6 +89,13 @@ impl SeatHandler for WinitState {
},
};

if seat_state.data_device_state.is_none() {
if let Some(data_device_manager) = &self.data_device_manager {
data_device_manager.init_seat_data_device(&seat, queue_handle);
seat_state.data_device_state = Some(DataDeviceState::new(self.loop_handle.clone()));
}
}

match capability {
SeatCapability::Touch if seat_state.touch.is_none() => {
seat_state.touch = self.seat_state.get_touch(queue_handle, &seat).ok();
Expand Down
Loading

0 comments on commit 2d5072e

Please sign in to comment.