Merge pull request 'floating-windows' (#9) from floating-windows into main

Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/9
This commit is contained in:
Ben Buhse 2026-02-06 21:39:50 +01:00
commit e5d439e27d
8 changed files with 581 additions and 48 deletions

View file

@ -10,13 +10,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
These are in rough order of my priority, though no promises I do them in this order.
- [ ] Support floating windows
- [ ] Support wallpapers
- [ ] Support a bar
- [ ] Support starting programs at WM launch
- [ ] Support overriding config location
- [ ] Add support for multimedia/brightness keys
- [ ] Make "orelse return" bits into errors; handle gracefully
- [ ] Support multiple seats
- [x] Support changeable primary count
- [ ] Support clipping floating windows on edge of/between outputs
- [x] Support changeable primary ratio
- [x] Support changeable primary count
- [x] Support multiple outputs
- [X] Support floating windows

View file

@ -1,30 +1,68 @@
// Whether new windows should go to the top or bottom of the window stack
attach_mode top
// Whether mousing over a new window should move focus
focus_follows_pointer #true
// Whether the focus should warp to the center of newly-focused windows
pointer_warp_on_focus_change #true
borders {
width 2
// 8 or 10 digit hex color
color_focused "0x89b4fa"
color_unfocused "0x1e1e2e"
}
keybinds {
// Swap a window
spawn Mod4 T foot
// Move focus up or down the windows stack
focus_next_window Mod4 J
focus_prev_window Mod4 K
focus_next_output Mod4+Shift J
focus_prev_output Mod4+Shift K
send_to_next_output Mod1+Shift J
send_to_prev_output Mod1+Shift K
// Move focus between windows
focus_next_output Mod4 Period
focus_prev_output Mod4 Comma
// Move windows between outputs
send_to_next_output Mod4+Shift Period
send_to_prev_output Mod4+Shift Comma
// Swap the currently-focused window with the current primary
zoom Mod4 Z
change_ratio Mod4 H +0.05
// Float/unfloat the currently-focused window
toggle_float Mod4+Shift F
// Change the primary ratio of the current output
change_ratio Mod4 H 0.05
change_ratio Mod4 L -0.05
// Change the number of windows in the primary side
increment_primary_count Mod4 I
decrement_primary_count Mod4 D
// Reload config file
reload_config Mod4+Shift R
// Toggle fullscreen on the currently-focused window
toggle_fullscreen Mod4 F
// Close the currently-focused window
close_window Mod4+Shift Q
// Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9
// Move windows up or down the stack
swap_next Mod4+Shift N
swap_prev Mod4+Shift P
// Move floating windows; noop on tiled windows
move_left Mod4+Shift H 100
move_down Mod4+Shift J 100
move_up Mod4+Shift K 100
move_right Mod4+Shift L 100
// Resize floating windows; noop on tiled windows
resize_width Mod4+Alt+Shift H -100
resize_height Mod4+Alt+Shift J 100
resize_height Mod4+Alt+Shift K -100
resize_width Mod4+Alt+Shift L 100
// Special command to generate keybinds for keys 1-9 and tags 1<<0 through 1<<9
tag_bind Mod4 set_output_tags
tag_bind Mod4+shift set_window_tags
tag_bind Mod4+ctrl toggle_output_tags
tag_bind Mod4+ctrl+shift toggle_window_tags
tag_bind Mod4+Shift set_window_tags
tag_bind Mod4+Ctrl toggle_output_tags
tag_bind Mod4+Ctrl+Shift toggle_window_tags
}
pointer_binds {
// Mod4 + Left click to move floating windows;
// tiled windows will automatically float if moved
move_window Mod4 BTN_LEFT
// Mod4 + Right click to resize floating windows;
// tiled windows will automatically float if resized
resize_window Mod4 BTN_RIGHT
}

View file

@ -21,8 +21,9 @@ focus_follows_pointer: bool = true,
pointer_warp_on_focus_change: bool = true,
/// Tag bind entries parsed from config (tag_bind nodes in keybinds block)
tag_binds: std.ArrayListUnmanaged(Keybind) = .{},
keybinds: std.ArrayListUnmanaged(Keybind) = .{},
tag_binds: std.ArrayList(Keybind) = .{},
keybinds: std.ArrayList(Keybind) = .{},
pointer_binds: std.ArrayList(PointerBind) = .{},
pub const Keybind = struct {
modifiers: river.SeatV1.Modifiers,
@ -30,6 +31,17 @@ pub const Keybind = struct {
keysym: ?xkbcommon.Keysym,
};
pub const PointerBind = struct {
modifiers: river.SeatV1.Modifiers,
button: u32, // Linux button code (BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112)
action: PointerAction,
};
pub const PointerAction = enum {
move_window,
resize_window,
};
pub const AttachMode = enum {
top,
bottom,
@ -41,6 +53,7 @@ const NodeName = enum {
pointer_warp_on_focus_change,
borders,
keybinds,
pointer_binds,
};
const BorderNodeName = enum {
@ -49,6 +62,11 @@ const BorderNodeName = enum {
color_unfocused,
};
const PointerBindNodeName = enum {
move_window,
resize_window,
};
// We can just directly use the tag type from Command as our node name
const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?;
@ -82,6 +100,7 @@ pub fn create() !*Config {
}
config.keybinds.clearAndFree(utils.allocator);
config.tag_binds.clearAndFree(utils.allocator);
config.pointer_binds.clearAndFree(utils.allocator);
config.* = .{};
};
}
@ -101,6 +120,7 @@ pub fn destroy(config: *Config) void {
}
config.keybinds.deinit(utils.allocator);
config.tag_binds.deinit(utils.allocator);
config.pointer_binds.deinit(utils.allocator);
utils.allocator.destroy(config);
}
@ -161,6 +181,9 @@ fn load(config: *Config, reader: *Io.Reader) !void {
.keybinds => {
next_child_block = .keybinds;
},
.pointer_binds => {
next_child_block = .pointer_binds;
},
}
} else {
logWarnInvalidNode(node.name);
@ -171,6 +194,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
switch (child_block) {
.borders => try config.loadBordersChildBlock(&parser),
.keybinds => try config.loadKeybindsChildBlock(&parser),
.pointer_binds => try config.loadPointerBindsChildBlock(&parser),
else => {
// Nothing else should ever be marked as a next_child_block
unreachable;
@ -330,16 +354,36 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
.focus_prev_output,
.send_to_next_output,
.send_to_prev_output,
.toggle_float,
.zoom,
.reload_config,
.toggle_fullscreen,
.close_window,
.increment_primary_count,
.decrement_primary_count,
.swap_next,
.swap_prev,
=> |cmd| {
// None of these have arguments, just create the union and give it back
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {});
},
inline .move_up,
.move_down,
.move_left,
.move_right,
.resize_width,
.resize_height,
=> |cmd| {
const amount_str = utils.stripQuotes(node.arg(parser, 2) orelse {
logWarnMissingNodeArg(name, "amount");
continue;
});
const amount = fmt.parseInt(i32, amount_str, 0) catch {
logWarnInvalidNodeArg(name, amount_str);
continue;
};
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), amount);
},
inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| {
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
logWarnMissingNodeArg(name, "tags");
@ -374,6 +418,76 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
}
}
fn loadPointerBindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
while (try parser.next()) |event| {
switch (event) {
.node => |node| {
const node_name = std.meta.stringToEnum(PointerBindNodeName, node.name);
if (node_name) |name| {
// Parse modifiers (arg 0)
const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse {
logWarnMissingNodeArg(name, "modifier(s)");
continue;
});
const modifiers = try utils.parseModifiers(mod_str) orelse {
logWarnInvalidNodeArg(name, mod_str);
continue;
};
// Parse button (arg 1)
const button_str = utils.stripQuotes(node.arg(parser, 1) orelse {
logWarnMissingNodeArg(name, "button");
continue;
});
const button = parseButton(button_str) orelse {
logWarnInvalidNodeArg(name, button_str);
continue;
};
const action: PointerAction = switch (name) {
.move_window => .move_window,
.resize_window => .resize_window,
};
try config.pointer_binds.append(utils.allocator, .{
.modifiers = modifiers,
.button = button,
.action = action,
});
log.debug("pointer_binds.{s}: {s} {s}", .{ @tagName(name), mod_str, button_str });
} else {
logWarnInvalidNode(node.name);
}
},
.child_block_begin => {
try config.skipChildBlock(parser);
},
.child_block_end => {
return;
},
}
}
}
fn parseButton(s: []const u8) ?u32 {
// Support both numeric and named buttons
var lower_buf: [16]u8 = undefined;
const len = @min(s.len, 16);
const lower = std.ascii.lowerString(lower_buf[0..len], s[0..len]);
if (mem.eql(u8, lower, "btn_left") or mem.eql(u8, lower, "button1")) {
return 0x110; // BTN_LEFT = 272
} else if (mem.eql(u8, lower, "btn_right") or mem.eql(u8, lower, "button3")) {
return 0x111; // BTN_RIGHT = 273
} else if (mem.eql(u8, lower, "btn_middle") or mem.eql(u8, lower, "button2")) {
return 0x112; // BTN_MIDDLE = 274
}
// Try parsing as hex or decimal
return fmt.parseInt(u32, s, 0) catch null;
}
/// Skips an entire child block including any nested child blocks
fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void {
log.warn("Unexpected child block. Skipping it", .{});
@ -421,6 +535,7 @@ fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void {
NodeName => log.warn("Invalid \"{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
BorderNodeName => log.warn("Invalid \"border.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }),
PointerBindNodeName => log.warn("Invalid \"pointer_binds.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }),
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
}
}
@ -429,6 +544,7 @@ fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void {
const node_name_type = @TypeOf(node_name);
switch (node_name_type) {
KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
PointerBindNodeName => log.warn("\"pointer_binds.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
}
}

View file

@ -188,9 +188,24 @@ pub fn manage(output: *Output) void {
}
pub fn render(output: *Output) void {
const seat = output.context.wm.seats.first();
const focused = if (seat) |s| s.focused_window else null;
var it = output.windows.iterator(.forward);
while (it.next()) |window| {
window.render();
// Make sure floating windows are above tiled windows
if (window.floating and output.tags & window.tags != 0 and window != focused) {
window.river_node_v1.placeTop();
}
}
// Make sure that the *focused* floating window goes above any other floating windows
if (focused) |f| {
if (f.floating and f.output == output and output.tags & f.tags != 0) {
f.river_node_v1.placeTop();
}
}
}
@ -206,8 +221,12 @@ fn calculatePrimaryStackLayout(output: *Output) void {
var it = output.windows.iterator(.forward);
while (it.next()) |window| {
if (output.tags & window.tags != 0x0000) {
active_list.append(&window.active_list_node);
active_count += 1;
// Floating windows should be shown but not included in this layout generation
const will_float = window.pending_manage.floating orelse window.floating;
if (!will_float) {
active_count += 1;
active_list.append(&window.active_list_node);
}
window.pending_render.show = true;
} else {
window.pending_render.show = false;

View file

@ -11,16 +11,27 @@ river_seat_v1: *river.SeatV1,
focused_window: ?*Window,
focused_output: ?*Output,
pointer_op: PointerOp = .none,
/// State consumed in manage phase, reset at end of manage().
pending_manage: PendingManage = .{},
link: wl.list.Link,
// Pointer bindings for interactive move/resize
move_pointer_binding: ?*river.PointerBindingV1 = null,
resize_pointer_binding: ?*river.PointerBindingV1 = null,
pub const PendingManage = struct {
window: ?PendingWindow = null,
output: ?PendingOutput = null,
should_warp_pointer: bool = false,
op_delta: ?struct { dx: i32, dy: i32 } = null,
op_released: bool = false,
pointer_move_request: ?*Window = null,
pointer_resize_request: ?struct { window: *Window, edges: river.WindowV1.Edges } = null,
pub const PendingWindow = union(enum) {
window: *Window,
clear_focus,
@ -32,6 +43,19 @@ pub const PendingManage = struct {
};
};
pub const PointerOp = union(enum) {
none,
move: struct { window: *Window, start_x: i32, start_y: i32 },
resize: struct {
window: *Window,
start_width: u31,
start_height: u31,
start_x: i32,
start_y: i32,
edges: river.WindowV1.Edges,
},
};
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
var seat = try utils.allocator.create(Seat);
errdefer seat.destroy();
@ -50,6 +74,8 @@ pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
}
pub fn destroy(seat: *Seat) void {
if (seat.move_pointer_binding) |binding| binding.destroy();
if (seat.resize_pointer_binding) |binding| binding.destroy();
seat.river_seat_v1.destroy();
utils.allocator.destroy(seat);
}
@ -65,6 +91,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
seat.setWindowFocus(ev.window);
},
.window_interaction => |ev| seat.setWindowFocus(ev.window),
.op_delta => |ev| {
seat.pending_manage.op_delta = .{ .dx = ev.dx, .dy = ev.dy };
},
.op_release => {
seat.pending_manage.op_released = true;
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
@ -129,12 +161,160 @@ pub fn manage(seat: *Seat) void {
seat.river_seat_v1.pointerWarp(pointer_x, pointer_y);
}
}
// Interactive move/resize operations
// Start move operation
if (seat.pending_manage.pointer_move_request) |window| {
if (window.floating) {
seat.pointer_op = .{
.move = .{
.window = window,
.start_x = window.float_x,
.start_y = window.float_y,
},
};
seat.river_seat_v1.opStartPointer();
}
}
// Start resize operation
if (seat.pending_manage.pointer_resize_request) |req| {
if (req.window.floating) {
seat.pointer_op = .{
.resize = .{
.window = req.window,
.start_width = req.window.float_width,
.start_height = req.window.float_height,
.start_x = req.window.float_x,
.start_y = req.window.float_y,
.edges = req.edges,
},
};
seat.river_seat_v1.opStartPointer();
req.window.river_window_v1.informResizeStart();
}
}
// Process pointer delta (mouse movement during operation)
if (seat.pending_manage.op_delta) |delta| {
switch (seat.pointer_op) {
.none => {},
.move => |op| {
const output = op.window.output orelse return;
const min_x = output.x;
const max_x = output.x + output.width - @as(i32, op.window.float_width);
const min_y = output.y;
const max_y = output.y + output.height - @as(i32, op.window.float_height);
op.window.float_x = std.math.clamp(op.start_x + delta.dx, min_x, @max(min_x, max_x));
op.window.float_y = std.math.clamp(op.start_y + delta.dy, min_y, @max(min_y, max_y));
op.window.pending_render.x = op.window.float_x;
op.window.pending_render.y = op.window.float_y;
},
.resize => |op| {
var new_width: i32 = op.start_width;
var new_height: i32 = op.start_height;
var new_x: i32 = op.start_x;
var new_y: i32 = op.start_y;
// Adjust based on which edges are being dragged
if (op.edges.right) new_width += delta.dx;
if (op.edges.left) {
new_width -= delta.dx;
new_x += delta.dx;
}
if (op.edges.bottom) new_height += delta.dy;
if (op.edges.top) {
new_height -= delta.dy;
new_y += delta.dy;
}
// Clamp to minimum size
const min_size: i32 = 50;
if (new_width < min_size) {
if (op.edges.left) new_x -= min_size - new_width;
new_width = min_size;
}
if (new_height < min_size) {
if (op.edges.top) new_y -= min_size - new_height;
new_height = min_size;
}
// Clamp position to output bounds
const output = op.window.output orelse return;
new_x = std.math.clamp(new_x, output.x, @max(output.x, output.x + output.width - new_width));
new_y = std.math.clamp(new_y, output.y, @max(output.y, output.y + output.height - new_height));
op.window.float_width = @intCast(new_width);
op.window.float_height = @intCast(new_height);
op.window.float_x = new_x;
op.window.float_y = new_y;
op.window.river_window_v1.proposeDimensions(
op.window.float_width,
op.window.float_height,
);
op.window.pending_render.x = op.window.float_x;
op.window.pending_render.y = op.window.float_y;
},
}
}
// Process pointer release (end of operation)
if (seat.pending_manage.op_released) {
switch (seat.pointer_op) {
.none => {},
.move => {
seat.river_seat_v1.opEnd();
seat.pointer_op = .none;
},
.resize => |op| {
op.window.river_window_v1.informResizeEnd();
seat.river_seat_v1.opEnd();
seat.pointer_op = .none;
},
}
}
}
pub fn render(seat: *Seat) void {
_ = seat;
}
pub fn movePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void {
switch (event) {
.pressed => {
const window = seat.focused_window orelse return;
if (!window.floating) {
// Auto-float on drag
window.pending_manage.floating = true;
}
seat.pending_manage.pointer_move_request = window;
seat.context.wm.river_window_manager_v1.manageDirty();
},
.released => {},
}
}
pub fn resizePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void {
switch (event) {
.pressed => {
const window = seat.focused_window orelse return;
if (!window.floating) {
// Auto-float on drag
window.pending_manage.floating = true;
}
seat.pending_manage.pointer_resize_request = .{
.window = window,
.edges = .{ .bottom = true, .right = true },
};
seat.context.wm.river_window_manager_v1.manageDirty();
},
.released => {},
}
}
const std = @import("std");
const assert = std.debug.assert;

View file

@ -9,6 +9,7 @@ context: *Context,
river_window_v1: *river.WindowV1,
river_node_v1: *river.NodeV1,
// TODO: Could switch this to a Rect { x, y, width, height }
width: u31 = 0,
height: u31 = 0,
x: i32 = 0,
@ -20,6 +21,12 @@ maximized: bool = false,
tags: u32 = 0x0001,
output: ?*Output,
floating: bool = false,
float_width: u31 = 0,
float_height: u31 = 0,
float_x: i32 = 0,
float_y: i32 = 0,
initialized: bool = false,
/// State consumed in manage() phase, reset at end of manage().
@ -44,6 +51,8 @@ pub const PendingManage = struct {
tags: ?u32 = null,
pending_output: ?PendingOutput = null,
floating: ?bool = null,
pub const PendingOutput = union(enum) {
output: *Output,
clear_output,
@ -88,24 +97,43 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
assert(window.river_window_v1 == river_window_v1);
switch (event) {
.closed => {
{
// If there's no output, we don't really care about focus and can skip this event
const output = if (window.output) |output| output else return;
var it = window.context.wm.seats.iterator(.forward);
while (it.next()) |seat| {
if (seat.focused_window == window) {
// Find another window to focus and warp pointer there
if (output.prevWindow(window)) |next_focus| {
if (next_focus != window) {
seat.pending_manage.window = .{ .window = next_focus };
seat.pending_manage.should_warp_pointer = true;
} else {
// Only window in list - clear focus
seat.pending_manage.window = .clear_focus;
}
// Clear any pointer operations referencing this window
var seat_it = window.context.wm.seats.iterator(.forward);
while (seat_it.next()) |seat| {
switch (seat.pointer_op) {
.move => |op| if (op.window == window) {
seat.river_seat_v1.opEnd();
seat.pointer_op = .none;
},
.resize => |op| if (op.window == window) {
seat.river_seat_v1.opEnd();
seat.pointer_op = .none;
},
.none => {},
}
if (seat.pending_manage.pointer_move_request == window)
seat.pending_manage.pointer_move_request = null;
if (seat.pending_manage.pointer_resize_request) |req| {
if (req.window == window)
seat.pending_manage.pointer_resize_request = null;
}
}
// If there's no output, we don't really care about focus and can skip this event
const output = if (window.output) |output| output else return;
var it = window.context.wm.seats.iterator(.forward);
while (it.next()) |seat| {
if (seat.focused_window == window) {
// Find another window to focus and warp pointer there
if (output.prevWindow(window)) |next_focus| {
if (next_focus != window) {
seat.pending_manage.window = .{ .window = next_focus };
seat.pending_manage.should_warp_pointer = true;
} else {
// Only window in list - clear focus
seat.pending_manage.window = .clear_focus;
}
} else {
seat.pending_manage.window = .clear_focus;
}
}
}
@ -128,6 +156,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
}
pub fn manage(window: *Window) void {
const river_window_v1 = window.river_window_v1;
if (!window.initialized) {
// Only happens once per Window
@branchHint(.unlikely);
@ -135,15 +164,50 @@ pub fn manage(window: *Window) void {
// TODO: We might want to think about paying attention to the decoration_hint event
// If we do, this would need to move, I think?
window.river_window_v1.useSsd();
river_window_v1.useSsd();
window.river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true });
window.river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true });
river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
}
// Updating state since the last manage event
defer window.pending_manage = .{};
const pending_manage = window.pending_manage;
// Floating status
if (pending_manage.floating) |floating| blk: {
// This needs to be before proposing the new dimensions since we want to save the current ones!
// Skip the rest of the block if floating matches what is already set
if (floating == window.floating) break :blk;
window.floating = floating;
if (floating) {
// Let the window know it isn't tiled
river_window_v1.setTiled(.{});
if (window.float_width == 0) {
// Never floated before; use current dimensions but centered on output
window.float_width = window.width;
window.float_height = window.height;
if (window.output) |output| {
// Need to find center and then subtract half of the window's width/height
window.float_x = output.x + @divTrunc(output.width, 2) - @divTrunc(window.width, 2);
window.float_y = output.y + @divTrunc(output.height, 2) - @divTrunc(window.height, 2);
}
} else {
// Window has floated before; re-use its old dimensions
river_window_v1.proposeDimensions(window.float_width, window.float_height);
}
window.pending_render.x = window.float_x;
window.pending_render.y = window.float_y;
} else {
river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
// Save floating dimensions in case the window gets floated again
window.float_width = window.width;
window.float_height = window.height;
window.float_x = window.x;
window.float_y = window.y;
}
}
// Layout (pre-computed by WindowManager.calculatePrimaryStackLayout())
if (pending_manage.width) |new_width| {
if (pending_manage.height) |new_height| {
@ -177,6 +241,7 @@ pub fn manage(window: *Window) void {
if (pending_manage.tags) |tags| {
window.tags = tags;
}
// New output
if (pending_manage.pending_output) |pending_output| {
switch (pending_output) {
.output => |output| {
@ -243,5 +308,6 @@ const river = wayland.client.river;
const utils = @import("utils.zig");
const Context = @import("Context.zig");
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const log = std.log.scoped(.Window);

View file

@ -110,6 +110,28 @@ fn manage_start(wm: *WindowManager) void {
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();
}
}
{
@ -172,6 +194,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
// 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| {

View file

@ -13,6 +13,7 @@ pub const Command = union(enum) {
send_to_next_output,
send_to_prev_output,
zoom,
toggle_float,
// Changes the ratio on the focused output only
change_ratio: f32,
// Changes the primary count on the focus output only
@ -29,6 +30,20 @@ pub const Command = union(enum) {
// spawn_tagmask: u32, // TODO
// focus_previous_tags, // TODO
// send_to_previous_tags, // TODO
// Move floating window by pixels
move_up: i32,
move_down: i32,
move_left: i32,
move_right: i32,
// Resize floating window by pixels
resize_width: i32,
resize_height: i32,
// Swap window position in stack
swap_next,
swap_prev,
};
const XkbBinding = struct {
@ -98,25 +113,36 @@ const XkbBinding = struct {
.window => |window| break :blk window,
}
} else seat.focused_window orelse return;
// Noop if the focused window is floating
if (current_focus.floating) return;
// Get the first tiled window to try zoom with
const output = current_focus.output orelse return;
const first_window: *Window = if (output.windows.first()) |first| blk: {
if (current_focus == first) {
// Try get the second window instead
const next = first.link.next orelse return;
// next is the sentinel; there's only one window
if (next == &output.windows.link) {
return;
const first_tiled: *Window = blk: {
var it = output.windows.iterator(.forward);
while (it.next()) |window| {
if (window != current_focus and !window.floating) {
break :blk window;
}
break :blk @fieldParentPtr("link", next);
} else {
seat.pending_manage.should_warp_pointer = true;
break :blk first;
}
} else {
// If current_focus is not null, we know that first_window *must not* be null.
unreachable;
// No (or only one) tiled windows, nothing to do
return;
};
current_focus.link.swapWith(&first_window.link);
current_focus.link.swapWith(&first_tiled.link);
// Don't warp pointer if the first was the one focused before
if (output.windows.first() == current_focus) {
seat.pending_manage.should_warp_pointer = true;
}
},
.toggle_float => {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
// Noop if the window is fullscreened
if (window.fullscreen) return;
window.pending_manage.floating = !window.floating;
context.wm.river_window_manager_v1.manageDirty();
},
.change_ratio => |diff| {
const seat = context.wm.seats.first() orelse return;
@ -202,6 +228,14 @@ const XkbBinding = struct {
context.wm.river_window_manager_v1.manageDirty();
}
},
.move_up => |pixels| moveFloatingWindow(context, 0, -pixels),
.move_down => |pixels| moveFloatingWindow(context, 0, pixels),
.move_left => |pixels| moveFloatingWindow(context, -pixels, 0),
.move_right => |pixels| moveFloatingWindow(context, pixels, 0),
.resize_width => |delta| resizeFloatingWindow(context, delta, 0),
.resize_height => |delta| resizeFloatingWindow(context, 0, delta),
.swap_next => swapWindow(context, .next),
.swap_prev => swapWindow(context, .prev),
}
}
@ -294,6 +328,61 @@ const XkbBinding = struct {
window.pending_manage.pending_output = .{ .output = output };
}
}
fn moveFloatingWindow(context: *Context, dx: i32, dy: i32) void {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
if (!window.floating) return;
const output = window.output orelse return;
const min_x = output.x;
const max_x = output.x + output.width - @as(i32, window.float_width);
const min_y = output.y;
const max_y = output.y + output.height - @as(i32, window.float_height);
window.float_x = std.math.clamp(window.float_x + dx, min_x, @max(min_x, max_x));
window.float_y = std.math.clamp(window.float_y + dy, min_y, @max(min_y, max_y));
window.pending_render.x = window.float_x;
window.pending_render.y = window.float_y;
context.wm.river_window_manager_v1.manageDirty();
}
fn resizeFloatingWindow(context: *Context, dw: i32, dh: i32) void {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
if (!window.floating) return;
const output = window.output orelse return;
const new_width: i32 = @as(i32, window.float_width) + dw;
const new_height: i32 = @as(i32, window.float_height) + dh;
window.float_width = @intCast(@max(50, new_width));
window.float_height = @intCast(@max(50, new_height));
// Clamp position to keep window on screen after resize
const max_x = output.x + output.width - @as(i32, window.float_width);
const max_y = output.y + output.height - @as(i32, window.float_height);
window.float_x = std.math.clamp(window.float_x, output.x, @max(output.x, max_x));
window.float_y = std.math.clamp(window.float_y, output.y, @max(output.y, max_y));
window.pending_render.x = window.float_x;
window.pending_render.y = window.float_y;
window.river_window_v1.proposeDimensions(window.float_width, window.float_height);
context.wm.river_window_manager_v1.manageDirty();
}
fn swapWindow(context: *Context, comptime direction: enum { next, prev }) void {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
const output = window.output orelse return;
const target = switch (direction) {
.next => output.nextWindow(window),
.prev => output.prevWindow(window),
} orelse return;
if (target != window) {
window.link.swapWith(&target.link);
context.wm.river_window_manager_v1.manageDirty();
}
}
};
context: *Context,