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

commitffd4ea8f6617feb2e377f459fc260a822e067360
parented24243bfe
authorIsaac Freund <[email protected]>
date2026-02-23 15:59
protocol: implement river_window_v1.set_dimension_bounds

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

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 23395e3..9add976 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1080,6 +1080,27 @@
       </description>
       <arg name="identifier" type="string" summary="unique identifier"/>
     </event>
+
+    <request name="set_dimension_bounds" since="4">
+      <description summary="recommend maximum dimensions to the window">
+        Recommend that the window keep its dimensions within a given
+        maximum width/height. This recommendation is only a hint and the window
+        may ignore it.
+
+        Setting the width and height to 0 indicates that there are no bounds
+        and is equivalent to having never made this request.
+
+        Setting width or height to a negative value is a protocol error.
+
+        The server should communicate this hint to an xdg_toplevel window with
+        the xdg_toplevel.configure_bounds event for example.
+
+        This request modifies window management state and may only be made as
+        part of a manage sequence, see the river_window_manager_v1 description.
+      </description>
+      <arg name="max_width" type="int" summary="maximum width"/>
+      <arg name="max_height" type="int" summary="maximum height"/>
+    </request>
   </interface>
 
   <interface name="river_decoration_v1" version="4">
diff --git a/river/Window.zig b/river/Window.zig
index 2ed1627..72699db 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -28,6 +28,11 @@ const XwaylandWindow = @import("XwaylandWindow.zig");
 
 const log = std.log.scoped(.wm);
 
+pub const Dimensions = struct {
+    width: u31,
+    height: u31,
+};
+
 pub const DimensionsHint = struct {
     min_width: u31 = 0,
     max_width: u31 = 0,
@@ -61,7 +66,8 @@ pub const Border = struct {
 
 /// Windowing state requested by the wm.
 const WmRequested = struct {
-    dimensions: ?struct { width: u31, height: u31 },
+    dimensions: ?Dimensions,
+    bounds: Dimensions,
     ssd: bool,
     tiled: river.WindowV1.Edges,
     capabilities: river.WindowV1.Capabilities,
@@ -73,6 +79,7 @@ const WmRequested = struct {
 
     pub const init: WmRequested = .{
         .dimensions = null,
+        .bounds = .{ .width = 0, .height = 0 },
         .ssd = false,
         .tiled = .{},
         .capabilities = .{
@@ -90,17 +97,30 @@ const WmRequested = struct {
 };
 
 pub const Configure = struct {
-    width: ?u31 = null,
-    height: ?u31 = null,
+    width: ?u31,
+    height: ?u31,
+    bounds: Dimensions,
     /// True if the window has keyboard focus from at least one seat.
-    activated: bool = false,
-    ssd: bool = false,
-    border: Border = .{},
-    tiled: river.WindowV1.Edges = .{},
-    capabilities: river.WindowV1.Capabilities = .{},
-    maximized: bool = false,
-    inform_fullscreen: bool = false,
-    resizing: bool = false,
+    activated: bool,
+    ssd: bool,
+    tiled: river.WindowV1.Edges,
+    capabilities: river.WindowV1.Capabilities,
+    maximized: bool,
+    inform_fullscreen: bool,
+    resizing: bool,
+
+    pub const init: Configure = .{
+        .width = null,
+        .height = null,
+        .bounds = .{ .width = 0, .height = 0 },
+        .activated = false,
+        .ssd = false,
+        .tiled = .{},
+        .capabilities = .{},
+        .maximized = false,
+        .inform_fullscreen = false,
+        .resizing = false,
+    };
 };
 
 /// Rendering state requested by the wm.
@@ -216,9 +236,9 @@ wm_sent: struct {
 wm_requested: WmRequested = .init,
 
 /// State to be sent to the window in the next configure.
-configure_scheduled: Configure = .{},
+configure_scheduled: Configure = .init,
 /// State sent to the window in the latest configure.
-configure_sent: Configure = .{},
+configure_sent: Configure = .init,
 
 /// State to be sent to the wm in the next render sequence.
 rendering_scheduled: struct {
@@ -690,6 +710,17 @@ fn handleRequest(
                 .height = args.height,
             };
         },
+        .set_dimension_bounds => |args| {
+            if (!server.wm.ensureWindowing()) return;
+            if (args.max_width < 0 or args.max_height < 0) {
+                window_v1.postError(.invalid_dimensions, "dimensions must be greater than or equal to 0 ");
+                return;
+            }
+            wm_requested.bounds = .{
+                .width = @intCast(args.max_width),
+                .height = @intCast(args.max_height),
+            };
+        },
     }
 }
 
@@ -718,45 +749,53 @@ pub fn manageFinish(window: *Window) bool {
         .closing => return false,
     }
 
-    window.configure_scheduled.ssd = wm_requested.ssd;
-    window.configure_scheduled.tiled = wm_requested.tiled;
-    window.configure_scheduled.capabilities = wm_requested.capabilities;
-    window.configure_scheduled.resizing = wm_requested.resizing;
-    window.configure_scheduled.maximized = wm_requested.maximized;
-    window.configure_scheduled.inform_fullscreen = wm_requested.inform_fullscreen;
-
     if (wm_requested.close) {
         window.close();
         wm_requested.close = false;
     }
 
-    {
-        window.configure_scheduled.activated = false;
+    const activated = blk: {
         var it = server.wm.sent.seats.iterator(.forward);
         while (it.next()) |seat| {
             if (seat.focused == .window and seat.focused.window == window) {
-                window.configure_scheduled.activated = true;
-                break;
+                break :blk true;
             }
         }
-    }
+        break :blk false;
+    };
 
-    if (wm_requested.fullscreen) |output| {
-        const width, const height = output.sent.dimensions();
-        if (window.configure_sent.width != width or
-            window.configure_sent.height != height)
-        {
-            window.configure_scheduled.width = width;
-            window.configure_scheduled.height = height;
+    const width, const height = blk: {
+        if (wm_requested.fullscreen) |output| {
+            const width, const height = output.sent.dimensions();
+            if (window.configure_sent.width != width or
+                window.configure_sent.height != height)
+            {
+                window.configure_scheduled.width = width;
+                window.configure_scheduled.height = height;
+                window.rendering_scheduled.resend_dimensions = true;
+                break :blk .{ width, height };
+            }
+        } else if (wm_requested.dimensions) |dimensions| {
             window.rendering_scheduled.resend_dimensions = true;
+            break :blk .{ dimensions.width, dimensions.height };
         }
-    } else if (wm_requested.dimensions) |dimensions| {
-        window.configure_scheduled.width = dimensions.width;
-        window.configure_scheduled.height = dimensions.height;
-        window.rendering_scheduled.resend_dimensions = true;
-    }
+        break :blk .{ null, null };
+    };
     wm_requested.dimensions = null;
 
+    window.configure_scheduled = .{
+        .width = width,
+        .height = height,
+        .bounds = wm_requested.bounds,
+        .activated = activated,
+        .ssd = wm_requested.ssd,
+        .tiled = wm_requested.tiled,
+        .capabilities = wm_requested.capabilities,
+        .resizing = wm_requested.resizing,
+        .maximized = wm_requested.maximized,
+        .inform_fullscreen = wm_requested.inform_fullscreen,
+    };
+
     const track_configure = switch (window.impl) {
         .toplevel => |*toplevel| toplevel.configure(),
         .xwayland => |*xwindow| xwindow.configure(),
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 7b27a33..11cb497 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -120,6 +120,7 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     };
 
     const scheduled = &toplevel.window.configure_scheduled;
+    const sent = &toplevel.window.configure_scheduled;
 
     if (!toplevel.needsConfigure()) {
         // If no new configure is required, continue to track a timed out configure
@@ -159,6 +160,11 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     if (toplevel.decoration) |decoration| {
         _ = decoration.wlr_decoration.setMode(if (scheduled.ssd) .server_side else .client_side);
     }
+    if (scheduled.bounds.width != sent.bounds.width or
+        scheduled.bounds.height != sent.bounds.height)
+    {
+        _ = wlr_toplevel.setBounds(scheduled.bounds.width, scheduled.bounds.height);
+    }
 
     const width: u31 = scheduled.width orelse switch (toplevel.configure_state) {
         .idle => @intCast(toplevel.geometry.width),
@@ -201,6 +207,8 @@ fn needsConfigure(toplevel: *XdgToplevel) bool {
 
     if (scheduled.width != null and scheduled.width != sent.width) return true;
     if (scheduled.height != null and scheduled.height != sent.height) return true;
+    if (scheduled.bounds.width != sent.bounds.width or
+        scheduled.bounds.height != sent.bounds.height) return true;
     if (scheduled.activated != sent.activated) return true;
     if (scheduled.ssd != sent.ssd) return true;
     if (!std.meta.eql(scheduled.tiled, sent.tiled)) return true;