Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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 {