From ab91a4a7055db567e8f54f2d90d449072f85f5d7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:23:43 -0800 Subject: [PATCH] Clipboard: a paste after a late answer reads the clipboard again The app that answers a late read may have lost the clipboard while the paste waited. Once the answer comes, the paste makes the same read again and uses the new answer, for arboard's reads and the core protocol's text read alike. --- apps/vectorcraft/src/clipboard.rs | 74 ++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/apps/vectorcraft/src/clipboard.rs b/apps/vectorcraft/src/clipboard.rs index fe0586312..575660028 100644 --- a/apps/vectorcraft/src/clipboard.rs +++ b/apps/vectorcraft/src/clipboard.rs @@ -796,16 +796,26 @@ mod portable { } /// What a paste of one of `mimes` takes, once this read's answer comes, waiting for it until - /// [`IMAGE_WAIT`] after it was sent, as long as a paste's bitmap read waits. `None`, so the - /// paste takes the internal clipboard, when the answer is what `ours` put on the clipboard - /// or doesn't come. Otherwise the clipboard is read as [`Clipboard::read_now_or_later`] and - /// the paste read it, this answer standing in for the read it answers: copied files, else + /// [`IMAGE_WAIT`] after it was sent, as long as a paste's bitmap read waits. Once it comes, + /// the same read is made again, since the clipboard may have changed hands meanwhile. + /// `None`, so the paste takes the internal clipboard, when that answer doesn't come or the + /// new one is what `ours` put on the clipboard. Otherwise the clipboard is read as + /// [`Clipboard::read_now_or_later`] and the paste read it, the new answer standing in for + /// the read it answers: copied files, else /// text, else the bitmap, and the bitmap when it is asked for and the text is blank or only /// names a picture (`adopt_system_clipboard_or_defer` in `vectorcraft-ui-egui`). Those reads /// wait as [`follow_up_wait`] says. fn paste(self, ours: Option<&Ours>, mimes: &[&'static str]) -> Option { let wait = (self.sent + IMAGE_WAIT).saturating_duration_since(Instant::now()); - let answer = super::loads::wait_for_answer(&self.answer, wait).ok()?; + super::loads::wait_for_answer(&self.answer, wait).ok()?; + let fresh = |request: Request| { + self.reads + .ask(request) + .and_then(|a| super::loads::wait_for_answer(&a, follow_up_wait(request, self.sent, Instant::now()))) + .unwrap_or_else(|why| Answer::failed(request, why)) + }; + // The late answer may come from an app that lost the clipboard while it was awaited. + let answer = fresh(self.request); let held = match (&answer, ours) { (Answer::Text(Ok(now)), Some(Ours::Text(t))) => now == t, (Answer::Image(Ok(now)), Some(Ours::Image(w, h))) => (now.width, now.height) == (*w, *h), @@ -817,11 +827,7 @@ mod portable { let mut answer = Some(answer); let mut read = |request: Request| match answer.take_if(|a| a.request() == request) { Some(answer) => answer, - None => self - .reads - .ask(request) - .and_then(|a| super::loads::wait_for_answer(&a, follow_up_wait(request, self.sent, Instant::now()))) - .unwrap_or_else(|why| Answer::failed(request, why)), + None => fresh(request), }; if let Some(f) = copied_file(read(Request::Files).files(), mimes) { return Some(f); @@ -852,7 +858,8 @@ mod portable { impl Late { /// What a paste of one of `mimes` takes once the answer comes ([`Unanswered::paste`]). The /// core protocol's text is waited for as long, until [`IMAGE_WAIT`] after its read was - /// sent, and pasted unless it is empty, what `ours` put on the clipboard, or doesn't come. + /// sent, then read again, and pasted unless it is empty, what `ours` put on the clipboard, + /// or doesn't come. fn paste(self, ours: Option<&Ours>, mimes: &[&'static str]) -> Option { match self { Self::Arboard(unanswered) => unanswered.paste(ours, mimes), @@ -1611,6 +1618,20 @@ mod portable { let pasted = paste.join().ok().flatten().expect("the text is pasted"); assert_eq!((pasted.mime, pasted.data), (TEXT, b"".to_vec())); + // The app that answers late may have lost the clipboard meanwhile: the paste reads it + // again and takes the new owner's text. + let texts_read = std::sync::atomic::AtomicUsize::new(0); + let (mut cb, release) = slow_owner(move |request| match request { + Request::Text if texts_read.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 0 => Answer::Text(Ok("old owner".into())), + Request::Text => Answer::Text(Ok("new owner".into())), + _ => Answer::failed(request, "none".into()), + }); + assert!(cb.read_waiting(Request::Text, Duration::ZERO).text().is_err()); + let unanswered = cb.unanswered(&[TEXT]).expect("the text read is kept for the paste"); + let paste = std::thread::spawn(move || unanswered.paste(None, &[TEXT])); + drop(release); + assert_eq!(paste.join().ok().flatten().map(|f| f.data), Some(b"new owner".to_vec())); + // An answer that came before the paste may be old: it is dropped, not pasted. let (mut cb, release) = slow_owner(|request| Answer::failed(request, "not asked".into())); assert!(cb.read_waiting(Request::Text, Duration::ZERO).text().is_err()); @@ -1668,6 +1689,23 @@ mod portable { std::thread::yield_now(); } assert!(reads.unanswered(&[TEXT]).is_none()); + + // The app that answers late may have lost the clipboard meanwhile: the paste reads it + // again and takes the new owner's text. + let (release, gate) = std::sync::mpsc::channel::<()>(); + let mut texts_read = 0; + let loads = Loads::spawn("test", Duration::ZERO, move |()| { + let _ = gate.recv(); + texts_read += 1; + Ok(if texts_read == 1 { "old owner" } else { "new owner" }.to_string()) + }) + .unwrap(); + let mut reads = TextReads::new(loads); + assert!(reads.get(Duration::ZERO).is_err()); + let late = Late::Wayland(reads.unanswered(&[TEXT]).expect("the read is kept for the paste")); + let paste = std::thread::spawn(move || late.paste(None, &[TEXT])); + drop(release); + assert_eq!(paste.join().ok().flatten().map(|f| f.data), Some(b"new owner".to_vec())); } #[test] @@ -1698,7 +1736,7 @@ mod wayland { use std::time::{Duration, Instant}; use super::TEXT; - use super::loads::{GONE, LOAD_WAIT, Loads, wait_for_answer}; + use super::loads::{Asker, GONE, LOAD_WAIT, Loads, wait_for_answer}; /// What a text read gives. type Text = Result; @@ -1724,12 +1762,18 @@ mod wayland { pub(super) struct Unanswered { answer: mpsc::Receiver, sent: Instant, + /// The worker, for the read made again once the answer comes. + reads: Asker<(), Text>, } impl Unanswered { - /// The text, once it comes, waiting for it until `limit` after the read was sent. + /// The text, read again once this read's answer comes, waiting for that answer until + /// `limit` after the read was sent, and for the new one as long, [`LOAD_WAIT`] at least. + /// The late answer may come from an app that lost the clipboard while it was awaited. pub(super) fn text(self, limit: Duration) -> Text { - wait_for_answer(&self.answer, (self.sent + limit).saturating_duration_since(Instant::now()))? + let left = |now: Instant| (self.sent + limit).saturating_duration_since(now); + let _late = wait_for_answer(&self.answer, left(Instant::now()))?; + wait_for_answer(&self.reads.ask(())?, LOAD_WAIT.max(left(Instant::now())))? } } @@ -1749,7 +1793,7 @@ mod wayland { Ok(text) => text, Err(why) => { if why != GONE { - self.unanswered = Some(Unanswered { answer, sent }); + self.unanswered = Some(Unanswered { answer, sent, reads: self.loads.asker() }); } Err(why) }