mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 16:31:07 +00:00
The paste handler read the system clipboard synchronously (2 s timeout) even though the paste event already carries that text, and discarded the read's result. On the desktop, the clipboard bridge delivers `event_text` that it read while the window thread was free; re-reading from the paste handler blocks that same window thread - and when the app itself owns the clipboard (a self-paste), its own data source can only be served by that very thread, so every self-paste froze the window for the full 2 s read timeout on Linux/Wayland. Prefer the event's text and only fall back to the synchronous read when the platform handed over none (chords, image-only clipboard, context-menu paste). A regression test pins that a paste event carrying text never triggers the synchronous read.
This commit is contained in:
1 parent
2dd1b5a53a
commit
7bf098d75d
2 files changed
+60
-3
No files matched your search
@@ -7,8 +7,8 @@ use crate::i18n::trf;
|
||||
/// Paste: the system clipboard wins when it holds something newer than our own copy (layers copied
|
||||
/// from another design app, an image, SVG or text from another app); otherwise our copied layers.
|
||||
/// `event_text` is the text the platform handed over with the paste event (the browser has no
|
||||
/// other way to read it). `at`: the page point to paste at (Paste here); `None` pastes in the
|
||||
/// middle of the view.
|
||||
/// other way to read it; the desktop's clipboard bridge has already read it). `at`: the page
|
||||
/// point to paste at (Paste here); `None` pastes in the middle of the view.
|
||||
/// In Dev Mode only a comment's link pastes (it opens the thread); everything else would edit.
|
||||
pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) {
|
||||
// A gesture injected through the control channel pastes only our own copied layers: it must not
|
||||
@@ -47,7 +47,15 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>)
|
||||
}
|
||||
return;
|
||||
}
|
||||
let sys_text = app.services.clipboard_get_text().or_else(|| event_text.map(str::to_string)).filter(|t| !t.is_empty());
|
||||
// The text the platform handed over with the paste event is the system clipboard's text: the
|
||||
// browser delivers it, and the desktop's async clipboard bridge read it while the window
|
||||
// thread was free to serve the transfer. Never re-read the clipboard synchronously for it —
|
||||
// on the desktop that read blocks the window thread for the read timeout, and when this app
|
||||
// itself owns the clipboard (a self-paste) its own source can only be served by that very
|
||||
// thread, so the read waits the full timeout and the window freezes for seconds (#2021).
|
||||
// A paste the platform didn't hand text over (a chord, an image, the menu) reads it, as before.
|
||||
let sys_text =
|
||||
if let Some(t) = event_text { (!t.is_empty()).then(|| t.to_string()) } else { app.services.clipboard_get_text().filter(|t| !t.is_empty()) };
|
||||
// The text matching `last_copy` means the system clipboard still holds what we mirrored only
|
||||
// while the engine clipboard is the one we mirrored. A control-channel copy changes the engine
|
||||
// clipboard without writing the system clipboard, so its generation no longer matches and the
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
//! whatever the system clipboard has. A copy that never reached the clipboard pasted the text that
|
||||
//! was there before as a text layer instead of the copied layers.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use egui::{Event, Key, Modifiers, OutputCommand, Pos2, RawInput, Rect, vec2};
|
||||
use figcraft_doc::NodeKind;
|
||||
use figcraft_engine::Session;
|
||||
@@ -223,3 +226,49 @@ fn a_mismatched_image_buffer_is_dropped() {
|
||||
assert!(out.platform_output.commands.is_empty(), "{w}x{h}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The system clipboard's text read, counted so a test can assert when it ran. The desktop's
|
||||
/// arboard read blocks the window thread up to `CLIPBOARD_TIMEOUT`; against the app's own Wayland
|
||||
/// source (a self-paste) it waits the full timeout — the whole UI freezes for seconds (#2021) —
|
||||
/// and its result is discarded in favour of the paste event's text. A paste whose platform event
|
||||
/// already carries the clipboard text must never perform that synchronous re-read.
|
||||
struct CountingReads {
|
||||
/// How often the synchronous text read was asked for.
|
||||
reads: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Services for CountingReads {
|
||||
fn clipboard_get_text(&mut self) -> Option<String> {
|
||||
self.reads.fetch_add(1, Ordering::Relaxed);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paste_event_its_own_text_never_triggers_the_synchronous_text_read() {
|
||||
// The platform (the desktop's async clipboard bridge, or the browser) already read the
|
||||
// clipboard and handed the text with the paste event.
|
||||
let reads = Arc::new(AtomicUsize::new(0));
|
||||
let mut s = Session::new();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
let mut p = Page {
|
||||
ctx: egui::Context::default(),
|
||||
app: FigApp::new(s, Box::new(CountingReads { reads: reads.clone() })),
|
||||
time: 0.0,
|
||||
clipboard: String::new(),
|
||||
};
|
||||
for _ in 0..3 {
|
||||
p.frame(vec![]);
|
||||
}
|
||||
|
||||
// The desktop delivers the copied layers' SVG as the paste event's text.
|
||||
let text = "<svg width=\"100\" height=\"50\"><rect/></svg>";
|
||||
p.frame(vec![Page::key(Key::V, true), Event::Paste(text.to_string())]);
|
||||
p.frame(vec![Page::key(Key::V, false)]);
|
||||
assert_eq!(reads.load(Ordering::Relaxed), 0, "the text came with the paste event: no blocking re-read of the system clipboard");
|
||||
|
||||
// Even a second paste must not re-read: the clipboard still holds the pasted text.
|
||||
p.frame(vec![Page::key(Key::V, true), Event::Paste(text.to_string())]);
|
||||
p.frame(vec![Page::key(Key::V, false)]);
|
||||
assert_eq!(reads.load(Ordering::Relaxed), 0, "still the event's text: no blocking re-read");
|
||||
}
|
||||
Reference in new issue
Block a user