Merge pull request #7485 from dotCooCoo/fix/quit-hides-all-windows-wayland

Clipboard: quitting on Linux takes every window off screen before the clipboard wait
This commit is contained in:
BFlat
2026-11-14 19:38:10 +07:00
committed by GitHub
7 changed files with 299 additions and 54 deletions
+3 -26
View File
@@ -108,37 +108,14 @@ impl eframe::App for App {
self.app.start_pending_update();
#[cfg(target_os = "macos")]
super::mac_cursor::release();
// Dropping the app can wait for the clipboard handles to let go: the windows leave the
// screen first (`quit_windows`).
#[cfg(not(any(target_os = "macos", windows)))]
hide_before_teardown(self.winit_window.as_deref());
super::quit_windows::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,
+7 -3
View File
@@ -60,6 +60,8 @@ mod portal_request;
mod portal_save;
mod prefs_dir;
mod printing;
#[cfg(not(any(target_os = "macos", windows)))]
mod quit_windows;
#[cfg(not(target_os = "macos"))]
mod single_instance;
#[cfg(windows)]
@@ -90,8 +92,6 @@ 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};
@@ -122,7 +122,7 @@ struct App {
/// 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),
/// and to hide the window on quit before the clipboard handles let go (`app::hide_before_teardown`).
/// and to take the window off screen on quit before the clipboard handles let go (`quit_windows`).
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,
@@ -418,6 +418,10 @@ fn main() -> std::process::ExitCode {
}));
options
};
// Quitting takes the X11 windows of floating panels and documents off screen with the main
// window, so eframe's windows are recorded as it makes and drops them (`quit_windows`).
#[cfg(target_os = "linux")]
let options = eframe::NativeOptions { viewport_hooks: Some(std::sync::Arc::new(quit_windows::Hooks)), ..options };
#[cfg(feature = "wgpu")]
let options = {
let mut options = options;
+6 -5
View File
@@ -242,7 +242,7 @@ fn parent(window: &winit::window::Window) -> (String, Option<()>) {
/// (<https://wayland.app/protocols/xdg-foreign-unstable-v2>), made as rfd's `window_identifier`
/// makes it.
#[cfg(target_os = "linux")]
mod wayland {
pub(crate) mod wayland {
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::wl_registry::{self, WlRegistry};
use wayland_client::protocol::wl_surface::WlSurface;
@@ -308,9 +308,10 @@ mod wayland {
}
}
/// `window`'s Wayland connection and surface, from its raw handles.
/// `window`'s Wayland connection and surface, from its raw handles. The caller holds `window`
/// while it uses them (`quit_windows` unmaps the main window's surface through them).
#[allow(unsafe_code)]
fn surface(window: &winit::window::Window) -> Option<(Connection, WlSurface)> {
pub(crate) fn surface(window: &winit::window::Window) -> Option<(Connection, WlSurface)> {
use wayland_backend::sys::client::{Backend, ObjectId};
use winit::raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _, RawDisplayHandle, RawWindowHandle};
let (RawWindowHandle::Wayland(w), RawDisplayHandle::Wayland(d)) =
@@ -319,8 +320,8 @@ mod wayland {
return None;
};
// SAFETY: the display is winit's open `wl_display`, which outlives `window` (the caller
// holds it until the export is dropped); a backend made from a foreign display does not
// close it when dropped.
// holds it while it uses the connection, and the export holds it until it is dropped); a
// backend made from a foreign display does not close it when dropped.
let backend = unsafe { Backend::from_foreign_display(d.display.as_ptr().cast()) };
let conn = Connection::from_backend(backend);
// SAFETY: the pointer is the window's live `wl_surface` proxy on that display.
+280
View File
@@ -0,0 +1,280 @@
//! Takes the app's windows off screen when it quits on Linux and the BSDs, 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 eframe closes the windows only after that wait.
//!
//! The main window is hidden on X11 (XWayland included). winit cannot hide a Wayland window, so on
//! Linux a Wayland main window is unmapped instead: its surface gets no buffer, and the window stays
//! open with the connection the Wayland clipboard copies and reads on. On Linux the X11 windows of
//! floating panels and documents are recorded as eframe creates and drops them ([`Hooks`]), and
//! quitting unmaps them through an X11 connection of its own. On native Wayland, floating panels and
//! documents are normally drawn inside the main window.
#[cfg(target_os = "linux")]
use std::collections::HashMap;
#[cfg(target_os = "linux")]
use std::sync::{LazyLock, Mutex, MutexGuard, PoisonError};
#[cfg(target_os = "linux")]
use winit::raw_window_handle::{HasWindowHandle as _, RawWindowHandle};
/// The main window as quitting sees it.
pub(crate) trait QuittingWindow {
/// The window is a Wayland surface.
fn is_wayland(&self) -> bool;
/// Hides an X11 window.
fn hide(&self);
/// Takes a Wayland window off screen and keeps it open.
fn unmap(&self);
}
impl QuittingWindow for winit::window::Window {
#[cfg(target_os = "linux")]
fn is_wayland(&self) -> bool {
matches!(self.window_handle().map(|h| h.as_raw()), Ok(RawWindowHandle::Wayland(_)))
}
/// The BSDs have no Wayland unmap: their windows are hidden through winit.
#[cfg(not(target_os = "linux"))]
fn is_wayland(&self) -> bool {
false
}
fn hide(&self) {
self.set_visible(false);
}
#[cfg(target_os = "linux")]
fn unmap(&self) {
unmap_wayland(self);
}
#[cfg(not(target_os = "linux"))]
fn unmap(&self) {
self.set_visible(false);
}
}
/// Takes the main window and the floating windows `x11` lists off screen: a Wayland main window is
/// unmapped and any other main window hidden, and `unmap_x11` unmaps the floating windows.
pub(crate) fn off_screen<W: QuittingWindow + ?Sized>(main: Option<&W>, x11: &[u32], unmap_x11: impl FnOnce(&[u32])) {
if let Some(window) = main {
if window.is_wayland() {
window.unmap();
} else {
window.hide();
}
}
if !x11.is_empty() {
unmap_x11(x11);
}
}
/// [`off_screen`] with the running app's main window and the floating windows [`Hooks`] recorded.
/// Runs on quit, before eframe drops the app.
pub(crate) fn before_teardown(main: Option<&winit::window::Window>) {
#[cfg(target_os = "linux")]
off_screen(main, &CHILD_WINDOWS.x11(), unmap_x11);
#[cfg(not(target_os = "linux"))]
off_screen(main, &[], |_| {});
}
/// The X11 windows of floating panels and documents, by viewport, while eframe holds them.
#[cfg(target_os = "linux")]
#[derive(Default)]
pub(crate) struct ChildWindows(Mutex<HashMap<egui::ViewportId, u32>>);
#[cfg(target_os = "linux")]
impl ChildWindows {
/// eframe made `viewport`'s window, with `handle`. The main window, and windows of other window
/// systems, are not recorded; a window made again for the same viewport replaces its record.
pub(crate) fn created(&self, viewport: egui::ViewportId, handle: Option<RawWindowHandle>) {
let mut windows = self.lock();
match handle.and_then(x11_window) {
Some(id) if viewport != egui::ViewportId::ROOT => windows.insert(viewport, id),
_ => windows.remove(&viewport),
};
}
/// eframe dropped `viewport`'s window.
pub(crate) fn destroyed(&self, viewport: egui::ViewportId) {
self.lock().remove(&viewport);
}
/// The recorded windows, in id order.
pub(crate) fn x11(&self) -> Vec<u32> {
let mut ids: Vec<u32> = self.lock().values().copied().collect();
ids.sort_unstable();
ids.dedup();
ids
}
fn lock(&self) -> MutexGuard<'_, HashMap<egui::ViewportId, u32>> {
self.0.lock().unwrap_or_else(PoisonError::into_inner)
}
}
/// The X11 window id of `handle`, None for another window system.
#[cfg(target_os = "linux")]
fn x11_window(handle: RawWindowHandle) -> Option<u32> {
match handle {
RawWindowHandle::Xlib(h) => u32::try_from(h.window).ok(),
RawWindowHandle::Xcb(h) => Some(h.window.get()),
_ => None,
}
}
/// The running app's floating windows, which [`Hooks`] records.
#[cfg(target_os = "linux")]
static CHILD_WINDOWS: LazyLock<ChildWindows> = LazyLock::new(ChildWindows::default);
/// Records the windows eframe creates and drops in [`CHILD_WINDOWS`]; set as
/// [`eframe::NativeOptions::viewport_hooks`].
#[cfg(target_os = "linux")]
pub(crate) struct Hooks;
#[cfg(target_os = "linux")]
impl eframe::ViewportHooks for Hooks {
fn window_created(&self, viewport: egui::ViewportId, window: &winit::window::Window) {
CHILD_WINDOWS.created(viewport, window.window_handle().ok().map(|h| h.as_raw()));
}
fn window_destroyed(&self, viewport: egui::ViewportId) {
CHILD_WINDOWS.destroyed(viewport);
}
}
/// Unmaps the X11 `windows` through a connection of its own, and waits until the server has done so.
/// An id whose window is gone gets an error, which is ignored.
#[cfg(target_os = "linux")]
fn unmap_x11(windows: &[u32]) {
use x11rb::protocol::xproto::ConnectionExt as _;
let conn = match x11rb::connect(None) {
Ok((conn, _)) => conn,
Err(e) => {
log::warn!("couldn't reach the X server to hide the floating windows on quit: {e}");
return;
}
};
for &window in windows {
if let Ok(cookie) = conn.unmap_window(window) {
cookie.ignore_error();
}
}
// The server answers once it has handled the requests before this one.
if let Ok(cookie) = conn.get_input_focus() {
let _ = cookie.reply();
}
}
/// Attaches no buffer to `window`'s Wayland surface and commits it, which unmaps the window: the
/// compositor stops showing it, and the window and its connection stay open.
#[cfg(target_os = "linux")]
fn unmap_wayland(window: &winit::window::Window) {
let Some((conn, surface)) = super::portal_save::wayland::surface(window) else { return };
surface.attach(None, 0, 0);
surface.commit();
if let Err(e) = conn.flush() {
log::warn!("couldn't reach the compositor to hide the window on quit: {e}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
/// A main window that records what quitting did to it.
struct Window<'a> {
wayland: bool,
done: &'a RefCell<Vec<String>>,
}
impl QuittingWindow for Window<'_> {
fn is_wayland(&self) -> bool {
self.wayland
}
fn hide(&self) {
self.done.borrow_mut().push("hide".into());
}
fn unmap(&self) {
self.done.borrow_mut().push("unmap".into());
}
}
fn quit(wayland: Option<bool>, x11: &[u32]) -> Vec<String> {
let done = RefCell::new(Vec::new());
let window = wayland.map(|wayland| Window { wayland, done: &done });
off_screen(window.as_ref(), x11, |ids| done.borrow_mut().push(format!("unmap x11 {ids:?}")));
done.into_inner()
}
/// winit cannot hide a Wayland window: quitting unmaps it, and never only asks winit to hide it.
#[test]
fn quitting_unmaps_a_wayland_main_window() {
assert_eq!(quit(Some(true), &[]), ["unmap"]);
}
/// An X11 main window (XWayland included) is hidden, and the floating panels' and documents' X11
/// windows are unmapped with it.
#[test]
fn quitting_hides_an_x11_main_window_and_its_floating_windows() {
assert_eq!(quit(Some(false), &[]), ["hide"]);
assert_eq!(quit(Some(false), &[7, 9]), ["hide", "unmap x11 [7, 9]"]);
}
/// Without a main window (none was made), the floating windows still go, and with none at all
/// nothing is done.
#[test]
fn quitting_without_a_main_window() {
assert_eq!(quit(None, &[3]), ["unmap x11 [3]"]);
assert!(quit(None, &[]).is_empty());
}
#[cfg(target_os = "linux")]
fn xlib(id: std::os::raw::c_ulong) -> Option<RawWindowHandle> {
Some(RawWindowHandle::Xlib(winit::raw_window_handle::XlibWindowHandle::new(id)))
}
#[cfg(target_os = "linux")]
fn viewport(name: &str) -> egui::ViewportId {
egui::ViewportId::from_hash_of(name)
}
/// The floating windows eframe made are recorded until it drops them; the main window, which
/// quitting reaches through winit, is not.
#[cfg(target_os = "linux")]
#[test]
fn floating_x11_windows_are_recorded_while_they_exist() {
let windows = ChildWindows::default();
windows.created(egui::ViewportId::ROOT, xlib(1));
windows.created(viewport("Layers"), xlib(9));
let xcb = winit::raw_window_handle::XcbWindowHandle::new(std::num::NonZeroU32::new(5).expect("not zero"));
windows.created(viewport("Untitled-2"), Some(RawWindowHandle::Xcb(xcb)));
windows.created(viewport("Color"), xlib(7));
assert_eq!(windows.x11(), [5, 7, 9]);
windows.destroyed(viewport("Color"));
assert_eq!(windows.x11(), [5, 9]);
// A viewport that eframe has already dropped, or never made, is no error.
windows.destroyed(viewport("Color"));
windows.destroyed(viewport("Swatches"));
assert_eq!(windows.x11(), [5, 9]);
}
/// A window made again for a viewport replaces its record, and one of another window system, or
/// one whose handle could not be read, removes it.
#[cfg(target_os = "linux")]
#[test]
fn a_window_made_again_replaces_its_record() {
let windows = ChildWindows::default();
windows.created(viewport("Layers"), xlib(9));
windows.created(viewport("Layers"), xlib(11));
assert_eq!(windows.x11(), [11]);
let surface = std::ptr::NonNull::<std::ffi::c_void>::dangling();
let wayland = winit::raw_window_handle::WaylandWindowHandle::new(surface);
windows.created(viewport("Layers"), Some(RawWindowHandle::Wayland(wayland)));
assert!(windows.x11().is_empty());
windows.created(viewport("Layers"), xlib(11));
windows.created(viewport("Layers"), None);
assert!(windows.x11().is_empty());
}
}
-18
View File
@@ -515,21 +515,3 @@ 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);
}
+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 (`wl_data_device`) on the window's own connection, and only while the window has focus. One Copy offers the PNG as `image/png`, the SVG as `image/svg+xml` (with Include SVG Code on) and the text as `text/plain;charset=utf-8`, `UTF8_STRING` and `text/plain`; a format with no data is not offered. Paste reads `image/svg+xml`, then text, then `image/png`, and blank text or text that only names a picture gives way to the PNG; the Paste menu item is enabled by the types offered, without reading any data. Whether the clipboard still holds VectorCraft's Copy is told by its data source, which the compositor cancels when another app copies. A read takes 64 MiB at most and fails beyond that. Reads run on a worker thread: text waits 2 s at most on the UI thread, SVG and PNG 10 s on the paste's background thread. An owner that never sends its data costs one read its wait, and later reads return at once until it answers. When a text read gets no answer in its 2 s, the paste waits on its background thread for that read's answer, up to 10 s after the read was sent, then reads the clipboard again unless it holds VectorCraft's Copy. Another app's paste of a Copy is sent on a thread of its own, 8 at once at most; a paste beyond them gets nothing. 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 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.
- **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 takes its windows off screen before these waits (`apps/vectorcraft/src/quit_windows.rs`). An X11 main window, an XWayland one included, is hidden. winit cannot hide a native Wayland window, so on Linux the app attaches no buffer to the main window's surface, which unmaps it while the window and its connection stay open for the Wayland clipboard. On Linux the X11 windows of floating panels and documents are unmapped through a second X11 connection. On native Wayland, floating panels and documents are normally drawn inside the main window; on the BSDs a native Wayland main window 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.
+2 -1
View File
@@ -1772,7 +1772,8 @@ 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`).
- 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/quit_windows.rs`).
- On Linux, quitting takes every window off screen before that wait: a native Wayland main window is unmapped (its surface gets no buffer) and stays open with the connection the Wayland clipboard uses, and the X11 windows of floating panels and documents are unmapped with the hidden main window (`apps/vectorcraft/src/quit_windows.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`).