From c139d3dbe6d6b561d337bca766d22b49dd46da0e Mon Sep 17 00:00:00 2001 From: Taylor Burke Date: Tue, 27 Oct 2026 14:44:34 -0500 Subject: [PATCH 1/2] Screen picker: a Wayland desktop picks through the portal when the window runs on Xwayland (#8977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a pen attached, or Preferences › Performance › Linux display server set to X11, the window opens through Xwayland on a Wayland desktop. The picker followed the window's display server and grabbed Xwayland's root window, which holds no desktop pixels: GetImage failed with BadMatch. Choose the backend from the desktop session instead, so Wayland always uses the portal's PickColor. Co-Authored-By: Claude Opus 5.5 --- apps/photocraft/src/main.rs | 9 ++++++++- apps/photocraft/src/screen_color.rs | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs index a4fe847cda..30845501c9 100644 --- a/apps/photocraft/src/main.rs +++ b/apps/photocraft/src/main.rs @@ -602,7 +602,14 @@ fn main() -> eframe::Result { // Caps Lock state (X11/Windows/macOS; `None` on native Wayland): the canvas shows the // precise crosshair for painting tools while it is toggled on (#1758). app.services.caps_lock = caps_lock::service(cc); - app.services.screen_pick = screen_color::service(cc, app.services.is_wayland); + // Pick through the portal on a Wayland desktop even when the window went through + // Xwayland (a pen, or the X11 display-server preference): Xwayland's root window has + // no desktop pixels to read (#8977). + #[cfg(target_os = "linux")] + let portal = screen_color::uses_portal(native_session); + #[cfg(not(target_os = "linux"))] + let portal = false; + app.services.screen_pick = screen_color::service(cc, portal); // Tablet pressure/tilt/eraser (winit drops them): the macOS monitor and the X11 reader // write into the stylus feed. The monitor goes in here, not before the event loop: // AppKit's shared application only exists once winit created it (#759). diff --git a/apps/photocraft/src/screen_color.rs b/apps/photocraft/src/screen_color.rs index ec8cd6378c..6a5ae643c5 100644 --- a/apps/photocraft/src/screen_color.rs +++ b/apps/photocraft/src/screen_color.rs @@ -38,6 +38,12 @@ pub fn service(cc: &eframe::CreationContext<'_>, wayland: bool) -> Option bool { + desktop == crate::linux_libs::DisplaySession::Wayland +} #[derive(Clone, Copy)] #[cfg_attr(not(any(target_os = "linux", target_os = "macos", target_os = "windows", test)), allow(dead_code))] struct Placement { @@ -480,6 +486,15 @@ mod tests { let offset = (y * first.width + x) * 4; assert_eq!(&first.rgba[offset..offset + 4], &[255, 128, 64, 255]); } + #[cfg(target_os = "linux")] + #[test] + fn a_wayland_desktop_picks_through_the_portal_even_with_an_xwayland_window() { + use crate::linux_libs::DisplaySession; + // A pen or the X11 display-server preference opens the window through Xwayland, but the + // session is still Wayland: the X11 grab fails there with BadMatch (#8977). + assert!(uses_portal(DisplaySession::Wayland)); + assert!(!uses_portal(DisplaySession::X11)); + } #[test] fn screen_capture_matches_negative_and_hidpi_monitor_geometry() { let m = [Placement { index: 2, x: -3840, y: 0, width: 3840, height: 2160 }]; From af31ea090a2cefccb98a8d58d5d28159196d3145 Mon Sep 17 00:00:00 2001 From: Taylor Burke Date: Tue, 27 Oct 2026 15:31:30 -0500 Subject: [PATCH 2/2] Extra windows share the main window's app id, so Wayland docks and window rules see them (#6697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document windows (Window › New Window), Camera Raw and the screen color picker were built from ViewportBuilder::default() with no app id, so on Wayland their class was empty: Hyprland rules couldn't match them and docks couldn't map them to the .desktop file. They now use the same app id as the main window, defined once in photocraft_ui_egui::os_window. Co-Authored-By: Claude Opus 5.5 --- apps/photocraft/src/main.rs | 3 +-- crates/ui-egui/src/canvas/tabs.rs | 2 +- .../src/dialogs/camera_raw_ui/controls.rs | 2 +- crates/ui-egui/src/dialogs/screen_picker.rs | 2 +- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/os_window.rs | 19 +++++++++++++++++++ 6 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/src/os_window.rs diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs index a4fe847cda..353eba6010 100644 --- a/apps/photocraft/src/main.rs +++ b/apps/photocraft/src/main.rs @@ -75,8 +75,7 @@ mod window_frame; use photocraft_engine::Session; use photocraft_ui_egui::PhotocraftApp; -/// Matches the `.desktop` file and hicolor icon name, so Wayland docks pick up the icon. -const APP_ID: &str = "ai.storyteller.photocraft"; +use photocraft_ui_egui::os_window::APP_ID; /// Windows and Linux: no OS title bar; the app's top bar is the title bar, with its own caption /// buttons and edge resizing (`photocraft_ui_egui::titlebar`), as Photoshop does on Windows. macOS diff --git a/crates/ui-egui/src/canvas/tabs.rs b/crates/ui-egui/src/canvas/tabs.rs index 5f59f3c028..7cbc70f57b 100644 --- a/crates/ui-egui/src/canvas/tabs.rs +++ b/crates/ui-egui/src/canvas/tabs.rs @@ -704,7 +704,7 @@ pub fn extra_windows(app: &mut PhotocraftApp, ctx: &egui::Context) { let Some(st) = app.session.documents().get(w.document) else { continue }; let title = format!("{} — window {}", st.doc.name, w.id); let vid = egui::ViewportId::from_hash_of(("docwin", w.id)); - let mut builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([800.0, 600.0]); + let mut builder = crate::os_window::builder().with_title(title).with_inner_size([800.0, 600.0]); if let Some([x, y]) = w.position { builder = builder.with_position([x, y]); } diff --git a/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs b/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs index 3f9e8ba19b..04de4a696c 100644 --- a/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs +++ b/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs @@ -197,7 +197,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { ui.painter().rect_filled(r, 0.0, Tokens::get(ui.ctx()).scrim); }); // A window of its own (title bar, moved and resized like any window), like Adobe Camera Raw. - let builder = egui::ViewportBuilder::default().with_title(window_title).with_inner_size([1360.0, 900.0]).with_min_inner_size([900.0, 600.0]); + let builder = crate::os_window::builder().with_title(window_title).with_inner_size([1360.0, 900.0]).with_min_inner_size([900.0, 600.0]); let mut close = false; ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("camera-raw-window"), builder, |ui, class| { if ui.ctx().input(|i| i.viewport().close_requested()) { diff --git a/crates/ui-egui/src/dialogs/screen_picker.rs b/crates/ui-egui/src/dialogs/screen_picker.rs index 59e36636e6..25c948a6c7 100644 --- a/crates/ui-egui/src/dialogs/screen_picker.rs +++ b/crates/ui-egui/src/dialogs/screen_picker.rs @@ -261,7 +261,7 @@ pub fn cancel(ctx: &Context) { /// The picker window: hidden until its snapshot has been painted (#3210). fn viewport_builder(ctx: &Context, image: &ScreenImage) -> egui::ViewportBuilder { - let mut builder = egui::ViewportBuilder::default() + let mut builder = crate::os_window::builder() .with_title(tl!("PhotoCraft — Screen Color Picker")) .with_decorations(false) .with_resizable(false) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ef7269dbaa..57b256bdfd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -30,6 +30,7 @@ pub mod menu_catalog; pub mod menus; pub mod monitor_status; pub mod notices; +pub mod os_window; pub mod panels; pub mod preset_files_ui; pub mod proxy; diff --git a/crates/ui-egui/src/os_window.rs b/crates/ui-egui/src/os_window.rs new file mode 100644 index 0000000000..4fa88f65e3 --- /dev/null +++ b/crates/ui-egui/src/os_window.rs @@ -0,0 +1,19 @@ +//! Extra OS windows (document windows, Camera Raw, the screen color picker) share the main +//! window's app id, so docks show the PhotoCraft icon on them and Wayland compositors' window +//! rules can match them (Hyprland's `class:`, #6697). Without one their app id is empty. + +/// Matches the `.desktop` file and hicolor icon name, so Wayland docks pick up the icon. +pub const APP_ID: &str = "ai.storyteller.photocraft"; + +/// A builder for an extra OS window, carrying [`APP_ID`]. +pub fn builder() -> egui::ViewportBuilder { + egui::ViewportBuilder::default().with_app_id(APP_ID) +} + +#[cfg(test)] +mod tests { + #[test] + fn extra_windows_carry_the_app_id() { + assert_eq!(super::builder().app_id.as_deref(), Some("ai.storyteller.photocraft")); + } +}