Linux: check the system clipboard for Paste off the UI thread

`has()` on an X11 clipboard owner that never answers can block for
seconds; the app called it every 0.25 s on the UI thread to decide
whether Paste is enabled, so a stuck owner froze the frame loop (the
window stopped drawing and could not be closed).

`clipboard_probe` owns a second clipboard handle on a worker thread,
publishes its verdict over a channel and wakes the UI only when it
changes; the frame reads the latest verdict without ever blocking.
Without a factory the check stays in line, throttled, exactly as before
(the web shows SVG text only; hosts that don't install one keep the old
path).

The factory is installed on Linux only for now (the environment the bug
reproduces in); other platforms keep the in-line check, so this cannot
change their behaviour.

Tests: a fake clipboard whose `has()` blocks on a gate, a probe factory
sharing a recorder, and a headless frame whose elapsed time is measured.
They assert the frame does not wait on the clipboard and that the check
runs on the probe thread, that the probe's verdict reaches `system_paste`
and the Paste menu, that dropping the app stops the probe, and that the
in-line check (no factory: the web, older hosts) still works.
This commit is contained in:
Oscar Sebastián Mellado Bustos authored and bflatastic committed 2026-10-08 22:34:29 +07:00
1 parent c0396c0761
commit 5db200d392
5 files changed
+335 -7

No files matched your search

+16
View File
@@ -231,6 +231,19 @@ fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
fileio::write_atomic(std::path::Path::new(path), bytes).map_err(|e| e.to_string())
}
/// The factory for the off-thread clipboard check: Linux only for now. On Linux an X11 clipboard
/// owner that never answers would otherwise block the frame loop (see `clipboard_probe`).
fn clipboard_probe_factory() -> Option<vectorcraft_ui_egui::ClipboardProbeFactory> {
#[cfg(target_os = "linux")]
{
Some(Box::new(clipboard::system_clipboard))
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
fn services() -> Services {
Services {
pick_open: Some(Box::new(|pick: &FilePick| file_dialog(pick).pick_file().map(|p| p.to_string_lossy().to_string()))),
@@ -256,6 +269,9 @@ fn services() -> Services {
write_shared: Some(std::sync::Arc::new(write_file)),
// Every format Copy offers and Paste reads (menu-bar Paste never sees egui's Paste event).
system_clipboard: Some(clipboard::system_clipboard()),
// Whether Paste has something to take is checked on a background thread (Linux), so a stuck
// clipboard owner can't freeze the UI.
clipboard_probe: clipboard_probe_factory(),
// Help → Discord / website / GitHub, the Discord button, About and Home links.
open_url: Some(Box::new(|url: &str| {
let _ = webbrowser::open(url);