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

commit8163a3b7749291d592404aed9d8663829a58c2f6
parent4176a7b431
authorIsaac Freund <[email protected]>
date2026-01-07 04:20
protocol: add river_window_v1.set_content_clip_box

 protocol/river-window-management-v1.xml |  28 +++++
 river/Decoration.zig                    |   2 +-
 river/Scene.zig                         |  56 ++++++----
 river/ShellSurface.zig                  |   2 +-
 river/Window.zig                        | 192 +++++++++++++++++++-------------
 5 files changed, 181 insertions(+), 99 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 60296ac..d005671 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -965,6 +965,8 @@
 
         Setting a clip box with 0 width or height disables clipping.
 
+        Both set_clip_box and set_content_clip_box may be enabled simultaneously.
+
         This request modifies rendering state and may only be made as part of a
         render sequence, see the river_window_manager_v1 description.
       </description>
@@ -989,6 +991,32 @@
       </description>
       <arg name="unreliable_pid" type="int"/>
     </event>
+
+    <request name="set_content_clip_box" since="3">
+      <description summary="clip the window content to a given box">
+        Clip the content of the window, excluding 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.
+
+        Borders drawn by the compositor (see set_borders) are placed around the
+        intersection of the window content (as defined by the dimensions event)
+        and the content clip box when content clipping is enabled.
+
+        The width and height arguments must be greater than or equal to 0.
+
+        Setting a clip box with 0 width or height disables content clipping.
+
+        Both set_clip_box and set_content_clip_box may be enabled simultaneously.
+
+        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="3">
diff --git a/river/Decoration.zig b/river/Decoration.zig
index 4e432ce..34538d0 100644
--- a/river/Decoration.zig
+++ b/river/Decoration.zig
@@ -142,7 +142,7 @@ pub fn renderFinish(decoration: *Decoration, window_clip: *const wlr.Box) void {
     if (rendering_requested.sync_next_commit) {
         rendering_requested.sync_next_commit = false;
 
-        if (!decoration.surfaces.saved.node.enabled) {
+        if (!decoration.surfaces.saved) {
             if (decoration.object) |object| {
                 object.postError(.no_commit,
                     \\no wl_surface.commit after sync_next_commit and before update_rendering_finish
diff --git a/river/Scene.zig b/river/Scene.zig
index feb3794..f2359e0 100644
--- a/river/Scene.zig
+++ b/river/Scene.zig
@@ -133,36 +133,50 @@ pub fn layerSurfaceTree(scene: *Scene, layer: zwlr.LayerShellV1.Layer) *wlr.Scen
 }
 
 pub const SaveableSurfaces = struct {
+    enabled: bool,
+    saved: bool,
     tree: *wlr.SceneTree,
-    saved: *wlr.SceneTree,
+    saved_tree: *wlr.SceneTree,
 
     pub fn init(parent: *wlr.SceneTree) !SaveableSurfaces {
         const surfaces: SaveableSurfaces = .{
+            .enabled = true,
+            .saved = false,
             .tree = try parent.createSceneTree(),
-            .saved = try parent.createSceneTree(),
+            .saved_tree = try parent.createSceneTree(),
         };
-        surfaces.saved.node.setEnabled(false);
+        surfaces.syncEnabled();
         return surfaces;
     }
 
-    pub fn save(surfaces: SaveableSurfaces) void {
-        if (surfaces.saved.node.enabled) return;
-        assert(surfaces.tree.node.enabled);
-        assert(surfaces.saved.children.empty());
+    fn syncEnabled(surfaces: *const SaveableSurfaces) void {
+        surfaces.tree.node.setEnabled(surfaces.enabled and !surfaces.saved);
+        surfaces.saved_tree.node.setEnabled(surfaces.enabled and surfaces.saved);
+    }
 
-        surfaces.tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, surfaces.saved);
+    pub fn setEnabled(surfaces: *SaveableSurfaces, enabled: bool) void {
+        if (enabled == surfaces.enabled) return;
+        surfaces.enabled = enabled;
+        surfaces.syncEnabled();
+    }
 
-        surfaces.tree.node.setEnabled(false);
-        surfaces.saved.node.setEnabled(true);
+    pub fn save(surfaces: *SaveableSurfaces) void {
+        if (surfaces.saved) return;
+        assert(surfaces.tree.node.enabled == surfaces.enabled);
+        assert(!surfaces.saved_tree.node.enabled);
+        assert(surfaces.saved_tree.children.empty());
+        surfaces.tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, surfaces.saved_tree);
+        surfaces.saved = true;
+        surfaces.syncEnabled();
     }
 
     fn saveSurfaceTreeIter(
         buffer: *wlr.SceneBuffer,
         sx: c_int,
         sy: c_int,
-        saved: *wlr.SceneTree,
+        saved_tree: *wlr.SceneTree,
     ) void {
-        const scene_buffer = saved.createSceneBuffer(buffer.buffer) catch {
+        const scene_buffer = saved_tree.createSceneBuffer(buffer.buffer) catch {
             std.log.err("out of memory", .{});
             return;
         };
@@ -172,15 +186,15 @@ pub const SaveableSurfaces = struct {
         scene_buffer.setTransform(buffer.transform);
     }
 
-    pub fn dropSaved(surfaces: SaveableSurfaces) void {
-        if (!surfaces.saved.node.enabled) return;
-
+    pub fn dropSaved(surfaces: *SaveableSurfaces) void {
+        if (!surfaces.saved) return;
         assert(!surfaces.tree.node.enabled);
-
-        var it = surfaces.saved.children.safeIterator(.forward);
-        while (it.next()) |node| node.destroy();
-
-        surfaces.saved.node.setEnabled(false);
-        surfaces.tree.node.setEnabled(true);
+        assert(surfaces.saved_tree.node.enabled == surfaces.enabled);
+        {
+            var it = surfaces.saved_tree.children.safeIterator(.forward);
+            while (it.next()) |node| node.destroy();
+        }
+        surfaces.saved = false;
+        surfaces.syncEnabled();
     }
 };
diff --git a/river/ShellSurface.zig b/river/ShellSurface.zig
index 7f8e4e7..5398a51 100644
--- a/river/ShellSurface.zig
+++ b/river/ShellSurface.zig
@@ -148,7 +148,7 @@ pub fn renderFinish(shell_surface: *ShellSurface) void {
     if (rendering_requested.sync_next_commit) {
         rendering_requested.sync_next_commit = false;
 
-        if (!shell_surface.surfaces.saved.node.enabled) {
+        if (!shell_surface.surfaces.saved) {
             shell_surface.object.postError(.no_commit,
                 \\no wl_surface.commit after sync_next_commit and before update_rendering_finish
             );
diff --git a/river/Window.zig b/river/Window.zig
index 3ce5615..9b15cf5 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -211,6 +211,7 @@ rendering_requested: struct {
     hidden: bool = false,
     border: Border = .{},
     clip: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
+    content_clip: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
 } = .{},
 
 /// The currently rendered position/dimensions of the window in the scene graph
@@ -630,6 +631,19 @@ fn handleRequest(
                 .height = args.height,
             };
         },
+        .set_content_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 ");
+                return;
+            }
+            rendering_requested.content_clip = .{
+                .x = args.x,
+                .y = args.y,
+                .width = args.width,
+                .height = args.height,
+            };
+        },
     }
 }
 
@@ -774,12 +788,14 @@ pub fn renderStart(window: *Window) void {
 }
 
 pub fn renderFinish(window: *Window) void {
+    const requested = &window.rendering_requested;
+
     window.tree.node.setEnabled(!window.rendering_requested.hidden);
     window.popup_tree.node.setEnabled(!window.rendering_requested.hidden);
 
     window.box = .{
-        .x = window.rendering_requested.x,
-        .y = window.rendering_requested.y,
+        .x = requested.x,
+        .y = requested.y,
         .width = window.rendering_sent.width,
         .height = window.rendering_sent.height,
     };
@@ -797,96 +813,120 @@ 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);
-        }
-    }
+    window.applySurfaceClip();
 
-    // 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;
-    const color: [4]f32 = .{
-        @floatCast(@as(f64, @floatFromInt(border.r)) / math.maxInt(u32)),
-        @floatCast(@as(f64, @floatFromInt(border.g)) / math.maxInt(u32)),
-        @floatCast(@as(f64, @floatFromInt(border.b)) / math.maxInt(u32)),
-        @floatCast(@as(f64, @floatFromInt(border.a)) / math.maxInt(u32)),
-    };
-
-    var left: wlr.Box = .{
-        .x = -@as(i32, border.width),
-        .y = 0,
-        .width = border.width,
-        .height = window.box.height,
-    };
-    var right: wlr.Box = .{
-        .x = window.box.width,
-        .y = 0,
-        .width = border.width,
-        .height = window.box.height,
-    };
-    var top: wlr.Box = .{
-        .x = 0,
-        .y = -@as(i32, border.width),
-        .width = window.box.width,
-        .height = border.width,
-    };
-    var bottom: wlr.Box = .{
+    var content: wlr.Box = .{
         .x = 0,
-        .y = window.box.height,
+        .y = 0,
         .width = window.box.width,
-        .height = border.width,
+        .height = window.box.height,
     };
+    if (requested.content_clip.empty() or
+        content.intersection(&content, &requested.content_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 = &requested.border;
+        const color: [4]f32 = .{
+            @floatCast(@as(f64, @floatFromInt(border.r)) / math.maxInt(u32)),
+            @floatCast(@as(f64, @floatFromInt(border.g)) / math.maxInt(u32)),
+            @floatCast(@as(f64, @floatFromInt(border.b)) / math.maxInt(u32)),
+            @floatCast(@as(f64, @floatFromInt(border.a)) / math.maxInt(u32)),
+        };
+
+        var left: wlr.Box = .{
+            .x = -@as(i32, border.width),
+            .y = 0,
+            .width = border.width,
+            .height = content.height,
+        };
+        var right: wlr.Box = .{
+            .x = content.width,
+            .y = 0,
+            .width = border.width,
+            .height = content.height,
+        };
+        var top: wlr.Box = .{
+            .x = 0,
+            .y = -@as(i32, border.width),
+            .width = content.width,
+            .height = border.width,
+        };
+        var bottom: wlr.Box = .{
+            .x = 0,
+            .y = content.height,
+            .width = content.width,
+            .height = border.width,
+        };
+
+        // Use left and right scene rects to draw the corners if needed
+        if (border.edges.top) {
+            left.y -= border.width;
+            left.height += border.width;
+            right.y -= border.width;
+            right.height += border.width;
+        }
+        if (border.edges.bottom) {
+            left.height += border.width;
+            right.height += border.width;
+        }
 
-    // Use left and right scene rects to draw the corners if needed
-    if (border.edges.top) {
-        left.y -= border.width;
-        left.height += border.width;
-        right.y -= border.width;
-        right.height += border.width;
-    }
-    if (border.edges.bottom) {
-        left.height += border.width;
-        right.height += border.width;
-    }
-
-    inline for (.{
-        .{ .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 };
+        inline for (.{
+            .{ .name = "left", .box = &left },
+            .{ .name = "right", .box = &right },
+            .{ .name = "top", .box = &top },
+            .{ .name = "bottom", .box = &bottom },
+        }) |edge| {
+            if (!requested.clip.empty()) {
+                if (!edge.box.intersection(edge.box, &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);
+            rect.setSize(edge.box.width, edge.box.height);
+            rect.setColor(&color);
         }
-        const rect = @field(window.border, edge.name);
-        rect.node.setEnabled(@field(border.edges, edge.name));
-        rect.node.setPosition(edge.box.x, edge.box.y);
-        rect.setSize(edge.box.width, edge.box.height);
-        rect.setColor(&color);
     }
 
     inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
         var it = decorations.iterator(.forward);
         while (it.next()) |decoration| {
-            decoration.renderFinish(&window.rendering_requested.clip);
+            decoration.renderFinish(&requested.clip);
         }
     }
 }
 
+fn applySurfaceClip(window: *Window) void {
+    const requested = &window.rendering_requested;
+    var surface_clip: wlr.Box = requested.clip;
+    if (!requested.clip.empty() and !requested.content_clip.empty()) {
+        if (!surface_clip.intersection(&requested.clip, &requested.content_clip)) {
+            // Clip boxes are both non-empty but don't intersect, all window
+            // content is clipped away.
+            window.surfaces.setEnabled(false);
+            return;
+        }
+    } else if (!requested.content_clip.empty()) {
+        surface_clip = requested.content_clip;
+    }
+    window.surfaces.setEnabled(true);
+    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);
+    }
+}
+
 /// Returns null if the window is currently being destroyed and no longer has
 /// an associated surface.
 /// May also return null for Xwayland windows that are not currently mapped.