diff --git a/Cargo.toml b/Cargo.toml index 6407095..64de8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "anvil" ] [dependencies] wayland-server = "0.21.1" wayland-sys = "0.21.1" +wayland-commons = "0.21.1" nix = "0.11" xkbcommon = "0.2.1" tempfile = "2.1.5" diff --git a/anvil/src/misc.rs b/anvil/src/misc.rs new file mode 100644 index 0000000..e69de29 diff --git a/anvil/src/shell.rs b/anvil/src/shell.rs index dd68aa1..07bc0d9 100644 --- a/anvil/src/shell.rs +++ b/anvil/src/shell.rs @@ -68,9 +68,9 @@ pub fn init_shell( compositor_token, move |shell_event| match shell_event { XdgRequest::NewToplevel { surface } => { - // place the window at a random location in the [0;300]x[0;300] square + // place the window at a random location in the [0;800]x[0;800] square use rand::distributions::{IndependentSample, Range}; - let range = Range::new(0, 300); + let range = Range::new(0, 800); let mut rng = rand::thread_rng(); let x = range.ind_sample(&mut rng); let y = range.ind_sample(&mut rng); diff --git a/anvil/src/udev.rs b/anvil/src/udev.rs index 97695ce..a3de2aa 100644 --- a/anvil/src/udev.rs +++ b/anvil/src/udev.rs @@ -42,6 +42,7 @@ use smithay::{ input::Libinput, wayland::{ compositor::CompositorToken, + data_device::{default_action_chooser, init_data_device, set_data_device_focus}, output::{Mode, Output, PhysicalProperties}, seat::{Seat, XkbConfig}, shm::init_shm_global, @@ -107,12 +108,20 @@ pub fn run_udev(mut display: Display, mut event_loop: EventLoop<()>, log: Logger let udev_session_id = notifier.register(&mut udev_backend); + init_data_device( + &mut display.borrow_mut(), + |_| {}, + default_action_chooser, + log.clone(), + ); + let (mut w_seat, _) = Seat::new(&mut display.borrow_mut(), session.seat(), log.clone()); let pointer = w_seat.add_pointer(); let keyboard = w_seat - .add_keyboard(XkbConfig::default(), 1000, 500) - .expect("Failed to initialize the keyboard"); + .add_keyboard(XkbConfig::default(), 1000, 500, |seat, focus| { + set_data_device_focus(seat, focus.and_then(|s| s.client())) + }).expect("Failed to initialize the keyboard"); let (output, _output_global) = Output::new( &mut display.borrow_mut(), diff --git a/anvil/src/winit.rs b/anvil/src/winit.rs index 29566bb..4d191bd 100644 --- a/anvil/src/winit.rs +++ b/anvil/src/winit.rs @@ -11,6 +11,7 @@ use smithay::{ winit, }, wayland::{ + data_device::{default_action_chooser, init_data_device, set_data_device_focus}, output::{Mode, Output, PhysicalProperties}, seat::{Seat, XkbConfig}, shm::init_shm_global, @@ -53,12 +54,16 @@ pub fn run_winit(display: &mut Display, event_loop: &mut EventLoop<()>, log: Log let (compositor_token, _, _, window_map) = init_shell(display, log.clone()); + init_data_device(display, |_| {}, default_action_chooser, log.clone()); + let (mut seat, _) = Seat::new(display, "winit".into(), log.clone()); let pointer = seat.add_pointer(); + let keyboard = seat - .add_keyboard(XkbConfig::default(), 1000, 500) - .expect("Failed to initialize the keyboard"); + .add_keyboard(XkbConfig::default(), 1000, 500, |seat, focus| { + set_data_device_focus(seat, focus.and_then(|s| s.client())) + }).expect("Failed to initialize the keyboard"); let (output, _) = Output::new( display, diff --git a/src/lib.rs b/src/lib.rs index 1e00641..24e5b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub extern crate image; #[cfg_attr(feature = "backend_session", macro_use)] extern crate nix; extern crate tempfile; +pub extern crate wayland_commons; pub extern crate wayland_protocols; pub extern crate wayland_server; extern crate wayland_sys; diff --git a/src/wayland/data_device/data_source.rs b/src/wayland/data_device/data_source.rs new file mode 100644 index 0000000..b1d4522 --- /dev/null +++ b/src/wayland/data_device/data_source.rs @@ -0,0 +1,50 @@ +use std::sync::Mutex; + +use wayland_server::{ + protocol::{ + wl_data_device_manager::DndAction, + wl_data_source::{Request, WlDataSource}, + }, + NewResource, Resource, +}; + +/// The metadata describing a data source +#[derive(Debug, Clone)] +pub struct SourceMetadata { + /// The MIME types supported by this source + pub mime_types: Vec, + /// The Drag'n'Drop actions supported by this source + pub dnd_action: DndAction, +} + +pub(crate) fn implement_data_source(src: NewResource) -> Resource { + src.implement( + |req, me| { + let data: &Mutex = me.user_data().unwrap(); + let mut guard = data.lock().unwrap(); + match req { + Request::Offer { mime_type } => guard.mime_types.push(mime_type), + Request::SetActions { dnd_actions } => { + guard.dnd_action = DndAction::from_bits_truncate(dnd_actions); + } + Request::Destroy => {} + } + }, + None::, + Mutex::new(SourceMetadata { + mime_types: Vec::new(), + dnd_action: DndAction::None, + }), + ) +} + +/// Access the metadata of a data source +pub fn with_source_metadata T>( + source: &Resource, + f: F, +) -> Result { + match source.user_data::>() { + Some(data) => Ok(f(&data.lock().unwrap())), + None => Err(()), + } +} diff --git a/src/wayland/data_device/dnd_grab.rs b/src/wayland/data_device/dnd_grab.rs new file mode 100644 index 0000000..4876f14 --- /dev/null +++ b/src/wayland/data_device/dnd_grab.rs @@ -0,0 +1,329 @@ +use std::sync::{Arc, Mutex}; + +use wayland_server::{ + protocol::{ + wl_data_device, wl_data_device_manager::DndAction, wl_data_offer, wl_data_source, wl_pointer, + wl_surface, + }, + NewResource, Resource, +}; + +use wayland::seat::{AxisFrame, PointerGrab, PointerInnerHandle, Seat}; + +use super::{with_source_metadata, DataDeviceData, SeatData}; + +pub(crate) struct DnDGrab { + data_source: Option>, + current_focus: Option>, + pending_offers: Vec>, + offer_data: Option>>, + origin: Resource, + seat: Seat, +} + +impl DnDGrab { + pub(crate) fn new( + source: Option>, + origin: Resource, + seat: Seat, + ) -> DnDGrab { + DnDGrab { + data_source: source, + current_focus: None, + pending_offers: Vec::with_capacity(1), + offer_data: None, + origin, + seat, + } + } +} + +impl PointerGrab for DnDGrab { + 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() { + // only leave if there is a data source or we are on the original client + if self.data_source.is_some() || self.origin.same_client_as(&surface) { + 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 if appropriate + if let Some(ref source) = self.data_source { + 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, + source.clone(), + offer_data.clone(), + action_choice, + ) + }).unwrap(); + // advertize the offer to the client + device.send(wl_data_device::Event::DataOffer { id: offer.clone() }); + with_source_metadata(source, |meta| { + for mime_type in meta.mime_types.iter().cloned() { + offer.send(wl_data_offer::Event::Offer { mime_type }) + } + offer.send(wl_data_offer::Event::SourceActions { + source_actions: meta.dnd_action.to_raw(), + }); + }).unwrap(); + 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); + } else { + // only send if we are on a surface of the same client + if self.origin.same_client_as(&surface) { + for device in &seat_data.known_devices { + if device.same_client_as(&surface) { + device.send(wl_data_device::Event::Enter { + serial, + x: x - sx, + y: y - sy, + surface: surface.clone(), + id: None, + }); + } + } + } + } + self.current_focus = Some(surface); + } else { + // make a move + if self.data_source.is_some() || self.origin.same_client_as(&surface) { + 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 { + if self.data_source.is_some() || self.origin.same_client_as(&surface) { + 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; + } + } + if let Some(ref source) = self.data_source { + source.send(wl_data_source::Event::DndDropPerformed); + if !validated { + source.send(wl_data_source::Event::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, + source: Resource, + offer_data: Arc>, + action_choice: Arc DndAction + Send + 'static>>, +) -> Resource { + 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 { + if let Err(()) = with_source_metadata(&source, |meta| { + data.accepted = meta.mime_types.contains(&mtype); + }) { + data.accepted = false; + } + } else { + data.accepted = false; + } + } + Request::Receive { mime_type, fd } => { + // check if the source and associated mime type is still valid + let valid = with_source_metadata(&source, |meta| meta.mime_types.contains(&mime_type)) + .unwrap_or(false) + && source.is_alive() + && data.active; + if valid { + source.send(wl_data_source::Event::Send { mime_type, fd }); + } + let _ = ::nix::unistd::close(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(), + ); + } + source.send(wl_data_source::Event::DndFinished); + 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 source_actions = + with_source_metadata(&source, |meta| meta.dnd_action).unwrap_or(DndAction::empty()); + let possible_actions = source_actions & 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(), + }); + source.send(wl_data_source::Event::Action { + dnd_action: data.chosen_action.to_raw(), + }); + } + } + }, + None::, + (), + ) +} diff --git a/src/wayland/data_device/mod.rs b/src/wayland/data_device/mod.rs new file mode 100644 index 0000000..0c2b84b --- /dev/null +++ b/src/wayland/data_device/mod.rs @@ -0,0 +1,468 @@ +//! Utilities for manipulating the data devices +//! +//! The data device is wayland's abstraction to represent both selection (copy/paste) and +//! drag'n'drop actions. This module provides logic to handle this part of the protocol. +//! Selection and drag'n'drop are per-seat notions. +//! +//! This module provides 2 main freestanding functions: +//! +//! - `init_data_device`: this function must be called during the compositor startup to initialize +//! the data device logic +//! - `set_data_device_focus`: this function sets the data device focus for a given seat; you'd +//! typically call it whenever the keyboard focus changes, to follow it (for example in the focus +//! hook of your keyboards) +//! +//! Using these two functions is enough for your clients to be able to interact with each other using +//! the data devices. +//! +//! The module also provides additionnal mechanisms allowing your compositor to see and interact with +//! the contents of the data device: +//! +//! - You can provide a callback closure to `init_data_device` to peek into the the actions of your clients +//! - the freestanding function `set_data_device_selection` allows you to set the contents of the selection +//! for your clients +//! - the freestanding function `start_dnd` allows you to initiate a drag'n'drop event from the compositor +//! itself and receive interactions of clients with it via an other dedicated callback. +//! +//! ## Initialization +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! use smithay::wayland::data_device::{init_data_device, default_action_chooser}; +//! +//! # fn main(){ +//! # let mut event_loop = wayland_server::calloop::EventLoop::<()>::new().unwrap(); +//! # let mut display = wayland_server::Display::new(event_loop.handle()); +//! // init the data device: +//! init_data_device( +//! &mut display, // the display +//! |dnd_event| { /* a callback to react to client DnD/selection actions */ }, +//! default_action_chooser, // a closure to choose the DnD action depending on clients +//! // negociation +//! None // insert a logger here +//! ); +//! # } +//! ``` + +use std::os::unix::io::RawFd; +use std::sync::{Arc, Mutex}; + +use wayland_server::{ + protocol::{ + wl_data_device, + wl_data_device_manager::{self, DndAction}, + wl_data_offer, wl_data_source, + }, + Client, Display, Global, NewResource, Resource, +}; + +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 { + /// A client has set the selection + NewSelection(Option>), + /// A client started a drag'n'drop as response to a user pointer action + DnDStarted(Option>), + /// A client requested to read the server-set selection + SendSelection { + /// the requested mime type + mime_type: String, + /// the fd to write into + fd: RawFd, + }, +} + +enum Selection { + Empty, + Client(Resource), + Compositor(SourceMetadata), +} + +struct SeatData { + known_devices: Vec>, + selection: Selection, + log: ::slog::Logger, + current_focus: Option, +} + +impl SeatData { + fn set_selection(&mut self, new_selection: Selection) { + self.selection = new_selection; + self.send_selection(); + } + + fn set_focus(&mut self, new_focus: Option) { + self.current_focus = new_focus; + self.send_selection(); + } + + fn send_selection(&mut self) { + let client = match self.current_focus.as_ref() { + Some(c) => c, + None => return, + }; + // first sanitize the selection, reseting it to null if the client holding + // it dropped it + let cleanup = if let Selection::Client(ref data_source) = self.selection { + !data_source.is_alive() + } else { + false + }; + if cleanup { + self.selection = Selection::Empty; + } + // then send it if appropriate + match self.selection { + Selection::Empty => { + // send an empty selection + for dd in &self.known_devices { + // skip data devices not belonging to our client + if dd.client().map(|c| !c.equals(client)).unwrap_or(true) { + continue; + } + dd.send(wl_data_device::Event::Selection { id: None }); + } + } + Selection::Client(ref data_source) => { + for dd in &self.known_devices { + // skip data devices not belonging to our client + if dd.client().map(|c| !c.equals(client)).unwrap_or(true) { + continue; + } + let source = data_source.clone(); + let log = self.log.clone(); + // create a corresponding data offer + let offer = client + .create_resource::(dd.version()) + .unwrap() + .implement( + move |req, _offer| match req { + wl_data_offer::Request::Receive { fd, mime_type } => { + // check if the source and associated mime type is still valid + let valid = with_source_metadata(&source, |meta| { + meta.mime_types.contains(&mime_type) + }).unwrap_or(false) + && source.is_alive(); + if !valid { + // deny the receive + debug!(log, "Denying a wl_data_offer.receive with invalid source."); + } else { + source.send(wl_data_source::Event::Send { mime_type, fd }); + } + let _ = ::nix::unistd::close(fd); + } + _ => { /* seleciton data offers only care about the `receive` event */ } + }, + None::, + (), + ); + // advertize the offer to the client + dd.send(wl_data_device::Event::DataOffer { id: offer.clone() }); + with_source_metadata(data_source, |meta| { + for mime_type in meta.mime_types.iter().cloned() { + offer.send(wl_data_offer::Event::Offer { mime_type }) + } + }).unwrap(); + dd.send(wl_data_device::Event::Selection { id: Some(offer) }); + } + } + Selection::Compositor(ref meta) => { + for dd in &self.known_devices { + // skip data devices not belonging to our client + if dd.client().map(|c| !c.equals(client)).unwrap_or(true) { + continue; + } + let log = self.log.clone(); + let offer_meta = meta.clone(); + let callback = dd.user_data::().unwrap().callback.clone(); + // create a corresponding data offer + let offer = client + .create_resource::(dd.version()) + .unwrap() + .implement( + move |req, _offer| match req { + wl_data_offer::Request::Receive { fd, mime_type } => { + // check if the associated mime type is valid + if !offer_meta.mime_types.contains(&mime_type) { + // deny the receive + debug!(log, "Denying a wl_data_offer.receive with invalid source."); + let _ = ::nix::unistd::close(fd); + } else { + (&mut *callback.lock().unwrap())(DataDeviceEvent::SendSelection { + mime_type, + fd, + }); + } + } + _ => { /* seleciton data offers only care about the `receive` event */ } + }, + None::, + (), + ); + // advertize the offer to the client + dd.send(wl_data_device::Event::DataOffer { id: offer.clone() }); + for mime_type in meta.mime_types.iter().cloned() { + offer.send(wl_data_offer::Event::Offer { mime_type }) + } + dd.send(wl_data_device::Event::Selection { id: Some(offer) }); + } + } + } + } +} + +impl SeatData { + fn new(log: ::slog::Logger) -> SeatData { + SeatData { + known_devices: Vec::new(), + selection: Selection::Empty, + log, + current_focus: None, + } + } +} + +/// Initialize the data device global +/// +/// You can provide a callback to peek into the actions of your clients over the data devices +/// (allowing you to retrieve the current selection buffer, or intercept DnD data). See the +/// `DataDeviceEvent` type for details about what notifications you can receive. Note that this +/// closure will not receive notifications about dnd actions the compositor initiated, see +/// `start_dnd` for details about that. +/// +/// You also need to provide a `(DndAction, DndAction) -> DndAction` closure that will arbitrate +/// the choice of action resulting from a drag'n'drop session. Its first argument is the set of +/// available actions (which is the intersection of the actions supported by the source and targets) +/// and the second argument is the preferred action reported by the target. If no action should be +/// chosen (and thus the drag'n'drop should abort on drop), return `DndAction::empty()`. +pub fn init_data_device( + display: &mut Display, + callback: C, + action_choice: F, + logger: L, +) -> Global +where + F: FnMut(DndAction, DndAction) -> DndAction + Send + 'static, + C: FnMut(DataDeviceEvent) + Send + 'static, + L: Into>, +{ + let log = ::slog_or_stdlog(logger).new(o!("smithay_module" => "data_device_mgr")); + let action_choice = Arc::new(Mutex::new(action_choice)); + let callback = Arc::new(Mutex::new(callback)); + let global = display.create_global(3, move |new_ddm, _version| { + implement_ddm(new_ddm, callback.clone(), action_choice.clone(), log.clone()); + }); + + global +} + +/// Set the data device focus to a certain client for a given seat +pub fn set_data_device_focus(seat: &Seat, client: Option) { + // ensure the seat user_data is ready + // TODO: find a better way to retrieve a logger without requiring the user + // to provide one ? + // This should be a rare path anyway, it is unlikely that a client gets focus + // before initializing its data device, which would already init the user_data. + seat.user_data().insert_if_missing(|| { + Mutex::new(SeatData::new( + seat.arc.log.new(o!("smithay_module" => "data_device_mgr")), + )) + }); + let seat_data = seat.user_data().get::>().unwrap(); + seat_data.lock().unwrap().set_focus(client); +} + +/// Set a compositor-provided selection for this seat +/// +/// You need to provide the available mime types for this selection. +/// +/// Whenever a client requests to read the selection, your callback will +/// receive a `DataDeviceEvent::SendSelection` event. +pub fn set_data_device_selection(seat: &Seat, mime_types: Vec) { + // 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")), + )) + }); + let seat_data = seat.user_data().get::>().unwrap(); + seat_data + .lock() + .unwrap() + .set_selection(Selection::Compositor(SourceMetadata { + mime_types, + dnd_action: DndAction::empty(), + })); +} + +/// 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. See `SeverDndEvent` for details about +/// which events can be generated and what response is expected from you to them. +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>, + action_choice: Arc>, + log: ::slog::Logger, +) -> Resource +where + F: FnMut(DndAction, DndAction) -> DndAction + Send + 'static, + C: FnMut(DataDeviceEvent) + Send + 'static, +{ + use self::wl_data_device_manager::Request; + new_ddm.implement( + move |req, _ddm| match req { + Request::CreateDataSource { id } => { + self::data_source::implement_data_source(id); + } + Request::GetDataDevice { id, seat } => match Seat::from_resource(&seat) { + Some(seat) => { + // ensure the seat user_data is ready + seat.user_data() + .insert_if_missing(|| Mutex::new(SeatData::new(log.clone()))); + let seat_data = seat.user_data().get::>().unwrap(); + let data_device = implement_data_device( + id, + seat.clone(), + callback.clone(), + action_choice.clone(), + log.clone(), + ); + seat_data.lock().unwrap().known_devices.push(data_device); + } + None => { + error!(log, "Unmanaged seat given to a data device."); + } + }, + }, + None::, + (), + ) +} + +struct DataDeviceData { + callback: Arc>, + action_choice: Arc DndAction + Send + 'static>>, +} + +fn implement_data_device( + new_dd: NewResource, + seat: Seat, + callback: Arc>, + action_choice: Arc>, + log: ::slog::Logger, +) -> Resource +where + F: FnMut(DndAction, DndAction) -> DndAction + Send + 'static, + C: FnMut(DataDeviceEvent) + Send + 'static, +{ + use self::wl_data_device::Request; + let dd_data = DataDeviceData { + callback: callback.clone(), + action_choice, + }; + new_dd.implement( + move |req, dd| match req { + Request::StartDrag { + source, + origin, + icon: _, + serial, + } => { + /* TODO: handle the icon */ + if let Some(pointer) = seat.get_pointer() { + 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()), serial); + return; + } + } + debug!(log, "denying drag from client without implicit grab"); + } + Request::SetSelection { source, serial: _ } => { + if let Some(keyboard) = seat.get_keyboard() { + if dd + .client() + .as_ref() + .map(|c| keyboard.has_focus(c)) + .unwrap_or(false) + { + let seat_data = seat.user_data().get::>().unwrap(); + (&mut *callback.lock().unwrap())(DataDeviceEvent::NewSelection(source.clone())); + // The client has kbd focus, it can set the selection + seat_data + .lock() + .unwrap() + .set_selection(source.map(Selection::Client).unwrap_or(Selection::Empty)); + return; + } + } + debug!(log, "denying setting selection by a non-focused client"); + } + Request::Release => { + // Clean up the known devices + seat.user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .known_devices + .retain(|ndd| ndd.is_alive() && (!ndd.equals(&dd))) + } + }, + None::, + dd_data, + ) +} + +/// A simple action chooser for DnD negociation +/// +/// If the preferred action is available, it'll pick it. Otherwise, it'll pick the first +/// available in the following order: Ask, Copy, Move. +pub fn default_action_chooser(available: DndAction, preferred: DndAction) -> DndAction { + // if the preferred action is valid (a single action) and in the available actions, use it + // otherwise, follow a fallback stategy + if [DndAction::Move, DndAction::Copy, DndAction::Ask].contains(&preferred) + && available.contains(preferred) + { + preferred + } else if available.contains(DndAction::Ask) { + DndAction::Ask + } else if available.contains(DndAction::Copy) { + DndAction::Copy + } else if available.contains(DndAction::Move) { + DndAction::Move + } else { + DndAction::empty() + } +} 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::, + (), + ) +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 1d1fd29..b61ba8d 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -15,6 +15,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; pub mod compositor; +pub mod data_device; pub mod output; pub mod seat; pub mod shell; diff --git a/src/wayland/seat/keyboard.rs b/src/wayland/seat/keyboard.rs index 0ca99c6..55c2622 100644 --- a/src/wayland/seat/keyboard.rs +++ b/src/wayland/seat/keyboard.rs @@ -11,7 +11,7 @@ use wayland_server::{ wl_keyboard::{Event, KeyState as WlKeyState, KeymapFormat, Request, WlKeyboard}, wl_surface::WlSurface, }, - NewResource, Resource, + Client, NewResource, Resource, }; use xkbcommon::xkb; pub use xkbcommon::xkb::{keysyms, Keysym}; @@ -112,6 +112,7 @@ struct KbdInternal { state: xkb::State, repeat_rate: i32, repeat_delay: i32, + focus_hook: Box>)>, } // This is OK because all parts of `xkb` will remain on the @@ -119,7 +120,12 @@ struct KbdInternal { unsafe impl Send for KbdInternal {} impl KbdInternal { - fn new(xkb_config: XkbConfig, repeat_rate: i32, repeat_delay: i32) -> Result { + fn new( + xkb_config: XkbConfig, + repeat_rate: i32, + repeat_delay: i32, + focus_hook: Box>)>, + ) -> Result { // we create a new contex for each keyboard because libxkbcommon is actually NOT threadsafe // so confining it inside the KbdInternal allows us to use Rusts mutability rules to make // sure nothing goes wrong. @@ -146,6 +152,7 @@ impl KbdInternal { state, repeat_rate, repeat_delay, + focus_hook, }) } @@ -219,21 +226,26 @@ pub enum Error { } /// Create a keyboard handler from a set of RMLVO rules -pub(crate) fn create_keyboard_handler( +pub(crate) fn create_keyboard_handler( xkb_config: XkbConfig, repeat_delay: i32, repeat_rate: i32, logger: &::slog::Logger, -) -> Result { + focus_hook: F, +) -> Result +where + F: FnMut(Option<&Resource>) + 'static, +{ let log = logger.new(o!("smithay_module" => "xkbcommon_handler")); info!(log, "Initializing a xkbcommon handler with keymap query"; "rules" => xkb_config.rules, "model" => xkb_config.model, "layout" => xkb_config.layout, "variant" => xkb_config.variant, "options" => &xkb_config.options ); - let internal = KbdInternal::new(xkb_config, repeat_rate, repeat_delay).map_err(|_| { - debug!(log, "Loading keymap failed"); - Error::BadKeymap - })?; + let internal = + KbdInternal::new(xkb_config, repeat_rate, repeat_delay, Box::new(focus_hook)).map_err(|_| { + debug!(log, "Loading keymap failed"); + Error::BadKeymap + })?; info!(log, "Loaded Keymap"; "name" => internal.keymap.layouts().next()); @@ -382,6 +394,14 @@ impl KeyboardHandle { keys: keys.clone(), }); }); + { + let KbdInternal { + ref focus, + ref mut focus_hook, + .. + } = *guard; + focus_hook(focus.as_ref()); + } if guard.focus.is_some() { trace!(self.arc.logger, "Focus set to new surface"); } else { @@ -392,6 +412,19 @@ impl KeyboardHandle { } } + /// Check if given client currently has keyboard focus + pub fn has_focus(&self, client: &Client) -> bool { + self.arc + .internal + .lock() + .unwrap() + .focus + .as_ref() + .and_then(|f| f.client()) + .map(|c| c.equals(client)) + .unwrap_or(false) + } + /// Register a new keyboard to this handler /// /// The keymap will automatically be sent to it diff --git a/src/wayland/seat/mod.rs b/src/wayland/seat/mod.rs index 65e0815..68a73b0 100644 --- a/src/wayland/seat/mod.rs +++ b/src/wayland/seat/mod.rs @@ -64,16 +64,26 @@ pub use self::{ pointer::{AxisFrame, PointerGrab, PointerHandle, PointerInnerHandle}, }; -use wayland_server::{protocol::wl_seat, Display, Global, NewResource, Resource}; +use wayland_commons::utils::UserDataMap; + +use wayland_server::{ + protocol::{wl_seat, wl_surface}, + Display, Global, NewResource, Resource, +}; struct Inner { - log: ::slog::Logger, - name: String, pointer: Option, keyboard: Option, known_seats: Vec>, } +pub(crate) struct SeatArc { + inner: Mutex, + user_data: UserDataMap, + pub(crate) log: ::slog::Logger, + name: String, +} + impl Inner { fn compute_caps(&self) -> wl_seat::Capability { let mut caps = wl_seat::Capability::empty(); @@ -101,9 +111,13 @@ impl Inner { /// /// It is directly inserted in the event loop by its `new` method. /// +/// This is an handle to the inner logic, it can be cloned and shared accross +/// threads. +/// /// See module-level documentation for details of use. +#[derive(Clone)] pub struct Seat { - inner: Arc>, + pub(crate) arc: Arc, } impl Seat { @@ -120,20 +134,23 @@ impl Seat { L: Into>, { let log = ::slog_or_stdlog(logger); - let inner = Arc::new(Mutex::new(Inner { + let arc = Arc::new(SeatArc { + inner: Mutex::new(Inner { + pointer: None, + keyboard: None, + known_seats: Vec::new(), + }), log: log.new(o!("smithay_module" => "seat_handler", "seat_name" => name.clone())), name, - pointer: None, - keyboard: None, - known_seats: Vec::new(), - })); - let seat = Seat { inner: inner.clone() }; + user_data: UserDataMap::new(), + }); + let seat = Seat { arc: arc.clone() }; let global = display.create_global(5, move |new_seat, _version| { - let seat = implement_seat(new_seat, inner.clone()); - let mut inner = inner.lock().unwrap(); + let seat = implement_seat(new_seat, arc.clone()); + let mut inner = arc.inner.lock().unwrap(); if seat.version() >= 2 { seat.send(wl_seat::Event::Name { - name: inner.name.clone(), + name: arc.name.clone(), }); } seat.send(wl_seat::Event::Capabilities { @@ -144,6 +161,16 @@ impl Seat { (seat, global) } + /// Attempt to retrieve a `Seat` from an existing resource + pub fn from_resource(seat: &Resource) -> Option { + seat.user_data::>().cloned().map(|arc| Seat { arc }) + } + + /// Acces the `UserDataMap` associated with this `Seat` + pub fn user_data(&self) -> &UserDataMap { + &self.arc.user_data + } + /// Adds the pointer capability to this seat /// /// You are provided a `PointerHandle`, which allows you to send input events @@ -153,7 +180,7 @@ impl Seat { /// will overwrite it, and will be seen by the clients as if the /// mouse was unplugged and a new one was plugged. pub fn add_pointer(&mut self) -> PointerHandle { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.arc.inner.lock().unwrap(); let pointer = self::pointer::create_pointer_handler(); if inner.pointer.is_some() { // there is already a pointer, remove it and notify the clients @@ -166,11 +193,16 @@ impl Seat { pointer } + /// Access the pointer of this seat if any + pub fn get_pointer(&self) -> Option { + self.arc.inner.lock().unwrap().pointer.clone() + } + /// Remove the pointer capability from this seat /// /// Clients will be appropriately notified. pub fn remove_pointer(&mut self) { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.arc.inner.lock().unwrap(); if inner.pointer.is_some() { inner.pointer = None; inner.send_all_caps(); @@ -205,18 +237,32 @@ impl Seat { /// }, /// 1000, /// 500, + /// |seat, focus| { + /// /* This closure is called whenever the keyboard focus + /// * changes, with the new focus as argument */ + /// } /// ) /// .expect("Failed to initialize the keyboard"); /// ``` - pub fn add_keyboard( + pub fn add_keyboard( &mut self, xkb_config: keyboard::XkbConfig, repeat_delay: i32, repeat_rate: i32, - ) -> Result { - let mut inner = self.inner.lock().unwrap(); - let keyboard = - self::keyboard::create_keyboard_handler(xkb_config, repeat_delay, repeat_rate, &inner.log)?; + mut focus_hook: F, + ) -> Result + where + F: FnMut(&Seat, Option<&Resource>) + 'static, + { + let me = self.clone(); + let mut inner = self.arc.inner.lock().unwrap(); + let keyboard = self::keyboard::create_keyboard_handler( + xkb_config, + repeat_delay, + repeat_rate, + &self.arc.log, + move |focus| focus_hook(&me, focus), + )?; if inner.keyboard.is_some() { // there is already a keyboard, remove it and notify the clients // of the change @@ -228,11 +274,16 @@ impl Seat { Ok(keyboard) } + /// Access the keyboard of this seat if any + pub fn get_keyboard(&self) -> Option { + self.arc.inner.lock().unwrap().keyboard.clone() + } + /// Remove the keyboard capability from this seat /// /// Clients will be appropriately notified. pub fn remove_keyboard(&mut self) { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.arc.inner.lock().unwrap(); if inner.keyboard.is_some() { inner.keyboard = None; inner.send_all_caps(); @@ -241,19 +292,23 @@ impl Seat { /// Checks whether a given `WlSeat` is associated with this `Seat` pub fn owns(&self, seat: &Resource) -> bool { - let inner = self.inner.lock().unwrap(); + let inner = self.arc.inner.lock().unwrap(); inner.known_seats.iter().any(|s| s.equals(seat)) } } -fn implement_seat( - new_seat: NewResource, - inner: Arc>, -) -> Resource { - let dest_inner = inner.clone(); +impl ::std::cmp::PartialEq for Seat { + fn eq(&self, other: &Seat) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +fn implement_seat(new_seat: NewResource, arc: Arc) -> Resource { + let dest_arc = arc.clone(); new_seat.implement( - move |request, _seat| { - let inner = inner.lock().unwrap(); + move |request, seat| { + let arc = seat.user_data::>().unwrap(); + let inner = arc.inner.lock().unwrap(); match request { wl_seat::Request::GetPointer { id } => { let pointer = self::pointer::implement_pointer(id, inner.pointer.as_ref()); @@ -281,12 +336,13 @@ fn implement_seat( } }, Some(move |seat| { - dest_inner + dest_arc + .inner .lock() .unwrap() .known_seats .retain(|s| !s.equals(&seat)); }), - (), + arc, ) } diff --git a/src/wayland/seat/pointer.rs b/src/wayland/seat/pointer.rs index ff9f6dc..39978d3 100644 --- a/src/wayland/seat/pointer.rs +++ b/src/wayland/seat/pointer.rs @@ -18,8 +18,10 @@ enum GrabStatus { struct PointerInternal { known_pointers: Vec>, focus: Option<(Resource, (f64, f64))>, + pending_focus: Option<(Resource, (f64, f64))>, location: (f64, f64), grab: GrabStatus, + pressed_buttons: Vec, } impl PointerInternal { @@ -27,8 +29,10 @@ impl PointerInternal { PointerInternal { known_pointers: Vec::new(), focus: None, + pending_focus: None, location: (0.0, 0.0), grab: GrabStatus::None, + pressed_buttons: Vec::new(), } } @@ -136,7 +140,9 @@ impl PointerHandle { serial: u32, time: u32, ) { - self.inner.lock().unwrap().with_grab(move |mut handle, grab| { + let mut inner = self.inner.lock().unwrap(); + inner.pending_focus = focus.clone(); + inner.with_grab(move |mut handle, grab| { grab.motion(&mut handle, location, focus, serial, time); }); } @@ -146,7 +152,16 @@ impl PointerHandle { /// This will internally send the appropriate button event to the client /// objects matching with the currently focused surface. pub fn button(&self, button: u32, state: ButtonState, serial: u32, time: u32) { - self.inner.lock().unwrap().with_grab(|mut handle, grab| { + let mut inner = self.inner.lock().unwrap(); + match state { + ButtonState::Pressed => { + inner.pressed_buttons.push(button); + } + ButtonState::Released => { + inner.pressed_buttons.retain(|b| *b != button); + } + } + inner.with_grab(|mut handle, grab| { grab.button(&mut handle, button, state, serial, time); }); } @@ -215,8 +230,14 @@ impl<'a> PointerInnerHandle<'a> { } /// Remove any current grab on this pointer, resetting it to the default behavior - pub fn unset_grab(&mut self) { + /// + /// This will also restore the focus of the underlying pointer + pub fn unset_grab(&mut self, serial: u32, time: u32) { self.inner.grab = GrabStatus::None; + // restore the focus + let location = self.current_location(); + let focus = self.inner.pending_focus.take(); + self.motion(location, focus, serial, time); } /// Access the current focus of this pointer @@ -229,6 +250,14 @@ impl<'a> PointerInnerHandle<'a> { self.inner.location } + /// A list of the currently physically pressed buttons + /// + /// This still includes buttons that your grab have intercepted and not sent + /// to the client. + pub fn current_pressed(&self) -> &[u32] { + &self.inner.pressed_buttons + } + /// Notify that the pointer moved /// /// You provide the new location of the pointer, in the form of: @@ -542,7 +571,6 @@ impl PointerGrab for DefaultGrab { handle.set_grab( serial, ClickGrab { - buttons: vec![button], current_focus, pending_focus: None, }, @@ -559,7 +587,6 @@ impl PointerGrab for DefaultGrab { // In case the user maintains several simultaneous clicks, release // the grab once all are released. struct ClickGrab { - buttons: Vec, current_focus: Option<(Resource, (f64, f64))>, pending_focus: Option<(Resource, (f64, f64))>, } @@ -585,17 +612,10 @@ impl PointerGrab for ClickGrab { serial: u32, time: u32, ) { - match state { - ButtonState::Pressed => self.buttons.push(button), - ButtonState::Released => self.buttons.retain(|b| *b != button), - } handle.button(button, state, serial, time); - if self.buttons.is_empty() { + if handle.current_pressed().len() == 0 { // no more buttons are pressed, release the grab - handle.unset_grab(); - // restore the focus - let location = handle.current_location(); - handle.motion(location, self.pending_focus.clone(), serial, time); + handle.unset_grab(serial, time); } } fn axis(&mut self, handle: &mut PointerInnerHandle, details: AxisFrame) {