iced_winit/
clipboard.rs

1//! Access the clipboard.
2
3use std::sync::Mutex;
4use std::{any::Any, borrow::Cow};
5
6use crate::core::clipboard::Kind;
7use crate::core::clipboard::{DndSource, DynIconSurface};
8use std::sync::Arc;
9use winit::dpi::LogicalSize;
10use winit::window::{Window, WindowId};
11
12use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon};
13use window_clipboard::{
14    dnd::DndProvider,
15    mime::{self, ClipboardData, ClipboardStoreData},
16};
17
18/// A buffer for short-term storage and transfer within and between
19/// applications.
20#[allow(missing_debug_implementations)]
21pub struct Clipboard {
22    state: State,
23    pub(crate) requested_logical_size: Arc<Mutex<Option<LogicalSize<f32>>>>,
24}
25
26pub(crate) struct StartDnd {
27    pub(crate) internal: bool,
28    pub(crate) source_surface: Option<DndSource>,
29    pub(crate) icon_surface: Option<DynIconSurface>,
30    pub(crate) content: Box<dyn mime::AsMimeTypes + Send + 'static>,
31    pub(crate) actions: DndAction,
32}
33
34enum State {
35    Connected {
36        clipboard: window_clipboard::Clipboard,
37        sender: ControlSender,
38        // Held until drop to satisfy the safety invariants of
39        // `window_clipboard::Clipboard`.
40        //
41        // Note that the field ordering is load-bearing.
42        #[allow(dead_code)]
43        window: Arc<dyn Window>,
44        queued_events: Vec<StartDnd>,
45    },
46    Unavailable,
47}
48
49#[derive(Debug, Clone)]
50pub(crate) struct ControlSender {
51    pub(crate) sender: iced_futures::futures::channel::mpsc::UnboundedSender<
52        crate::program::Control,
53    >,
54    pub(crate) proxy: winit::event_loop::EventLoopProxy,
55}
56
57impl dnd::Sender<DndSurface> for ControlSender {
58    fn send(
59        &self,
60        event: dnd::DndEvent<DndSurface>,
61    ) -> Result<(), std::sync::mpsc::SendError<dnd::DndEvent<DndSurface>>> {
62        let res = self
63            .sender
64            .unbounded_send(crate::program::Control::Dnd(event))
65            .map_err(|_err| {
66                std::sync::mpsc::SendError(dnd::DndEvent::Offer(
67                    None,
68                    dnd::OfferEvent::Leave,
69                ))
70            });
71        self.proxy.wake_up();
72        res
73    }
74}
75
76impl Clipboard {
77    /// Creates a new [`Clipboard`] for the given window.
78    pub(crate) fn connect(
79        window: Arc<dyn Window>,
80        sender: ControlSender,
81    ) -> Clipboard {
82        #[allow(unsafe_code)]
83        let state =
84            unsafe { window_clipboard::Clipboard::connect(window.as_ref()) }
85                .ok()
86                .map(|c| State::Connected {
87                    clipboard: c,
88                    sender: sender.clone(),
89                    window,
90                    queued_events: Vec::new(),
91                })
92                .unwrap_or(State::Unavailable);
93
94        #[cfg(target_os = "linux")]
95        if let State::Connected { clipboard, .. } = &state {
96            clipboard.init_dnd(Box::new(sender));
97        }
98
99        Clipboard {
100            state,
101            requested_logical_size: Arc::new(Mutex::new(None)),
102        }
103    }
104
105    pub(crate) fn proxy(&self) -> Option<winit::event_loop::EventLoopProxy> {
106        if let State::Connected {
107            sender: ControlSender { proxy, .. },
108            ..
109        } = &self.state
110        {
111            Some(proxy.clone())
112        } else {
113            None
114        }
115    }
116
117    /// Creates a new [`Clipboard`] that isn't associated with a window.
118    /// This clipboard will never contain a copied value.
119    pub fn unconnected() -> Clipboard {
120        Clipboard {
121            state: State::Unavailable,
122            requested_logical_size: Arc::new(Mutex::new(None)),
123        }
124    }
125
126    pub(crate) fn get_queued(&mut self) -> Vec<StartDnd> {
127        match &mut self.state {
128            State::Connected { queued_events, .. } => {
129                std::mem::take(queued_events)
130            }
131            State::Unavailable => {
132                log::error!("Invalid request for queued dnd events");
133                Vec::<StartDnd>::new()
134            }
135        }
136    }
137
138    /// Reads the current content of the [`Clipboard`] as text.
139    pub fn read(&self, kind: Kind) -> Option<String> {
140        match &self.state {
141            State::Connected { clipboard, .. } => match kind {
142                Kind::Standard => clipboard.read().ok(),
143                Kind::Primary => clipboard.read_primary().and_then(Result::ok),
144            },
145            State::Unavailable => None,
146        }
147    }
148
149    /// Writes the given text contents to the [`Clipboard`].
150    pub fn write(&mut self, kind: Kind, contents: String) {
151        match &mut self.state {
152            State::Connected { clipboard, .. } => {
153                let result = match kind {
154                    Kind::Standard => clipboard.write(contents),
155                    Kind::Primary => {
156                        clipboard.write_primary(contents).unwrap_or(Ok(()))
157                    }
158                };
159
160                match result {
161                    Ok(()) => {}
162                    Err(error) => {
163                        log::warn!("error writing to clipboard: {error}");
164                    }
165                }
166            }
167            State::Unavailable => {}
168        }
169    }
170
171    /// Returns the identifier of the window used to create the [`Clipboard`], if any.
172    pub fn window_id(&self) -> Option<WindowId> {
173        match &self.state {
174            State::Connected { window, .. } => Some(window.id()),
175            State::Unavailable => None,
176        }
177    }
178
179    pub(crate) fn start_dnd_winit(
180        &self,
181        internal: bool,
182        source_surface: DndSurface,
183        icon_surface: Option<Icon>,
184        content: Box<dyn mime::AsMimeTypes + Send + 'static>,
185        actions: DndAction,
186    ) {
187        match &self.state {
188            State::Connected { clipboard, .. } => {
189                _ = clipboard.start_dnd(
190                    internal,
191                    source_surface,
192                    icon_surface,
193                    content,
194                    actions,
195                )
196            }
197            State::Unavailable => {}
198        }
199    }
200}
201
202impl crate::core::Clipboard for Clipboard {
203    fn read(&self, kind: Kind) -> Option<String> {
204        match (&self.state, kind) {
205            (State::Connected { clipboard, .. }, Kind::Standard) => {
206                clipboard.read().ok()
207            }
208            (State::Connected { clipboard, .. }, Kind::Primary) => {
209                clipboard.read_primary().and_then(|res| res.ok())
210            }
211            (State::Unavailable, _) => None,
212        }
213    }
214
215    fn write(&mut self, kind: Kind, contents: String) {
216        match (&mut self.state, kind) {
217            (State::Connected { clipboard, .. }, Kind::Standard) => {
218                _ = clipboard.write(contents)
219            }
220            (State::Connected { clipboard, .. }, Kind::Primary) => {
221                _ = clipboard.write_primary(contents)
222            }
223            (State::Unavailable, _) => {}
224        }
225    }
226    fn read_data(
227        &self,
228        kind: Kind,
229        mimes: Vec<String>,
230    ) -> Option<(Vec<u8>, String)> {
231        match (&self.state, kind) {
232            (State::Connected { clipboard, .. }, Kind::Standard) => {
233                clipboard.read_raw(mimes).and_then(|res| res.ok())
234            }
235            (State::Connected { clipboard, .. }, Kind::Primary) => {
236                clipboard.read_primary_raw(mimes).and_then(|res| res.ok())
237            }
238            (State::Unavailable, _) => None,
239        }
240    }
241
242    fn write_data(
243        &mut self,
244        kind: Kind,
245        contents: ClipboardStoreData<
246            Box<dyn Send + Sync + 'static + mime::AsMimeTypes>,
247        >,
248    ) {
249        match (&mut self.state, kind) {
250            (State::Connected { clipboard, .. }, Kind::Standard) => {
251                _ = clipboard.write_data(contents)
252            }
253            (State::Connected { clipboard, .. }, Kind::Primary) => {
254                _ = clipboard.write_primary_data(contents)
255            }
256            (State::Unavailable, _) => {}
257        }
258    }
259
260    fn start_dnd(
261        &mut self,
262        internal: bool,
263        source_surface: Option<DndSource>,
264        icon_surface: Option<DynIconSurface>,
265        content: Box<dyn mime::AsMimeTypes + Send + 'static>,
266        actions: DndAction,
267    ) {
268        match &mut self.state {
269            State::Connected {
270                queued_events,
271                sender,
272                ..
273            } => {
274                _ = sender
275                    .sender
276                    .unbounded_send(crate::program::Control::StartDnd);
277                queued_events.push(StartDnd {
278                    internal,
279                    source_surface,
280                    icon_surface,
281                    content,
282                    actions,
283                });
284            }
285            State::Unavailable => {}
286        }
287    }
288
289    fn register_dnd_destination(
290        &self,
291        surface: DndSurface,
292        rectangles: Vec<DndDestinationRectangle>,
293    ) {
294        match &self.state {
295            State::Connected { clipboard, .. } => {
296                _ = clipboard.register_dnd_destination(surface, rectangles)
297            }
298            State::Unavailable => {}
299        }
300    }
301
302    fn end_dnd(&self) {
303        match &self.state {
304            State::Connected { clipboard, .. } => _ = clipboard.end_dnd(),
305            State::Unavailable => {}
306        }
307    }
308
309    fn peek_dnd(&self, mime: String) -> Option<(Vec<u8>, String)> {
310        match &self.state {
311            State::Connected { clipboard, .. } => clipboard
312                .peek_offer::<ClipboardData>(Some(Cow::Owned(mime)))
313                .ok()
314                .map(|res| (res.0, res.1)),
315            State::Unavailable => None,
316        }
317    }
318
319    fn set_action(&self, action: DndAction) {
320        match &self.state {
321            State::Connected { clipboard, .. } => {
322                _ = clipboard.set_action(action)
323            }
324            State::Unavailable => {}
325        }
326    }
327
328    fn request_logical_window_size(&self, width: f32, height: f32) {
329        let mut logical_size = self.requested_logical_size.lock().unwrap();
330        *logical_size = Some(LogicalSize::new(width, height));
331    }
332}