diff --git a/src/wayland/data_device/dnd_grab.rs b/src/wayland/data_device/dnd_grab.rs index 9cdc683..4876f14 100644 --- a/src/wayland/data_device/dnd_grab.rs +++ b/src/wayland/data_device/dnd_grab.rs @@ -10,25 +10,23 @@ use wayland_server::{ use wayland::seat::{AxisFrame, PointerGrab, PointerInnerHandle, Seat}; -use super::{with_source_metadata, SeatData}; +use super::{with_source_metadata, DataDeviceData, SeatData}; -pub(crate) struct DnDGrab { +pub(crate) struct DnDGrab { data_source: Option>, current_focus: Option>, pending_offers: Vec>, offer_data: Option>>, origin: Resource, seat: Seat, - action_choice: Arc>, } -impl DnDGrab { +impl DnDGrab { pub(crate) fn new( source: Option>, origin: Resource, seat: Seat, - action_choice: Arc>, - ) -> DnDGrab { + ) -> DnDGrab { DnDGrab { data_source: source, current_focus: None, @@ -36,15 +34,11 @@ impl DnDGrab { offer_data: None, origin, seat, - action_choice, } } } -impl PointerGrab for DnDGrab -where - F: FnMut(DndAction, DndAction) -> DndAction + Send + 'static, -{ +impl PointerGrab for DnDGrab { fn motion( &mut self, _handle: &mut PointerInnerHandle, @@ -99,6 +93,11 @@ where .iter() .filter(|d| d.same_client_as(&surface)) { + let action_choice = device + .user_data::() + .unwrap() + .action_choice + .clone(); // create a data offer let offer = client .create_resource::(device.version()) @@ -107,7 +106,7 @@ where offer, source.clone(), offer_data.clone(), - self.action_choice.clone(), + action_choice, ) }).unwrap(); // advertize the offer to the client @@ -233,15 +232,12 @@ struct OfferData { chosen_action: DndAction, } -fn implement_dnd_data_offer( +fn implement_dnd_data_offer( offer: NewResource, source: Resource, offer_data: Arc>, - action_choice: Arc>, -) -> Resource -where - F: FnMut(DndAction, DndAction) -> DndAction + Send + 'static, -{ + action_choice: Arc DndAction + Send + 'static>>, +) -> Resource { use self::wl_data_offer::Request; offer.implement( move |req, offer| { diff --git a/src/wayland/data_device/mod.rs b/src/wayland/data_device/mod.rs index f76731c..ade4d5e 100644 --- a/src/wayland/data_device/mod.rs +++ b/src/wayland/data_device/mod.rs @@ -14,8 +14,10 @@ use wayland::seat::Seat; mod data_source; mod dnd_grab; +mod server_dnd_grab; pub use self::data_source::{with_source_metadata, SourceMetadata}; +pub use self::server_dnd_grab::ServerDndEvent; /// Events that are generated by interactions of the clients with the data device pub enum DataDeviceEvent { @@ -25,9 +27,9 @@ pub enum DataDeviceEvent { DnDStarted(Option>), /// A client requested to read the server-set selection SendSelection { - // the requested mime type + /// the requested mime type mime_type: String, - // the fd to write into + /// the fd to write into fd: RawFd, }, } @@ -134,10 +136,7 @@ impl SeatData { } let log = self.log.clone(); let offer_meta = meta.clone(); - let callback = dd - .user_data::>>() - .unwrap() - .clone(); + let callback = dd.user_data::().unwrap().callback.clone(); // create a corresponding data offer let offer = client .create_resource::(dd.version()) @@ -252,6 +251,29 @@ pub fn set_data_device_selection(seat: &Seat, mime_types: Vec) { })); } +/// Start a drag'n'drop from a ressource controlled by the compositor +/// +/// You'll receive events generated by the interaction of clients with your +/// drag'n'drop in the provided callback. +pub fn start_dnd(seat: &Seat, serial: u32, metadata: SourceMetadata, callback: C) +where + C: FnMut(ServerDndEvent) + Send + 'static, +{ + // TODO: same question as in set_data_device_focus + seat.user_data().insert_if_missing(|| { + Mutex::new(SeatData::new( + seat.arc.log.new(o!("smithay_module" => "data_device_mgr")), + )) + }); + if let Some(pointer) = seat.get_pointer() { + pointer.set_grab( + server_dnd_grab::ServerDnDGrab::new(metadata, seat.clone(), Arc::new(Mutex::new(callback))), + serial, + ); + return; + } +} + fn implement_ddm( new_ddm: NewResource, callback: Arc>, @@ -293,6 +315,11 @@ where ) } +struct DataDeviceData { + callback: Arc>, + action_choice: Arc DndAction + Send + 'static>>, +} + fn implement_data_device( new_dd: NewResource, seat: Seat, @@ -305,7 +332,10 @@ where C: FnMut(DataDeviceEvent) + Send + 'static, { use self::wl_data_device::Request; - let callback2 = callback.clone(); + let dd_data = DataDeviceData { + callback: callback.clone(), + action_choice, + }; new_dd.implement( move |req, dd| match req { Request::StartDrag { @@ -319,10 +349,7 @@ where if pointer.has_grab(serial) { // The StartDrag is in response to a pointer implicit grab, all is good (&mut *callback.lock().unwrap())(DataDeviceEvent::DnDStarted(source.clone())); - pointer.set_grab( - dnd_grab::DnDGrab::new(source, origin, seat.clone(), action_choice.clone()), - serial, - ); + pointer.set_grab(dnd_grab::DnDGrab::new(source, origin, seat.clone()), serial); return; } } @@ -360,6 +387,6 @@ where } }, None::, - callback2 as Arc>, + dd_data, ) } diff --git a/src/wayland/data_device/server_dnd_grab.rs b/src/wayland/data_device/server_dnd_grab.rs new file mode 100644 index 0000000..ec8e474 --- /dev/null +++ b/src/wayland/data_device/server_dnd_grab.rs @@ -0,0 +1,320 @@ +use std::os::unix::io::RawFd; +use std::sync::{Arc, Mutex}; + +use wayland_server::{ + protocol::{wl_data_device, wl_data_device_manager::DndAction, wl_data_offer, wl_pointer, wl_surface}, + NewResource, Resource, +}; + +use wayland::seat::{AxisFrame, PointerGrab, PointerInnerHandle, Seat}; + +use super::{DataDeviceData, SeatData}; + +/// Event generated by the interactions of clients with a server innitiated drag'n'drop +pub enum ServerDndEvent { + /// The client chose an action + Action(DndAction), + /// The DnD resource was dropped by the user + /// + /// After that, the client can still interract with your ressource + Dropped, + /// The Dnd was cancelled + /// + /// The client can no longer interact + Cancelled, + /// The client requested for data to be sent + Send { + /// The requested mime type + mime_type: String, + /// The FD to write into + fd: RawFd, + }, + /// The client has finished interacting with the resource + /// + /// This can only happen after the resource was dropped. + Finished, +} + +pub(crate) struct ServerDnDGrab { + metadata: super::SourceMetadata, + current_focus: Option>, + pending_offers: Vec>, + offer_data: Option>>, + seat: Seat, + callback: Arc>, +} + +impl ServerDnDGrab { + pub(crate) fn new( + metadata: super::SourceMetadata, + seat: Seat, + callback: Arc>, + ) -> ServerDnDGrab { + ServerDnDGrab { + metadata, + current_focus: None, + pending_offers: Vec::with_capacity(1), + offer_data: None, + seat, + callback, + } + } +} + +impl PointerGrab for ServerDnDGrab +where + C: FnMut(ServerDndEvent) + Send + 'static, +{ + fn motion( + &mut self, + _handle: &mut PointerInnerHandle, + location: (f64, f64), + focus: Option<(Resource, (f64, f64))>, + serial: u32, + time: u32, + ) { + let (x, y) = location; + let seat_data = self + .seat + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap(); + if focus.as_ref().map(|&(ref s, _)| s) != self.current_focus.as_ref() { + // focus changed, we need to make a leave if appropriate + if let Some(surface) = self.current_focus.take() { + for device in &seat_data.known_devices { + if device.same_client_as(&surface) { + device.send(wl_data_device::Event::Leave); + } + } + // disable the offers + self.pending_offers.clear(); + if let Some(offer_data) = self.offer_data.take() { + offer_data.lock().unwrap().active = false; + } + } + } + if let Some((surface, (sx, sy))) = focus { + // early return if the surface is no longer valid + let client = match surface.client() { + Some(c) => c, + None => return, + }; + if self.current_focus.is_none() { + // We entered a new surface, send the data offer + let offer_data = Arc::new(Mutex::new(OfferData { + active: true, + dropped: false, + accepted: true, + chosen_action: DndAction::empty(), + })); + for device in seat_data + .known_devices + .iter() + .filter(|d| d.same_client_as(&surface)) + { + let action_choice = device + .user_data::() + .unwrap() + .action_choice + .clone(); + // create a data offer + let offer = client + .create_resource::(device.version()) + .map(|offer| { + implement_dnd_data_offer( + offer, + self.metadata.clone(), + offer_data.clone(), + self.callback.clone(), + action_choice, + ) + }).unwrap(); + // advertize the offer to the client + device.send(wl_data_device::Event::DataOffer { id: offer.clone() }); + for mime_type in self.metadata.mime_types.iter().cloned() { + offer.send(wl_data_offer::Event::Offer { mime_type }) + } + offer.send(wl_data_offer::Event::SourceActions { + source_actions: self.metadata.dnd_action.to_raw(), + }); + device.send(wl_data_device::Event::Enter { + serial, + x: x - sx, + y: y - sy, + surface: surface.clone(), + id: Some(offer.clone()), + }); + self.pending_offers.push(offer); + } + self.offer_data = Some(offer_data); + self.current_focus = Some(surface); + } else { + // make a move + for device in &seat_data.known_devices { + if device.same_client_as(&surface) { + device.send(wl_data_device::Event::Motion { + time, + x: x - sx, + y: y - sy, + }); + } + } + } + } + } + + fn button( + &mut self, + handle: &mut PointerInnerHandle, + _button: u32, + _state: wl_pointer::ButtonState, + serial: u32, + time: u32, + ) { + if handle.current_pressed().len() == 0 { + // the user dropped, proceed to the drop + let seat_data = self + .seat + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap(); + let validated = if let Some(ref data) = self.offer_data { + let data = data.lock().unwrap(); + data.accepted && (!data.chosen_action.is_empty()) + } else { + false + }; + if let Some(ref surface) = self.current_focus { + for device in &seat_data.known_devices { + if device.same_client_as(surface) { + if validated { + device.send(wl_data_device::Event::Drop); + } else { + device.send(wl_data_device::Event::Leave); + } + } + } + } + if let Some(ref offer_data) = self.offer_data { + let mut data = offer_data.lock().unwrap(); + if validated { + data.dropped = true; + } else { + data.active = false; + } + } + let mut callback = self.callback.lock().unwrap(); + (&mut *callback)(ServerDndEvent::Dropped); + if !validated { + (&mut *callback)(ServerDndEvent::Cancelled); + } + // in all cases abandon the drop + // no more buttons are pressed, release the grab + handle.unset_grab(serial, time); + } + } + + fn axis(&mut self, handle: &mut PointerInnerHandle, details: AxisFrame) { + // we just forward the axis events as is + handle.axis(details); + } +} + +struct OfferData { + active: bool, + dropped: bool, + accepted: bool, + chosen_action: DndAction, +} + +fn implement_dnd_data_offer( + offer: NewResource, + metadata: super::SourceMetadata, + offer_data: Arc>, + callback: Arc>, + action_choice: Arc DndAction + Send + 'static>>, +) -> Resource +where + C: FnMut(ServerDndEvent) + Send + 'static, +{ + use self::wl_data_offer::Request; + offer.implement( + move |req, offer| { + let mut data = offer_data.lock().unwrap(); + match req { + Request::Accept { serial: _, mime_type } => { + if let Some(mtype) = mime_type { + data.accepted = metadata.mime_types.contains(&mtype); + } else { + data.accepted = false; + } + } + Request::Receive { mime_type, fd } => { + // check if the source and associated mime type is still valid + if metadata.mime_types.contains(&mime_type) && data.active { + (&mut *callback.lock().unwrap())(ServerDndEvent::Send { mime_type, fd }); + } + } + Request::Destroy => {} + Request::Finish => { + if !data.active { + offer.post_error( + wl_data_offer::Error::InvalidFinish as u32, + "Cannot finish a data offer that is no longer active.".into(), + ); + } + if !data.accepted { + offer.post_error( + wl_data_offer::Error::InvalidFinish as u32, + "Cannot finish a data offer that has not been accepted.".into(), + ); + } + if !data.dropped { + offer.post_error( + wl_data_offer::Error::InvalidFinish as u32, + "Cannot finish a data offer that has not been dropped.".into(), + ); + } + if data.chosen_action.is_empty() { + offer.post_error( + wl_data_offer::Error::InvalidFinish as u32, + "Cannot finish a data offer with no valid action.".into(), + ); + } + (&mut *callback.lock().unwrap())(ServerDndEvent::Finished); + data.active = false; + } + Request::SetActions { + dnd_actions, + preferred_action, + } => { + let preferred_action = DndAction::from_bits_truncate(preferred_action); + if ![DndAction::Move, DndAction::Copy, DndAction::Ask].contains(&preferred_action) { + offer.post_error( + wl_data_offer::Error::InvalidAction as u32, + "Invalid preferred action.".into(), + ); + } + let possible_actions = metadata.dnd_action & DndAction::from_bits_truncate(dnd_actions); + data.chosen_action = + (&mut *action_choice.lock().unwrap())(possible_actions, preferred_action); + // check that the user provided callback respects that one precise action should be chosen + debug_assert!( + [DndAction::Move, DndAction::Copy, DndAction::Ask].contains(&data.chosen_action) + ); + offer.send(wl_data_offer::Event::Action { + dnd_action: data.chosen_action.to_raw(), + }); + (&mut *callback.lock().unwrap())(ServerDndEvent::Action(data.chosen_action)); + } + } + }, + None::, + (), + ) +}