git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit07ed0904af77f4cad2f095d3c27fb0b15a1f36d3
parent81e4c21f08
authorLucas Galante <[email protected]>
date2026-07-28 20:11
feat: PrintScreen captures a screenshot

Action::Screenshot (default binding: bare Print) runs the same capture
as 'ccectl screenshot' — next frame of the enabled output, saved to
~/Pictures/screenshots — so artifacts can be captured from the keyboard
in any compositor state, including zoomed out.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/window.rs             | 20 ++++++++++++++++++++
 src/server/window_manager.rs     |  5 +++++
 src/server/wlroots_log_wrapper.c | 19 +++++++++++++++++++
 wrapper.h                        |  1 +
 4 files changed, 45 insertions(+)

diff --git a/src/server/window.rs b/src/server/window.rs
index 582bcdb..ef894ad 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1993,6 +1993,11 @@ impl Window {
                         let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
                         ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
                     }
+                    // Keep the opaque region in step with the dest scale —
+                    // unscaled it covers the shrunken node's translucent CSD
+                    // margins and occlusion culling stops repainting behind
+                    // the client shadow (stale pixels show through it).
+                    ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
                 }
                 // Non-surface buffers are frozen SAVED copies (see
                 // save_surface_tree_iter): their natural buffer size is
@@ -2201,6 +2206,11 @@ impl Window {
                     let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
                 }
+                // Keep the opaque region in step with the dest scale —
+                // unscaled it covers the shrunken node's translucent CSD
+                // margins and occlusion culling stops repainting behind
+                // the client shadow (stale pixels show through it).
+                ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
             }
             // Non-surface buffers are frozen SAVED copies (see
             // save_surface_tree_iter): their natural buffer size is
@@ -3604,6 +3614,11 @@ impl Decoration {
                     let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
                 }
+                // Keep the opaque region in step with the dest scale —
+                // unscaled it covers the shrunken node's translucent CSD
+                // margins and occlusion culling stops repainting behind
+                // the client shadow (stale pixels show through it).
+                ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
             }
             // Non-surface buffers are frozen SAVED copies (see
             // save_surface_tree_iter): their natural buffer size is
@@ -3674,6 +3689,11 @@ impl Decoration {
                     let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
                 }
+                // Keep the opaque region in step with the dest scale —
+                // unscaled it covers the shrunken node's translucent CSD
+                // margins and occlusion culling stops repainting behind
+                // the client shadow (stale pixels show through it).
+                ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
             }
             // Non-surface buffers are frozen SAVED copies (see
             // save_surface_tree_iter): their natural buffer size is
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index df2ecce..099203d 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2129,6 +2129,11 @@ impl WindowManager {
             Action::WindowSwitcherPrev => {
                 self.launch_window_switcher(true);
             }
+            Action::Screenshot => {
+                // Same capture as `ccectl screenshot`: the enabled output's
+                // next frame, saved under ~/Pictures/screenshots.
+                let _ = self.process_ipc_command("screenshot");
+            }
             Action::Reload => {
                 log::info!("monolithic execute_action: Reload requested");
                 match self.reload_config() {
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index e3e3313..ed4068e 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -48,6 +48,7 @@ void river_init_wlroots_log(enum wlr_log_importance importance) {
 #include <time.h>
 #include <scenefx/types/wlr_scene.h>
 #include <wlr/types/wlr_output.h>
+#include <wlr/util/region.h>
 #include <wlr/types/wlr_compositor.h>
 #include <wlr/types/wlr_input_device.h>
 #include <wlr/types/wlr_keyboard.h>
@@ -899,6 +900,24 @@ int river_scene_buffer_get_height(struct wlr_scene_buffer *scene_buffer) {
 	return scene_buffer->dst_height;
 }
 
+/* Scale a surface's opaque region to match a custom buffer dest size and
+ * apply it to the scene buffer. The scene keeps opaque regions in SURFACE
+ * coordinates and scenefx's occlusion culling only intersects them with the
+ * node box — so a zoomed-down window's unscaled opaque rect still covered
+ * (almost) the whole scaled node, INCLUDING its translucent CSD shadow
+ * margins. Culling then skipped repainting behind the shadow ring: stale
+ * pixels showed through the translucent shadow around focused windows when
+ * zoomed (worst at the bottom, where Chromium's ring is tallest). */
+void river_scene_buffer_set_scaled_opaque_region(struct wlr_scene_buffer *scene_buffer,
+		struct wlr_surface *surface, double scale) {
+	pixman_region32_t scaled;
+	pixman_region32_init(&scaled);
+	pixman_region32_copy(&scaled, &surface->opaque_region);
+	wlr_region_scale(&scaled, &scaled, (float)scale);
+	wlr_scene_buffer_set_opaque_region(scene_buffer, &scaled);
+	pixman_region32_fini(&scaled);
+}
+
 int river_scene_buffer_get_dest_width(struct wlr_scene_buffer *scene_buffer) {
 	return scene_buffer->dst_width;
 }
diff --git a/wrapper.h b/wrapper.h
index bd996e3..3d7f448 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -269,6 +269,7 @@ void river_scene_node_set_corner_radius(struct wlr_scene_node *node, int radius)
 void river_scene_buffer_set_dest_size_if_changed(struct wlr_scene_buffer *scene_buffer, int width, int height);
 int river_scene_buffer_get_width(struct wlr_scene_buffer *scene_buffer);
 int river_scene_buffer_get_height(struct wlr_scene_buffer *scene_buffer);
+void river_scene_buffer_set_scaled_opaque_region(struct wlr_scene_buffer *scene_buffer, struct wlr_surface *surface, double scale);
 int river_scene_buffer_get_dest_width(struct wlr_scene_buffer *scene_buffer);
 int river_scene_buffer_get_dest_height(struct wlr_scene_buffer *scene_buffer);
 bool river_scene_node_get_enabled(struct wlr_scene_node *node);