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

commit8a042ad3723751c8ef654e8eb7085548d2df0ab0
parente0f2e7a4f8
authorIsaac Freund <[email protected]>
date2025-03-21 19:38
rivercompat: start implementing 0.3 window management

 rivercompat/Output.zig        | 224 ++++++++++++++++++++++++++++++++++++++++++
 rivercompat/Seat.zig          |  46 +++++----
 rivercompat/Window.zig        |  41 ++++++--
 rivercompat/WindowManager.zig |  38 +++++--
 4 files changed, 314 insertions(+), 35 deletions(-)

diff --git a/rivercompat/Output.zig b/rivercompat/Output.zig
new file mode 100644
index 0000000..24f786d
--- /dev/null
+++ b/rivercompat/Output.zig
@@ -0,0 +1,224 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const Output = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const river = wayland.client.river;
+
+const Window = @import("Window.zig");
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+wm: *WindowManager,
+output_v1: *river.OutputV1,
+pending: struct {
+    new: bool = false,
+    removed: bool = false,
+},
+
+x: i32 = 0,
+y: i32 = 0,
+width: u31 = 0,
+height: u31 = 0,
+
+tags: u32 = (1 << 0),
+stack_focus: wl.list.Head(Window, .link_focus),
+stack_wm: wl.list.Head(Window, .link_wm),
+
+link: wl.list.Link,
+
+pub fn create(wm: *WindowManager, output_v1: *river.OutputV1) void {
+    const output = gpa.create(Output) catch @panic("OOM");
+    output.* = .{
+        .wm = wm,
+        .output_v1 = output_v1,
+        .pending = .{ .new = true },
+        .stack_focus = undefined,
+        .stack_wm = undefined,
+        .link = undefined,
+    };
+    output.stack_focus.init();
+    output.stack_wm.init();
+    wm.outputs.append(output);
+
+    output_v1.setListener(*Output, handleEvent, output);
+}
+
+fn handleEvent(output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
+    assert(output.output_v1 == output_v1);
+    switch (event) {
+        .removed => output.pending.removed = true,
+        .position => |args| {
+            output.x = args.x;
+            output.y = args.y;
+        },
+        .dimensions => |args| {
+            output.width = @intCast(args.width);
+            output.height = @intCast(args.height);
+        },
+    }
+}
+
+pub fn updateWindowing(output: *Output, wm: *WindowManager) void {
+    if (output.pending.removed) {
+        // XXX
+        output.output_v1.destroy();
+        gpa.destroy(output);
+    }
+
+    if (output.pending.new) {
+        {
+            var it = wm.fallback_stack_wm.iterator(.forward);
+            while (it.next()) |window| {
+                window.output = output;
+                window.link_wm.remove();
+                window.link_focus.remove();
+                output.stack_focus.prepend(window);
+                output.stack_wm.prepend(window);
+            }
+        }
+        {
+            var it = wm.seats.iterator(.forward);
+            while (it.next()) |seat| {
+                if (seat.focused_output == null) {
+                    seat.focused_output = output;
+                    seat.focus(null);
+                }
+            }
+        }
+    }
+
+    output.pending = .{};
+}
+
+const config = struct {
+    const main_count = 1;
+    const main_location: enum { left, right, top, bottom } = .left;
+    const outer_padding = 10;
+    const window_padding = 10;
+    const main_ratio = 0.60;
+};
+
+pub fn layout(output: *Output) void {
+    var count: u31 = 0;
+    {
+        var it = output.stack_wm.iterator(.forward);
+        while (it.next()) |window| {
+            if (window.tags & output.tags != 0) {
+                count += 1;
+            }
+        }
+    }
+    if (count == 0) return;
+
+    const main_count = @min(config.main_count, count);
+    const secondary_count = count -| main_count;
+
+    const usable_width = switch (config.main_location) {
+        .left, .right => output.width -| (2 *| config.outer_padding),
+        .top, .bottom => output.height -| (2 *| config.outer_padding),
+    };
+    const usable_height = switch (config.main_location) {
+        .left, .right => output.height -| (2 *| config.outer_padding),
+        .top, .bottom => output.width -| (2 *| config.outer_padding),
+    };
+
+    // to make things pixel-perfect, we make the first main and first secondary
+    // view slightly larger if the height is not evenly divisible
+    var main_width: u31 = undefined;
+    var main_height: u31 = undefined;
+    var main_height_rem: u31 = undefined;
+
+    var secondary_width: u31 = undefined;
+    var secondary_height: u31 = undefined;
+    var secondary_height_rem: u31 = undefined;
+
+    if (secondary_count > 0) {
+        main_width = @intFromFloat(config.main_ratio * @as(f64, @floatFromInt(usable_width)));
+        main_height = usable_height / main_count;
+        main_height_rem = usable_height % main_count;
+
+        secondary_width = usable_width - main_width;
+        secondary_height = usable_height / secondary_count;
+        secondary_height_rem = usable_height % secondary_count;
+    } else {
+        main_width = usable_width;
+        main_height = usable_height / main_count;
+        main_height_rem = usable_height % main_count;
+    }
+
+    {
+        var i: u31 = 0;
+        var it = output.stack_wm.iterator(.forward);
+        while (it.next()) |window| {
+            if (window.tags & output.tags == 0) continue;
+            defer i += 1;
+
+            var x: i32 = undefined;
+            var y: i32 = undefined;
+            var width: u31 = undefined;
+            var height: u31 = undefined;
+
+            if (i < main_count) {
+                x = 0;
+                y = (i * main_height) + if (i > 0) main_height_rem else 0;
+                width = main_width;
+                height = main_height + if (i == 0) main_height_rem else 0;
+            } else {
+                x = main_width;
+                y = (i - main_count) * secondary_height + if (i > main_count) secondary_height_rem else 0;
+                width = secondary_width;
+                height = secondary_height + if (i == main_count) secondary_height_rem else 0;
+            }
+
+            x +|= config.window_padding;
+            y +|= config.window_padding;
+            width -|= 2 *| config.window_padding;
+            height -|= 2 *| config.window_padding;
+
+            switch (config.main_location) {
+                .left => {
+                    window.node_v1.setPosition(x +| config.outer_padding, y +| config.outer_padding);
+                    window.window_v1.proposeDimensions(width, height);
+                },
+                .right => {
+                    window.node_v1.setPosition(usable_width - width - x +| config.outer_padding, y +| config.outer_padding);
+                    window.window_v1.proposeDimensions(width, height);
+                },
+                .top => {
+                    window.node_v1.setPosition(y +| config.outer_padding, x +| config.outer_padding);
+                    window.window_v1.proposeDimensions(height, width);
+                },
+                .bottom => {
+                    window.node_v1.setPosition(y +| config.outer_padding, usable_width - width - x +| config.outer_padding);
+                    window.window_v1.proposeDimensions(height, width);
+                },
+            }
+        }
+    }
+
+    {
+        var it = output.stack_focus.iterator(.reverse);
+        while (it.next()) |window| {
+            window.node_v1.placeTop();
+        }
+    }
+}
diff --git a/rivercompat/Seat.zig b/rivercompat/Seat.zig
index 388300a..febd173 100644
--- a/rivercompat/Seat.zig
+++ b/rivercompat/Seat.zig
@@ -25,6 +25,7 @@ const river = wayland.client.river;
 
 const c = @import("c.zig");
 
+const Output = @import("Output.zig");
 const Window = @import("Window.zig");
 const WindowManager = @import("WindowManager.zig");
 const XkbBinding = @import("XkbBinding.zig");
@@ -43,6 +44,7 @@ wm: *WindowManager,
 seat_v1: *river.SeatV1,
 pending: State = .{},
 focused: ?*Window = null,
+focused_output: ?*Output = null,
 link: wl.list.Link,
 
 pub fn create(wm: *WindowManager, seat_v1: *river.SeatV1) void {
@@ -83,6 +85,8 @@ fn handleEvent(seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) v
 
 pub fn updateWindowing(seat: *Seat) void {
     if (seat.pending.new) {
+        seat.focused_output = seat.wm.outputs.first();
+
         XkbBinding.create(seat, xkb.Keysym.n, .{ .mod4 = true }, .focus_next);
         XkbBinding.create(seat, xkb.Keysym.h, .{ .mod4 = true }, .hide_focused);
         XkbBinding.create(seat, xkb.Keysym.k, .{ .mod4 = true }, .close_focused);
@@ -115,7 +119,7 @@ pub const Action = enum {
 
 pub fn execute(seat: *Seat, action: Action) void {
     switch (action) {
-        .focus_next => seat.focusNext(),
+        .focus_next => {}, // XXX
         .close_focused => if (seat.focused) |window| window.window_v1.close(),
         .hide_focused => if (seat.focused) |window| window.window_v1.hide(),
         .show_all => {
@@ -145,26 +149,34 @@ pub fn execute(seat: *Seat, action: Action) void {
     }
 }
 
-pub fn focus(seat: *Seat, target: ?*Window) void {
-    if (target) |window| {
-        seat.seat_v1.focusWindow(window.window_v1);
-        seat.focused = window;
-
-        window.link.remove();
-        seat.wm.windows.prepend(window);
+pub fn focus(seat: *Seat, _target: ?*Window) void {
+    if (seat.focused_output == null) return;
+    if (seat.wm.session_locked) return;
 
-        window.node_v1.placeTop();
-    } else {
-        seat.seat_v1.clearFocus();
+    var target = _target;
+    if (target) |window| {
+        if (window.output == null or window.output.?.tags & window.tags == 0) {
+            target = null;
+        } else if (window.output.? != seat.focused_output.?) {
+            seat.focused_output = window.output;
+        }
     }
-}
 
-pub fn focusNext(seat: *Seat) void {
-    if (seat.focused != null) {
-        if (seat.wm.windows.length() >= 2) {
-            seat.focus(seat.wm.windows.last().?);
+    if (target == null) {
+        var it = seat.focused_output.?.stack_focus.iterator(.forward);
+        while (it.next()) |window| {
+            if (window.tags & seat.focused_output.?.tags != 0) {
+                target = window;
+                break;
+            }
         }
+    }
+
+    if (target) |window| {
+        window.link_focus.remove();
+        seat.focused_output.?.stack_focus.prepend(window);
+        seat.seat_v1.focusWindow(window.window_v1);
     } else {
-        seat.focus(seat.wm.windows.first());
+        seat.seat_v1.clearFocus();
     }
 }
diff --git a/rivercompat/Window.zig b/rivercompat/Window.zig
index 4aa2ede..9cd8a1b 100644
--- a/rivercompat/Window.zig
+++ b/rivercompat/Window.zig
@@ -24,6 +24,7 @@ const wl = wayland.client.wl;
 const wp = wayland.client.wp;
 const river = wayland.client.river;
 
+const Output = @import("Output.zig");
 const WindowManager = @import("WindowManager.zig");
 
 const gpa = std.heap.c_allocator;
@@ -40,7 +41,13 @@ rendering: struct {
     width: i32 = 0,
     height: i32 = 0,
 } = .{},
+
+output: ?*Output = null,
+tags: u32 = 0,
+
 link: wl.list.Link,
+link_focus: wl.list.Link,
+link_wm: wl.list.Link,
 
 shadow_surface: *wl.Surface,
 shadow_decoration: *river.DecorationV1,
@@ -65,12 +72,17 @@ pub fn create(window_v1: *river.WindowV1, wm: *WindowManager) void {
         .node_v1 = window_v1.getNode() catch @panic("OOM"),
         .windowing = .{ .new = true },
         .link = undefined,
+        .link_focus = undefined,
+        .link_wm = undefined,
         .shadow_surface = shadow_surface,
         .shadow_decoration = shadow_decoration,
         .shadow_viewport = shadow_viewport,
         .shadow_buffer = shadow_buffer,
     };
     wm.windows.append(window);
+    window.link_focus.init();
+    window.link_wm.init();
+
     window_v1.setListener(*Window, handleEvent, window);
 }
 
@@ -106,8 +118,7 @@ pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
             var it = wm.seats.iterator(.forward);
             while (it.next()) |seat| {
                 if (seat.focused == window) {
-                    seat.focused = null;
-                    seat.focusNext();
+                    seat.focus(null);
                 }
             }
         }
@@ -116,7 +127,6 @@ pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
     }
 
     if (window.windowing.new) {
-        window.node_v1.placeTop();
         window.window_v1.useSsd();
 
         const rgb = 0x586e75;
@@ -129,15 +139,26 @@ pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
             0xffff_ffff,
         );
 
-        {
-            var it = wm.seats.iterator(.forward);
-            while (it.next()) |seat| {
-                seat.focus(window);
+        if (if (wm.seats.first()) |seat| seat.focused_output else null) |output| {
+            window.output = output;
+            window.tags = output.tags;
+            window.link_wm.remove();
+            window.link_focus.remove();
+            output.stack_focus.prepend(window);
+            output.stack_wm.prepend(window);
+            {
+                var it = wm.seats.iterator(.forward);
+                while (it.next()) |seat| {
+                    seat.focus(window);
+                }
             }
+        } else {
+            window.tags = (1 << 0); // XXX
+            window.link_wm.remove();
+            window.link_focus.remove();
+            wm.fallback_stack_focus.prepend(window);
+            wm.fallback_stack_wm.prepend(window);
         }
-
-        window.node_v1.setPosition(20, 20);
-        window.window_v1.proposeDimensions(400, 400);
     }
 
     window.windowing = .{};
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index ecd788d..e0ccf73 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -24,6 +24,7 @@ const wl = wayland.client.wl;
 const wp = wayland.client.wp;
 const river = wayland.client.river;
 
+const Output = @import("Output.zig");
 const Seat = @import("Seat.zig");
 const ShellSurface = @import("ShellSurface.zig");
 const Window = @import("Window.zig");
@@ -33,10 +34,16 @@ compositor: *wl.Compositor,
 viewporter: *wp.Viewporter,
 single_pixel: *wp.SinglePixelBufferManagerV1,
 
+session_locked: bool = false,
+
 windows: wl.list.Head(Window, .link),
 seats: wl.list.Head(Seat, .link),
+outputs: wl.list.Head(Output, .link),
 shell_surfaces: wl.list.Head(ShellSurface, .link),
 
+fallback_stack_wm: wl.list.Head(Window, .link_wm),
+fallback_stack_focus: wl.list.Head(Window, .link_focus),
+
 pub fn init(
     wm: *WindowManager,
     wm_v1: *river.WindowManagerV1,
@@ -51,11 +58,17 @@ pub fn init(
         .single_pixel = single_pixel,
         .windows = undefined,
         .seats = undefined,
+        .outputs = undefined,
         .shell_surfaces = undefined,
+        .fallback_stack_wm = undefined,
+        .fallback_stack_focus = undefined,
     };
     wm.windows.init();
     wm.seats.init();
+    wm.outputs.init();
     wm.shell_surfaces.init();
+    wm.fallback_stack_wm.init();
+    wm.fallback_stack_focus.init();
 
     wm_v1.setListener(*WindowManager, handleEvent, wm);
 
@@ -75,19 +88,21 @@ fn handleEvent(wm_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event
             wm.updateRendering();
             wm_v1.updateRenderingFinish();
         },
-        .session_locked => {},
-        .session_unlocked => {},
+        .session_locked => wm.session_locked = true,
+        .session_unlocked => wm.session_locked = false,
         .window => |args| Window.create(args.id, wm),
-        .output => |args| {
-            _ = args;
-        },
-        .seat => |args| {
-            Seat.create(wm, args.id);
-        },
+        .output => |args| Output.create(wm, args.id),
+        .seat => |args| Seat.create(wm, args.id),
     }
 }
 
 fn updateWindowing(wm: *WindowManager) void {
+    {
+        var it = wm.outputs.iterator(.forward);
+        while (it.next()) |output| {
+            output.updateWindowing(wm);
+        }
+    }
     {
         var it = wm.seats.iterator(.forward);
         while (it.next()) |seat| {
@@ -106,6 +121,13 @@ fn updateWindowing(wm: *WindowManager) void {
             shell_surface.updateWindowing(wm);
         }
     }
+
+    {
+        var it = wm.outputs.iterator(.forward);
+        while (it.next()) |output| {
+            output.layout();
+        }
+    }
 }
 
 fn updateRendering(wm: *WindowManager) void {