diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs index 2a83b78ce5..27e259ce70 100644 --- a/apps/photocraft/src/main.rs +++ b/apps/photocraft/src/main.rs @@ -76,8 +76,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 @@ -646,7 +645,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 }]; diff --git a/crates/ui-egui/src/canvas/tabs.rs b/crates/ui-egui/src/canvas/tabs.rs index d3aa28546a..2ab777b532 100644 --- a/crates/ui-egui/src/canvas/tabs.rs +++ b/crates/ui-egui/src/canvas/tabs.rs @@ -730,7 +730,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 389af64d96..426872e347 100644 --- a/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs +++ b/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs @@ -210,7 +210,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 4c6309e254..7c7ef766d2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -41,6 +41,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")); + } +}