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
|
||||
|
||||
## TODOs
|
||||
[ ] Support multiple outputs
|
||||
[ ] Support multiple seats
|
||||
[ ] Support floating windows
|
||||
[ ] Support wallpapers
|
||||
[ ] Support a bar
|
||||
[ ] Support starting programs at WM launch
|
||||
[ ] Support changeable primary ratio
|
||||
[ ] Support changeable primary count
|
||||
[ ] Support overriding config location
|
||||
|
||||
These are in rough order of my priority, though no promises I do them in this order.
|
||||
|
||||
- [ ] Support floating windows
|
||||
- [ ] Support wallpapers
|
||||
- [ ] Support a bar
|
||||
- [ ] Support changeable primary count
|
||||
- [ ] Support starting programs at WM launch
|
||||
- [ ] 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 {
|
||||
spawn Mod4 T foot
|
||||
focus_next Mod4 J
|
||||
focus_prev Mod4 K
|
||||
focus_next_window Mod4 J
|
||||
focus_prev_window Mod4 K
|
||||
focus_next_output Mod4+Shift J
|
||||
focus_prev_output Mod4+Shift K
|
||||
send_to_next_output Mod1+Shift J
|
||||
send_to_prev_output Mod1+Shift K
|
||||
zoom Mod4 Z
|
||||
change_ratio Mod4 H +0.05
|
||||
change_ratio Mod4 L -0.05
|
||||
reload_config Mod4+Shift R
|
||||
toggle_fullscreen Mod4 F
|
||||
close_window Mod4+Shift Q
|
||||
// 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 };
|
||||
},
|
||||
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
|
||||
break :sw @unionInit(
|
||||
XkbBindings.Command,
|
||||
@tagName(cmd),
|
||||
{},
|
||||
);
|
||||
break :sw @unionInit(XkbBindings.Command, @tagName(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 {
|
||||
|
|
@ -341,11 +347,7 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
|
|||
logWarnInvalidNodeArg(name, tags_str);
|
||||
continue;
|
||||
};
|
||||
break :sw @unionInit(
|
||||
XkbBindings.Command,
|
||||
@tagName(cmd),
|
||||
tags,
|
||||
);
|
||||
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), tags);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
210
src/Output.zig
210
src/Output.zig
|
|
@ -13,15 +13,27 @@ height: i32 = 0,
|
|||
x: 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: u32 = 0x0001,
|
||||
|
||||
/// State consumed in manage() phase, reset at end of manage().
|
||||
pending_manage: PendingManage = .{},
|
||||
|
||||
windows: wl.list.Head(Window, .link),
|
||||
|
||||
link: wl.list.Link,
|
||||
|
||||
pub const PendingManage = struct {
|
||||
width: ?i32 = null,
|
||||
height: ?i32 = null,
|
||||
x: ?i32 = null,
|
||||
y: ?i32 = null,
|
||||
|
||||
tags: ?u32 = null,
|
||||
primary_ratio: ?f32 = null,
|
||||
};
|
||||
|
||||
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.* = .{
|
||||
.context = context,
|
||||
.river_output_v1 = river_output_v1,
|
||||
.windows = undefined, // we will initialize this shortly
|
||||
.link = undefined, // Handled by the wl.list
|
||||
};
|
||||
|
||||
output.windows.init();
|
||||
|
||||
output.river_output_v1.setListener(*Output, outputListener, output);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
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();
|
||||
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 {
|
||||
assert(output.river_output_v1 == river_output_v1);
|
||||
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| {
|
||||
log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name});
|
||||
},
|
||||
.dimensions => |ev| {
|
||||
output.width = ev.width;
|
||||
output.height = ev.height;
|
||||
output.pending_manage.width = ev.width;
|
||||
output.pending_manage.height = ev.height;
|
||||
output.context.wm.river_window_manager_v1.manageDirty();
|
||||
},
|
||||
.position => |ev| {
|
||||
output.x = ev.x;
|
||||
output.y = ev.y;
|
||||
output.pending_manage.x = ev.x;
|
||||
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 {
|
||||
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| {
|
||||
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 {
|
||||
_ = 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 assert = std.debug.assert;
|
||||
const DoublyLinkedList = std.DoublyLinkedList;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
|
|
@ -83,5 +280,6 @@ const river = wayland.client.river;
|
|||
|
||||
const utils = @import("utils.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
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,
|
||||
|
||||
focused: ?*Window,
|
||||
focused_window: ?*Window,
|
||||
focused_output: ?*Output,
|
||||
|
||||
/// State consumed in manage phase, reset at end of manage().
|
||||
pending_manage: PendingManage = .{},
|
||||
|
|
@ -16,13 +17,19 @@ pending_manage: PendingManage = .{},
|
|||
link: wl.list.Link,
|
||||
|
||||
pub const PendingManage = struct {
|
||||
pending_focus: ?PendingFocus = null,
|
||||
window: ?PendingWindow = null,
|
||||
output: ?PendingOutput = null,
|
||||
should_warp_pointer: bool = false,
|
||||
|
||||
pub const PendingFocus = union(enum) {
|
||||
pub const PendingWindow = union(enum) {
|
||||
window: *Window,
|
||||
clear_focus,
|
||||
};
|
||||
|
||||
pub const PendingOutput = union(enum) {
|
||||
output: *Output,
|
||||
clear_focus,
|
||||
};
|
||||
};
|
||||
|
||||
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.* = .{
|
||||
.context = context,
|
||||
.river_seat_v1 = river_seat_v1,
|
||||
.focused = null,
|
||||
.focused_window = null,
|
||||
.focused_output = null,
|
||||
.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
|
||||
fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void {
|
||||
const wv1 = river_window_v1 orelse return;
|
||||
const window: *Window = @ptrCast(@alignCast(wv1.getUserData()));
|
||||
seat.pending_manage.pending_focus = .{ .window = window };
|
||||
const window: *Window = @ptrCast(@alignCast(wv1.getUserData() orelse return));
|
||||
seat.pending_manage.window = .{ .window = window };
|
||||
}
|
||||
|
||||
pub fn manage(seat: *Seat) void {
|
||||
defer seat.pending_manage = .{};
|
||||
|
||||
if (seat.pending_manage.pending_focus) |pending_focus| {
|
||||
switch (pending_focus) {
|
||||
if (seat.pending_manage.window) |pending_window| {
|
||||
switch (pending_window) {
|
||||
.window => |window| {
|
||||
if (seat.focused) |focused| {
|
||||
if (seat.focused_window) |focused| {
|
||||
// Tell the previously focused Window that it's no longer focused
|
||||
if (focused != window) {
|
||||
focused.pending_render.focused = false;
|
||||
}
|
||||
}
|
||||
seat.focused = window;
|
||||
seat.focused_window = window;
|
||||
seat.river_seat_v1.focusWindow(window.river_window_v1);
|
||||
window.pending_render.focused = true;
|
||||
},
|
||||
.clear_focus => {
|
||||
if (seat.focused) |focused| {
|
||||
if (seat.focused_window) |focused| {
|
||||
// Tell the previously focused Window that it's no longer focused
|
||||
focused.pending_render.focused = false;
|
||||
}
|
||||
seat.focused = null;
|
||||
seat.focused_window = null;
|
||||
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.context.config.pointer_warp_on_focus_change) {
|
||||
const window = seat.focused orelse {
|
||||
log.err("Trying to warp-on-focus-change without a focused window.", .{});
|
||||
const window = seat.focused_window orelse {
|
||||
log.warn("Trying to warp-on-focus-change without a focused window.", .{});
|
||||
break :blk;
|
||||
};
|
||||
// Warp pointer to center of focused window;
|
||||
|
|
@ -126,6 +144,7 @@ const river = wayland.client.river;
|
|||
|
||||
const utils = @import("utils.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.Seat);
|
||||
|
|
|
|||
|
|
@ -18,11 +18,7 @@ fullscreen: bool = false,
|
|||
maximized: bool = false,
|
||||
|
||||
tags: u32 = 0x0001,
|
||||
// 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,
|
||||
output: ?*Output,
|
||||
|
||||
initialized: bool = false,
|
||||
|
||||
|
|
@ -46,7 +42,12 @@ pub const PendingManage = struct {
|
|||
maximized: ?bool = null,
|
||||
|
||||
tags: ?u32 = null,
|
||||
// output: ?*Output = null,
|
||||
pending_output: ?PendingOutput = null,
|
||||
|
||||
pub const PendingOutput = union(enum) {
|
||||
output: *Output,
|
||||
clear_output,
|
||||
};
|
||||
};
|
||||
|
||||
pub const PendingRender = struct {
|
||||
|
|
@ -58,7 +59,7 @@ pub const PendingRender = struct {
|
|||
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);
|
||||
errdefer window.destroy();
|
||||
|
||||
|
|
@ -66,8 +67,8 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: *Outp
|
|||
.context = context,
|
||||
.river_window_v1 = river_window_v1,
|
||||
.river_node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"),
|
||||
// .output = output,
|
||||
.tags = if (output.tags != 0) output.tags else 0x0001,
|
||||
.output = output,
|
||||
.tags = if (output) |o| o.tags else 0x0001,
|
||||
.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);
|
||||
switch (event) {
|
||||
.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);
|
||||
while (it.next()) |seat| {
|
||||
if (seat.focused == window) {
|
||||
if (seat.focused_window == window) {
|
||||
// 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) {
|
||||
seat.pending_manage.pending_focus = .{ .window = next_focus };
|
||||
seat.pending_manage.window = .{ .window = next_focus };
|
||||
seat.pending_manage.should_warp_pointer = true;
|
||||
} else {
|
||||
// Only window in list - clear focus
|
||||
seat.pending_manage.pending_focus = .clear_focus;
|
||||
seat.pending_manage.window = .clear_focus;
|
||||
}
|
||||
} 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();
|
||||
},
|
||||
.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);
|
||||
window.width = @intCast(ev.width);
|
||||
window.height = @intCast(ev.height);
|
||||
|
|
@ -151,10 +153,11 @@ pub fn manage(window: *Window) void {
|
|||
}
|
||||
}
|
||||
// Fullscreen and maximize operations
|
||||
if (pending_manage.fullscreen) |fullscreen| {
|
||||
if (pending_manage.fullscreen) |fullscreen| blk: {
|
||||
window.fullscreen = 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.informFullscreen();
|
||||
} else {
|
||||
|
|
@ -174,6 +177,14 @@ pub fn manage(window: *Window) void {
|
|||
if (pending_manage.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 {
|
||||
|
|
|
|||
|
|
@ -12,18 +12,9 @@ river_window_manager_v1: *river.WindowManagerV1,
|
|||
|
||||
seats: wl.list.Head(Seat, .link),
|
||||
outputs: wl.list.Head(Output, .link),
|
||||
windows: wl.list.Head(Window, .link),
|
||||
|
||||
window_count: u8 = 0,
|
||||
|
||||
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,
|
||||
};
|
||||
/// Place to store windows if all Outputs have been disconnected
|
||||
orphan_windows: wl.list.Head(Window, .link),
|
||||
|
||||
pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager {
|
||||
const wm = try utils.allocator.create(WindowManager);
|
||||
|
|
@ -34,12 +25,12 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi
|
|||
.river_window_manager_v1 = window_manager_v1,
|
||||
.seats = undefined, // we will initialize these shortly
|
||||
.outputs = undefined,
|
||||
.windows = undefined,
|
||||
.orphan_windows = undefined,
|
||||
};
|
||||
|
||||
wm.seats.init();
|
||||
wm.outputs.init();
|
||||
wm.windows.init();
|
||||
wm.orphan_windows.init();
|
||||
|
||||
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 {
|
||||
{
|
||||
var it = wm.windows.safeIterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
window.link.remove();
|
||||
window.destroy();
|
||||
}
|
||||
}
|
||||
{
|
||||
var it = wm.outputs.safeIterator(.forward);
|
||||
while (it.next()) |output| {
|
||||
|
|
@ -72,118 +56,23 @@ pub fn destroy(wm: *WindowManager) void {
|
|||
utils.allocator.destroy(wm);
|
||||
}
|
||||
|
||||
/// Get the next window in the list, wrapping to first if at end
|
||||
pub fn getNextWindow(wm: *WindowManager, current: *Window) ?*Window {
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
if (window == current) {
|
||||
return it.next() orelse wm.windows.first();
|
||||
}
|
||||
}
|
||||
return wm.windows.first();
|
||||
/// Get the next output in the list, wrapping to first if at end
|
||||
pub fn nextOutput(wm: *WindowManager, current: *Output) ?*Output {
|
||||
const next_link = current.link.next orelse return wm.outputs.first();
|
||||
// If we've reached the sentinel (head's link), wrap to first
|
||||
if (next_link == &wm.outputs.link) return wm.outputs.first();
|
||||
return @fieldParentPtr("link", next_link);
|
||||
}
|
||||
|
||||
/// Get the previous window in the list, wrapping to last if at beginning
|
||||
pub fn getPrevWindow(wm: *WindowManager, current: *Window) ?*Window {
|
||||
var prev: ?*Window = null;
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
if (window == current) {
|
||||
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);
|
||||
/// Get the previous output in the list, wrapping to last if at beginning
|
||||
pub fn prevOutput(wm: *WindowManager, current: *Output) ?*Output {
|
||||
const prev_link = current.link.prev orelse return wm.outputs.last();
|
||||
// If we've reached the sentinel (head's link), wrap to last
|
||||
if (prev_link == &wm.outputs.link) return wm.outputs.last();
|
||||
return @fieldParentPtr("link", prev_link);
|
||||
}
|
||||
|
||||
fn manage_start(wm: *WindowManager) void {
|
||||
defer wm.pending_manage = .{};
|
||||
|
||||
const river_window_manager_v1 = wm.river_window_manager_v1;
|
||||
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);
|
||||
while (it.next()) |output| {
|
||||
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);
|
||||
while (it.next()) |seat| {
|
||||
|
|
@ -267,12 +141,6 @@ fn render_start(wm: *WindowManager) void {
|
|||
output.render();
|
||||
}
|
||||
}
|
||||
{
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
window.render();
|
||||
}
|
||||
}
|
||||
river_window_manager_v1.renderFinish();
|
||||
}
|
||||
|
||||
|
|
@ -286,10 +154,25 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
|||
.manage_start => wm.manage_start(),
|
||||
.render_start => wm.render_start(),
|
||||
.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");
|
||||
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| {
|
||||
// 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");
|
||||
wm.seats.append(seat);
|
||||
|
||||
// If there was already an output, but no seats, set the first output as focused
|
||||
seat.pending_manage.output = .{ .output = wm.outputs.first() orelse return };
|
||||
},
|
||||
.window => |ev| {
|
||||
// TODO: Support multiple seats
|
||||
const seat = wm.seats.first() orelse @panic("Failed to get seat");
|
||||
// TODO: Support multiple outputs
|
||||
const output = wm.outputs.first() orelse @panic("Failed to get output");
|
||||
const window = Window.create(context, ev.id, output) catch @panic("Out of memory");
|
||||
const focused_output = seat.focused_output;
|
||||
const window_list = if (focused_output) |output|
|
||||
&output.windows
|
||||
else
|
||||
&wm.orphan_windows;
|
||||
const window = Window.create(context, ev.id, focused_output) catch @panic("Out of memory");
|
||||
|
||||
switch (context.config.attach_mode) {
|
||||
.top => wm.windows.prepend(window),
|
||||
.bottom => wm.windows.append(window),
|
||||
.top => window_list.prepend(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;
|
||||
|
||||
wm.window_count += 1;
|
||||
},
|
||||
else => |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 assert = std.debug.assert;
|
||||
const fatal = std.process.fatal;
|
||||
const DoublyLinkedList = std.DoublyLinkedList;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ const XkbBindings = @This();
|
|||
|
||||
pub const Command = union(enum) {
|
||||
spawn: []const []const u8,
|
||||
focus_next,
|
||||
focus_prev,
|
||||
focus_next_window,
|
||||
focus_prev_window,
|
||||
focus_next_output,
|
||||
focus_prev_output,
|
||||
send_to_next_output,
|
||||
send_to_prev_output,
|
||||
zoom,
|
||||
// Changes the ratio on the focused output only
|
||||
change_ratio: f32,
|
||||
reload_config,
|
||||
toggle_fullscreen,
|
||||
|
|
@ -29,6 +34,8 @@ const XkbBinding = struct {
|
|||
context: *Context,
|
||||
link: wl.list.Link,
|
||||
|
||||
const FocusDirection = enum { next, prev };
|
||||
|
||||
fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding {
|
||||
var xkb_binding = try utils.allocator.create(XkbBinding);
|
||||
errdefer xkb_binding.destroy();
|
||||
|
|
@ -67,55 +74,36 @@ const XkbBinding = struct {
|
|||
const context = xkb_binding.context;
|
||||
switch (xkb_binding.command) {
|
||||
.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| {
|
||||
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
|
||||
};
|
||||
},
|
||||
.focus_next => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
const pending_focus = if (seat.focused) |current|
|
||||
context.wm.getNextWindow(current)
|
||||
else
|
||||
// No window focused, focus the first one
|
||||
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;
|
||||
}
|
||||
},
|
||||
.focus_next_window => focusWindow(context, .next),
|
||||
.focus_prev_window => focusWindow(context, .prev),
|
||||
.focus_next_output => focusOutput(context, .next),
|
||||
.focus_prev_output => focusOutput(context, .prev),
|
||||
.send_to_next_output => sendWindowToOutput(context, .next),
|
||||
.send_to_prev_output => sendWindowToOutput(context, .prev),
|
||||
.zoom => {
|
||||
const wm = context.wm;
|
||||
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) {
|
||||
.clear_focus => return,
|
||||
.window => |window| break :blk window,
|
||||
}
|
||||
} else seat.focused orelse return;
|
||||
const first_window: *Window = if (wm.windows.first()) |first| blk: {
|
||||
} else seat.focused_window orelse return;
|
||||
const output = current_focus.output orelse return;
|
||||
const first_window: *Window = if (output.windows.first()) |first| blk: {
|
||||
if (current_focus == first) {
|
||||
// Try get the second window instead
|
||||
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 {
|
||||
seat.pending_manage.should_warp_pointer = true;
|
||||
break :blk first;
|
||||
|
|
@ -127,7 +115,9 @@ const XkbBinding = struct {
|
|||
current_focus.link.swapWith(&first_window.link);
|
||||
},
|
||||
.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 => {
|
||||
// Try create new config
|
||||
|
|
@ -137,25 +127,30 @@ const XkbBinding = struct {
|
|||
log.err("Failed to reload Config. Not deleting old one", .{});
|
||||
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
|
||||
context.pending_manage.config = new_config;
|
||||
context.wm.river_window_manager_v1.manageDirty();
|
||||
},
|
||||
.toggle_fullscreen => {
|
||||
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;
|
||||
// context.wm.window_manager_v1.manageDirty();
|
||||
context.wm.river_window_manager_v1.manageDirty();
|
||||
},
|
||||
.close_window => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
if (seat.focused) |window| {
|
||||
if (seat.focused_window) |window| {
|
||||
window.river_window_v1.close();
|
||||
}
|
||||
},
|
||||
.set_output_tags => |tags| {
|
||||
// TODO: Support multiple outputs
|
||||
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;
|
||||
output.pending_manage.tags = tags;
|
||||
context.wm.river_window_manager_v1.manageDirty();
|
||||
},
|
||||
|
|
@ -163,12 +158,14 @@ const XkbBinding = struct {
|
|||
const seat = context.wm.seats.first() orelse return;
|
||||
// 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.focused orelse return;
|
||||
const window = seat.focused_window orelse return;
|
||||
window.pending_manage.tags = tags;
|
||||
context.wm.river_window_manager_v1.manageDirty();
|
||||
},
|
||||
.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 new_tags = old_tags ^ tags;
|
||||
if (new_tags != 0) {
|
||||
|
|
@ -180,7 +177,7 @@ const XkbBinding = struct {
|
|||
const seat = context.wm.seats.first() orelse return;
|
||||
// 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.focused orelse return;
|
||||
const window = seat.focused_window orelse return;
|
||||
const old_tags = window.pending_manage.tags orelse window.tags;
|
||||
const new_tags = old_tags ^ tags;
|
||||
if (new_tags != 0) {
|
||||
|
|
@ -188,9 +185,96 @@ const XkbBinding = struct {
|
|||
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