Merge pull request #7110 from dotCooCoo/fix/quit-waits-for-clipboard-read

Clipboard: the window closes at once and quitting waits up to 27 s for the Paste check
This commit is contained in:
BFlat
2026-11-14 00:51:39 +07:00
committed by GitHub
11 changed files with 180 additions and 21 deletions
+27
View File
@@ -108,10 +108,37 @@ impl eframe::App for App {
self.app.start_pending_update();
#[cfg(target_os = "macos")]
super::mac_cursor::release();
#[cfg(not(any(target_os = "macos", windows)))]
hide_before_teardown(self.winit_window.as_deref());
end_before_teardown(self.frames);
}
}
/// The main window as [`hide_before_teardown`] sees it.
#[cfg(not(any(target_os = "macos", windows)))]
pub(crate) trait QuittingWindow {
fn hide(&self);
}
#[cfg(not(any(target_os = "macos", windows)))]
impl QuittingWindow for winit::window::Window {
fn hide(&self) {
self.set_visible(false);
}
}
/// On Linux and the BSDs, hide the main window before eframe drops the app. Dropping the app waits
/// up to [`vectorcraft_ui_egui::CLIPBOARD_PROBE_STOP_WAIT`] for the clipboard handles to let go, and
/// the window would stay on screen for that time. The window is hidden, not closed: the Wayland
/// clipboard reads text on the window's connection. winit hides X11 windows (XWayland included) and
/// cannot hide a Wayland window.
#[cfg(not(any(target_os = "macos", windows)))]
pub(crate) fn hide_before_teardown<W: QuittingWindow + ?Sized>(window: Option<&W>) {
if let Some(window) = window {
window.hide();
}
}
impl App {
/// The frame's times for the hang report, and its last stage ([`hang_beacon::after_frame`]): a
/// focused, shown window whose windows had input waits for the renderer and the next frame,
+10
View File
@@ -1504,6 +1504,16 @@ mod portable {
assert_eq!(finish(true), Duration::ZERO);
}
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))]
#[test]
fn quitting_waits_for_the_paste_checks_handle_as_long_as_its_reads_last() {
// The Paste check's handle is one of these: when the app quits, its unanswered X11 read
// ends `X11_READ_LIMIT` after it was sent, and an idle worker stops within `LOAD_WAIT`.
// Quitting waits for that handle until both are over.
assert!(vectorcraft_ui_egui::CLIPBOARD_PROBE_STOP_WAIT >= finish(false) + super::super::loads::LOAD_WAIT);
assert!(vectorcraft_ui_egui::CLIPBOARD_PROBE_STOP_WAIT >= finish(true) + super::super::loads::LOAD_WAIT);
}
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))]
#[test]
fn a_bitmap_paste_waits_for_the_owner_off_the_ui_thread() {
+3 -2
View File
@@ -393,8 +393,9 @@ pub(crate) fn services(parent: Parent, ui: egui::Context) -> Services {
// Its calls are marked for the hang report (`hang_watch`).
system_clipboard: Some(Box::new(vectorcraft_ui_egui::hang_beacon::Marked(clipboard::system_clipboard(clip)))),
// Linux checks whether Paste has something to take on a background thread: an X11 clipboard
// owner that never answers holds a read for up to 4 s. Windows only asks which formats the
// clipboard holds and macOS asks the pasteboard server, so they check in line.
// owner that never answers holds a text read for up to 4 s for each format it tries, and
// quitting waits up to `CLIPBOARD_PROBE_STOP_WAIT` for that thread. Windows only asks which
// formats the clipboard holds and macOS asks the pasteboard server, so they check in line.
// The probe's handle is made on its own thread, from the window (`Send`), not its raw display.
clipboard_probe: cfg!(target_os = "linux").then(|| Box::new(move || clipboard::system_clipboard(clip_probe)) as ClipboardProbeFactory),
// Help → Discord / website / GitHub, the Discord button, About and Home links.
+4 -1
View File
@@ -87,6 +87,8 @@ mod titlebar;
use app::bring_window_forward;
use app::native_input_hook;
use app::open_or_hold;
#[cfg(all(test, not(any(target_os = "macos", windows))))]
use app::{QuittingWindow, hide_before_teardown};
use host_services::services;
#[cfg(all(test, feature = "wgpu"))]
use prefs::{KEEP_UI_PREFS, decode_saved_prefs, keep_prefs_copy, prefs_bytes};
@@ -116,7 +118,8 @@ struct App {
frames: u64,
/// Keeps the custom title bar's window borderless (Windows, #1758).
window_frame: Option<window_frame::WindowFrameFn>,
/// The main window; used to read and set maximized state the UI layer does not see on Linux (#3635).
/// The main window; used to read and set maximized state the UI layer does not see on Linux (#3635),
/// and to hide the window on quit before the clipboard handles let go (`app::hide_before_teardown`).
winit_window: Option<std::sync::Arc<winit::window::Window>>,
/// A saved or fitted geometry asked for maximize once the window is mapped (Linux).
pending_maximize: bool,
+18
View File
@@ -460,3 +460,21 @@ fn the_linux_desktop_entry_opens_every_readable_format() {
}
}
}
/// On Linux and the BSDs, quitting hides the main window before eframe drops the app, whose
/// clipboard handles may take up to `CLIPBOARD_PROBE_STOP_WAIT` to let go.
#[cfg(not(any(target_os = "macos", windows)))]
#[test]
fn quitting_hides_the_window_before_the_clipboards_let_go() {
struct Window(std::cell::Cell<u32>);
impl QuittingWindow for Window {
fn hide(&self) {
self.0.set(self.0.get() + 1);
}
}
let window = Window(std::cell::Cell::new(0));
hide_before_teardown(Some(&window));
assert_eq!(window.0.get(), 1, "the window was not hidden");
// Without a window (none was made), there is nothing to hide.
hide_before_teardown::<Window>(None);
}
+23 -12
View File
@@ -9,17 +9,11 @@ use std::time::{Duration, Instant};
use vectorcraft_engine::cmd::clipboard::PASTE_ORDER;
use crate::sysclip::ClipboardProbeFactory;
use crate::sysclip::{CLIPBOARD_PROBE_STOP_WAIT as STOP_WAIT, ClipboardProbeFactory};
/// How often the worker looks at the clipboard while the UI wants to know.
const PROBE_INTERVAL: Duration = Duration::from_millis(250);
/// How long dropping the probe (when the app quits) waits for the worker to let go of its clipboard
/// handle (VectorCraft's own bound). arboard's X11 reads wait up to 4 s for each text format they
/// ask for; when a look is still waiting after this bound, the app quits without waiting longer,
/// and what it copied may not reach the clipboard manager.
const STOP_WAIT: Duration = Duration::from_secs(5);
/// The background clipboard check ([`crate::VectorcraftApp`]'s `clipboard_probe`).
pub(crate) struct Probe {
/// Tells the worker whether Paste depends on the system clipboard; dropped to stop it.
@@ -28,8 +22,14 @@ pub(crate) struct Probe {
verdicts: Receiver<bool>,
/// The worker's last verdict.
last: bool,
/// How `Drop` waits for the worker: [`wait_for_worker`], or a controllable clock in tests.
pub(crate) stop: StopWait,
}
/// Waits on the worker's verdicts, from the quit to the deadline [`stop_by`] gives, until the worker
/// is gone or the deadline passes.
pub(crate) type StopWait = Box<dyn FnMut(&Receiver<bool>, Instant, Instant) + Send>;
impl Probe {
/// Start the worker. `None` on wasm, or when the thread can't be spawned: the caller then falls
/// back to the in-line check.
@@ -65,13 +65,14 @@ impl Probe {
Err(RecvTimeoutError::Disconnected) => break,
}
}
// The handle goes before the app stops waiting (`Drop`): on X11 the last one alive
// hands what the app copied to the clipboard manager, which must finish before exit.
// The handle goes before `Drop` stops waiting, unless dropping it takes longer than
// `STOP_WAIT`: on X11 the last one alive hands what the app copied to the clipboard
// manager, which must finish before exit.
drop(clipboard);
drop(tx);
})
.ok()?;
Some(Self { wants: Some(wants), wanted: false, verdicts, last: false })
Some(Self { wants: Some(wants), wanted: false, verdicts, last: false, stop: Box::new(wait_for_worker) })
}
/// Paste can take from the system clipboard: `wanted` (nothing copied in the app, a document
@@ -96,7 +97,17 @@ impl Drop for Probe {
/// never answers) until it has let go of its clipboard handle.
fn drop(&mut self) {
self.wants = None;
let end = Instant::now() + STOP_WAIT;
while self.verdicts.recv_timeout(end.saturating_duration_since(Instant::now())).is_ok() {}
let quit = Instant::now();
(self.stop)(&self.verdicts, quit, stop_by(quit));
}
}
/// When dropping the probe at `quit` stops waiting for its worker.
pub(crate) fn stop_by(quit: Instant) -> Instant {
quit + STOP_WAIT
}
/// Waits until the worker has dropped its sender (it has let go of its clipboard handle) or `end`.
fn wait_for_worker(verdicts: &Receiver<bool>, _quit: Instant, end: Instant) {
while verdicts.recv_timeout(end.saturating_duration_since(Instant::now())).is_ok() {}
}
+5 -4
View File
@@ -108,7 +108,7 @@ use vectorcraft_engine::{Session, ViewInfo};
pub use control::{ControlRequest, ControlResponse, Ticket as ControlTicket};
pub use state::{UiState, View};
pub use sysclip::{ClipboardProbeFactory, ClipboardRead, MakeFlavour, SystemClipboard};
pub use sysclip::{CLIPBOARD_PROBE_STOP_WAIT, ClipboardProbeFactory, ClipboardRead, MakeFlavour, SystemClipboard};
mod app;
mod caches;
@@ -138,9 +138,10 @@ pub struct VectorcraftApp {
pub(crate) view_uids: Vec<u64>,
/// The thread that checks the system clipboard for `system_paste` instead
/// ([`Services::clipboard_probe`]). It is declared before `services` so that it is dropped
/// first: on X11 the last clipboard handle alive hands what the app copied to the clipboard
/// manager, and that must be the app's own handle in `services`, dropped on the UI thread
/// before the process exits.
/// first, waiting up to [`CLIPBOARD_PROBE_STOP_WAIT`] for its thread to let go of its handle: on
/// X11 the last clipboard handle alive hands what the app copied to the clipboard manager, and
/// that should be the app's own handle in `services`, dropped on the UI thread before the
/// process exits.
clipboard_probe: Option<clipboard_probe::Probe>,
pub services: Services,
pub canvas: CanvasCache,
+6
View File
@@ -85,6 +85,12 @@ impl ClipboardRead {
/// thread and the handle lives and dies there, so the handle needn't be `Send`.
pub type ClipboardProbeFactory = Box<dyn FnOnce() -> Box<dyn SystemClipboard> + Send>;
/// How long quitting waits for the probe's thread ([`ClipboardProbeFactory`]) to drop its handle
/// before the app drops its own: 27 s, VectorCraft's own bound. A host's handle should finish a
/// look and its own teardown within it, or the app's handle may not be the last one dropped; a
/// handle that never lets go still lets the app quit after this wait.
pub const CLIPBOARD_PROBE_STOP_WAIT: std::time::Duration = std::time::Duration::from_secs(27);
/// The Copy or Cut whose formats go to the system clipboard ([`VectorcraftApp::publish_clipboard`]).
#[derive(Default)]
pub(crate) struct Publishing {
+82 -1
View File
@@ -12,7 +12,7 @@ use vectorcraft_engine::Session;
use vectorcraft_engine::cmd::clipboard::Flavour;
use crate::tests_sysclip::{copy_rect, run};
use crate::{Services, SystemClipboard, VectorcraftApp, menus};
use crate::{Services, SystemClipboard, VectorcraftApp, clipboard_probe, menus};
/// What the clipboard handles saw: the thread each `has()` ran on, how many handles are alive, and
/// the thread each handle was dropped on, in order.
@@ -206,6 +206,87 @@ fn quitting_drops_the_probes_clipboard_before_the_apps() {
assert_eq!(drops, vec![Some("vectorcraft-clip".to_owned()), ui], "handles dropped on: {drops:?}");
}
/// The probe's clipboard: dropping it tells `dropping` it started, then waits until `release` sends
/// (a handle that lets go only once an unanswered read ends), and tells `dropped` once it is gone.
struct Releasing {
rec: Arc<Recorder>,
dropping: std::sync::mpsc::Sender<()>,
release: std::sync::mpsc::Receiver<()>,
dropped: std::sync::mpsc::Sender<()>,
}
impl Drop for Releasing {
fn drop(&mut self) {
let _ = self.dropping.send(());
let _ = self.release.recv();
self.rec.drops.lock().unwrap().push(std::thread::current().name().map(str::to_owned));
let _ = self.dropped.send(());
}
}
impl SystemClipboard for Releasing {
fn write(&mut self, _flavours: &[Flavour]) -> Result<(), String> {
Ok(())
}
fn holds_ours(&mut self) -> bool {
false
}
fn read(&mut self, _mimes: &[&'static str]) -> Option<Flavour> {
None
}
fn has(&mut self, _mimes: &[&'static str]) -> bool {
false
}
}
#[test]
fn quitting_waits_while_the_probes_clipboard_is_still_letting_go() {
let rec = Arc::new(Recorder::default());
let (dropping, dropping_seen) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
let (dropped, dropped_seen) = std::sync::mpsc::channel();
let make = {
let rec = Arc::clone(&rec);
move || -> Box<dyn SystemClipboard> { Box::new(Releasing { rec, dropping, release: released, dropped }) }
};
let services = Services {
system_clipboard: Some(Slow::boxed(&rec, Duration::ZERO, Some(false))),
clipboard_probe: Some(Box::new(make)),
..Default::default()
};
let mut app = VectorcraftApp::new(Session::new(), services);
frame(&mut app);
// A controllable clock: the probe's handle lets go 26 s after the quit (a text read unanswered
// just before it, and the worker's teardown). The wait sees the handle start to drop, then
// either reaches that time and waits for the worker, or ends at its deadline first.
let lets_go = Duration::from_secs(26);
let release_at_deadline = release.clone();
app.clipboard_probe.as_mut().expect("the probe started").stop = Box::new(move |verdicts, quit, end| {
dropping_seen.recv().expect("the probe's handle was never dropped");
if end >= quit + lets_go {
let _ = release_at_deadline.send(());
while verdicts.recv().is_ok() {}
}
});
drop(app);
// A wait that ended first left the handle dropping: let it finish to read the order.
let _ = release.send(());
dropped_seen.recv().expect("the probe's handle was never dropped");
let drops = rec.drops.lock().unwrap().clone();
let ui = std::thread::current().name().map(str::to_owned);
assert_eq!(drops, vec![Some("vectorcraft-clip".to_owned()), ui], "handles dropped on: {drops:?}");
}
#[test]
fn quitting_waits_out_an_unanswered_x11_read_and_its_teardown() {
let quit = Instant::now();
// A look whose text read went unanswered just before the quit: the Linux host's worker holds its
// handle until 25 s after the read was sent, and an idle worker's teardown takes 2 s at most.
assert!(clipboard_probe::stop_by(quit) >= quit + Duration::from_secs(25) + Duration::from_secs(2));
// A handle that never lets go still lets the app quit.
assert_eq!(clipboard_probe::stop_by(quit), quit + Duration::from_secs(27));
}
#[test]
fn without_its_thread_the_check_runs_in_line_again() {
let rec = Arc::new(Recorder::default());
+1 -1
View File
@@ -126,7 +126,7 @@ On GNOME/Wayland, enable **Preferences › User Interface › System Title Bar**
The window runs natively on Wayland (eframe's `wayland` feature) and on X11. On machines with two GPUs it renders on the one the desktop runs on unless Preferences say otherwise; see [Desktop graphics processor](#desktop-graphics-processor) (#502). The system clipboard (`apps/vectorcraft/src/clipboard.rs`) is arboard with its `wayland-data-control` feature:
- **Clipboard:** under Wayland arboard talks to the compositor through the data-control protocol (wlroots compositors such as Sway and Hyprland, KDE Plasma), so bitmaps (`image/png`), SVG markup and text copied in any app paste (#398). Where the compositor lacks the protocol (GNOME's Mutter, [arboard#223](https://github.com/1Password/arboard/issues/223)), arboard falls back to the X11 clipboard through XWayland, which only holds what X11 apps copied. Where there is no X11 either (the Flatpak on a Wayland session: `--socket=fallback-x11` gives it none), Copy and Paste use the compositor's core data-device protocol on the window's own connection through smithay-clipboard: text only (a copy's text or its SVG markup), and only while the window has focus. smithay-clipboard waits for the owner's data with no limit, so its reads run on a worker thread and the UI thread waits 2 s at most: an owner that never sends its data costs one read 2 s, and later reads return at once until it answers. When a read gets no answer in its 2 s, such as a paste's check whether the clipboard still holds what VectorCraft copied, the paste waits on its background thread for that read's answer, up to 10 s after the read was sent, and pastes the text unless it is empty or what VectorCraft copied. A clipboard neither backend opens is tried again after 1 s, doubling to a minute (a Copy or Paste of the user's at most once a second), and logged once when it goes and once when it comes back: before, the background Paste check reconnected and logged a warning every 250 ms. egui's own text paste into fields goes through smithay-clipboard and works either way.
- **Clipboard owners that never answer (#510):** reading the clipboard waits on the app that owns it: arboard gives up on an X11 owner after 4 s for each format it tries, and a Wayland read waits for the owner to send its data. So on Linux the check that enables the Paste menu items runs on a background thread with a second clipboard handle (`crates/ui-egui/src/clipboard_probe.rs`, installed as `Services::clipboard_probe`), only while nothing is copied in the app and a document is open, and the window keeps drawing. Every arboard read on Linux and the BSDs, including the UI thread's (a paste, and a Copy or Cut checking what the clipboard held), runs on a worker thread with a handle of its own and waits 2 s at most: an owner that never sends its data costs one read that wait, and later reads fail at once until it answers. A paste's bitmap read waits up to 10 s on the paste's background thread, so the UI thread does not wait for it. When one of a paste's reads gets no answer in its 2 s, or a paste follows a read that has not been answered yet, such as the paste's own check whether the clipboard still holds what VectorCraft copied, the paste waits on that thread for that read's answer, up to 10 s after the read was sent, and pastes what it holds. When the answer is what VectorCraft copied, holds nothing to paste, or does not come, the paste takes the internal clipboard. On X11, quitting within 25 s of a read that got no answer waits for that read to end, so the app's own handle is the last one and hands what it copied to the clipboard manager. Windows and macOS keep the check on the UI thread: Windows only asks which formats the clipboard holds, macOS asks the pasteboard server.
- **Clipboard owners that never answer (#510):** reading the clipboard waits on the app that owns it: arboard gives up on an X11 owner after 4 s for each format it tries, and a Wayland read waits for the owner to send its data. So on Linux the check that enables the Paste menu items runs on a background thread with a second clipboard handle (`crates/ui-egui/src/clipboard_probe.rs`, installed as `Services::clipboard_probe`), only while nothing is copied in the app and a document is open, and the window keeps drawing. Every arboard read on Linux and the BSDs, including the UI thread's (a paste, and a Copy or Cut checking what the clipboard held), runs on a worker thread with a handle of its own and waits 2 s at most: an owner that never sends its data costs one read that wait, and later reads fail at once until it answers. A paste's bitmap read waits up to 10 s on the paste's background thread, so the UI thread does not wait for it. When one of a paste's reads gets no answer in its 2 s, or a paste follows a read that has not been answered yet, such as the paste's own check whether the clipboard still holds what VectorCraft copied, the paste waits on that thread for that read's answer, up to 10 s after the read was sent, and pastes what it holds. When the answer is what VectorCraft copied, holds nothing to paste, or does not come, the paste takes the internal clipboard. On X11, quitting within 25 s of a read that got no answer waits for that read to end, so the app's own handle is the last one and hands what it copied to the clipboard manager when one runs. That includes a read of the Paste menu check's handle: quitting waits up to 27 s for the background thread to drop it before the app drops its own, and quits after that wait even when the handle has not been dropped. The app hides its main window before these waits (`hide_before_teardown` in `apps/vectorcraft/src/app.rs`), so an X11 window, an XWayland one included, closes at once; winit cannot hide a native Wayland window, which stays on screen until the waits end. Windows and macOS keep the check on the UI thread: Windows only asks which formats the clipboard holds, macOS asks the pasteboard server.
- **Copied files:** files copied in a file manager (`text/uri-list`; `CF_HDROP` on Windows, file URLs on macOS) paste as the first one that is art (SVG, PDF, EPS, EMF/WMF or a bitmap; a `.ai` or `.ait` file from the editing data it holds, else as the PDF or PostScript it holds), before the clipboard's other formats (which include the files' paths as text).
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.37 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
- **Pens and pen displays under Wayland (#491, #2028):** winit 0.30 doesn't bind the Wayland tablet protocol (`zwp_tablet_v2`; tablet input arrives in winit 0.31, [winit#4318](https://github.com/rust-windowing/winit/pull/4318), [egui#7731](https://github.com/emilk/egui/pull/7731)). Since Plasma 6.3, KWin no longer turns tablet input into pointer input for such apps ([kwin!6336](https://invent.kde.org/plasma/kwin/-/merge_requests/6336), commit [25c65abd](https://invent.kde.org/plasma/kwin/-/commit/25c65abddd95eb87fb892a8594faccbf509ff62f)), and GNOME's Mutter does not turn it into pointer input either (that commit's message): on KWin the pen moves the compositor's cursor over the window, but the app gets no motion or presses and shows none of its own cursors. Until eframe moves to winit 0.31, the app runs under XWayland, which supports the tablet protocol and hands the pen to X11 apps as a pointer: Preferences › Performance › Linux Display Server's Automatic (the default) and X11 (XWayland) When a Pen Is Attached (`x11WithPen`) open the window through XWayland when `/proc/bus/input/devices` lists a pen as the app starts, a device with the `BTN_TOOL_PEN` key and the `ABS_X` and `ABS_Y` axes, and log `pen found: opening the window through XWayland`, as PhotoCraft does (#4967; `VECTORCRAFT_NATIVE_WAYLAND=1` keeps the window on Wayland). For a pen attached after the app started, either run the app under XWayland (Preferences › Performance › Linux Display Server › X11 (XWayland); `WAYLAND_DISPLAY= vectorcraft`, or `env -u WAYLAND_DISPLAY vectorcraft`; for a desktop launcher, put `env WAYLAND_DISPLAY=` before the command in its `Exec=` line), or turn KWin's deprecated emulation back on for the whole session by setting `KWIN_WAYLAND_EMULATE_TABLET=1` in KWin's environment (for example in a file under `~/.config/environment.d/` or in `/etc/environment`) and logging in again. The pen then works as a mouse. Under XWayland the app also reads the pen's pressure (next item); with KWin's emulation on native Wayland the pen has no pressure. Other compositors that don't emulate a pointer for tablets behave the same way.
+1
View File
@@ -1669,6 +1669,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- `file.print`, `print.preview` and `print.setup` take a print preset by name (`preset`: `[Default]` or a saved preset, in any case): its settings are used instead of the document's, with `settings` over them, `print.setup` saves them with the document, and an unknown name is refused with the list of presets.
- Print's Copies per Sheet (`copiesPerSheet`, `gutter`; off by default) prints as many copies of each artboard, with its bleed and marks, as fit the imageable area, across then down, as one grid placed like a single copy; the preview shows them, and an artboard that does not fit prints one copy per sheet with a warning (#1283).
- Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
- On X11, quitting while the Paste menu's background check waits on a clipboard owner that never answers waits up to 27 s for that check to drop its clipboard handle, so the app's own handle is the last one dropped when the check drops its handle within that wait, and the app quits after the wait when it has not; the window is hidden before that wait, so an X11 window closes at once (`crates/ui-egui/src/tests/sysclip_probe.rs`, `apps/vectorcraft/src/clipboard.rs`, `apps/vectorcraft/src/tests.rs`).
- File › Place reads RTF files as area type with their formatting: each run's font, size, bold and italic, underline, strikethrough, caps, superscript or subscript, baseline shift and color, and each paragraph's alignment, indents, space before and after and direction. Table cells are separated by tabs; hidden text, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests/textimport.rs`).
- File › Place reads Word `.docx` documents as area type with the same formatting, taken from the document defaults, the paragraph and character styles (their toggle properties reversing each other) and direct formatting, the theme's fonts included. Table rows are paragraphs with their cells separated by tabs; hidden and deleted text, field instructions, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests/textimport.rs`).
- A Word paragraph justified with a low, medium or high kashida length places justified with Insert Kashidas set to Short, Medium or Long (`file.place`, `crates/engine/src/tests/textimport.rs`).