diff --git a/Cargo.toml b/Cargo.toml index bb4d72ad2a..bebc12e20c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = [ serde = { workspace = true, optional = true } smol_str = "0.2.0" tracing = { version = "0.1.40", default-features = false } +url = "2.5.4" [dev-dependencies] image = { version = "0.25.0", default-features = false, features = ["png"] } diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index a7ec5f0acc..894791ea7e 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -16,6 +16,7 @@ on how to add them: - On X11, add `Window::even_more_rare_api`. - On Wayland, add `Window::common_api`. - On Windows, add `Window::some_rare_api`. +- On Wayland, add support for `DroppedFile`, `HoveredFile` and `HoveredFileCancelled` window events. ``` When the change requires non-trivial amount of work for users to comply diff --git a/src/platform_impl/linux/wayland/seat/data_device/mod.rs b/src/platform_impl/linux/wayland/seat/data_device/mod.rs new file mode 100644 index 0000000000..4d4742c2f8 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/data_device/mod.rs @@ -0,0 +1,178 @@ +use sctk::data_device_manager::data_device::DataDeviceHandler; +use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer}; +use sctk::data_device_manager::data_source::DataSourceHandler; +use sctk::data_device_manager::WritePipe; +use wayland_client::protocol::wl_data_device::WlDataDevice; +use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_data_source::WlDataSource; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Connection, Proxy, QueueHandle}; + +use crate::event::WindowEvent; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::types::dnd::DndOfferState; +use crate::platform_impl::wayland::{self}; + +const SUPPORTED_MIME_TYPES: &[&str] = &["text/uri-list"]; + +fn filter_mime(mime_types: &[String]) -> Option { + for mime in mime_types { + if SUPPORTED_MIME_TYPES.contains(&mime.as_str()) { + return Some(mime.clone()); + } + } + + None +} + +impl DataDeviceHandler for WinitState { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_data_device: &WlDataDevice, + _: f64, + _: f64, + _: &WlSurface, + ) { + let data_device = self.get_data_device(wl_data_device); + + if let Some(data_device) = data_device { + if let Some(offer) = data_device.data().drag_offer() { + if let Some(mime_type) = offer.with_mime_types(filter_mime) { + offer.accept_mime_type(offer.serial, Some(mime_type.clone())); + offer.set_actions(DndAction::Copy, DndAction::Copy); + + if let Ok(read_pipe) = offer.receive(mime_type) { + let data_device_id = data_device.inner().id(); + let surface = offer.surface; + let window_id = wayland::make_wid(&surface); + + self.read_file_paths(read_pipe, move |state, path| { + state.dnd_offers.insert(data_device_id.clone(), DndOfferState { + surface: surface.clone(), + path: path.clone(), + }); + + state + .events_sink + .push_window_event(WindowEvent::HoveredFile(path), window_id); + }); + } + } + } + } + } + + fn leave(&mut self, _: &Connection, _: &QueueHandle, wl_data_device: &WlDataDevice) { + let data_device = self.get_data_device(wl_data_device); + + if let Some(data_device) = data_device { + if let Some(dnd_offer) = self.dnd_offers.remove(&data_device.inner().id()) { + let window_id = wayland::make_wid(&dnd_offer.surface); + self.events_sink.push_window_event(WindowEvent::HoveredFileCancelled, window_id); + } + } + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_data_device: &WlDataDevice, + _: f64, + _: f64, + ) { + let data_device = self.get_data_device(wl_data_device); + + if let Some(data_device) = data_device { + if let Some(offer) = data_device.data().drag_offer() { + let window_id = wayland::make_wid(&offer.surface); + + if let Some(dnd_offer) = self.dnd_offers.get(&data_device.inner().id()) { + self.events_sink.push_window_event( + WindowEvent::HoveredFile(dnd_offer.path.to_path_buf()), + window_id, + ); + } + } + } + } + + fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn drop_performed( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_data_device: &WlDataDevice, + ) { + let data_device = self.get_data_device(wl_data_device); + + if let Some(data_device) = data_device { + if let Some(offer) = data_device.data().drag_offer() { + let window_id = wayland::make_wid(&offer.surface); + + if let Some(dnd_offer) = self.dnd_offers.remove(&data_device.inner().id()) { + self.events_sink + .push_window_event(WindowEvent::DroppedFile(dnd_offer.path), window_id); + + offer.finish(); + offer.destroy(); + } + } + } + } +} + +impl DataSourceHandler for WinitState { + fn accept_mime( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + _: Option, + ) { + } + + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + _: String, + _: WritePipe, + ) { + } + + fn cancelled(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} + + fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} + + fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} + + fn action(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource, _: DndAction) {} +} + +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + _: &Connection, + _: &QueueHandle, + offer: &mut DragOffer, + _: DndAction, + ) { + offer.set_actions(DndAction::Copy, DndAction::Copy); + } + + fn selected_action( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &mut DragOffer, + _: DndAction, + ) { + } +} + +sctk::delegate_data_device!(WinitState); diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index eaecd93b33..9a45814293 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -1,8 +1,14 @@ //! Seat handling. +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; use std::sync::Arc; use ahash::AHashMap; +use calloop::PostAction; +use sctk::data_device_manager::data_device::DataDevice; +use sctk::data_device_manager::ReadPipe; use tracing::warn; use sctk::reexports::client::backend::ObjectId; @@ -14,11 +20,14 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3:: use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; +use url::Url; +use wayland_client::protocol::wl_data_device::WlDataDevice; use crate::event::WindowEvent; use crate::keyboard::ModifiersState; use crate::platform_impl::wayland::state::WinitState; +mod data_device; mod keyboard; mod pointer; mod text_input; @@ -57,6 +66,9 @@ pub struct WinitSeatState { /// Whether we have pending modifiers. modifiers_pending: bool, + + /// The current data device. + data_device: Option, } impl WinitSeatState { @@ -141,6 +153,11 @@ impl SeatHandler for WinitState { TextInputData::default(), ))); } + + if seat_state.data_device.is_none() { + let data_device = self.data_device_manager_state.get_data_device(queue_handle, &seat); + seat_state.data_device = Some(data_device); + } } fn remove_capability( @@ -230,6 +247,40 @@ impl WinitState { } } } + + fn get_data_device(&self, wl_data_device: &WlDataDevice) -> Option<&DataDevice> { + self.seats.values().find_map(|seat| { + let data_device = seat.data_device.as_ref()?; + (data_device.inner() == wl_data_device).then_some(data_device) + }) + } + + fn read_file_paths( + &self, + read_pipe: ReadPipe, + callback: F, + ) { + self.loop_handle + .insert_source(read_pipe, move |_, file, state| { + let mut data = String::new(); + + let file: &mut File = unsafe { file.get_mut() }; + if file.read_to_string(&mut data).is_ok() { + if let Some(line) = data.lines().next() { + if let Ok(url) = Url::parse(line) { + if url.scheme() == "file" { + if let Ok(path) = url.to_file_path() { + callback(state, path) + } + } + } + } + } + + PostAction::Remove + }) + .ok(); + } } sctk::delegate_seat!(WinitState); diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index 13ef99c26a..c292fe6e23 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex}; use ahash::AHashMap; +use sctk::data_device_manager::DataDeviceManagerState; use sctk::reexports::calloop::LoopHandle; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::globals::GlobalList; @@ -29,6 +30,7 @@ use crate::platform_impl::wayland::seat::{ PointerConstraintsState, RelativePointerState, TextInputState, WinitPointerData, WinitPointerDataExt, WinitSeatState, }; +use crate::platform_impl::wayland::types::dnd::DndOfferState; use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; use crate::platform_impl::wayland::types::wp_fractional_scaling::FractionalScalingManager; use crate::platform_impl::wayland::types::wp_viewporter::ViewporterState; @@ -54,6 +56,12 @@ pub struct WinitState { /// The seat state responsible for all sorts of input. pub seat_state: SeatState, + // The state of the data device manager. + pub data_device_manager_state: DataDeviceManagerState, + + // The active drag and drop offers. + pub dnd_offers: AHashMap, + /// The shm for software buffers, such as cursors. pub shm: Shm, @@ -141,6 +149,9 @@ impl WinitState { let output_state = OutputState::new(globals, queue_handle); let monitors = output_state.outputs().map(MonitorHandle::new).collect(); + let data_device_manager_state = + DataDeviceManagerState::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let seat_state = SeatState::new(globals, queue_handle); let mut seats = AHashMap::default(); @@ -164,6 +175,10 @@ impl WinitState { subcompositor_state: subcompositor_state.map(Arc::new), output_state, seat_state, + + data_device_manager_state, + dnd_offers: AHashMap::default(), + shm, custom_cursor_pool, diff --git a/src/platform_impl/linux/wayland/types/dnd.rs b/src/platform_impl/linux/wayland/types/dnd.rs new file mode 100644 index 0000000000..eb4b3ea78b --- /dev/null +++ b/src/platform_impl/linux/wayland/types/dnd.rs @@ -0,0 +1,8 @@ +use std::path::PathBuf; + +use wayland_client::protocol::wl_surface::WlSurface; + +pub struct DndOfferState { + pub surface: WlSurface, + pub path: PathBuf, +} diff --git a/src/platform_impl/linux/wayland/types/mod.rs b/src/platform_impl/linux/wayland/types/mod.rs index 77e67f48be..c9f599c0e0 100644 --- a/src/platform_impl/linux/wayland/types/mod.rs +++ b/src/platform_impl/linux/wayland/types/mod.rs @@ -1,6 +1,7 @@ //! Wayland protocol implementation boilerplate. pub mod cursor; +pub mod dnd; pub mod kwin_blur; pub mod wp_fractional_scaling; pub mod wp_viewporter;