From cc9ab6de69b374791d480d556932e461ba5330f7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 25 Mar 2024 15:32:58 -0400 Subject: [PATCH] feat: dnd sources --- examples/clipboard.rs | 135 +++++++++++++++++++++++++++++++-- rust-toolchain | 1 + src/dnd/mod.rs | 24 ++++-- src/dnd/state.rs | 169 ++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- src/state.rs | 94 ++++++++++++++--------- 6 files changed, 360 insertions(+), 65 deletions(-) create mode 100644 rust-toolchain diff --git a/examples/clipboard.rs b/examples/clipboard.rs index 774956d..f227d59 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -8,15 +8,18 @@ use std::str::{FromStr, Utf8Error}; use sctk::compositor::{CompositorHandler, CompositorState}; use sctk::output::{OutputHandler, OutputState}; -use sctk::reexports::calloop::{EventLoop, LoopHandle}; +use sctk::reexports::calloop::{self, 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::protocol::{ + wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, +}; use sctk::reexports::client::{Connection, Proxy, QueueHandle}; use sctk::registry::{ProvidesRegistryState, RegistryState}; use sctk::seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers}; +use sctk::seat::pointer::{PointerEventKind, PointerHandler, BTN_LEFT, BTN_RIGHT}; use sctk::seat::{Capability, SeatHandler, SeatState}; use sctk::shell::xdg::window::{Window, WindowConfigure, WindowDecorations, WindowHandler}; use sctk::shell::xdg::XdgShell; @@ -24,10 +27,10 @@ use sctk::shell::WaylandSurface; use sctk::shm::slot::{Buffer, SlotPool}; use sctk::shm::{Shm, ShmHandler}; use sctk::{ - delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat, - delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers, + delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry, + delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers, }; -use smithay_clipboard::dnd::{DndDestinationRectangle, Rectangle}; +use smithay_clipboard::dnd::{DndDestinationRectangle, OfferEvent, Rectangle, SourceEvent}; use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType, ALLOWED_TEXT_MIME_TYPES}; use smithay_clipboard::{Clipboard, SimpleClipboard}; use thiserror::Error; @@ -70,8 +73,38 @@ fn main() { let (tx, rx) = sctk::reexports::calloop::channel::sync_channel(10); clipboard.init_dnd(Box::new(tx)).expect("Failed to set up DnD"); - _ = event_loop.handle().insert_source(rx, |event, _, _state| { - dbg!(event); + _ = event_loop.handle().insert_source(rx, |event, _, state| { + let calloop::channel::Event::Msg(event) = event else { + return; + }; + match event { + smithay_clipboard::dnd::DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) => { + let s = smithay_clipboard::text::Text::try_from((data, mime_type)).unwrap(); + println!("Received DnD data for {}: {}", id.unwrap_or_default(), s.0); + }, + smithay_clipboard::dnd::DndEvent::Offer(id, OfferEvent::Leave) => { + if state.internal_dnd { + if state.pointer_focus { + println!("Internal drop completed!"); + } else { + // Internal DnD will be ignored after leaving the window in which it + // started. Another approach might be to allow it to + // re-enter before some time has passed. + state.internal_dnd = false; + state.clipboard.end_dnd(); + } + } else { + println!("Dnd offer left {id:?}."); + } + }, + smithay_clipboard::dnd::DndEvent::Source(SourceEvent::Finished) => { + println!("Finished sending data."); + state.internal_dnd = false; + }, + e => { + dbg!(e); + }, + } }); clipboard.register_dnd_destination(window.wl_surface().clone(), vec![ @@ -85,6 +118,36 @@ fn main() { actions: DndAction::all(), preferred: DndAction::Copy, }, + DndDestinationRectangle { + id: 1, + rectangle: Rectangle { x: 256., y: 0., width: 256., height: 256. }, + mime_types: ALLOWED_TEXT_MIME_TYPES + .iter() + .map(|m| MimeType::from(Cow::from(m.to_string()))) + .collect(), + actions: DndAction::all(), + preferred: DndAction::Copy, + }, + DndDestinationRectangle { + id: 2, + rectangle: Rectangle { x: 0., y: 256., width: 256., height: 256. }, + mime_types: ALLOWED_TEXT_MIME_TYPES + .iter() + .map(|m| MimeType::from(Cow::from(m.to_string()))) + .collect(), + actions: DndAction::Copy, + preferred: DndAction::Copy, + }, + DndDestinationRectangle { + id: 3, + rectangle: Rectangle { x: 256., y: 256., width: 256., height: 256. }, + mime_types: ALLOWED_TEXT_MIME_TYPES + .iter() + .map(|m| MimeType::from(Cow::from(m.to_string()))) + .collect(), + actions: DndAction::Move, + preferred: DndAction::Move, + }, ]); let mut simple_window = SimpleWindow { @@ -102,7 +165,10 @@ fn main() { buffer: None, window, keyboard: None, + pointer: None, + internal_dnd: false, keyboard_focus: false, + pointer_focus: false, loop_handle: event_loop.handle(), }; @@ -131,7 +197,10 @@ struct SimpleWindow { buffer: Option, window: Window, keyboard: Option, + pointer: Option, + internal_dnd: bool, keyboard_focus: bool, + pointer_focus: bool, loop_handle: LoopHandle<'static, SimpleWindow>, } @@ -255,6 +324,12 @@ impl SeatHandler for SimpleWindow { self.keyboard = Some(keyboard); } + if capability == Capability::Pointer && self.pointer.is_none() { + println!("Set pointer capability"); + let pointer = self.seat_state.get_pointer(qh, &seat).expect("Failed to create pointer"); + + self.pointer = Some(pointer); + } } fn remove_capability( @@ -273,6 +348,51 @@ impl SeatHandler for SimpleWindow { fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} } +impl PointerHandler for SimpleWindow { + fn pointer_frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _pointer: &sctk::reexports::client::protocol::wl_pointer::WlPointer, + events: &[sctk::seat::pointer::PointerEvent], + ) { + for e in events { + match &e.kind { + PointerEventKind::Press { button, .. } if *button == BTN_LEFT => { + println!("Starting a drag!"); + self.clipboard.start_dnd( + false, + self.window.wl_surface().clone(), + None, + smithay_clipboard::text::Text("Clipboard Drag and Drop!".to_string()), + DndAction::all(), + ); + }, + PointerEventKind::Press { button, .. } if *button == BTN_RIGHT => { + println!("Starting an internal drag!"); + self.internal_dnd = true; + self.clipboard.start_dnd( + true, + self.window.wl_surface().clone(), + None, + smithay_clipboard::text::Text( + "Internal clipboard Drag and Drop!".to_string(), + ), + DndAction::all(), + ); + }, + PointerEventKind::Leave { .. } => { + self.pointer_focus = false; + }, + PointerEventKind::Enter { .. } => { + self.pointer_focus = true; + }, + _ => {}, + } + } + } +} + impl KeyboardHandler for SimpleWindow { fn enter( &mut self, @@ -509,6 +629,7 @@ delegate_shm!(SimpleWindow); delegate_seat!(SimpleWindow); delegate_keyboard!(SimpleWindow); +delegate_pointer!(SimpleWindow); delegate_xdg_shell!(SimpleWindow); delegate_xdg_window!(SimpleWindow); diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..07ade69 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly \ No newline at end of file diff --git a/src/dnd/mod.rs b/src/dnd/mod.rs index 99c80da..64c88ad 100644 --- a/src/dnd/mod.rs +++ b/src/dnd/mod.rs @@ -61,7 +61,7 @@ pub enum SourceEvent { Action(DndAction), /// Mime accepted by destination. /// If [`None`], no mime types are accepted. - Mime(Option), + Mime(Option), /// DnD Dropped. The operation is still ongoing until receiving a /// [`Finished`] event. Dropped, @@ -72,7 +72,7 @@ pub enum OfferEvent { Enter { x: f64, y: f64, - mime_types: Vec, + mime_types: Vec, surface: T, }, Motion { @@ -91,7 +91,7 @@ pub enum OfferEvent { SelectedAction(DndAction), Data { data: Vec, - mime_type: String, + mime_type: MimeType, }, } @@ -131,12 +131,16 @@ pub enum DndRequest { Surface(DndSurface, Vec), /// Start a Dnd operation with the given source surface and data. StartDnd { + internal: bool, source: DndSurface, icon: Option>, content: Box, + 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 Clipboard { /// Start a DnD operation on the given surface with some data pub fn start_dnd( &self, + internal: bool, source_surface: T, icon_surface: Option, 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 Clipboard { } /// 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))); + } } diff --git a/src/dnd/state.rs b/src/dnd/state.rs index 4c6c09b..607cdad 100644 --- a/src/dnd/state.rs +++ b/src/dnd/state.rs @@ -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 { pub(crate) sender: Option>>, destinations: HashMap, Vec)>, - dnd_sources: Option, + pub(crate) dnd_source: Option, active_surface: Option<(DndSurface, Option)>, source_actions: DndAction, selected_action: DndAction, selected_mime: Option, - pub(crate) source_content: Box, + pub(crate) source_content: Option>, pub(crate) source_mime_types: Rc>, + accept_ctr: u32, } impl Default for DndState { @@ -37,13 +39,14 @@ impl Default for DndState { 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, + icon: Option>, + content: Box, + 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, + } + } + }); + } } diff --git a/src/lib.rs b/src/lib.rs index 1c0895f..670ddf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/state.rs b/src/state.rs index 716397b..5e4c786 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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 { pub exit: bool, registry_state: RegistryState, - seat_state: SeatState, + pub(crate) seat_state: SeatState, pub(crate) seats: HashMap, /// The latest seat which got an event. pub(crate) latest_seat: Option, pub(crate) loop_handle: LoopHandle<'static, Self>, - queue_handle: QueueHandle, + pub(crate) queue_handle: QueueHandle, primary_sources: Vec, primary_selection_content: Box, @@ -148,10 +148,6 @@ impl State { 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 State { (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 State { 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 State { 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 DataSourceHandler for State { &mut self, _: &Connection, _: &QueueHandle, - _: &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, 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 DataSourceHandler for State { _: &Connection, _: &QueueHandle, _: &WlDataSource, - _: Option, + m: Option, ) { + #[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, _: &WlDataSource) {} + fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle, _: &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, _: &WlDataSource, _: DndAction) {} + fn action(&mut self, _: &Connection, _: &QueueHandle, _: &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, _: &WlDataSource) {} + fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle, _: &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 DataOfferHandler for State { @@ -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 {