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:
commit
e5d439e27d
8 changed files with 581 additions and 48 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
120
src/Config.zig
120
src/Config.zig
|
|
@ -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) ++ "\""),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
180
src/Seat.zig
180
src/Seat.zig
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
102
src/Window.zig
102
src/Window.zig
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue