diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs
--- 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
--- 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<Servic
         Pending { receiver: rx, cancelled }
     }))
 }
+/// Whether to pick through the desktop portal: on a Wayland desktop, whichever display server the
+/// window opened on. Xwayland serves only its own windows, so reading its root window fails (#8977).
+#[cfg(target_os = "linux")]
+pub fn uses_portal(desktop: crate::linux_libs::DisplaySession) -> 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 }];
