Merge pull request 'Add multi-output support' (#7) from multi-output-support into main
Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/7
This commit is contained in:
commit
26949d7b8a
8 changed files with 478 additions and 269 deletions
22
README.md
22
README.md
|
|
@ -7,12 +7,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# beansprout wm
|
# beansprout wm
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
[ ] Support multiple outputs
|
|
||||||
[ ] Support multiple seats
|
These are in rough order of my priority, though no promises I do them in this order.
|
||||||
[ ] Support floating windows
|
|
||||||
[ ] Support wallpapers
|
- [ ] Support floating windows
|
||||||
[ ] Support a bar
|
- [ ] Support wallpapers
|
||||||
[ ] Support starting programs at WM launch
|
- [ ] Support a bar
|
||||||
[ ] Support changeable primary ratio
|
- [ ] Support changeable primary count
|
||||||
[ ] Support changeable primary count
|
- [ ] Support starting programs at WM launch
|
||||||
[ ] Support overriding config location
|
- [ ] Support overriding config location
|
||||||
|
- [ ] Add support for multimedia/brightness keys
|
||||||
|
- [ ] Support multiple seats
|
||||||
|
- [x] Support changeable primary ratio
|
||||||
|
- [x] Support multiple outputs
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,16 @@ borders {
|
||||||
}
|
}
|
||||||
keybinds {
|
keybinds {
|
||||||
spawn Mod4 T foot
|
spawn Mod4 T foot
|
||||||
focus_next Mod4 J
|
focus_next_window Mod4 J
|
||||||
focus_prev Mod4 K
|
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
|
||||||
zoom Mod4 Z
|
zoom Mod4 Z
|
||||||
change_ratio Mod4 H +0.05
|
change_ratio Mod4 H +0.05
|
||||||
change_ratio Mod4 L -0.05
|
change_ratio Mod4 L -0.05
|
||||||
|
reload_config Mod4+Shift R
|
||||||
toggle_fullscreen Mod4 F
|
toggle_fullscreen Mod4 F
|
||||||
close_window Mod4+Shift Q
|
close_window Mod4+Shift Q
|
||||||
// Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9
|
// Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9
|
||||||
|
|
|
||||||
|
|
@ -324,13 +324,19 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
|
||||||
};
|
};
|
||||||
break :sw .{ .change_ratio = diff };
|
break :sw .{ .change_ratio = diff };
|
||||||
},
|
},
|
||||||
inline .focus_next, .focus_prev, .zoom, .reload_config, .toggle_fullscreen, .close_window => |cmd| {
|
inline .focus_next_window,
|
||||||
|
.focus_prev_window,
|
||||||
|
.focus_next_output,
|
||||||
|
.focus_prev_output,
|
||||||
|
.send_to_next_output,
|
||||||
|
.send_to_prev_output,
|
||||||
|
.zoom,
|
||||||
|
.reload_config,
|
||||||
|
.toggle_fullscreen,
|
||||||
|
.close_window,
|
||||||
|
=> |cmd| {
|
||||||
// None of these have arguments, just create the union and give it back
|
// None of these have arguments, just create the union and give it back
|
||||||
break :sw @unionInit(
|
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {});
|
||||||
XkbBindings.Command,
|
|
||||||
@tagName(cmd),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| {
|
inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| {
|
||||||
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
|
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
|
||||||
|
|
@ -341,11 +347,7 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
|
||||||
logWarnInvalidNodeArg(name, tags_str);
|
logWarnInvalidNodeArg(name, tags_str);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
break :sw @unionInit(
|
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), tags);
|
||||||
XkbBindings.Command,
|
|
||||||
@tagName(cmd),
|
|
||||||
tags,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
210
src/Output.zig
210
src/Output.zig
|
|
@ -13,15 +13,27 @@ height: i32 = 0,
|
||||||
x: i32 = 0,
|
x: i32 = 0,
|
||||||
y: i32 = 0,
|
y: i32 = 0,
|
||||||
|
|
||||||
|
/// Proportion of output width taken by the primary stack
|
||||||
|
primary_ratio: f32 = 0.55,
|
||||||
|
|
||||||
/// Tags are 32-bit bitfield. A window can be active on one(?) or more tags.
|
/// Tags are 32-bit bitfield. A window can be active on one(?) or more tags.
|
||||||
tags: u32 = 0x0001,
|
tags: u32 = 0x0001,
|
||||||
|
|
||||||
|
/// State consumed in manage() phase, reset at end of manage().
|
||||||
pending_manage: PendingManage = .{},
|
pending_manage: PendingManage = .{},
|
||||||
|
|
||||||
|
windows: wl.list.Head(Window, .link),
|
||||||
|
|
||||||
link: wl.list.Link,
|
link: wl.list.Link,
|
||||||
|
|
||||||
pub const PendingManage = struct {
|
pub const PendingManage = struct {
|
||||||
|
width: ?i32 = null,
|
||||||
|
height: ?i32 = null,
|
||||||
|
x: ?i32 = null,
|
||||||
|
y: ?i32 = null,
|
||||||
|
|
||||||
tags: ?u32 = null,
|
tags: ?u32 = null,
|
||||||
|
primary_ratio: ?f32 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
|
pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
|
||||||
|
|
@ -31,33 +43,106 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
|
||||||
output.* = .{
|
output.* = .{
|
||||||
.context = context,
|
.context = context,
|
||||||
.river_output_v1 = river_output_v1,
|
.river_output_v1 = river_output_v1,
|
||||||
|
.windows = undefined, // we will initialize this shortly
|
||||||
.link = undefined, // Handled by the wl.list
|
.link = undefined, // Handled by the wl.list
|
||||||
};
|
};
|
||||||
|
|
||||||
|
output.windows.init();
|
||||||
|
|
||||||
output.river_output_v1.setListener(*Output, outputListener, output);
|
output.river_output_v1.setListener(*Output, outputListener, output);
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(output: *Output) void {
|
pub fn destroy(output: *Output) void {
|
||||||
|
var it = output.windows.safeIterator(.forward);
|
||||||
|
while (it.next()) |window| {
|
||||||
|
window.link.remove();
|
||||||
|
window.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
output.river_output_v1.destroy();
|
output.river_output_v1.destroy();
|
||||||
utils.allocator.destroy(output);
|
utils.allocator.destroy(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the next window in the list, wrapping to first if at end
|
||||||
|
pub fn nextWindow(output: *Output, current: *Window) ?*Window {
|
||||||
|
const next_link = current.link.next orelse return output.windows.first();
|
||||||
|
// If we've reached the sentinel (head's link), wrap to first
|
||||||
|
if (next_link == &output.windows.link) return output.windows.first();
|
||||||
|
return @fieldParentPtr("link", next_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the previous window in the list, wrapping to last if at beginning
|
||||||
|
pub fn prevWindow(output: *Output, current: *Window) ?*Window {
|
||||||
|
const prev_link = current.link.prev orelse return output.windows.last();
|
||||||
|
// If we've reached the sentinel (head's link), wrap to last
|
||||||
|
if (prev_link == &output.windows.link) return output.windows.last();
|
||||||
|
return @fieldParentPtr("link", prev_link);
|
||||||
|
}
|
||||||
|
|
||||||
fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
|
fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
|
||||||
assert(output.river_output_v1 == river_output_v1);
|
assert(output.river_output_v1 == river_output_v1);
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.removed => output.destroy(),
|
.removed => {
|
||||||
|
const context = output.context;
|
||||||
|
const wm = context.wm;
|
||||||
|
|
||||||
|
// Move windows to the previous output in the list.
|
||||||
|
// If this was the only output, windows become orphans.
|
||||||
|
const prev_output: ?*Output = if (wm.prevOutput(output)) |prev| blk: {
|
||||||
|
if (prev == output) break :blk null; // Only output; wrapped to itself
|
||||||
|
break :blk prev; // We got the previous list
|
||||||
|
} else unreachable;
|
||||||
|
|
||||||
|
const window_pending_output: Window.PendingManage.PendingOutput =
|
||||||
|
if (prev_output) |prev|
|
||||||
|
.{ .output = prev }
|
||||||
|
else
|
||||||
|
.clear_output;
|
||||||
|
|
||||||
|
// Update each window's output before moving the list
|
||||||
|
var it = output.windows.iterator(.forward);
|
||||||
|
while (it.next()) |window| {
|
||||||
|
window.pending_manage.pending_output = window_pending_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move windows to new destination
|
||||||
|
const dest_list = if (prev_output) |prev| &prev.windows else &wm.orphan_windows;
|
||||||
|
dest_list.appendList(&output.windows);
|
||||||
|
|
||||||
|
blk: {
|
||||||
|
// If the removed output was focused, move focus to the next
|
||||||
|
// available output (and its first window, if any).
|
||||||
|
// TODO: Support multiple seats
|
||||||
|
const seat = wm.seats.first() orelse break :blk;
|
||||||
|
if (seat.focused_output != output) break :blk;
|
||||||
|
|
||||||
|
const next_output = wm.nextOutput(output);
|
||||||
|
if (next_output == output) break :blk;
|
||||||
|
const o = next_output orelse break :blk;
|
||||||
|
|
||||||
|
seat.pending_manage.output = .{ .output = o };
|
||||||
|
if (o.windows.first()) |window| {
|
||||||
|
seat.pending_manage.window = .{ .window = window };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.link.remove();
|
||||||
|
output.destroy();
|
||||||
|
},
|
||||||
.wl_output => |ev| {
|
.wl_output => |ev| {
|
||||||
log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name});
|
log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name});
|
||||||
},
|
},
|
||||||
.dimensions => |ev| {
|
.dimensions => |ev| {
|
||||||
output.width = ev.width;
|
output.pending_manage.width = ev.width;
|
||||||
output.height = ev.height;
|
output.pending_manage.height = ev.height;
|
||||||
|
output.context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
.position => |ev| {
|
.position => |ev| {
|
||||||
output.x = ev.x;
|
output.pending_manage.x = ev.x;
|
||||||
output.y = ev.y;
|
output.pending_manage.y = ev.y;
|
||||||
|
output.context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,17 +150,129 @@ fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event,
|
||||||
pub fn manage(output: *Output) void {
|
pub fn manage(output: *Output) void {
|
||||||
defer output.pending_manage = .{};
|
defer output.pending_manage = .{};
|
||||||
|
|
||||||
|
if (output.pending_manage.width) |width| {
|
||||||
|
output.width = width;
|
||||||
|
}
|
||||||
|
if (output.pending_manage.height) |height| {
|
||||||
|
output.height = height;
|
||||||
|
}
|
||||||
|
if (output.pending_manage.x) |x| {
|
||||||
|
output.x = x;
|
||||||
|
}
|
||||||
|
if (output.pending_manage.y) |y| {
|
||||||
|
output.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
if (output.pending_manage.tags) |tags| {
|
if (output.pending_manage.tags) |tags| {
|
||||||
output.tags = tags;
|
output.tags = tags;
|
||||||
}
|
}
|
||||||
|
if (output.pending_manage.primary_ratio) |primary_ratio| {
|
||||||
|
// Ratios outside of this range could cause crashes (when doing the layout calculation)
|
||||||
|
output.primary_ratio = std.math.clamp(primary_ratio, 0.10, 0.90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate layout before managing windows
|
||||||
|
output.calculatePrimaryStackLayout();
|
||||||
|
var it = output.windows.iterator(.forward);
|
||||||
|
while (it.next()) |window| {
|
||||||
|
window.manage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(output: *Output) void {
|
pub fn render(output: *Output) void {
|
||||||
_ = output;
|
var it = output.windows.iterator(.forward);
|
||||||
|
while (it.next()) |window| {
|
||||||
|
window.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - CONFIG: Allow primary on the left
|
||||||
|
// TODO - CONFIG: Allow setting a ratio for single-window width (useful for ultrawides)
|
||||||
|
/// Calculate primary/stack layout positions for all windows.
|
||||||
|
/// - Single window: maximized
|
||||||
|
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
||||||
|
fn calculatePrimaryStackLayout(output: *Output) void {
|
||||||
|
// Get a list of active windows
|
||||||
|
var active_list: DoublyLinkedList = .{};
|
||||||
|
var active_count: u31 = 0;
|
||||||
|
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;
|
||||||
|
window.pending_render.show = true;
|
||||||
|
} else {
|
||||||
|
window.pending_render.show = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_count == 0) return;
|
||||||
|
|
||||||
|
// Output dimensions come as i32 from the protocol, convert to u31 for window dimensions
|
||||||
|
// since they can't be negative.
|
||||||
|
const output_width: u31 = @intCast(output.width);
|
||||||
|
const output_height: u31 = @intCast(output.height);
|
||||||
|
const output_x = output.x;
|
||||||
|
const output_y = output.y;
|
||||||
|
|
||||||
|
// Iterate through the active windows and apply the tags
|
||||||
|
var i: u31 = 0;
|
||||||
|
while (active_list.popFirst()) |node| : (i += 1) {
|
||||||
|
const window: *Window = @fieldParentPtr("active_list_node", node);
|
||||||
|
if (active_count == 1) {
|
||||||
|
// Single window: maximize
|
||||||
|
window.pending_render.x = output_x;
|
||||||
|
window.pending_render.y = output_y;
|
||||||
|
window.pending_manage.width = output_width;
|
||||||
|
window.pending_manage.height = output_height;
|
||||||
|
window.pending_manage.maximized = true;
|
||||||
|
} else {
|
||||||
|
// Multiple windows: primary/stack layout
|
||||||
|
// TODO: Support multiple windows in primary stack
|
||||||
|
const primary_width: u31 = @intFromFloat(@as(f32, @floatFromInt(output_width)) * output.primary_ratio);
|
||||||
|
const stack_width: u31 = output_width - primary_width;
|
||||||
|
const stack_count = active_count - 1;
|
||||||
|
const stack_height: u31 = @divFloor(output_height, stack_count);
|
||||||
|
window.pending_manage.maximized = false;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
// Primary window (first window) - right side
|
||||||
|
window.pending_render.x = output_x + @as(i32, stack_width);
|
||||||
|
window.pending_render.y = output_y;
|
||||||
|
window.pending_manage.width = primary_width;
|
||||||
|
window.pending_manage.height = output_height;
|
||||||
|
} else {
|
||||||
|
// Stack window(s) - left side
|
||||||
|
const stack_index = i - 1;
|
||||||
|
window.pending_render.x = output_x;
|
||||||
|
window.pending_render.y = output_y + @as(i32, stack_index) * @as(i32, stack_height);
|
||||||
|
window.pending_manage.width = stack_width;
|
||||||
|
// Last stack window gets remaining height to avoid gaps from integer division
|
||||||
|
if (i == active_count - 1) {
|
||||||
|
window.pending_manage.height = output_height - stack_index * stack_height;
|
||||||
|
} else {
|
||||||
|
window.pending_manage.height = stack_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make space for borders; this is the same for all windows.
|
||||||
|
// Borders are automatically disabled when a window is fullscreened so we don't
|
||||||
|
// have to worry about that.
|
||||||
|
const border_width = output.context.config.border_width;
|
||||||
|
// We use .? because we know we set the windows height, width, x, and y above
|
||||||
|
window.pending_manage.height.? -= 2 * border_width;
|
||||||
|
window.pending_manage.width.? -= 2 * border_width;
|
||||||
|
window.pending_render.x.? += border_width;
|
||||||
|
window.pending_render.y.? += border_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we went through the whole list
|
||||||
|
assert(active_list.first == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const DoublyLinkedList = std.DoublyLinkedList;
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
const wl = wayland.client.wl;
|
const wl = wayland.client.wl;
|
||||||
|
|
@ -83,5 +280,6 @@ const river = wayland.client.river;
|
||||||
|
|
||||||
const utils = @import("utils.zig");
|
const utils = @import("utils.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
|
const Window = @import("Window.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.Output);
|
const log = std.log.scoped(.Output);
|
||||||
|
|
|
||||||
47
src/Seat.zig
47
src/Seat.zig
|
|
@ -8,7 +8,8 @@ context: *Context,
|
||||||
|
|
||||||
river_seat_v1: *river.SeatV1,
|
river_seat_v1: *river.SeatV1,
|
||||||
|
|
||||||
focused: ?*Window,
|
focused_window: ?*Window,
|
||||||
|
focused_output: ?*Output,
|
||||||
|
|
||||||
/// State consumed in manage phase, reset at end of manage().
|
/// State consumed in manage phase, reset at end of manage().
|
||||||
pending_manage: PendingManage = .{},
|
pending_manage: PendingManage = .{},
|
||||||
|
|
@ -16,13 +17,19 @@ pending_manage: PendingManage = .{},
|
||||||
link: wl.list.Link,
|
link: wl.list.Link,
|
||||||
|
|
||||||
pub const PendingManage = struct {
|
pub const PendingManage = struct {
|
||||||
pending_focus: ?PendingFocus = null,
|
window: ?PendingWindow = null,
|
||||||
|
output: ?PendingOutput = null,
|
||||||
should_warp_pointer: bool = false,
|
should_warp_pointer: bool = false,
|
||||||
|
|
||||||
pub const PendingFocus = union(enum) {
|
pub const PendingWindow = union(enum) {
|
||||||
window: *Window,
|
window: *Window,
|
||||||
clear_focus,
|
clear_focus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const PendingOutput = union(enum) {
|
||||||
|
output: *Output,
|
||||||
|
clear_focus,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
||||||
|
|
@ -32,7 +39,8 @@ pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
||||||
seat.* = .{
|
seat.* = .{
|
||||||
.context = context,
|
.context = context,
|
||||||
.river_seat_v1 = river_seat_v1,
|
.river_seat_v1 = river_seat_v1,
|
||||||
.focused = null,
|
.focused_window = null,
|
||||||
|
.focused_output = null,
|
||||||
.link = undefined, // Handled by the wl.list
|
.link = undefined, // Handled by the wl.list
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,41 +74,51 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
|
||||||
// river_window_v1 needs to be optional because ev.window is optional
|
// river_window_v1 needs to be optional because ev.window is optional
|
||||||
fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void {
|
fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void {
|
||||||
const wv1 = river_window_v1 orelse return;
|
const wv1 = river_window_v1 orelse return;
|
||||||
const window: *Window = @ptrCast(@alignCast(wv1.getUserData()));
|
const window: *Window = @ptrCast(@alignCast(wv1.getUserData() orelse return));
|
||||||
seat.pending_manage.pending_focus = .{ .window = window };
|
seat.pending_manage.window = .{ .window = window };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn manage(seat: *Seat) void {
|
pub fn manage(seat: *Seat) void {
|
||||||
defer seat.pending_manage = .{};
|
defer seat.pending_manage = .{};
|
||||||
|
|
||||||
if (seat.pending_manage.pending_focus) |pending_focus| {
|
if (seat.pending_manage.window) |pending_window| {
|
||||||
switch (pending_focus) {
|
switch (pending_window) {
|
||||||
.window => |window| {
|
.window => |window| {
|
||||||
if (seat.focused) |focused| {
|
if (seat.focused_window) |focused| {
|
||||||
// Tell the previously focused Window that it's no longer focused
|
// Tell the previously focused Window that it's no longer focused
|
||||||
if (focused != window) {
|
if (focused != window) {
|
||||||
focused.pending_render.focused = false;
|
focused.pending_render.focused = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seat.focused = window;
|
seat.focused_window = window;
|
||||||
seat.river_seat_v1.focusWindow(window.river_window_v1);
|
seat.river_seat_v1.focusWindow(window.river_window_v1);
|
||||||
window.pending_render.focused = true;
|
window.pending_render.focused = true;
|
||||||
},
|
},
|
||||||
.clear_focus => {
|
.clear_focus => {
|
||||||
if (seat.focused) |focused| {
|
if (seat.focused_window) |focused| {
|
||||||
// Tell the previously focused Window that it's no longer focused
|
// Tell the previously focused Window that it's no longer focused
|
||||||
focused.pending_render.focused = false;
|
focused.pending_render.focused = false;
|
||||||
}
|
}
|
||||||
seat.focused = null;
|
seat.focused_window = null;
|
||||||
seat.river_seat_v1.clearFocus();
|
seat.river_seat_v1.clearFocus();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (seat.pending_manage.output) |pending_output| {
|
||||||
|
switch (pending_output) {
|
||||||
|
.output => |output| {
|
||||||
|
seat.focused_output = output;
|
||||||
|
},
|
||||||
|
.clear_focus => {
|
||||||
|
seat.focused_output = null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (seat.pending_manage.should_warp_pointer) blk: {
|
if (seat.pending_manage.should_warp_pointer) blk: {
|
||||||
if (seat.context.config.pointer_warp_on_focus_change) {
|
if (seat.context.config.pointer_warp_on_focus_change) {
|
||||||
const window = seat.focused orelse {
|
const window = seat.focused_window orelse {
|
||||||
log.err("Trying to warp-on-focus-change without a focused window.", .{});
|
log.warn("Trying to warp-on-focus-change without a focused window.", .{});
|
||||||
break :blk;
|
break :blk;
|
||||||
};
|
};
|
||||||
// Warp pointer to center of focused window;
|
// Warp pointer to center of focused window;
|
||||||
|
|
@ -126,6 +144,7 @@ const river = wayland.client.river;
|
||||||
|
|
||||||
const utils = @import("utils.zig");
|
const utils = @import("utils.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
|
const Output = @import("Output.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.Seat);
|
const log = std.log.scoped(.Seat);
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,7 @@ fullscreen: bool = false,
|
||||||
maximized: bool = false,
|
maximized: bool = false,
|
||||||
|
|
||||||
tags: u32 = 0x0001,
|
tags: u32 = 0x0001,
|
||||||
// output: ?*Output,
|
output: ?*Output,
|
||||||
|
|
||||||
// XXX: Do I really even need to store `show`?
|
|
||||||
// I think I only would if I was trying not to send extraneous requests.
|
|
||||||
show: bool = true,
|
|
||||||
|
|
||||||
initialized: bool = false,
|
initialized: bool = false,
|
||||||
|
|
||||||
|
|
@ -46,7 +42,12 @@ pub const PendingManage = struct {
|
||||||
maximized: ?bool = null,
|
maximized: ?bool = null,
|
||||||
|
|
||||||
tags: ?u32 = null,
|
tags: ?u32 = null,
|
||||||
// output: ?*Output = null,
|
pending_output: ?PendingOutput = null,
|
||||||
|
|
||||||
|
pub const PendingOutput = union(enum) {
|
||||||
|
output: *Output,
|
||||||
|
clear_output,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PendingRender = struct {
|
pub const PendingRender = struct {
|
||||||
|
|
@ -58,7 +59,7 @@ pub const PendingRender = struct {
|
||||||
show: ?bool = null,
|
show: ?bool = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: *Output) !*Window {
|
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window {
|
||||||
var window = try utils.allocator.create(Window);
|
var window = try utils.allocator.create(Window);
|
||||||
errdefer window.destroy();
|
errdefer window.destroy();
|
||||||
|
|
||||||
|
|
@ -66,8 +67,8 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: *Outp
|
||||||
.context = context,
|
.context = context,
|
||||||
.river_window_v1 = river_window_v1,
|
.river_window_v1 = river_window_v1,
|
||||||
.river_node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"),
|
.river_node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"),
|
||||||
// .output = output,
|
.output = output,
|
||||||
.tags = if (output.tags != 0) output.tags else 0x0001,
|
.tags = if (output) |o| o.tags else 0x0001,
|
||||||
.link = undefined, // Handled by the wl.list
|
.link = undefined, // Handled by the wl.list
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -87,22 +88,23 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
assert(window.river_window_v1 == river_window_v1);
|
assert(window.river_window_v1 == river_window_v1);
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.closed => {
|
.closed => {
|
||||||
window.context.wm.window_count -= 1;
|
|
||||||
{
|
{
|
||||||
|
// 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);
|
var it = window.context.wm.seats.iterator(.forward);
|
||||||
while (it.next()) |seat| {
|
while (it.next()) |seat| {
|
||||||
if (seat.focused == window) {
|
if (seat.focused_window == window) {
|
||||||
// Find another window to focus and warp pointer there
|
// Find another window to focus and warp pointer there
|
||||||
if (window.context.wm.getPrevWindow(window)) |next_focus| {
|
if (output.prevWindow(window)) |next_focus| {
|
||||||
if (next_focus != window) {
|
if (next_focus != window) {
|
||||||
seat.pending_manage.pending_focus = .{ .window = next_focus };
|
seat.pending_manage.window = .{ .window = next_focus };
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
} else {
|
} else {
|
||||||
// Only window in list - clear focus
|
// Only window in list - clear focus
|
||||||
seat.pending_manage.pending_focus = .clear_focus;
|
seat.pending_manage.window = .clear_focus;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
seat.pending_manage.pending_focus = .clear_focus;
|
seat.pending_manage.window = .clear_focus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +113,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
||||||
window.destroy();
|
window.destroy();
|
||||||
},
|
},
|
||||||
.dimensions => |ev| {
|
.dimensions => |ev| {
|
||||||
// The protocol requires these are strictly greather than zero.
|
// The protocol requires these are strictly greater than zero.
|
||||||
assert(ev.width > 0 and ev.height > 0);
|
assert(ev.width > 0 and ev.height > 0);
|
||||||
window.width = @intCast(ev.width);
|
window.width = @intCast(ev.width);
|
||||||
window.height = @intCast(ev.height);
|
window.height = @intCast(ev.height);
|
||||||
|
|
@ -151,10 +153,11 @@ pub fn manage(window: *Window) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fullscreen and maximize operations
|
// Fullscreen and maximize operations
|
||||||
if (pending_manage.fullscreen) |fullscreen| {
|
if (pending_manage.fullscreen) |fullscreen| blk: {
|
||||||
window.fullscreen = fullscreen;
|
window.fullscreen = fullscreen;
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
const output = window.context.wm.outputs.first() orelse @panic("Failed to get output");
|
// If the window isn't on an output, just skip fullscreening
|
||||||
|
const output = window.output orelse break :blk;
|
||||||
window.river_window_v1.fullscreen(output.river_output_v1);
|
window.river_window_v1.fullscreen(output.river_output_v1);
|
||||||
window.river_window_v1.informFullscreen();
|
window.river_window_v1.informFullscreen();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -174,6 +177,14 @@ pub fn manage(window: *Window) void {
|
||||||
if (pending_manage.tags) |tags| {
|
if (pending_manage.tags) |tags| {
|
||||||
window.tags = tags;
|
window.tags = tags;
|
||||||
}
|
}
|
||||||
|
if (pending_manage.pending_output) |pending_output| {
|
||||||
|
switch (pending_output) {
|
||||||
|
.output => |output| {
|
||||||
|
window.output = output;
|
||||||
|
},
|
||||||
|
.clear_output => window.output = null,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(window: *Window) void {
|
pub fn render(window: *Window) void {
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,9 @@ river_window_manager_v1: *river.WindowManagerV1,
|
||||||
|
|
||||||
seats: wl.list.Head(Seat, .link),
|
seats: wl.list.Head(Seat, .link),
|
||||||
outputs: wl.list.Head(Output, .link),
|
outputs: wl.list.Head(Output, .link),
|
||||||
windows: wl.list.Head(Window, .link),
|
|
||||||
|
|
||||||
window_count: u8 = 0,
|
/// Place to store windows if all Outputs have been disconnected
|
||||||
|
orphan_windows: wl.list.Head(Window, .link),
|
||||||
primary_ratio: f32 = 0.55,
|
|
||||||
|
|
||||||
/// State consumed in manage phase, reset at end of manage_start().
|
|
||||||
pending_manage: PendingManage = .{},
|
|
||||||
|
|
||||||
pub const PendingManage = struct {
|
|
||||||
primary_ratio: ?f32 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager {
|
pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager {
|
||||||
const wm = try utils.allocator.create(WindowManager);
|
const wm = try utils.allocator.create(WindowManager);
|
||||||
|
|
@ -34,12 +25,12 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi
|
||||||
.river_window_manager_v1 = window_manager_v1,
|
.river_window_manager_v1 = window_manager_v1,
|
||||||
.seats = undefined, // we will initialize these shortly
|
.seats = undefined, // we will initialize these shortly
|
||||||
.outputs = undefined,
|
.outputs = undefined,
|
||||||
.windows = undefined,
|
.orphan_windows = undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
wm.seats.init();
|
wm.seats.init();
|
||||||
wm.outputs.init();
|
wm.outputs.init();
|
||||||
wm.windows.init();
|
wm.orphan_windows.init();
|
||||||
|
|
||||||
wm.river_window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
|
wm.river_window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
|
||||||
|
|
||||||
|
|
@ -47,13 +38,6 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(wm: *WindowManager) void {
|
pub fn destroy(wm: *WindowManager) void {
|
||||||
{
|
|
||||||
var it = wm.windows.safeIterator(.forward);
|
|
||||||
while (it.next()) |window| {
|
|
||||||
window.link.remove();
|
|
||||||
window.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
var it = wm.outputs.safeIterator(.forward);
|
var it = wm.outputs.safeIterator(.forward);
|
||||||
while (it.next()) |output| {
|
while (it.next()) |output| {
|
||||||
|
|
@ -72,118 +56,23 @@ pub fn destroy(wm: *WindowManager) void {
|
||||||
utils.allocator.destroy(wm);
|
utils.allocator.destroy(wm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next window in the list, wrapping to first if at end
|
/// Get the next output in the list, wrapping to first if at end
|
||||||
pub fn getNextWindow(wm: *WindowManager, current: *Window) ?*Window {
|
pub fn nextOutput(wm: *WindowManager, current: *Output) ?*Output {
|
||||||
var it = wm.windows.iterator(.forward);
|
const next_link = current.link.next orelse return wm.outputs.first();
|
||||||
while (it.next()) |window| {
|
// If we've reached the sentinel (head's link), wrap to first
|
||||||
if (window == current) {
|
if (next_link == &wm.outputs.link) return wm.outputs.first();
|
||||||
return it.next() orelse wm.windows.first();
|
return @fieldParentPtr("link", next_link);
|
||||||
}
|
|
||||||
}
|
|
||||||
return wm.windows.first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the previous window in the list, wrapping to last if at beginning
|
/// Get the previous output in the list, wrapping to last if at beginning
|
||||||
pub fn getPrevWindow(wm: *WindowManager, current: *Window) ?*Window {
|
pub fn prevOutput(wm: *WindowManager, current: *Output) ?*Output {
|
||||||
var prev: ?*Window = null;
|
const prev_link = current.link.prev orelse return wm.outputs.last();
|
||||||
var it = wm.windows.iterator(.forward);
|
// If we've reached the sentinel (head's link), wrap to last
|
||||||
while (it.next()) |window| {
|
if (prev_link == &wm.outputs.link) return wm.outputs.last();
|
||||||
if (window == current) {
|
return @fieldParentPtr("link", prev_link);
|
||||||
return prev orelse wm.windows.last();
|
|
||||||
}
|
|
||||||
prev = window;
|
|
||||||
}
|
|
||||||
return wm.windows.last();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate primary/stack layout positions for all windows.
|
|
||||||
/// - Single window: maximized
|
|
||||||
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
|
||||||
fn calculatePrimaryStackLayout(wm: *WindowManager) void {
|
|
||||||
// TODO: Support multiple outputs
|
|
||||||
const output = wm.outputs.first() orelse return;
|
|
||||||
|
|
||||||
// Get a list of active windows
|
|
||||||
var active_list: DoublyLinkedList = .{};
|
|
||||||
var active_count: u31 = 0;
|
|
||||||
var it = wm.windows.iterator(.forward);
|
|
||||||
while (it.next()) |window| {
|
|
||||||
if (output.tags & window.tags != 0x0000) {
|
|
||||||
active_list.append(&window.active_list_node);
|
|
||||||
active_count += 1;
|
|
||||||
window.pending_render.show = true;
|
|
||||||
} else {
|
|
||||||
window.pending_render.show = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active_count == 0) return;
|
|
||||||
|
|
||||||
// Output dimensions come as i32 from the protocol, convert to u31 for window dimensions
|
|
||||||
// since they can't be negative.
|
|
||||||
const output_width: u31 = @intCast(output.width);
|
|
||||||
const output_height: u31 = @intCast(output.height);
|
|
||||||
const output_x = output.x;
|
|
||||||
const output_y = output.y;
|
|
||||||
|
|
||||||
// Iterate through the active windows and apply the tags
|
|
||||||
var i: u31 = 0;
|
|
||||||
while (active_list.popFirst()) |node| : (i += 1) {
|
|
||||||
const window: *Window = @fieldParentPtr("active_list_node", node);
|
|
||||||
if (active_count == 1) {
|
|
||||||
// Single window: maximize
|
|
||||||
window.pending_render.x = output_x;
|
|
||||||
window.pending_render.y = output_y;
|
|
||||||
window.pending_manage.width = output_width;
|
|
||||||
window.pending_manage.height = output_height;
|
|
||||||
window.pending_manage.maximized = true;
|
|
||||||
} else {
|
|
||||||
// Multiple windows: primary/stack layout
|
|
||||||
// TODO: Support multiple windows in primary stack
|
|
||||||
const primary_width: u31 = @intFromFloat(@as(f32, @floatFromInt(output_width)) * wm.primary_ratio);
|
|
||||||
const stack_width: u31 = output_width - primary_width;
|
|
||||||
const stack_count = active_count - 1;
|
|
||||||
const stack_height: u31 = @divFloor(output_height, stack_count);
|
|
||||||
window.pending_manage.maximized = false;
|
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
// Primary window (first window) - right side
|
|
||||||
window.pending_render.x = output_x + @as(i32, stack_width);
|
|
||||||
window.pending_render.y = output_y;
|
|
||||||
window.pending_manage.width = primary_width;
|
|
||||||
window.pending_manage.height = output_height;
|
|
||||||
} else {
|
|
||||||
// Stack window(s) - left side
|
|
||||||
const stack_index = i - 1;
|
|
||||||
window.pending_render.x = output_x;
|
|
||||||
window.pending_render.y = output_y + @as(i32, stack_index) * @as(i32, stack_height);
|
|
||||||
window.pending_manage.width = stack_width;
|
|
||||||
// Last stack window gets remaining height to avoid gaps from integer division
|
|
||||||
if (i == active_count - 1) {
|
|
||||||
window.pending_manage.height = output_height - stack_index * stack_height;
|
|
||||||
} else {
|
|
||||||
window.pending_manage.height = stack_height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Make space for borders; this is the same for all windows.
|
|
||||||
// Borders are automatically disabled when a window is fullscreened so we don't
|
|
||||||
// have to worry about that.
|
|
||||||
const border_width = wm.context.config.border_width;
|
|
||||||
// We use .? because we know we set the windows height, width, x, and y above
|
|
||||||
window.pending_manage.height.? -= 2 * border_width;
|
|
||||||
window.pending_manage.width.? -= 2 * border_width;
|
|
||||||
window.pending_render.x.? += border_width;
|
|
||||||
window.pending_render.y.? += border_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we went through the whole list
|
|
||||||
assert(active_list.first == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_start(wm: *WindowManager) void {
|
fn manage_start(wm: *WindowManager) void {
|
||||||
defer wm.pending_manage = .{};
|
|
||||||
|
|
||||||
const river_window_manager_v1 = wm.river_window_manager_v1;
|
const river_window_manager_v1 = wm.river_window_manager_v1;
|
||||||
const context = wm.context;
|
const context = wm.context;
|
||||||
|
|
||||||
|
|
@ -223,27 +112,12 @@ fn manage_start(wm: *WindowManager) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manage the WM itself before outputs/windows/seats
|
|
||||||
if (wm.pending_manage.primary_ratio) |primary_ratio| {
|
|
||||||
// Ratios outside of this range can cause crashes anyways (when doing the layout calulcation)
|
|
||||||
std.debug.assert(primary_ratio >= 0.10 and primary_ratio <= 0.90);
|
|
||||||
wm.primary_ratio = primary_ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var it = wm.outputs.iterator(.forward);
|
var it = wm.outputs.iterator(.forward);
|
||||||
while (it.next()) |output| {
|
while (it.next()) |output| {
|
||||||
output.manage();
|
output.manage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
|
||||||
// Calculate layout before managing windows
|
|
||||||
wm.calculatePrimaryStackLayout();
|
|
||||||
var it = wm.windows.iterator(.forward);
|
|
||||||
while (it.next()) |window| {
|
|
||||||
window.manage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
var it = wm.seats.iterator(.forward);
|
var it = wm.seats.iterator(.forward);
|
||||||
while (it.next()) |seat| {
|
while (it.next()) |seat| {
|
||||||
|
|
@ -267,12 +141,6 @@ fn render_start(wm: *WindowManager) void {
|
||||||
output.render();
|
output.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
|
||||||
var it = wm.windows.iterator(.forward);
|
|
||||||
while (it.next()) |window| {
|
|
||||||
window.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
river_window_manager_v1.renderFinish();
|
river_window_manager_v1.renderFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,10 +154,25 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
.manage_start => wm.manage_start(),
|
.manage_start => wm.manage_start(),
|
||||||
.render_start => wm.render_start(),
|
.render_start => wm.render_start(),
|
||||||
.output => |ev| {
|
.output => |ev| {
|
||||||
log.debug("initializing new wl_output: {d}", .{ev.id.getId()});
|
|
||||||
// TODO: Support multi-output
|
|
||||||
const output = Output.create(context, ev.id) catch @panic("Out of memory");
|
const output = Output.create(context, ev.id) catch @panic("Out of memory");
|
||||||
wm.outputs.append(output);
|
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;
|
||||||
|
}
|
||||||
|
output.windows.appendList(&wm.orphan_windows);
|
||||||
},
|
},
|
||||||
.seat => |ev| {
|
.seat => |ev| {
|
||||||
// TODO: Support multi-seat (maybe ?)
|
// TODO: Support multi-seat (maybe ?)
|
||||||
|
|
@ -302,22 +185,26 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
|
|
||||||
const seat = Seat.create(context, river_seat_v1) catch @panic("Out of memory");
|
const seat = Seat.create(context, river_seat_v1) catch @panic("Out of memory");
|
||||||
wm.seats.append(seat);
|
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| {
|
.window => |ev| {
|
||||||
// TODO: Support multiple seats
|
// TODO: Support multiple seats
|
||||||
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
||||||
// TODO: Support multiple outputs
|
const focused_output = seat.focused_output;
|
||||||
const output = wm.outputs.first() orelse @panic("Failed to get output");
|
const window_list = if (focused_output) |output|
|
||||||
const window = Window.create(context, ev.id, output) catch @panic("Out of memory");
|
&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) {
|
switch (context.config.attach_mode) {
|
||||||
.top => wm.windows.prepend(window),
|
.top => window_list.prepend(window),
|
||||||
.bottom => wm.windows.append(window),
|
.bottom => window_list.append(window),
|
||||||
}
|
}
|
||||||
seat.pending_manage.pending_focus = .{ .window = window };
|
seat.pending_manage.window = .{ .window = window };
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
|
|
||||||
wm.window_count += 1;
|
|
||||||
},
|
},
|
||||||
else => |ev| {
|
else => |ev| {
|
||||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||||
|
|
@ -328,7 +215,6 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const fatal = std.process.fatal;
|
const fatal = std.process.fatal;
|
||||||
const DoublyLinkedList = std.DoublyLinkedList;
|
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
const wl = wayland.client.wl;
|
const wl = wayland.client.wl;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,14 @@ const XkbBindings = @This();
|
||||||
|
|
||||||
pub const Command = union(enum) {
|
pub const Command = union(enum) {
|
||||||
spawn: []const []const u8,
|
spawn: []const []const u8,
|
||||||
focus_next,
|
focus_next_window,
|
||||||
focus_prev,
|
focus_prev_window,
|
||||||
|
focus_next_output,
|
||||||
|
focus_prev_output,
|
||||||
|
send_to_next_output,
|
||||||
|
send_to_prev_output,
|
||||||
zoom,
|
zoom,
|
||||||
|
// Changes the ratio on the focused output only
|
||||||
change_ratio: f32,
|
change_ratio: f32,
|
||||||
reload_config,
|
reload_config,
|
||||||
toggle_fullscreen,
|
toggle_fullscreen,
|
||||||
|
|
@ -29,6 +34,8 @@ const XkbBinding = struct {
|
||||||
context: *Context,
|
context: *Context,
|
||||||
link: wl.list.Link,
|
link: wl.list.Link,
|
||||||
|
|
||||||
|
const FocusDirection = enum { next, prev };
|
||||||
|
|
||||||
fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding {
|
fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding {
|
||||||
var xkb_binding = try utils.allocator.create(XkbBinding);
|
var xkb_binding = try utils.allocator.create(XkbBinding);
|
||||||
errdefer xkb_binding.destroy();
|
errdefer xkb_binding.destroy();
|
||||||
|
|
@ -67,55 +74,36 @@ const XkbBinding = struct {
|
||||||
const context = xkb_binding.context;
|
const context = xkb_binding.context;
|
||||||
switch (xkb_binding.command) {
|
switch (xkb_binding.command) {
|
||||||
.spawn => |cmd| {
|
.spawn => |cmd| {
|
||||||
var child = std.process.Child.init(cmd, std.heap.c_allocator);
|
var child = std.process.Child.init(cmd, utils.allocator);
|
||||||
_ = child.spawn() catch |err| {
|
_ = child.spawn() catch |err| {
|
||||||
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
|
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.focus_next => {
|
.focus_next_window => focusWindow(context, .next),
|
||||||
const seat = context.wm.seats.first() orelse return;
|
.focus_prev_window => focusWindow(context, .prev),
|
||||||
const pending_focus = if (seat.focused) |current|
|
.focus_next_output => focusOutput(context, .next),
|
||||||
context.wm.getNextWindow(current)
|
.focus_prev_output => focusOutput(context, .prev),
|
||||||
else
|
.send_to_next_output => sendWindowToOutput(context, .next),
|
||||||
// No window focused, focus the first one
|
.send_to_prev_output => sendWindowToOutput(context, .prev),
|
||||||
context.wm.windows.first();
|
|
||||||
|
|
||||||
if (pending_focus) |window| {
|
|
||||||
seat.pending_manage.pending_focus = .{ .window = window };
|
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
|
||||||
} else {
|
|
||||||
seat.pending_manage.pending_focus = .clear_focus;
|
|
||||||
}
|
|
||||||
// context.wm.window_manager_v1.manageDirty();
|
|
||||||
},
|
|
||||||
.focus_prev => {
|
|
||||||
const seat = context.wm.seats.first() orelse return;
|
|
||||||
const pending_focus = if (seat.focused) |current|
|
|
||||||
context.wm.getPrevWindow(current)
|
|
||||||
else
|
|
||||||
// No window focused, focus the last one
|
|
||||||
context.wm.windows.last();
|
|
||||||
|
|
||||||
if (pending_focus) |window| {
|
|
||||||
seat.pending_manage.pending_focus = .{ .window = window };
|
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
|
||||||
} else {
|
|
||||||
seat.pending_manage.pending_focus = .clear_focus;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.zoom => {
|
.zoom => {
|
||||||
const wm = context.wm;
|
const wm = context.wm;
|
||||||
const seat = wm.seats.first() orelse return;
|
const seat = wm.seats.first() orelse return;
|
||||||
const current_focus = if (seat.pending_manage.pending_focus) |pending_focus| blk: {
|
const current_focus = if (seat.pending_manage.window) |pending_focus| blk: {
|
||||||
switch (pending_focus) {
|
switch (pending_focus) {
|
||||||
.clear_focus => return,
|
.clear_focus => return,
|
||||||
.window => |window| break :blk window,
|
.window => |window| break :blk window,
|
||||||
}
|
}
|
||||||
} else seat.focused orelse return;
|
} else seat.focused_window orelse return;
|
||||||
const first_window: *Window = if (wm.windows.first()) |first| blk: {
|
const output = current_focus.output orelse return;
|
||||||
|
const first_window: *Window = if (output.windows.first()) |first| blk: {
|
||||||
if (current_focus == first) {
|
if (current_focus == first) {
|
||||||
// Try get the second window instead
|
// Try get the second window instead
|
||||||
break :blk @fieldParentPtr("link", first.link.next orelse return);
|
const next = first.link.next orelse return;
|
||||||
|
// next is the sentinel; there's only one window
|
||||||
|
if (next == &output.windows.link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break :blk @fieldParentPtr("link", next);
|
||||||
} else {
|
} else {
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
break :blk first;
|
break :blk first;
|
||||||
|
|
@ -127,7 +115,9 @@ const XkbBinding = struct {
|
||||||
current_focus.link.swapWith(&first_window.link);
|
current_focus.link.swapWith(&first_window.link);
|
||||||
},
|
},
|
||||||
.change_ratio => |diff| {
|
.change_ratio => |diff| {
|
||||||
context.wm.pending_manage.primary_ratio = std.math.clamp(context.wm.primary_ratio + diff, 0.10, 0.90);
|
const seat = context.wm.seats.first() orelse return;
|
||||||
|
const output = seat.focused_output orelse return;
|
||||||
|
output.pending_manage.primary_ratio = output.primary_ratio + diff;
|
||||||
},
|
},
|
||||||
.reload_config => {
|
.reload_config => {
|
||||||
// Try create new config
|
// Try create new config
|
||||||
|
|
@ -137,25 +127,30 @@ const XkbBinding = struct {
|
||||||
log.err("Failed to reload Config. Not deleting old one", .{});
|
log.err("Failed to reload Config. Not deleting old one", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if (context.pending_manage.config) |old_pending| {
|
||||||
|
// Need to prevent memory leaks in case multiple reloads are sent before a manage
|
||||||
|
old_pending.destroy();
|
||||||
|
}
|
||||||
// Send the config to the WM to handle during next manage
|
// Send the config to the WM to handle during next manage
|
||||||
context.pending_manage.config = new_config;
|
context.pending_manage.config = new_config;
|
||||||
context.wm.river_window_manager_v1.manageDirty();
|
context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
.toggle_fullscreen => {
|
.toggle_fullscreen => {
|
||||||
const seat = context.wm.seats.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
const window = seat.focused orelse return;
|
const window = seat.focused_window orelse return;
|
||||||
window.pending_manage.fullscreen = !window.fullscreen;
|
window.pending_manage.fullscreen = !window.fullscreen;
|
||||||
// context.wm.window_manager_v1.manageDirty();
|
context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
.close_window => {
|
.close_window => {
|
||||||
const seat = context.wm.seats.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
if (seat.focused) |window| {
|
if (seat.focused_window) |window| {
|
||||||
window.river_window_v1.close();
|
window.river_window_v1.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.set_output_tags => |tags| {
|
.set_output_tags => |tags| {
|
||||||
// TODO: Support multiple outputs
|
// TODO: Support multiple seats
|
||||||
const output = context.wm.outputs.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
|
const output = seat.focused_output orelse return;
|
||||||
output.pending_manage.tags = tags;
|
output.pending_manage.tags = tags;
|
||||||
context.wm.river_window_manager_v1.manageDirty();
|
context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
|
|
@ -163,12 +158,14 @@ const XkbBinding = struct {
|
||||||
const seat = context.wm.seats.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
// TODO: I don't think pending_focus should ever be set at this point?
|
// TODO: I don't think pending_focus should ever be set at this point?
|
||||||
// const window = seat.pending_manage.pending_focus orelse seat.focused;
|
// const window = seat.pending_manage.pending_focus orelse seat.focused;
|
||||||
const window = seat.focused orelse return;
|
const window = seat.focused_window orelse return;
|
||||||
window.pending_manage.tags = tags;
|
window.pending_manage.tags = tags;
|
||||||
context.wm.river_window_manager_v1.manageDirty();
|
context.wm.river_window_manager_v1.manageDirty();
|
||||||
},
|
},
|
||||||
.toggle_output_tags => |tags| {
|
.toggle_output_tags => |tags| {
|
||||||
const output = context.wm.outputs.first() orelse return;
|
// TODO: Support multiple seats
|
||||||
|
const seat = context.wm.seats.first() orelse return;
|
||||||
|
const output = seat.focused_output orelse return;
|
||||||
const old_tags = output.pending_manage.tags orelse output.tags;
|
const old_tags = output.pending_manage.tags orelse output.tags;
|
||||||
const new_tags = old_tags ^ tags;
|
const new_tags = old_tags ^ tags;
|
||||||
if (new_tags != 0) {
|
if (new_tags != 0) {
|
||||||
|
|
@ -180,7 +177,7 @@ const XkbBinding = struct {
|
||||||
const seat = context.wm.seats.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
// TODO: I don't think pending_focus should ever be set at this point?
|
// TODO: I don't think pending_focus should ever be set at this point?
|
||||||
// const window = seat.pending_manage.pending_focus orelse seat.focused;
|
// const window = seat.pending_manage.pending_focus orelse seat.focused;
|
||||||
const window = seat.focused orelse return;
|
const window = seat.focused_window orelse return;
|
||||||
const old_tags = window.pending_manage.tags orelse window.tags;
|
const old_tags = window.pending_manage.tags orelse window.tags;
|
||||||
const new_tags = old_tags ^ tags;
|
const new_tags = old_tags ^ tags;
|
||||||
if (new_tags != 0) {
|
if (new_tags != 0) {
|
||||||
|
|
@ -188,9 +185,96 @@ const XkbBinding = struct {
|
||||||
context.wm.river_window_manager_v1.manageDirty();
|
context.wm.river_window_manager_v1.manageDirty();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// .spawn_tagmask, .focus_previous_tags, .send_to_previous_tags => {
|
}
|
||||||
// @panic("Unimplemented");
|
}
|
||||||
// },
|
|
||||||
|
fn focusWindow(context: *Context, direction: FocusDirection) void {
|
||||||
|
const seat = context.wm.seats.first() orelse return;
|
||||||
|
const output = seat.focused_output orelse return;
|
||||||
|
const pending_focus = if (seat.focused_window) |current| blk: {
|
||||||
|
assert(current.output == output);
|
||||||
|
break :blk switch (direction) {
|
||||||
|
.next => output.nextWindow(current),
|
||||||
|
.prev => output.prevWindow(current),
|
||||||
|
};
|
||||||
|
} else switch (direction) {
|
||||||
|
.next => output.windows.first(),
|
||||||
|
.prev => output.windows.last(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pending_focus) |window| {
|
||||||
|
seat.pending_manage.window = .{ .window = window };
|
||||||
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
|
} else {
|
||||||
|
seat.pending_manage.window = .clear_focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focusOutput(context: *Context, direction: FocusDirection) void {
|
||||||
|
const wm = context.wm;
|
||||||
|
const seat = wm.seats.first() orelse return;
|
||||||
|
if (seat.focused_window) |window| {
|
||||||
|
assert(window.output == seat.focused_output);
|
||||||
|
}
|
||||||
|
const pending_focus = if (seat.focused_output) |current|
|
||||||
|
switch (direction) {
|
||||||
|
.next => wm.nextOutput(current),
|
||||||
|
.prev => wm.prevOutput(current),
|
||||||
|
}
|
||||||
|
else switch (direction) {
|
||||||
|
.next => wm.outputs.first(),
|
||||||
|
.prev => wm.outputs.last(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pending_focus) |output| {
|
||||||
|
seat.pending_manage.output = .{ .output = output };
|
||||||
|
|
||||||
|
// We got the new output, but we need to switch window focus, too
|
||||||
|
// First tell the old one
|
||||||
|
if (seat.focused_window) |current_focus| {
|
||||||
|
current_focus.pending_render.focused = false;
|
||||||
|
}
|
||||||
|
// Then set the new one
|
||||||
|
if (output.windows.first()) |window| {
|
||||||
|
seat.pending_manage.window = .{ .window = window };
|
||||||
|
// Pointer won't warp if window is empty
|
||||||
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
|
} else {
|
||||||
|
// Clear old focus
|
||||||
|
seat.pending_manage.window = .clear_focus;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seat.pending_manage.output = .clear_focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - CONFIG: Allow configuring whether focus follows the window
|
||||||
|
// TODO - CONFIG: Allow configuring whether window is prepended or appended
|
||||||
|
// TODO - CONFIG: Allow taking new output's tags
|
||||||
|
fn sendWindowToOutput(context: *Context, direction: FocusDirection) void {
|
||||||
|
const wm = context.wm;
|
||||||
|
const seat = wm.seats.first() orelse return;
|
||||||
|
const window = seat.focused_window orelse return;
|
||||||
|
assert(window.output == seat.focused_output);
|
||||||
|
|
||||||
|
const pending_output = if (seat.focused_output) |current|
|
||||||
|
switch (direction) {
|
||||||
|
.next => wm.nextOutput(current),
|
||||||
|
.prev => wm.prevOutput(current),
|
||||||
|
}
|
||||||
|
else switch (direction) {
|
||||||
|
.next => wm.outputs.first(),
|
||||||
|
.prev => wm.outputs.last(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pending_output) |output| {
|
||||||
|
// We have to remove window from current output's windows list first
|
||||||
|
window.link.remove();
|
||||||
|
output.windows.append(window);
|
||||||
|
|
||||||
|
seat.pending_manage.output = .{ .output = output };
|
||||||
|
seat.pending_manage.should_warp_pointer = true;
|
||||||
|
window.pending_manage.pending_output = .{ .output = output };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue