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

commit81dcf8f5e0f241147da8ce6ce605d1242771ef8c
parent6d253fb8c6
authorIsaac Freund <[email protected]>
date2026-01-08 16:21
protocol: specify fullscreen/clip interaction

The compositor now ignores clip/content clip boxes and always clips to
output bounds while a window is fullscreen.

 protocol/river-window-management-v1.xml | 13 +++++++-
 river/Window.zig                        | 57 +++++++++++++++++----------------
 2 files changed, 41 insertions(+), 29 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index f433a48..cdb273e 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -601,6 +601,8 @@
         the left edge will not extend vertically beyond the top edge of the
         window.
 
+        Borders are not drawn while the window is fullscreen.
+
         The color is defined by four 32-bit RGBA values. Unless specified in
         another protocol extension, the RGBA values use pre-multiplied alpha.
 
@@ -909,6 +911,11 @@
         shall not affect the current position and dimensions of a fullscreen
         window.
 
+        The compositor will clip window content, decoration surfaces, and
+        borders to the given output's dimensions while the window is fullscreen.
+        The effects of set_clip_box and set_content_clip_box are ignored while
+        the window is fullscreen.
+
         If the output on which a window is currently fullscreen is removed, the
         windowing state is modified as if there were an exit_fullscreen request
         made in the same manage sequence as the river_output_v1.removed event.
@@ -965,6 +972,8 @@
 
         Setting a clip box with 0 width or height disables clipping.
 
+        The clip box is ignored while the window is fullscreen.
+
         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
@@ -1005,7 +1014,9 @@
 
         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.
+        Setting a box with 0 width or height disables content clipping.
+
+        The content clip box is ignored while the window is fullscreen.
 
         Both set_clip_box and set_content_clip_box may be enabled simultaneously.
 
diff --git a/river/Window.zig b/river/Window.zig
index 8e7a5ec..789ac27 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -818,32 +818,42 @@ pub fn renderStart(window: *Window) void {
 
 pub fn renderFinish(window: *Window) void {
     const requested = &window.rendering_requested;
+    window.tree.node.setEnabled(!requested.hidden);
+    window.popup_tree.node.setEnabled(!requested.hidden);
 
-    window.tree.node.setEnabled(!window.rendering_requested.hidden);
-    window.popup_tree.node.setEnabled(!window.rendering_requested.hidden);
-
-    window.box = .{
-        .x = requested.x,
-        .y = requested.y,
-        .width = window.rendering_sent.width,
-        .height = window.rendering_sent.height,
-    };
+    window.box.width = window.rendering_sent.width;
+    window.box.height = window.rendering_sent.height;
 
+    var clip: wlr.Box = requested.clip;
+    var content_clip: wlr.Box = requested.content_clip;
     if (window.wm_requested.fullscreen) |output| {
         window.box.x = output.sent.x;
         window.box.y = output.sent.y;
         window.fullscreen_background.node.setEnabled(true);
         const width, const height = output.sent.dimensions();
         window.fullscreen_background.setSize(width, height);
+        clip = .{ .x = 0, .y = 0, .width = width, .height = height };
+        content_clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 };
     } else {
+        window.box.x = requested.x;
+        window.box.y = requested.y;
         window.fullscreen_background.node.setEnabled(false);
+        window.drawBorders();
     }
-
     window.tree.node.setPosition(window.box.x, window.box.y);
     window.popup_tree.node.setPosition(window.box.x, window.box.y);
 
-    window.applySurfaceClip();
+    window.applySurfaceClip(&clip, &content_clip);
+    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
+        var it = decorations.iterator(.forward);
+        while (it.next()) |decoration| {
+            decoration.renderFinish(&clip);
+        }
+    }
+}
 
+fn drawBorders(window: *Window) void {
+    const requested = &window.rendering_requested;
     var content: wlr.Box = .{
         .x = 0,
         .y = 0,
@@ -862,7 +872,6 @@ pub fn renderFinish(window: *Window) void {
             @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,
@@ -887,7 +896,6 @@ pub fn renderFinish(window: *Window) void {
             .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;
@@ -899,7 +907,6 @@ pub fn renderFinish(window: *Window) void {
             left.height += border.width;
             right.height += border.width;
         }
-
         inline for (.{
             .{ .name = "left", .box = &left },
             .{ .name = "right", .box = &right },
@@ -920,27 +927,21 @@ pub fn renderFinish(window: *Window) void {
             rect.setColor(&color);
         }
     }
-
-    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
-        var it = decorations.iterator(.forward);
-        while (it.next()) |decoration| {
-            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)) {
+fn applySurfaceClip(window: *Window, a: *const wlr.Box, b: *const wlr.Box) void {
+    var surface_clip: wlr.Box = undefined;
+    if (!a.empty() and !b.empty()) {
+        if (!surface_clip.intersection(a, b)) {
             // 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;
+    } else if (!a.empty()) {
+        surface_clip = a.*;
+    } else {
+        surface_clip = b.*;
     }
     window.surfaces.setEnabled(true);
     switch (window.impl) {