Add interactive move/resize operations using configurable pointer bindings (Mod4+BTN_LEFT to move, Mod4+BTN_RIGHT to resize). Tiled windows automatically float when dragged or resized. Add keyboard commands for floating windows: - move_up/down/left/right: move by pixel amount - resize_width/height: resize by pixel amount - swap_next/swap_prev: swap position in window stack Fix float dimension initialization when windows first become floating, and fix clamp crash when resizing windows larger than output bounds. Update example config with documented keybinds and new pointer_binds block.
255 lines
9.2 KiB
Zig
255 lines
9.2 KiB
Zig
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
const WindowManager = @This();
|
|
|
|
const MIN_RIVER_SEAT_V1_VERSION: u2 = 3;
|
|
|
|
context: *Context,
|
|
|
|
river_window_manager_v1: *river.WindowManagerV1,
|
|
|
|
seats: wl.list.Head(Seat, .link),
|
|
outputs: wl.list.Head(Output, .link),
|
|
|
|
/// Place to store windows if all Outputs have been disconnected
|
|
orphan_windows: wl.list.Head(Window, .link),
|
|
|
|
pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager {
|
|
const wm = try utils.allocator.create(WindowManager);
|
|
errdefer wm.destroy();
|
|
|
|
wm.* = .{
|
|
.context = context,
|
|
.river_window_manager_v1 = window_manager_v1,
|
|
.seats = undefined, // we will initialize these shortly
|
|
.outputs = undefined,
|
|
.orphan_windows = undefined,
|
|
};
|
|
|
|
wm.seats.init();
|
|
wm.outputs.init();
|
|
wm.orphan_windows.init();
|
|
|
|
wm.river_window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
|
|
|
|
return wm;
|
|
}
|
|
|
|
pub fn destroy(wm: *WindowManager) void {
|
|
{
|
|
var it = wm.outputs.safeIterator(.forward);
|
|
while (it.next()) |output| {
|
|
output.link.remove();
|
|
output.destroy();
|
|
}
|
|
}
|
|
{
|
|
var it = wm.seats.safeIterator(.forward);
|
|
while (it.next()) |seat| {
|
|
seat.link.remove();
|
|
seat.destroy();
|
|
}
|
|
}
|
|
|
|
utils.allocator.destroy(wm);
|
|
}
|
|
|
|
/// Get the next output in the list, wrapping to first if at end
|
|
pub fn nextOutput(wm: *WindowManager, current: *Output) ?*Output {
|
|
const next_link = current.link.next orelse return wm.outputs.first();
|
|
// If we've reached the sentinel (head's link), wrap to first
|
|
if (next_link == &wm.outputs.link) return wm.outputs.first();
|
|
return @fieldParentPtr("link", next_link);
|
|
}
|
|
|
|
/// Get the previous output in the list, wrapping to last if at beginning
|
|
pub fn prevOutput(wm: *WindowManager, current: *Output) ?*Output {
|
|
const prev_link = current.link.prev orelse return wm.outputs.last();
|
|
// If we've reached the sentinel (head's link), wrap to last
|
|
if (prev_link == &wm.outputs.link) return wm.outputs.last();
|
|
return @fieldParentPtr("link", prev_link);
|
|
}
|
|
|
|
fn manage_start(wm: *WindowManager) void {
|
|
const river_window_manager_v1 = wm.river_window_manager_v1;
|
|
const context = wm.context;
|
|
|
|
// This gets used shortly, so it goes at the beginning
|
|
context.manage();
|
|
|
|
if (!context.initialized) {
|
|
// This code runs during initial startup and after config reloads.
|
|
@branchHint(.cold);
|
|
context.initialized = true;
|
|
|
|
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
|
const river_seat_v1 = seat.river_seat_v1;
|
|
|
|
// Tag bindings
|
|
for (context.config.tag_binds.items) |tag_bind| {
|
|
comptime var i: u8 = 1;
|
|
comptime var buffer: [1]u8 = undefined;
|
|
inline while (i <= 9) : (i += 1) {
|
|
const tags: u32 = 1 << (i - 1);
|
|
buffer[0] = i + '0';
|
|
const command: XkbBindings.Command = switch (tag_bind.command) {
|
|
.set_output_tags => .{ .set_output_tags = tags },
|
|
.set_window_tags => .{ .set_window_tags = tags },
|
|
.toggle_output_tags => .{ .toggle_output_tags = tags },
|
|
.toggle_window_tags => .{ .toggle_window_tags = tags },
|
|
else => unreachable,
|
|
};
|
|
context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), tag_bind.modifiers, command);
|
|
}
|
|
}
|
|
// Rest of the keybinds
|
|
for (context.config.keybinds.items) |keybind| {
|
|
// Keysyms should only be null in tag_binds (above)
|
|
std.debug.assert(keybind.keysym != null);
|
|
context.xkb_bindings.addBinding(river_seat_v1, keybind.keysym.?, keybind.modifiers, keybind.command);
|
|
}
|
|
|
|
// Pointer bindings
|
|
for (context.config.pointer_binds.items) |pointer_bind| {
|
|
const binding = river_seat_v1.getPointerBinding(pointer_bind.button, pointer_bind.modifiers) catch {
|
|
log.err("Failed to create pointer binding", .{});
|
|
continue;
|
|
};
|
|
|
|
switch (pointer_bind.action) {
|
|
.move_window => {
|
|
if (seat.move_pointer_binding) |old| old.destroy();
|
|
binding.setListener(*Seat, Seat.movePointerBindingListener, seat);
|
|
seat.move_pointer_binding = binding;
|
|
},
|
|
.resize_window => {
|
|
if (seat.resize_pointer_binding) |old| old.destroy();
|
|
binding.setListener(*Seat, Seat.resizePointerBindingListener, seat);
|
|
seat.resize_pointer_binding = binding;
|
|
},
|
|
}
|
|
binding.enable();
|
|
}
|
|
}
|
|
|
|
{
|
|
var it = wm.outputs.iterator(.forward);
|
|
while (it.next()) |output| {
|
|
output.manage();
|
|
}
|
|
}
|
|
{
|
|
var it = wm.seats.iterator(.forward);
|
|
while (it.next()) |seat| {
|
|
seat.manage();
|
|
}
|
|
}
|
|
river_window_manager_v1.manageFinish();
|
|
}
|
|
|
|
fn render_start(wm: *WindowManager) void {
|
|
const river_window_manager_v1 = wm.river_window_manager_v1;
|
|
{
|
|
var it = wm.seats.iterator(.forward);
|
|
while (it.next()) |seat| {
|
|
seat.render();
|
|
}
|
|
}
|
|
{
|
|
var it = wm.outputs.iterator(.forward);
|
|
while (it.next()) |output| {
|
|
output.render();
|
|
}
|
|
}
|
|
river_window_manager_v1.renderFinish();
|
|
}
|
|
|
|
fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event, wm: *WindowManager) void {
|
|
assert(wm.river_window_manager_v1 == window_manager_v1);
|
|
const context = wm.context;
|
|
switch (event) {
|
|
.unavailable => {
|
|
fatal("Window manager unavailable (some other wm instance is running). Exiting", .{});
|
|
},
|
|
.manage_start => wm.manage_start(),
|
|
.render_start => wm.render_start(),
|
|
.output => |ev| {
|
|
const output = Output.create(context, ev.id) catch @panic("Out of memory");
|
|
wm.outputs.append(output);
|
|
// If there was already a seat, but no outputs, set this new output as focused
|
|
const seat = wm.seats.first() orelse return;
|
|
if (seat.focused_output == null and seat.pending_manage.output == null) {
|
|
seat.pending_manage.output = .{ .output = output };
|
|
}
|
|
|
|
// If there are orphan windows, send them to the new output
|
|
var it = wm.orphan_windows.iterator(.forward);
|
|
while (it.next()) |window| {
|
|
// We need to make sure to set up their new output
|
|
window.pending_manage.pending_output = .{ .output = output };
|
|
}
|
|
if (wm.orphan_windows.first()) |first| {
|
|
// and focus the first one
|
|
first.pending_render.focused = true;
|
|
}
|
|
// We clear any orphaned_windows if an output is added
|
|
output.windows.appendList(&wm.orphan_windows);
|
|
},
|
|
.seat => |ev| {
|
|
// TODO: Support multi-seat (maybe ?)
|
|
const river_seat_v1 = ev.id;
|
|
const river_seat_v1_version = river_seat_v1.getVersion();
|
|
if (river_seat_v1_version < MIN_RIVER_SEAT_V1_VERSION) {
|
|
@branchHint(.cold); // If we're in here, the program is exiting anyways
|
|
utils.versionNotSupported(river.SeatV1, river_seat_v1_version, MIN_RIVER_SEAT_V1_VERSION);
|
|
}
|
|
|
|
const seat = Seat.create(context, river_seat_v1) catch @panic("Out of memory");
|
|
wm.seats.append(seat);
|
|
|
|
// If there was already an output, but no seats, set the first output as focused
|
|
seat.pending_manage.output = .{ .output = wm.outputs.first() orelse return };
|
|
},
|
|
.window => |ev| {
|
|
// TODO: Support multiple seats
|
|
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
|
const focused_output = seat.focused_output;
|
|
const window_list = if (focused_output) |output|
|
|
&output.windows
|
|
else
|
|
&wm.orphan_windows;
|
|
const window = Window.create(context, ev.id, focused_output) catch @panic("Out of memory");
|
|
|
|
switch (context.config.attach_mode) {
|
|
.top => window_list.prepend(window),
|
|
.bottom => window_list.append(window),
|
|
}
|
|
seat.pending_manage.window = .{ .window = window };
|
|
seat.pending_manage.should_warp_pointer = true;
|
|
},
|
|
else => |ev| {
|
|
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
|
},
|
|
}
|
|
}
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const fatal = std.process.fatal;
|
|
|
|
const wayland = @import("wayland");
|
|
const wl = wayland.client.wl;
|
|
const river = wayland.client.river;
|
|
|
|
const xkbcommon = @import("xkbcommon");
|
|
|
|
const utils = @import("utils.zig");
|
|
const Context = @import("Context.zig");
|
|
const Output = @import("Output.zig");
|
|
const Seat = @import("Seat.zig");
|
|
const Window = @import("Window.zig");
|
|
const XkbBindings = @import("XkbBindings.zig");
|
|
|
|
const log = std.log.scoped(.WindowManager);
|