Merge pull request 'Add multi-output support' (#7) from multi-output-support into main
Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/7
This commit is contained in:
commit
26949d7b8a
8 changed files with 478 additions and 269 deletions
22
README.md
22
README.md
|
|
@ -7,12 +7,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
# beansprout wm
|
||||
|
||||
## 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