//! Access the clipboard. use std::sync::Mutex; use std::borrow::Cow; use crate::Control; use crate::core::clipboard::Kind; use crate::core::clipboard::{DndSource, DynIconSurface}; use log::trace; use std::sync::Arc; use winit::dpi::LogicalSize; use winit::window::{Window, WindowId}; use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; use window_clipboard::{ dnd::DndProvider, mime::{self, ClipboardData, ClipboardStoreData}, }; /// A buffer for short-term storage and transfer within and between /// applications. pub struct Clipboard { state: State, pub(crate) requested_logical_size: Arc>>>, } pub(crate) struct StartDnd { pub(crate) internal: bool, pub(crate) source_surface: Option, pub(crate) icon_surface: Option, pub(crate) content: Box, pub(crate) actions: DndAction, } enum State { Connected { clipboard: window_clipboard::Clipboard, sender: ControlSender, // Held until drop to satisfy the safety invariants of // `window_clipboard::Clipboard`. // // Note that the field ordering is load-bearing. #[allow(dead_code)] window: Arc, queued_events: Vec, }, Unavailable, } #[derive(Debug, Clone)] pub(crate) struct ControlSender { pub(crate) sender: iced_futures::futures::channel::mpsc::UnboundedSender, pub(crate) proxy: winit::event_loop::EventLoopProxy, } impl dnd::Sender for ControlSender { fn send( &self, event: dnd::DndEvent, ) -> Result<(), std::sync::mpsc::SendError>> { let res = self.sender .unbounded_send(Control::Dnd(event)) .map_err(|_err| { std::sync::mpsc::SendError(dnd::DndEvent::Offer( None, dnd::OfferEvent::Leave, )) }); self.proxy.wake_up(); res } } impl Clipboard { /// Creates a new [`Clipboard`] for the given window. pub(crate) fn connect( window: Arc, sender: ControlSender, ) -> Clipboard { #[allow(unsafe_code)] let state = unsafe { window_clipboard::Clipboard::connect(window.as_ref()) } .ok() .map(|c| State::Connected { clipboard: c, sender: sender.clone(), window, queued_events: Vec::new(), }) .unwrap_or(State::Unavailable); #[cfg(target_os = "linux")] if let State::Connected { clipboard, .. } = &state { clipboard.init_dnd(Box::new(sender)); } Clipboard { state, requested_logical_size: Arc::new(Mutex::new(None)), } } pub(crate) fn proxy(&self) -> Option { if let State::Connected { sender: ControlSender { proxy, .. }, .. } = &self.state { Some(proxy.clone()) } else { None } } /// Creates a new [`Clipboard`] that isn't associated with a window. /// This clipboard will never contain a copied value. pub fn unconnected() -> Clipboard { Clipboard { state: State::Unavailable, requested_logical_size: Arc::new(Mutex::new(None)), } } pub(crate) fn get_queued(&mut self) -> Vec { match &mut self.state { State::Connected { queued_events, .. } => { std::mem::take(queued_events) } State::Unavailable => { log::error!("Invalid request for queued dnd events"); Vec::::new() } } } /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self, kind: Kind) -> Option { match &self.state { State::Connected { clipboard, .. } => match kind { Kind::Standard => clipboard.read().ok(), Kind::Primary => clipboard.read_primary().and_then(Result::ok), }, State::Unavailable => None, } } /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, kind: Kind, contents: String) { match &mut self.state { State::Connected { clipboard, .. } => { let result = match kind { Kind::Standard => clipboard.write(contents), Kind::Primary => { clipboard.write_primary(contents).unwrap_or(Ok(())) } }; match result { Ok(()) => {} Err(error) => { log::warn!("error writing to clipboard: {error}"); } } } State::Unavailable => {} } } /// Returns the identifier of the window used to create the [`Clipboard`], if any. pub fn window_id(&self) -> Option { match &self.state { State::Connected { window, .. } => Some(window.id()), State::Unavailable => None, } } pub fn read_data( &self, kind: Kind, mimes: Vec, ) -> Option<(Vec, String)> { match (&self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { clipboard.read_raw(mimes).and_then(|res| res.ok()) } (State::Connected { clipboard, .. }, Kind::Primary) => { clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) } (State::Unavailable, _) => None, } } pub fn write_data( &mut self, kind: Kind, contents: ClipboardStoreData< Box, >, ) { match (&mut self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { _ = clipboard.write_data(contents) } (State::Connected { clipboard, .. }, Kind::Primary) => { _ = clipboard.write_primary_data(contents) } (State::Unavailable, _) => {} } } pub fn start_dnd( &mut self, internal: bool, source_surface: Option, icon_surface: Option, content: Box, actions: DndAction, ) { match &mut self.state { State::Connected { queued_events, sender, .. } => { log::debug!( "clipboard::start_dnd queued internal={} source={:?}", internal, source_surface ); _ = sender.sender.unbounded_send(crate::Control::StartDnd); queued_events.push(StartDnd { internal, source_surface, icon_surface, content, actions, }); } State::Unavailable => {} } } pub fn register_dnd_destination( &self, surface: DndSurface, rectangles: Vec, ) { match &self.state { State::Connected { clipboard, .. } => { trace!( target: "iced::winit::clipboard", "register destination surface={:?} count={}", surface, rectangles.len() ); for rect in &rectangles { trace!( target: "iced::winit::clipboard", "rect id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) actions={:?} preferred={:?}", rect.id, rect.rectangle.x, rect.rectangle.y, rect.rectangle.width, rect.rectangle.height, rect.actions, rect.preferred ); } _ = clipboard.register_dnd_destination(surface, rectangles) } State::Unavailable => {} } } pub fn end_dnd(&self) { match &self.state { State::Connected { clipboard, .. } => _ = clipboard.end_dnd(), State::Unavailable => {} } } pub fn peek_dnd(&self, mime: String) -> Option<(Vec, String)> { match &self.state { State::Connected { clipboard, .. } => clipboard .peek_offer::(Some(Cow::Owned(mime))) .ok() .map(|res| (res.0, res.1)), State::Unavailable => None, } } pub fn set_action(&self, action: DndAction) { match &self.state { State::Connected { clipboard, .. } => { _ = clipboard.set_action(action) } State::Unavailable => {} } } pub fn request_logical_window_size(&self, width: f32, height: f32) { let mut logical_size = self.requested_logical_size.lock().unwrap(); *logical_size = Some(LogicalSize::new(width, height)); } pub(crate) fn start_dnd_winit( &self, internal: bool, source_surface: DndSurface, icon_surface: Option, content: Box, actions: DndAction, ) { match &self.state { State::Connected { clipboard, .. } => { _ = clipboard.start_dnd( internal, source_surface, icon_surface, content, actions, ) } State::Unavailable => {} } } } impl crate::core::Clipboard for Clipboard { fn read(&self, kind: Kind) -> Option { match (&self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { clipboard.read().ok() } (State::Connected { clipboard, .. }, Kind::Primary) => { clipboard.read_primary().and_then(|res| res.ok()) } (State::Unavailable, _) => None, } } fn write(&mut self, kind: Kind, contents: String) { match (&mut self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { _ = clipboard.write(contents) } (State::Connected { clipboard, .. }, Kind::Primary) => { _ = clipboard.write_primary(contents) } (State::Unavailable, _) => {} } } fn read_data( &self, kind: Kind, mimes: Vec, ) -> Option<(Vec, String)> { match (&self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { clipboard.read_raw(mimes).and_then(|res| res.ok()) } (State::Connected { clipboard, .. }, Kind::Primary) => { clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) } (State::Unavailable, _) => None, } } fn write_data( &mut self, kind: Kind, contents: ClipboardStoreData< Box, >, ) { match (&mut self.state, kind) { (State::Connected { clipboard, .. }, Kind::Standard) => { _ = clipboard.write_data(contents) } (State::Connected { clipboard, .. }, Kind::Primary) => { _ = clipboard.write_primary_data(contents) } (State::Unavailable, _) => {} } } fn start_dnd( &mut self, internal: bool, source_surface: Option, icon_surface: Option, content: Box, actions: DndAction, ) { match &mut self.state { State::Connected { queued_events, sender, .. } => { log::debug!( "clipboard::start_dnd queued internal={} source={:?}", internal, source_surface ); _ = sender.sender.unbounded_send(Control::StartDnd); queued_events.push(StartDnd { internal, source_surface, icon_surface, content, actions, }); } State::Unavailable => {} } } fn register_dnd_destination( &self, surface: DndSurface, rectangles: Vec, ) { match &self.state { State::Connected { clipboard, .. } => { trace!( target: "iced::winit::clipboard", "register destination surface={:?} count={}", surface, rectangles.len() ); for rect in &rectangles { trace!( target: "iced::winit::clipboard", "rect id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) actions={:?} preferred={:?}", rect.id, rect.rectangle.x, rect.rectangle.y, rect.rectangle.width, rect.rectangle.height, rect.actions, rect.preferred ); } _ = clipboard.register_dnd_destination(surface, rectangles) } State::Unavailable => {} } } fn end_dnd(&self) { match &self.state { State::Connected { clipboard, .. } => _ = clipboard.end_dnd(), State::Unavailable => {} } } fn peek_dnd(&self, mime: String) -> Option<(Vec, String)> { match &self.state { State::Connected { clipboard, .. } => clipboard .peek_offer::(Some(Cow::Owned(mime))) .ok() .map(|res| (res.0, res.1)), State::Unavailable => None, } } fn set_action(&self, action: DndAction) { match &self.state { State::Connected { clipboard, .. } => { _ = clipboard.set_action(action) } State::Unavailable => {} } } fn request_logical_window_size(&self, width: f32, height: f32) { let mut logical_size = self.requested_logical_size.lock().unwrap(); *logical_size = Some(LogicalSize::new(width, height)); } }