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

commitc13bca08b013d55ccfd9988edc6b11fdfefbe25f
parenta04d5341c7
authorIsaac Freund <[email protected]>
date2025-12-27 22:01
protocol: add river_window_v1.set_clip_box

 protocol/river-window-management-v1.xml | 21 +++++++++++++
 river/Window.zig                        | 55 ++++++++++++++++++++++++++++-----
 river/XdgToplevel.zig                   |  2 +-
 3 files changed, 70 insertions(+), 8 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index cda16a5..ea621aa 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -319,6 +319,8 @@
         summary="proposed dimensions out of bounds"/>
       <entry name="invalid_border" value="2"
         summary="invalid arg to set_borders"/>
+      <entry name="invalid_clip_box" value="3"
+        summary="invalid arg to set_clip_box"/>
     </enum>
 
     <request name="destroy" type="destructor">
@@ -945,6 +947,25 @@
         state has been sent by the server.
       </description>
     </event>
+
+    <request name="set_clip_box" since="2">
+      <description summary="clip the window to a given box">
+        Clip the window, including borders and decoration surfaces, to the box
+        specified by the x, y, width, and height arguments. The x/y position of
+        the box is relative to the top left corner of the window.
+
+        The width and height arguments must be greater than or equal to 0.
+
+        Setting a clip box with 0 width or height disables clipping.
+
+        This request modifies rendering state and may only be made as part of a
+        render sequence, see the river_window_manager_v1 description.
+      </description>
+      <arg name="x" type="int"/>
+      <arg name="y" type="int"/>
+      <arg name="width" type="int"/>
+      <arg name="height" type="int"/>
+    </request>
   </interface>
 
   <interface name="river_decoration_v1" version="2">
diff --git a/river/Window.zig b/river/Window.zig
index b116136..190fce2 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -223,6 +223,7 @@ rendering_requested: struct {
     y: i32 = 0,
     hidden: bool = false,
     border: Border = .{},
+    clip: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
 } = .{},
 
 /// The currently rendered position/dimensions of the window in the scene graph
@@ -621,6 +622,18 @@ fn handleRequest(
             if (!server.wm.ensureWindowing()) return;
             wm_requested.fullscreen = null;
         },
+        .set_clip_box => |args| {
+            if (!server.wm.ensureRendering()) return;
+            if (args.width < 0 or args.height < 0) {
+                window_v1.postError(.invalid_clip_box, "width/height must be greater than or equal to 0 ");
+            }
+            rendering_requested.clip = .{
+                .x = args.x,
+                .y = args.y,
+                .width = args.width,
+                .height = args.height,
+            };
+        },
     }
 }
 
@@ -788,6 +801,21 @@ pub fn renderFinish(window: *Window) void {
     window.tree.node.setPosition(window.box.x, window.box.y);
     window.popup_tree.node.setPosition(window.box.x, window.box.y);
 
+    {
+        var surface_clip = window.rendering_requested.clip;
+        switch (window.impl) {
+            .toplevel => |toplevel| {
+                surface_clip.x += toplevel.geometry.x;
+                surface_clip.y += toplevel.geometry.y;
+            },
+            .xwayland, .destroying => {},
+        }
+        // wlroots asserts that a subsurface tree is present.
+        if (!window.surfaces.tree.children.empty()) {
+            window.surfaces.tree.node.subsurfaceTreeSetClip(&surface_clip);
+        }
+    }
+
     // f32 cannot represent all u32 values exactly, therefore we must initially use f64
     // (which can) and then cast to f32, potentially losing precision.
     const border = &window.rendering_requested.border;
@@ -810,13 +838,13 @@ pub fn renderFinish(window: *Window) void {
         .width = border.width,
         .height = window.box.height,
     };
-    const top: wlr.Box = .{
+    var top: wlr.Box = .{
         .x = 0,
         .y = -@as(i32, border.width),
         .width = window.box.width,
         .height = border.width,
     };
-    const bottom: wlr.Box = .{
+    var bottom: wlr.Box = .{
         .x = 0,
         .y = window.box.height,
         .width = window.box.width,
@@ -836,11 +864,18 @@ pub fn renderFinish(window: *Window) void {
     }
 
     inline for (.{
-        .{ .name = "left", .box = left },
-        .{ .name = "right", .box = right },
-        .{ .name = "top", .box = top },
-        .{ .name = "bottom", .box = bottom },
+        .{ .name = "left", .box = &left },
+        .{ .name = "right", .box = &right },
+        .{ .name = "top", .box = &top },
+        .{ .name = "bottom", .box = &bottom },
     }) |edge| {
+        if (!window.rendering_requested.clip.empty()) {
+            if (!edge.box.intersection(edge.box, &window.rendering_requested.clip)) {
+                // TODO(wlroots): remove this redundant code after fixed upstream
+                // https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5084
+                edge.box.* = .{ .x = 0, .y = 0, .width = 0, .height = 0 };
+            }
+        }
         const rect = @field(window.border, edge.name);
         rect.node.setEnabled(@field(border.edges, edge.name));
         rect.node.setPosition(edge.box.x, edge.box.y);
@@ -850,7 +885,13 @@ pub fn renderFinish(window: *Window) void {
 
     inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
         var it = decorations.iterator(.forward);
-        while (it.next()) |decoration| decoration.renderFinish();
+        while (it.next()) |decoration| {
+            decoration.renderFinish();
+            // wlroots asserts that a subsurface tree is present.
+            if (!decoration.surfaces.tree.children.empty()) {
+                decoration.surfaces.tree.node.subsurfaceTreeSetClip(&window.rendering_requested.clip);
+            }
+        }
     }
 }
 
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index d1bb9b7..04073db 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -356,7 +356,7 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
                 old_geometry.y != toplevel.geometry.y)
             {
                 // We need to update the surface clip box to reflect the geometry change.
-                // TODO actually update the clip box when clipping is supported
+                window.renderFinish();
             }
         },
         // If the client has not yet acked our configure, we need to send a