feat: dnd sources

This commit is contained in:
Ashley Wulber 2024-03-25 15:32:58 -04:00
parent 90dc61bb57
commit cc9ab6de69
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
6 changed files with 360 additions and 65 deletions

View file

@ -61,7 +61,7 @@ pub enum SourceEvent {
Action(DndAction),
/// Mime accepted by destination.
/// If [`None`], no mime types are accepted.
Mime(Option<String>),
Mime(Option<MimeType>),
/// DnD Dropped. The operation is still ongoing until receiving a
/// [`Finished`] event.
Dropped,
@ -72,7 +72,7 @@ pub enum OfferEvent<T> {
Enter {
x: f64,
y: f64,
mime_types: Vec<String>,
mime_types: Vec<MimeType>,
surface: T,
},
Motion {
@ -91,7 +91,7 @@ pub enum OfferEvent<T> {
SelectedAction(DndAction),
Data {
data: Vec<u8>,
mime_type: String,
mime_type: MimeType,
},
}
@ -131,12 +131,16 @@ pub enum DndRequest<T> {
Surface(DndSurface<T>, Vec<DndDestinationRectangle>),
/// Start a Dnd operation with the given source surface and data.
StartDnd {
internal: bool,
source: DndSurface<T>,
icon: Option<DndSurface<T>>,
content: Box<dyn AsMimeTypes + Send>,
actions: DndAction,
},
/// Set the DnD action chosen by the user.
SetAction(DndAction),
/// End an active DnD Source
DndEnd,
}
#[derive(Debug)]
@ -171,21 +175,27 @@ impl<T: RawSurface> Clipboard<T> {
/// Start a DnD operation on the given surface with some data
pub fn start_dnd<D: AsMimeTypes + Send + 'static>(
&self,
internal: bool,
source_surface: T,
icon_surface: Option<T>,
content: D,
actions: DndAction,
) {
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 {
internal,
source,
icon,
content: Box::new(content),
actions,
}));
}
/// End the current DnD operation, if there is one
pub fn end_dnd() {}
pub fn end_dnd(&self) {
_ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::DndEnd));
}
/// Register a surface for receiving DnD offers
/// Rectangles should be provided in order of decreasing priority.
@ -198,5 +208,9 @@ impl<T: RawSurface> Clipboard<T> {
}
/// Set the final action after presenting the user with a choice
pub fn set_action(&self, action: DndAction) {}
pub fn set_action(&self, action: DndAction) {
_ = self
.request_sender
.send(crate::worker::Command::DndRequest(DndRequest::SetAction(action)));
}
}

View file

@ -1,12 +1,13 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{Error, ErrorKind, Read};
use std::io::{Error, ErrorKind, Read, Write};
use std::mem;
use std::os::unix::io::AsRawFd;
use std::rc::Rc;
use sctk::data_device_manager::data_offer::DragOffer;
use sctk::data_device_manager::data_source::DragSource;
use sctk::data_device_manager::WritePipe;
use sctk::reexports::calloop::PostAction;
use sctk::reexports::client::protocol::wl_data_device::WlDataDevice;
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
@ -23,13 +24,14 @@ use super::{DndDestinationRectangle, DndEvent, DndRequest, DndSurface, OfferEven
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>,
pub(crate) dnd_source: Option<DragSource>,
active_surface: Option<(DndSurface<T>, Option<DndDestinationRectangle>)>,
source_actions: DndAction,
selected_action: DndAction,
selected_mime: Option<MimeType>,
pub(crate) source_content: Box<dyn AsMimeTypes>,
pub(crate) source_content: Option<Box<dyn AsMimeTypes>>,
pub(crate) source_mime_types: Rc<Cow<'static, [MimeType]>>,
accept_ctr: u32,
}
impl<T> Default for DndState<T> {
@ -37,13 +39,14 @@ impl<T> Default for DndState<T> {
Self {
sender: Default::default(),
destinations: Default::default(),
dnd_sources: Default::default(),
dnd_source: Default::default(),
active_surface: None,
source_actions: DndAction::empty(),
selected_action: DndAction::empty(),
selected_mime: None,
source_content: Box::new(Text(String::new())),
source_content: None,
source_mime_types: Rc::new(Cow::Owned(Vec::new())),
accept_ctr: 1,
}
}
}
@ -104,7 +107,8 @@ where
));
}
dnd_state.set_actions(DndAction::empty(), DndAction::empty());
dnd_state.accept_mime_type(dnd_state.serial, None);
dnd_state.accept_mime_type(self.dnd_state.accept_ctr, None);
self.dnd_state.accept_ctr = self.dnd_state.accept_ctr.wrapping_add(1);
self.dnd_state.selected_action = DndAction::empty();
self.dnd_state.selected_mime = None;
}
@ -116,7 +120,8 @@ where
{
dnd_state.set_actions(action, preferred_action);
self.dnd_state.selected_mime = Some(mime_type.clone());
dnd_state.accept_mime_type(dnd_state.serial, Some(mime_type.to_string()))
dnd_state.accept_mime_type(self.dnd_state.accept_ctr, Some(mime_type.to_string()));
self.dnd_state.accept_ctr = self.dnd_state.accept_ctr.wrapping_add(1);
}
(s.clone(), Some(dest))
});
@ -174,7 +179,6 @@ where
if self.dnd_state.sender.is_none() {
return;
}
dbg!(&self.dnd_state.destinations);
let Some(data_device) = self
.seats
.iter()
@ -182,8 +186,12 @@ where
else {
return;
};
let dnd_state = data_device.data().drag_offer();
self.update_active_surface(surface, x, y, dnd_state.as_ref());
let drag_offer = data_device.data().drag_offer();
if drag_offer.is_none() && self.dnd_state.source_content.is_none() {
// Ignore cancelled internal DnD
return;
}
self.update_active_surface(surface, x, y, drag_offer.as_ref());
let Some((surface, id)) = self
.dnd_state
.active_surface
@ -220,9 +228,13 @@ where
else {
return;
};
let dnd_state = data_device.data().drag_offer();
let drag_offer = data_device.data().drag_offer();
if drag_offer.is_none() && self.dnd_state.source_content.is_none() {
// Ignore cancelled internal DnD
return;
}
if dest.is_none() {
self.update_active_surface(&surface.surface, x, y, dnd_state.as_ref());
self.update_active_surface(&surface.surface, x, y, drag_offer.as_ref());
}
let id = self.cur_id();
if let Some(tx) = self.dnd_state.sender.as_ref() {
@ -245,13 +257,99 @@ where
DndRequest::Surface(s, dests) => {
self.dnd_state.destinations.insert(s.surface.id(), (s, dests));
},
DndRequest::StartDnd { source, icon, content } => {},
DndRequest::SetAction(_) => {
todo!()
DndRequest::StartDnd { internal, source, icon, content, actions } => {
_ = self.start_dnd(internal, source, icon, content, actions);
},
DndRequest::SetAction(a) => {
_ = self.user_selected_action(a);
},
DndRequest::DndEnd => {
self.dnd_state.source_content = None;
self.dnd_state.dnd_source = None;
},
};
}
fn start_dnd(
&mut self,
internal: bool,
source_surface: DndSurface<T>,
icon: Option<DndSurface<T>>,
content: Box<dyn AsMimeTypes + Send>,
actions: DndAction,
) -> std::io::Result<()> {
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"))?;
let serial = seat.latest_serial;
let data_device = seat
.data_device
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::Other, "data device missing"))?;
if internal {
DragSource::start_internal_drag(
data_device,
&source_surface.surface,
icon.as_ref().map(|s| &s.surface),
serial,
)
} else {
let mime_types = content.available();
let source = self
.data_device_manager_state
.as_ref()
.map(|s| {
s.create_drag_and_drop_source(
&self.queue_handle,
mime_types.iter().map(|m| m.as_ref()),
actions,
)
})
.ok_or_else(|| Error::new(ErrorKind::Other, "data device manager missing"))?;
source.start_drag(
data_device,
&source_surface.surface,
icon.as_ref().map(|s| &s.surface),
serial,
);
self.dnd_state.dnd_source = Some(source);
self.dnd_state.source_content = Some(content);
self.dnd_state.source_actions = actions;
}
Ok(())
}
fn user_selected_action(&mut self, a: DndAction) -> std::io::Result<()> {
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"))?;
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."))?;
offer.set_actions(a, a);
if let Some(mime_type) = self.dnd_state.selected_mime.clone() {
_ = self.load_dnd(mime_type);
}
Ok(())
}
/// 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();
@ -290,7 +388,7 @@ where
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(),
mime_type: mem::take(&mut mime_type),
}));
break PostAction::Remove;
},
@ -306,4 +404,43 @@ where
Ok(())
}
pub(crate) fn send_dnd_request(&self, write_pipe: WritePipe, mime: String) {
let Some(content) = self.dnd_state.source_content.as_ref() else {
return;
};
let Some(mime_type) = MimeType::find_allowed(&[mime], &content.available()) else {
return;
};
// Mark FD as non-blocking so we won't block ourselves.
unsafe {
if set_non_blocking(write_pipe.as_raw_fd()).is_err() {
return;
}
}
// Don't access the content on the state directly, since it could change during
// the send.
let contents = content.as_bytes(&mime_type);
let Some(contents) = contents else {
return;
};
let mut written = 0;
let _ = self.loop_handle.insert_source(write_pipe, move |_, file, _| {
let file = unsafe { file.get_mut() };
loop {
match file.write(&contents[written..]) {
Ok(n) if written + n == contents.len() => {
written += n;
break PostAction::Remove;
},
Ok(n) => written += n,
Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue,
Err(_) => break PostAction::Remove,
}
}
});
}
}

View file

@ -18,7 +18,7 @@ use sctk::reexports::client::Connection;
pub mod dnd;
pub mod mime;
mod state;
mod text;
pub mod text;
mod worker;
use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};

View file

@ -39,7 +39,7 @@ use sctk::reexports::protocols::wp::primary_selection::zv1::client::{
use wayland_backend::client::ObjectId;
use crate::dnd::state::DndState;
use crate::dnd::DndSurface;
use crate::dnd::{DndEvent, DndSurface};
use crate::mime::{AsMimeTypes, MimeType};
use crate::text::Text;
@ -50,14 +50,14 @@ pub struct State<T> {
pub exit: bool,
registry_state: RegistryState,
seat_state: SeatState,
pub(crate) seat_state: SeatState,
pub(crate) seats: HashMap<ObjectId, ClipboardSeatState>,
/// The latest seat which got an event.
pub(crate) latest_seat: Option<ObjectId>,
pub(crate) loop_handle: LoopHandle<'static, Self>,
queue_handle: QueueHandle<Self>,
pub(crate) queue_handle: QueueHandle<Self>,
primary_sources: Vec<PrimarySelectionSource>,
primary_selection_content: Box<dyn AsMimeTypes>,
@ -148,10 +148,6 @@ impl<T: 'static + Clone> 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(())
@ -211,21 +207,6 @@ impl<T: 'static + Clone> 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.first() 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.
@ -262,8 +243,6 @@ impl<T: 'static + Clone> State<T> {
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
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;
};
@ -280,8 +259,6 @@ impl<T: 'static + Clone> State<T> {
let contents = match ty {
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 {
@ -471,15 +448,34 @@ impl<T: 'static + Clone> DataSourceHandler for State<T> {
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
_source: &WlDataSource,
mime: String,
write_pipe: WritePipe,
) {
#[cfg(feature = "dnd")]
if self
.dnd_state
.dnd_source
.as_ref()
.map(|my_source| my_source.inner() == _source)
.unwrap_or_default()
{
self.send_dnd_request(write_pipe, mime);
return;
}
self.send_request(Target::Clipboard, write_pipe, mime)
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, deleted: &WlDataSource) {
self.data_sources.retain(|source| source.inner() != deleted)
self.data_sources.retain(|source| source.inner() != deleted);
#[cfg(feature = "dnd")]
{
self.dnd_state.source_content = None;
self.dnd_state.dnd_source = None;
if let Some(s) = self.dnd_state.sender.as_ref() {
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Cancelled));
}
}
}
fn accept_mime(
@ -487,15 +483,44 @@ impl<T: 'static + Clone> DataSourceHandler for State<T> {
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
_: Option<String>,
m: Option<String>,
) {
#[cfg(feature = "dnd")]
{
if let Some(s) = self.dnd_state.sender.as_ref() {
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Mime(m.map(|s| MimeType::from(Cow::Owned(s))))));
}
}
}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
#[cfg(feature = "dnd")]
{
if let Some(s) = self.dnd_state.sender.as_ref() {
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Dropped))
}
}
}
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: DndAction) {}
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, a: DndAction) {
#[cfg(feature = "dnd")]
{
if let Some(s) = self.dnd_state.sender.as_ref() {
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Action(a)))
}
}
}
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
#[cfg(feature = "dnd")]
{
self.dnd_state.source_content = None;
self.dnd_state.dnd_source = None;
if let Some(s) = self.dnd_state.sender.as_ref() {
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Finished));
}
}
}
}
impl<T: 'static + Clone> DataOfferHandler for State<T> {
@ -605,9 +630,6 @@ pub enum Target {
Clipboard,
/// The target is primary selection.
Primary,
#[cfg(feature = "dnd")]
/// The targe is a DnD offer.
DnD,
}
#[derive(Debug, Default)]
@ -619,7 +641,7 @@ pub(crate) struct ClipboardSeatState {
pub(crate) has_focus: bool,
/// The latest serial used to set the selection content.
latest_serial: u32,
pub(crate) latest_serial: u32,
}
impl Drop for ClipboardSeatState {