wip: dnd offer handling

This commit is contained in:
Ashley Wulber 2024-03-22 18:41:31 -04:00
parent 10e534c1be
commit 1063256706
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
7 changed files with 591 additions and 92 deletions

View file

@ -12,12 +12,12 @@ rust-version = "1.65.0"
[dependencies]
libc = "0.2.149"
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] }
wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] }
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", default-features = false, features = ["calloop"] }
wayland-backend = { version = "0.3.3", default_features = false, features = ["client_system"] }
[dev-dependencies]
dirs = "5.0.1"
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", default-features = false, features = ["calloop", "xkbcommon"] }
thiserror = "1.0.57"
url = "2.5.0"

View file

@ -11,6 +11,7 @@ use sctk::output::{OutputHandler, OutputState};
use sctk::reexports::calloop::{EventLoop, LoopHandle};
use sctk::reexports::calloop_wayland_source::WaylandSource;
use sctk::reexports::client::globals::registry_queue_init;
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface};
use sctk::reexports::client::{Connection, Proxy, QueueHandle};
@ -26,7 +27,8 @@ use sctk::{
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
};
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
use smithay_clipboard::dnd::{DndDestinationRectangle, Rectangle};
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType, ALLOWED_TEXT_MIME_TYPES};
use smithay_clipboard::{Clipboard, SimpleClipboard};
use thiserror::Error;
use url::Url;
@ -66,12 +68,22 @@ fn main() {
let pool = SlotPool::new(MIN_DIM_SIZE * MIN_DIM_SIZE * 4, &shm).expect("Failed to create pool");
let (tx, rx) = sctk::reexports::calloop::channel::sync_channel(10);
clipboard.init_dnd(Box::new(tx));
clipboard.init_dnd(Box::new(tx)).expect("Failed to set up DnD");
event_loop.handle().insert_source(rx, |event, _, state| {
_ = event_loop.handle().insert_source(rx, |event, _, _state| {
dbg!(event);
});
clipboard.register_dnd_destination(window.wl_surface().clone(), vec![
DndDestinationRectangle {
id: 0,
rectangle: Rectangle { x: 0., y: 0., width: 256., height: 256. },
mime_types: ALLOWED_TEXT_MIME_TYPES.iter().map(|m| Cow::from(m.to_string())).collect(),
actions: DndAction::all(),
preferred: DndAction::Copy,
},
]);
let mut simple_window = SimpleWindow {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &queue_handle),
@ -367,6 +379,7 @@ impl KeyboardHandler for SimpleWindow {
_: &wl_keyboard::WlKeyboard,
_serial: u32,
_modifiers: Modifiers,
_: u32,
) {
}
}

View file

@ -1,16 +1,22 @@
use std::borrow::Cow;
use std::ffi::c_void;
use std::fmt::Debug;
use std::sync::mpsc::SendError;
use sctk::reexports::calloop;
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::{Connection, Proxy};
use wayland_backend::client::{InvalidId, ObjectId};
use crate::mime::AsMimeTypes;
use crate::Clipboard;
pub mod state;
#[derive(Clone)]
pub struct DndSurface<T> {
pub surface: WlSurface,
pub(crate) surface: WlSurface,
pub s: T,
}
@ -46,9 +52,100 @@ pub trait Sender<T> {
fn send(&self, t: DndEvent<T>) -> Result<(), SendError<DndEvent<T>>>;
}
#[derive(Debug)]
pub enum SourceEvent {
/// DnD operation ended.
Finished,
/// DnD Cancelled.
Cancelled,
/// DnD action chosen by the compositor.
Action(DndAction),
/// Mime accepted by destination.
/// If [`None`], no mime types are accepted.
Mime(Option<String>),
/// DnD Dropped. The operation is still ongoing until receiving a
/// [`Finished`] event.
Dropped,
}
#[derive(Debug)]
pub enum OfferEvent<T> {
Enter {
x: f64,
y: f64,
mime_types: Vec<String>,
surface: T,
},
Motion {
x: f64,
y: f64,
},
/// The offer is no longer on a DnD destination.
LeaveDestination,
/// The offer has left the surface.
Leave,
/// An offer was dropped
Drop,
/// If the selected action is ASK, the user must be presented with a choice.
/// [`Clipboard::set_actions`] should then be called before data can be
/// requested and th DnD operation can be finished.
SelectedAction(DndAction),
Data {
data: Vec<u8>,
mime_type: String,
},
}
/// A rectangle with a logical location and size relative to a [`Surface`]
#[derive(Debug, Default, Clone)]
pub struct Rectangle {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl Rectangle {
fn contains(&self, x: f64, y: f64) -> bool {
self.x <= x && self.x + self.width >= x && self.y <= y && self.y + self.height >= y
}
}
#[derive(Debug, Clone)]
pub struct DndDestinationRectangle {
/// A unique ID
pub id: u128,
/// The rectangle representing this destination.
pub rectangle: Rectangle,
/// Accepted mime types in this rectangle
pub mime_types: Vec<Cow<'static, str>>,
/// Accepted actions in this rectangle
pub actions: DndAction,
/// Prefered action in this rectangle
pub preferred: DndAction,
}
pub enum DndRequest<T> {
/// Init DnD
InitDnD(Box<dyn crate::dnd::Sender<T> + Send>),
/// Register a surface for receiving Dnd events.
Surface(DndSurface<T>, Vec<DndDestinationRectangle>),
/// Start a Dnd operation with the given source surface and data.
StartDnd {
source: DndSurface<T>,
icon: Option<DndSurface<T>>,
content: Box<dyn AsMimeTypes + Send>,
},
/// Set the DnD action chosen by the user.
SetAction(DndAction),
}
#[derive(Debug)]
pub enum DndEvent<T> {
Test(T),
/// Dnd Offer event with the corresponding destination rectangle ID.
Offer(Option<u128>, OfferEvent<T>),
/// Dnd Source event.
Source(SourceEvent),
}
impl<T> Sender<T> for calloop::channel::Sender<DndEvent<T>> {
@ -63,21 +160,44 @@ impl<T> Sender<T> for calloop::channel::SyncSender<DndEvent<T>> {
}
}
impl<T> Clipboard<T> {
impl<T: RawSurface> Clipboard<T> {
/// Set up DnD operations for the Clipboard
pub fn init_dnd(
&self,
tx: Box<dyn Sender<T> + Send>,
) -> Result<(), SendError<crate::worker::Command<T>>> {
self.request_sender.send(crate::worker::Command::InitDnD(tx))
self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::InitDnD(tx)))
}
/// Start a DnD operation on the given surface with some data
pub fn start_dnd<D: RawSurface>(&self, s: D) {
let s = DndSurface::new(s, &self.connection).unwrap();
dbg!(&s.surface);
pub fn start_dnd<D: AsMimeTypes + Send + 'static>(
&self,
source_surface: T,
icon_surface: Option<T>,
content: D,
) {
let source = DndSurface::new(source_surface, &self.connection).unwrap();
let icon = icon_surface.map(|s| DndSurface::new(s, &self.connection).unwrap());
_ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::StartDnd {
source,
icon,
content: Box::new(content),
}));
}
/// End the current DnD operation, if there is one
pub fn end_dnd() {}
/// Register a surface for receiving DnD offers
/// Rectangles should be provided in order of decreasing priority.
pub fn register_dnd_destination(&self, surface: T, rectangles: Vec<DndDestinationRectangle>) {
let s = DndSurface::new(surface, &self.connection).unwrap();
_ = self
.request_sender
.send(crate::worker::Command::DndRequest(DndRequest::Surface(s, rectangles)));
}
/// Set the final action after presenting the user with a choice
pub fn set_action(&self, action: DndAction) {}
}

315
src/dnd/state.rs Normal file
View file

@ -0,0 +1,315 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{Error, ErrorKind, Read};
use std::mem;
use std::os::unix::io::{AsRawFd, RawFd};
use std::rc::Rc;
use sctk::data_device_manager::data_offer::DragOffer;
use sctk::data_device_manager::data_source::DragSource;
use sctk::reexports::calloop::PostAction;
use sctk::reexports::client::protocol::wl_data_device::WlDataDevice;
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::Proxy;
use wayland_backend::client::ObjectId;
use crate::mime::{AsMimeTypes, MimeType};
use crate::state::{set_non_blocking, State, Target};
use crate::text::Text;
use super::{DndDestinationRectangle, DndEvent, DndRequest, DndSurface, OfferEvent};
pub(crate) struct DndState<T> {
pub(crate) sender: Option<Box<dyn crate::dnd::Sender<T>>>,
destinations: HashMap<ObjectId, (DndSurface<T>, Vec<DndDestinationRectangle>)>,
dnd_sources: Option<DragSource>,
active_surface: Option<(DndSurface<T>, Option<DndDestinationRectangle>)>,
source_actions: DndAction,
selected_action: DndAction,
selected_mime: Option<String>,
pub(crate) source_content: Box<dyn AsMimeTypes>,
pub(crate) source_mime_types: Rc<Cow<'static, [MimeType]>>,
}
impl<T> Default for DndState<T> {
fn default() -> Self {
Self {
sender: Default::default(),
destinations: Default::default(),
dnd_sources: Default::default(),
active_surface: None,
source_actions: DndAction::empty(),
selected_action: DndAction::empty(),
selected_mime: None,
source_content: Box::new(Text(String::new())),
source_mime_types: Rc::new(Cow::Owned(Vec::new())),
}
}
}
impl<T> DndState<T> {
pub(crate) fn selected_action(&mut self, a: DndAction) {
self.selected_action = a;
}
}
impl<T> State<T>
where
T: Clone + 'static,
{
pub fn update_active_surface(
&mut self,
surface: &WlSurface,
x: f64,
y: f64,
dnd_state: Option<&DragOffer>,
) {
let had_dest = self
.dnd_state
.active_surface
.as_ref()
.map(|(_, d)| d.as_ref().map(|d| d.id))
.unwrap_or_default();
self.dnd_state.active_surface =
self.dnd_state.destinations.get(&surface.id()).map(|(s, dests)| {
let Some((dest, mime, actions)) = dests.iter().find_map(|r| {
let actions = dnd_state.as_ref().map(|s| {
(
s.source_actions.intersection(r.actions),
s.source_actions.intersection(r.preferred),
)
});
let mime = dnd_state.as_ref().and_then(|dnd_state| {
r.mime_types.iter().find(|m| {
dnd_state.with_mime_types(|mimes| mimes.iter().any(|a| &a == m))
})
});
dbg!(actions);
dbg!(mime);
(r.rectangle.contains(x, y)
&& (r.mime_types.is_empty() || mime.is_some())
&& (r.actions.is_all()
|| dnd_state
.as_ref()
.map(|dnd_state| dnd_state.source_actions.intersects(r.actions))
.unwrap_or(true)))
.then(|| (r.clone(), mime, actions))
}) else {
if let Some(old_id) = had_dest {
if let Some(dnd_state) = dnd_state.as_ref() {
if let Some(tx) = self.dnd_state.sender.as_ref() {
_ = tx.send(DndEvent::Offer(
Some(old_id),
super::OfferEvent::LeaveDestination,
));
}
dnd_state.set_actions(DndAction::empty(), DndAction::empty());
dnd_state.accept_mime_type(dnd_state.serial, None);
self.dnd_state.selected_action = DndAction::empty();
self.dnd_state.selected_mime = None;
}
}
return (s.clone(), None);
};
if let (Some((action, preferred_action)), Some(mime_type), Some(dnd_state)) =
(actions, mime, dnd_state.as_ref())
{
dnd_state.set_actions(action, preferred_action);
self.dnd_state.selected_mime = Some(mime_type.to_string());
dnd_state.accept_mime_type(dnd_state.serial, Some(mime_type.to_string()))
}
(s.clone(), Some(dest))
});
}
fn cur_id(&self) -> Option<u128> {
self.dnd_state.active_surface.as_ref().and_then(|(_, rect)| rect.as_ref().map(|r| r.id))
}
pub(crate) fn offer_drop(&mut self, wl_data_device: &WlDataDevice) {
let Some(tx) = self.dnd_state.sender.as_ref() else {
return;
};
let id = self.cur_id();
_ = tx.send(DndEvent::Offer(id, super::OfferEvent::Drop));
let Some(data_device) = self
.seats
.iter()
.find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device))
else {
return;
};
let Some(dnd_state) = data_device.data().drag_offer() else {
return;
};
if self.dnd_state.selected_action == DndAction::Ask {
_ = tx.send(DndEvent::Offer(
id,
super::OfferEvent::SelectedAction(self.dnd_state.selected_action),
));
return;
} else if self.dnd_state.selected_action.is_empty() {
return;
}
let Some(mime) = self.dnd_state.selected_mime.take() else {
dnd_state.accept_mime_type(dnd_state.serial, None);
return;
};
dnd_state.set_actions(self.dnd_state.selected_action, self.dnd_state.selected_action);
dnd_state.accept_mime_type(dnd_state.serial, Some(mime.clone()));
_ = self.load_dnd(MimeType::Other(mime.into()));
}
pub(crate) fn offer_enter(
&mut self,
x: f64,
y: f64,
surface: &WlSurface,
wl_data_device: &WlDataDevice,
) {
if self.dnd_state.sender.is_none() {
return;
}
dbg!(&self.dnd_state.destinations);
let Some(data_device) = self
.seats
.iter()
.find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device))
else {
return;
};
let dnd_state = data_device.data().drag_offer();
self.update_active_surface(surface, x, y, dnd_state.as_ref());
let Some((surface, id)) = self
.dnd_state
.active_surface
.as_ref()
.map(|(s, d)| (s.clone(), d.as_ref().map(|d| d.id)))
else {
return;
};
let Some(tx) = self.dnd_state.sender.as_ref() else {
return;
};
// TODO accept mime / action
_ = tx.send(DndEvent::Offer(id, super::OfferEvent::Enter {
x,
y,
surface: surface.s,
mime_types: Vec::new(),
}));
}
pub(crate) fn offer_motion(&mut self, x: f64, y: f64, wl_data_device: &WlDataDevice) {
let Some((surface, dest)) = self
.dnd_state
.active_surface
.clone()
.map(|(s, dest)| (s, dest.filter(|d| d.rectangle.contains(x, y))))
else {
return;
};
let Some(data_device) = self
.seats
.iter()
.find_map(|(_, s)| s.data_device.as_ref().filter(|dev| dev.inner() == wl_data_device))
else {
return;
};
let dnd_state = data_device.data().drag_offer();
if dest.is_none() {
self.update_active_surface(&surface.surface, x, y, dnd_state.as_ref());
}
let id = self.cur_id();
if let Some(tx) = self.dnd_state.sender.as_ref() {
_ = tx.send(DndEvent::Offer(id, super::OfferEvent::Motion { x, y }));
}
}
pub(crate) fn offer_leave(&mut self) {
if let Some(tx) = self.dnd_state.sender.as_ref() {
self.dnd_state.active_surface = None;
self.dnd_state.selected_action = DndAction::empty();
self.dnd_state.selected_mime = None;
_ = tx.send(DndEvent::Offer(None, super::OfferEvent::Leave))
}
}
pub(crate) fn handle_dnd_request(&mut self, r: DndRequest<T>) {
match r {
DndRequest::InitDnD(sender) => self.dnd_state.sender = Some(sender),
DndRequest::Surface(s, dests) => {
self.dnd_state.destinations.insert(s.surface.id(), (s, dests));
},
DndRequest::StartDnd { source, icon, content } => {},
DndRequest::SetAction(_) => {
todo!()
},
};
}
/// Load data for the given target.
pub fn load_dnd(&mut self, mut mime_type: MimeType) -> std::io::Result<()> {
let cur_id = self.cur_id();
let latest = self
.latest_seat
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?;
let seat = self
.seats
.get_mut(latest)
.ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?;
if !seat.has_focus {
return Err(Error::new(ErrorKind::Other, "client doesn't have focus"));
}
let offer = seat
.data_device
.as_ref()
.and_then(|d| d.data().drag_offer())
.ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?;
let read_pipe = { offer.receive(mime_type.to_string())? };
// Mark FD as non-blocking so we won't block ourselves.
unsafe {
set_non_blocking(read_pipe.as_raw_fd())?;
}
let mut reader_buffer = [0; 4096];
let mut content = Vec::new();
let _ = self.loop_handle.insert_source(read_pipe, move |_, file, state| {
let file = unsafe { file.get_mut() };
let Some(tx) = state.dnd_state.sender.as_ref() else {
return PostAction::Remove;
};
loop {
match file.read(&mut reader_buffer) {
Ok(0) => {
offer.finish();
let _ = tx.send(DndEvent::Offer(cur_id, OfferEvent::Data {
data: mem::take(&mut content),
mime_type: mem::take(&mut mime_type).to_string(),
}));
break PostAction::Remove;
},
Ok(n) => content.extend_from_slice(&reader_buffer[..n]),
Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue,
Err(_) => {
// let _ = state.dnd_state.sender.unwrap().send(Err(err));
break PostAction::Remove;
},
};
}
});
Ok(())
}
}

View file

@ -11,6 +11,7 @@ use std::sync::mpsc::{self, Receiver};
use sctk::reexports::calloop::channel::{self, Sender};
use sctk::reexports::client::backend::Backend;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::Connection;
#[cfg(feature = "dnd")]
@ -24,7 +25,7 @@ use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
use state::SelectionTarget;
use text::Text;
pub type SimpleClipboard = Clipboard<()>;
pub type SimpleClipboard = Clipboard<WlSurface>;
/// Access to a Wayland clipboard.
pub struct Clipboard<T> {
@ -34,7 +35,7 @@ pub struct Clipboard<T> {
connection: Connection,
}
impl<T: 'static + Send> Clipboard<T> {
impl<T: 'static + Send + Clone> Clipboard<T> {
/// Creates new clipboard which will be running on its own thread with its
/// own event queue to handle clipboard requests.
///
@ -62,7 +63,7 @@ impl<T: 'static + Send> Clipboard<T> {
///
/// Load the requested type from a clipboard on the last observed seat.
pub fn load<D: AllowedMimeTypes + 'static>(&self) -> Result<D> {
self.load_inner(SelectionTarget::Clipboard, D::allowed())
self.load_inner(Target::Clipboard, D::allowed())
}
/// Load clipboard data.
@ -77,7 +78,7 @@ impl<T: 'static + Send> Clipboard<T> {
/// Load the requested type from a primary clipboard on the last observed
/// seat.
pub fn load_primary<D: AllowedMimeTypes + 'static>(&self) -> Result<D> {
self.load_inner(SelectionTarget::Primary, D::allowed())
self.load_inner(Target::Primary, D::allowed())
}
/// Load primary clipboard data.
@ -94,7 +95,7 @@ impl<T: 'static + Send> Clipboard<T> {
&self,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<(Vec<u8>, MimeType)> {
self.load_inner(SelectionTarget::Clipboard, allowed)
self.load_inner(Target::Clipboard, allowed)
}
/// Load raw primary clipboard data.
@ -104,14 +105,14 @@ impl<T: 'static + Send> Clipboard<T> {
&self,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<(Vec<u8>, MimeType)> {
self.load_inner(SelectionTarget::Primary, allowed)
self.load_inner(Target::Primary, allowed)
}
/// Store custom data to a clipboard.
///
/// Stores data of the provided type to a clipboard on a last observed seat.
pub fn store<D: AsMimeTypes + Send + 'static>(&self, data: D) {
self.store_inner(data, SelectionTarget::Clipboard);
self.store_inner(data, Target::Clipboard);
}
/// Store to a clipboard.
@ -126,7 +127,7 @@ impl<T: 'static + Send> Clipboard<T> {
/// Stores data of the provided type to a primary clipboard on a last
/// observed seat.
pub fn store_primary<D: AsMimeTypes + Send + 'static>(&self, data: D) {
self.store_inner(data, SelectionTarget::Primary);
self.store_inner(data, Target::Primary);
}
/// Store to a primary clipboard.
@ -138,7 +139,7 @@ impl<T: 'static + Send> Clipboard<T> {
fn load_inner<D: TryFrom<(Vec<u8>, MimeType)> + 'static>(
&self,
target: SelectionTarget,
target: Target,
allowed: impl Into<Cow<'static, [MimeType]>>,
) -> Result<D> {
let _ = self.request_sender.send(worker::Command::Load(allowed.into(), target));
@ -158,7 +159,7 @@ impl<T: 'static + Send> Clipboard<T> {
}
}
fn store_inner<D: AsMimeTypes + Send + 'static>(&self, data: D, target: SelectionTarget) {
fn store_inner<D: AsMimeTypes + Send + 'static>(&self, data: D, target: Target) {
let request = worker::Command::Store(Box::new(data), target);
let _ = self.request_sender.send(request);
}

View file

@ -9,11 +9,12 @@ use std::sync::mpsc::Sender;
use sctk::data_device_manager::data_device::{DataDevice, DataDeviceHandler};
use sctk::data_device_manager::data_offer::{DataOfferError, DataOfferHandler, DragOffer};
use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler};
use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler, DragSource};
use sctk::data_device_manager::{DataDeviceManagerState, WritePipe};
use sctk::primary_selection::device::{PrimarySelectionDevice, PrimarySelectionDeviceHandler};
use sctk::primary_selection::selection::{PrimarySelectionSource, PrimarySelectionSourceHandler};
use sctk::primary_selection::PrimarySelectionManagerState;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::registry::{ProvidesRegistryState, RegistryState};
use sctk::seat::pointer::{PointerData, PointerEvent, PointerEventKind, PointerHandler};
use sctk::seat::{Capability, SeatHandler, SeatState};
@ -37,6 +38,8 @@ use sctk::reexports::protocols::wp::primary_selection::zv1::client::{
};
use wayland_backend::client::ObjectId;
use crate::dnd::state::DndState;
use crate::dnd::{DndDestinationRectangle, DndSurface};
use crate::mime::{AsMimeTypes, MimeType};
use crate::text::Text;
@ -49,11 +52,11 @@ pub struct State<T> {
registry_state: RegistryState,
seat_state: SeatState,
seats: HashMap<ObjectId, ClipboardSeatState>,
pub(crate) seats: HashMap<ObjectId, ClipboardSeatState>,
/// The latest seat which got an event.
latest_seat: Option<ObjectId>,
pub(crate) latest_seat: Option<ObjectId>,
loop_handle: LoopHandle<'static, Self>,
pub(crate) loop_handle: LoopHandle<'static, Self>,
queue_handle: QueueHandle<Self>,
primary_sources: Vec<PrimarySelectionSource>,
@ -64,11 +67,11 @@ pub struct State<T> {
data_selection_content: Box<dyn AsMimeTypes>,
data_selection_mime_types: Rc<Cow<'static, [MimeType]>>,
#[cfg(feature = "dnd")]
pub(crate) sender: Option<Box<dyn crate::dnd::Sender<T>>>,
pub(crate) dnd_state: crate::dnd::state::DndState<T>,
_phantom: PhantomData<T>,
}
impl<T: 'static> State<T> {
impl<T: 'static + Clone> State<T> {
#[must_use]
pub fn new(
globals: &GlobalList,
@ -110,7 +113,7 @@ impl<T: 'static> State<T> {
primary_selection_mime_types: Rc::new(Default::default()),
data_selection_mime_types: Rc::new(Default::default()),
#[cfg(feature = "dnd")]
sender: None,
dnd_state: DndState::default(),
_phantom: PhantomData,
})
}
@ -118,11 +121,7 @@ impl<T: 'static> State<T> {
/// Store selection for the given target.
///
/// Selection source is only created when `Some(())` is returned.
pub fn store_selection(
&mut self,
ty: SelectionTarget,
contents: Box<dyn AsMimeTypes>,
) -> Option<()> {
pub fn store_selection(&mut self, ty: Target, contents: Box<dyn AsMimeTypes>) -> Option<()> {
let latest = self.latest_seat.as_ref()?;
let seat = self.seats.get_mut(latest)?;
@ -131,7 +130,7 @@ impl<T: 'static> State<T> {
}
match ty {
SelectionTarget::Clipboard => {
Target::Clipboard => {
let mgr = self.data_device_manager_state.as_ref()?;
let mime_types = contents.available();
self.data_selection_content = contents;
@ -140,7 +139,7 @@ impl<T: 'static> State<T> {
source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial);
self.data_sources.push(source);
},
SelectionTarget::Primary => {
Target::Primary => {
let mgr = self.primary_selection_manager_state.as_ref()?;
let mime_types = contents.available();
self.primary_selection_content = contents;
@ -149,17 +148,17 @@ impl<T: 'static> State<T> {
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
self.primary_sources.push(source);
},
#[cfg(feature = "dnd")]
Target::DnD => {
unreachable!()
},
}
Some(())
}
/// Load selection for the given target.
pub fn load_selection(
&mut self,
ty: SelectionTarget,
allowed_mime_types: &[MimeType],
) -> Result<()> {
/// Load data for the given target.
pub fn load(&mut self, ty: Target, allowed_mime_types: &[MimeType]) -> Result<()> {
let latest = self
.latest_seat
.as_ref()
@ -174,7 +173,7 @@ impl<T: 'static> State<T> {
}
let (read_pipe, mut mime_type) = match ty {
SelectionTarget::Clipboard => {
Target::Clipboard => {
let selection = seat
.data_device
.as_ref()
@ -197,7 +196,7 @@ impl<T: 'static> State<T> {
mime_type,
)
},
SelectionTarget::Primary => {
Target::Primary => {
let selection = seat
.primary_device
.as_ref()
@ -212,6 +211,21 @@ impl<T: 'static> State<T> {
(selection.receive(mime_type.to_string())?, mime_type)
},
#[cfg(feature = "dnd")]
Target::DnD => {
let offer = seat
.data_device
.as_ref()
.and_then(|d| d.data().drag_offer())
.ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?;
let Some(mime) = allowed_mime_types.get(0) else {
return Err(Error::new(
ErrorKind::NotFound,
"supported mime-type is not found",
));
};
(offer.receive(mime.to_string())?, mime.clone())
},
};
// Mark FD as non-blocking so we won't block ourselves.
@ -244,10 +258,12 @@ impl<T: 'static> State<T> {
Ok(())
}
fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) {
fn send_request(&mut self, ty: Target, write_pipe: WritePipe, mime: String) {
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
SelectionTarget::Clipboard => &self.data_selection_mime_types,
SelectionTarget::Primary => &self.primary_selection_mime_types,
Target::Clipboard => &self.data_selection_mime_types,
Target::Primary => &self.primary_selection_mime_types,
#[cfg(feature = "dnd")]
Target::DnD => &self.dnd_state.source_mime_types,
}) else {
return;
};
@ -262,8 +278,10 @@ impl<T: 'static> State<T> {
// Don't access the content on the state directly, since it could change during
// the send.
let contents = match ty {
SelectionTarget::Clipboard => self.data_selection_content.as_bytes(&mime_type),
SelectionTarget::Primary => self.primary_selection_content.as_bytes(&mime_type),
Target::Clipboard => self.data_selection_content.as_bytes(&mime_type),
Target::Primary => self.primary_selection_content.as_bytes(&mime_type),
#[cfg(feature = "dnd")]
Target::DnD => self.dnd_state.source_content.as_bytes(&mime_type),
};
let Some(contents) = contents else {
@ -288,7 +306,7 @@ impl<T: 'static> State<T> {
}
}
impl<T: 'static> SeatHandler for State<T> {
impl<T: 'static + Clone> SeatHandler for State<T> {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
@ -371,7 +389,7 @@ impl<T: 'static> SeatHandler for State<T> {
}
}
impl<T: 'static> PointerHandler for State<T> {
impl<T: 'static + Clone> PointerHandler for State<T> {
fn pointer_frame(
&mut self,
_: &Connection,
@ -405,20 +423,50 @@ impl<T: 'static> PointerHandler for State<T> {
}
}
impl<T: 'static> DataDeviceHandler for State<T> {
fn enter(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
impl<T: 'static + Clone> DataDeviceHandler for State<T>
where
DndSurface<T>: Clone,
{
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
wl_data_device: &WlDataDevice,
x: f64,
y: f64,
surface: &WlSurface,
) {
#[cfg(feature = "dnd")]
self.offer_enter(x, y, surface, wl_data_device);
}
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {
#[cfg(feature = "dnd")]
self.offer_leave();
}
fn motion(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
fn motion(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
wl_data_device: &WlDataDevice,
x: f64,
y: f64,
) {
#[cfg(feature = "dnd")]
self.offer_motion(x, y, wl_data_device);
}
fn drop_performed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
fn drop_performed(&mut self, _: &Connection, _: &QueueHandle<Self>, d: &WlDataDevice) {
#[cfg(feature = "dnd")]
self.offer_drop(d)
}
// The selection is finished and ready to be used.
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
}
impl<T: 'static> DataSourceHandler for State<T> {
impl<T: 'static + Clone> DataSourceHandler for State<T> {
fn send_request(
&mut self,
_: &Connection,
@ -427,7 +475,7 @@ impl<T: 'static> DataSourceHandler for State<T> {
mime: String,
write_pipe: WritePipe,
) {
self.send_request(SelectionTarget::Clipboard, write_pipe, mime)
self.send_request(Target::Clipboard, write_pipe, mime)
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, deleted: &WlDataSource) {
@ -450,7 +498,7 @@ impl<T: 'static> DataSourceHandler for State<T> {
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
}
impl<T: 'static> DataOfferHandler for State<T> {
impl<T: 'static + Clone> DataOfferHandler for State<T> {
fn source_actions(
&mut self,
_: &Connection,
@ -465,12 +513,14 @@ impl<T: 'static> DataOfferHandler for State<T> {
_: &Connection,
_: &QueueHandle<Self>,
_: &mut DragOffer,
_: DndAction,
action: DndAction,
) {
#[cfg(feature = "dnd")]
self.dnd_state.selected_action(action);
}
}
impl<T: 'static> ProvidesRegistryState for State<T> {
impl<T: 'static + Clone> ProvidesRegistryState for State<T> {
registry_handlers![SeatState];
fn registry(&mut self) -> &mut RegistryState {
@ -478,7 +528,7 @@ impl<T: 'static> ProvidesRegistryState for State<T> {
}
}
impl<T: 'static> PrimarySelectionDeviceHandler for State<T> {
impl<T: 'static + Clone> PrimarySelectionDeviceHandler for State<T> {
fn selection(
&mut self,
_: &Connection,
@ -488,7 +538,7 @@ impl<T: 'static> PrimarySelectionDeviceHandler for State<T> {
}
}
impl<T: 'static> PrimarySelectionSourceHandler for State<T> {
impl<T: 'static + Clone> PrimarySelectionSourceHandler for State<T> {
fn send_request(
&mut self,
_: &Connection,
@ -497,7 +547,7 @@ impl<T: 'static> PrimarySelectionSourceHandler for State<T> {
mime: String,
write_pipe: WritePipe,
) {
self.send_request(SelectionTarget::Primary, write_pipe, mime);
self.send_request(Target::Primary, write_pipe, mime);
}
fn cancelled(
@ -510,7 +560,7 @@ impl<T: 'static> PrimarySelectionSourceHandler for State<T> {
}
}
impl<T: 'static> Dispatch<WlKeyboard, ObjectId, State<T>> for State<T> {
impl<T: 'static + Clone> Dispatch<WlKeyboard, ObjectId, State<T>> for State<T> {
fn event(
state: &mut State<T>,
_: &WlKeyboard,
@ -543,27 +593,30 @@ impl<T: 'static> Dispatch<WlKeyboard, ObjectId, State<T>> for State<T> {
}
}
delegate_seat!(@<T: 'static> State<T>);
delegate_pointer!(@<T: 'static> State<T>);
delegate_data_device!(@<T: 'static> State<T>);
delegate_primary_selection!(@<T: 'static> State<T>);
delegate_registry!(@<T: 'static> State<T>);
delegate_seat!(@<T: 'static + Clone> State<T>);
delegate_pointer!(@<T: 'static + Clone> State<T>);
delegate_data_device!(@<T: 'static + Clone> State<T>);
delegate_primary_selection!(@<T: 'static + Clone> State<T>);
delegate_registry!(@<T: 'static + Clone> State<T>);
#[derive(Debug, Clone, Copy)]
pub enum SelectionTarget {
pub enum Target {
/// The target is clipboard selection.
Clipboard,
/// The target is primary selection.
Primary,
#[cfg(feature = "dnd")]
/// The targe is a DnD offer.
DnD,
}
#[derive(Debug, Default)]
struct ClipboardSeatState {
pub(crate) struct ClipboardSeatState {
keyboard: Option<WlKeyboard>,
pointer: Option<WlPointer>,
data_device: Option<DataDevice>,
pub(crate) data_device: Option<DataDevice>,
primary_device: Option<PrimarySelectionDevice>,
has_focus: bool,
pub(crate) has_focus: bool,
/// The latest serial used to set the selection content.
latest_serial: u32,
@ -585,7 +638,7 @@ impl Drop for ClipboardSeatState {
}
}
unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> {
pub(crate) unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> {
let flags = libc::fcntl(raw_fd, libc::F_GETFL);
if flags < 0 {

View file

@ -9,12 +9,13 @@ use sctk::reexports::calloop_wayland_source::WaylandSource;
use sctk::reexports::client::globals::registry_queue_init;
use sctk::reexports::client::Connection;
use crate::dnd::DndRequest;
use crate::mime::{AsMimeTypes, MimeType};
use crate::state::{SelectionTarget, State};
use crate::state::{State, Target};
/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles
/// clipboard requests.
pub fn spawn<T: 'static + Send>(
pub fn spawn<T: 'static + Send + Clone>(
name: String,
display: Connection,
rx_chan: Channel<Command<T>>,
@ -31,12 +32,12 @@ pub fn spawn<T: 'static + Send>(
/// Clipboard worker thread command.
pub enum Command<T> {
/// Loads data for the first available mime type in the provided list.
Load(Cow<'static, [MimeType]>, SelectionTarget),
Load(Cow<'static, [MimeType]>, Target),
/// Store Data with the given mime types.
Store(Box<dyn AsMimeTypes + Send>, SelectionTarget),
Store(Box<dyn AsMimeTypes + Send>, Target),
#[cfg(feature = "dnd")]
/// Init DnD
InitDnD(Box<dyn crate::dnd::Sender<T> + Send>),
DndRequest(DndRequest<T>),
/// Shutdown the worker.
Exit,
/// Phantom data
@ -44,7 +45,7 @@ pub enum Command<T> {
}
/// Handle clipboard requests.
fn worker_impl<T: 'static>(
fn worker_impl<T: 'static + Clone>(
connection: Connection,
rx_chan: Channel<Command<T>>,
reply_tx: Sender<Result<(Vec<u8>, MimeType)>>,
@ -71,21 +72,17 @@ fn worker_impl<T: 'static>(
Command::Store(data, target) => {
state.store_selection(target, data);
},
Command::Load(mime_types, SelectionTarget::Clipboard)
Command::Load(mime_types, Target::Clipboard)
if state.data_device_manager_state.is_some() =>
{
if let Err(err) =
state.load_selection(SelectionTarget::Clipboard, &mime_types)
{
if let Err(err) = state.load(Target::Clipboard, &mime_types) {
let _ = state.reply_tx.send(Err(err));
}
},
Command::Load(mime_types, SelectionTarget::Primary)
Command::Load(mime_types, Target::Primary)
if state.primary_selection_manager_state.is_some() =>
{
if let Err(err) =
state.load_selection(SelectionTarget::Primary, &mime_types)
{
if let Err(err) = state.load(Target::Primary, &mime_types) {
let _ = state.reply_tx.send(Err(err));
}
},
@ -96,8 +93,8 @@ fn worker_impl<T: 'static>(
)));
},
#[cfg(feature = "dnd")]
Command::InitDnD(sender) => {
state.sender = Some(sender);
Command::DndRequest(r) => {
state.handle_dnd_request(r);
},
Command::Phantom(_) => unreachable!(),
}