Add multi-output support
Primary ratio is per-output.
If an output is disconnected/disabled, its windows get sent to the
previous output in the output list. If all outputs are disconnected,
windows are added to an orphan list in the WM. Once an output is
re-added, the orphans are all given to that output.
When a window is sent to a new output, it keeps the same tags as it
had before. I may add an option to take the new output's tags.
- Rename focus_next/focus_prev to focus_next_window/focus_prev_window
- Add focus_next_output/focus_prev_output
- Add send_to_next_output/send_to_prev_output commands to move windows
between outputs
Split Seat.PendingManage.PendingFocus into separate pending output
and pending window structs
Fix window outputs when closing outputs
This commit is contained in:
parent
342c0fdf8f
commit
0f85278aea
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