1use std::rc::Rc;
10
11use inset_embedder::{
12 ClipboardEvent, ClipboardItem, ClipboardItemsFuture, ClipboardWriteFuture, Content,
13 CopyOptions, DataTransferItem,
14};
15use inset_foundation::{App, Handle, Task};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct ClipboardData {
23 pub text: Option<String>,
29}
30
31impl ClipboardData {
32 pub fn new(text: impl Into<String>) -> ClipboardData {
34 ClipboardData {
35 text: Some(text.into()),
36 }
37 }
38}
39
40pub struct Clipboard;
42
43impl Clipboard {
44 pub const K_TEXT_PLAIN: &'static str = "text/plain";
48
49 pub fn set_data(app: &mut App, data: ClipboardData) -> Task<()> {
51 let Some(text) = data.text else {
52 return Task::ready(());
53 };
54 let item = ClipboardItem {
55 representations: vec![(
56 Self::K_TEXT_PLAIN.to_owned(),
57 Content::Data(text.into_bytes()),
58 )],
59 };
60 match Self::write(app, vec![item]) {
61 Some(written) => app.spawn(async move |_cx| {
62 let _refused = written.await;
64 }),
65 None => Task::ready(()),
66 }
67 }
68
69 pub fn get_data(app: &mut App, format: &str) -> Task<Option<ClipboardData>> {
76 if format != Self::K_TEXT_PLAIN {
77 return Task::ready(None);
78 }
79 match Self::readable(app) {
80 Some(readable) => {
81 app.spawn(async move |_cx| readable.text().await.map(ClipboardData::new))
82 }
83 None => Task::ready(None),
84 }
85 }
86
87 pub fn has_strings(app: &mut App) -> Task<bool> {
89 match Self::readable(app) {
90 Some(readable) => app.spawn(async move |_cx| readable.holds_text().await),
91 None => Task::ready(false),
92 }
93 }
94
95 pub fn handle_event<R>(
103 app: &mut App,
104 event: ClipboardEvent,
105 handle: impl FnOnce(&mut App) -> R,
106 ) -> R {
107 let in_hand = Self::event_in_hand(app);
108 let outer = app.get_mut(in_hand).0.replace(Rc::new(event));
109 let result = handle(app);
110 app.get_mut(in_hand).0 = outer;
111 result
112 }
113
114 fn write(app: &mut App, items: Vec<ClipboardItem>) -> Option<ClipboardWriteFuture> {
117 let in_hand = Self::event_in_hand(app);
118 match app.get(in_hand).0.as_deref() {
119 Some(ClipboardEvent::Copy(request) | ClipboardEvent::Cut(request)) => {
120 Some(request.write(items, CopyOptions::new()))
121 }
122 Some(ClipboardEvent::Paste(_)) | None => app
123 .platform()
124 .clipboard()
125 .map(|clipboard| clipboard.write(items, CopyOptions::new())),
126 }
127 }
128
129 fn readable(app: &mut App) -> Option<Readable> {
132 let in_hand = Self::event_in_hand(app);
133 match app.get(in_hand).0.clone() {
134 Some(event) if matches!(*event, ClipboardEvent::Paste(_)) => {
135 Some(Readable::Pasted(event))
136 }
137 _ => app
138 .platform()
139 .clipboard()
140 .map(|clipboard| Readable::Listing(clipboard.items())),
141 }
142 }
143
144 fn event_in_hand(app: &mut App) -> Handle<EventInHand> {
145 app.singleton()
146 }
147}
148
149#[derive(Default)]
151struct EventInHand(Option<Rc<ClipboardEvent>>);
152
153enum Readable {
155 Pasted(Rc<ClipboardEvent>),
157 Listing(ClipboardItemsFuture),
159}
160
161impl Readable {
162 async fn text(self) -> Option<String> {
165 match self {
166 Readable::Pasted(paste) => text_of(pasted_items(&paste)).await,
167 Readable::Listing(items) => text_of(&items.await).await,
168 }
169 }
170
171 async fn holds_text(self) -> bool {
173 match self {
174 Readable::Pasted(paste) => holds_text(pasted_items(&paste)),
175 Readable::Listing(items) => holds_text(&items.await),
176 }
177 }
178}
179
180fn pasted_items(paste: &ClipboardEvent) -> &[Box<dyn DataTransferItem>] {
182 match paste {
183 ClipboardEvent::Paste(items) => items,
184 ClipboardEvent::Copy(_) | ClipboardEvent::Cut(_) => &[],
185 }
186}
187
188fn offers_text(item: &dyn DataTransferItem) -> bool {
189 item.types()
190 .iter()
191 .any(|mime| mime == Clipboard::K_TEXT_PLAIN)
192}
193
194fn holds_text(items: &[Box<dyn DataTransferItem>]) -> bool {
195 items.iter().any(|item| offers_text(item.as_ref()))
196}
197
198async fn text_of(items: &[Box<dyn DataTransferItem>]) -> Option<String> {
199 let item = items.iter().find(|item| offers_text(item.as_ref()))?;
200 let bytes = item.read(Clipboard::K_TEXT_PLAIN).await.ok()?;
201 Some(String::from_utf8_lossy(&bytes).into_owned())
202}
203
204#[cfg(test)]
205mod tests {
206 use std::cell::RefCell;
207 use std::rc::Rc;
208 use std::task::{Context, Poll, Waker};
209
210 use inset_embedder::{Clipboard as _, CopyRequest};
211 use inset_foundation::AppCell;
212 use inset_test::{MockClipboard, TestPlatform};
213
214 use super::*;
215
216 fn app_with(clipboard: &MockClipboard) -> Rc<AppCell> {
217 let clipboard: Rc<dyn inset_embedder::Clipboard> = Rc::new(clipboard.clone());
218 AppCell::with_platform(Rc::new(TestPlatform::new().with_clipboard(clipboard)))
219 }
220
221 fn awaited<T: 'static>(cell: &Rc<AppCell>, task: Task<T>) -> Option<T> {
223 let answer = Rc::new(RefCell::new(None));
224 let out = Rc::clone(&answer);
225 cell.borrow()
226 .spawn(async move |_cx| *out.borrow_mut() = Some(task.await));
227 cell.checkpoint();
228 answer.take()
229 }
230
231 fn listed(clipboard: &MockClipboard) -> Vec<Box<dyn DataTransferItem>> {
233 let mut items = clipboard.items();
234 match items.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
235 Poll::Ready(items) => items,
236 Poll::Pending => panic!("a mock clipboard lists at once"),
237 }
238 }
239
240 #[test]
242 fn get_data_returns_text() {
243 let mock_clipboard = MockClipboard::new();
244 let cell = app_with(&mock_clipboard);
245 mock_clipboard.set_clipboard_data(Some("Hello world".to_owned()));
246
247 let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
248 let data = awaited(&cell, data).expect("the read completed");
249
250 assert_eq!(data, Some(ClipboardData::new("Hello world")));
251 }
252
253 #[test]
255 fn get_data_returns_none() {
256 let mock_clipboard = MockClipboard::new();
257 let cell = app_with(&mock_clipboard);
258 mock_clipboard.set_clipboard_data(None);
259
260 let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
261 let data = awaited(&cell, data).expect("the read completed");
262
263 assert_eq!(data, None);
264 }
265
266 #[test]
268 fn set_data_sets_text() {
269 let mock_clipboard = MockClipboard::new();
270 let cell = app_with(&mock_clipboard);
271
272 let written =
273 Clipboard::set_data(&mut cell.borrow_mut(), ClipboardData::new("Hello world"));
274 awaited(&cell, written).expect("the write completed");
275
276 assert_eq!(
277 mock_clipboard.clipboard_data().as_deref(),
278 Some("Hello world")
279 );
280 }
281
282 #[test]
283 fn the_text_is_the_first_item_offered_as_text() {
284 let mock_clipboard = MockClipboard::new();
285 let cell = app_with(&mock_clipboard);
286 let png = ClipboardItem {
287 representations: vec![("image/png".to_owned(), Content::Data(vec![0x89]))],
288 };
289 let text = ClipboardItem {
290 representations: vec![("text/plain".to_owned(), Content::Data(b"second".to_vec()))],
291 };
292 drop(mock_clipboard.write(vec![png, text], CopyOptions::new()));
293
294 let has_strings = Clipboard::has_strings(&mut cell.borrow_mut());
295 assert_eq!(awaited(&cell, has_strings), Some(true));
296 let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
297 assert_eq!(
298 awaited(&cell, data).expect("the read completed"),
299 Some(ClipboardData::new("second"))
300 );
301 }
302
303 #[test]
304 fn a_paste_being_handled_is_read_in_place_of_the_clipboard() {
305 let on_clipboard = MockClipboard::new();
306 on_clipboard.set_clipboard_data(Some("on the clipboard".to_owned()));
307 let pasted = MockClipboard::new();
308 pasted.set_clipboard_data(Some("pasted".to_owned()));
309 let cell = app_with(&on_clipboard);
310
311 let data = Clipboard::handle_event(
312 &mut cell.borrow_mut(),
313 ClipboardEvent::Paste(listed(&pasted)),
314 |app| Clipboard::get_data(app, Clipboard::K_TEXT_PLAIN),
315 );
316 assert_eq!(
317 awaited(&cell, data).expect("the read completed"),
318 Some(ClipboardData::new("pasted"))
319 );
320
321 let after = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
322 assert_eq!(
323 awaited(&cell, after).expect("the read completed"),
324 Some(ClipboardData::new("on the clipboard")),
325 "a read after the paste reads the clipboard"
326 );
327 }
328
329 #[test]
330 fn a_copy_being_handled_writes_through_its_request() {
331 let on_clipboard = MockClipboard::new();
332 let copied = MockClipboard::new();
333 let cell = app_with(&on_clipboard);
334 let into = copied.clone();
335 let request = CopyRequest::new(move |items, options| into.write(items, options));
336
337 let written = Clipboard::handle_event(
338 &mut cell.borrow_mut(),
339 ClipboardEvent::Copy(request),
340 |app| Clipboard::set_data(app, ClipboardData::new("copied")),
341 );
342 awaited(&cell, written).expect("the write completed");
343
344 assert_eq!(copied.clipboard_data().as_deref(), Some("copied"));
345 assert_eq!(on_clipboard.clipboard_data(), None);
346 }
347}