diff --git a/README.md b/README.md index f67f0b9..28a0aa0 100644 --- a/README.md +++ b/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 diff --git a/examples/config.kdl b/examples/config.kdl index d405f8f..5ce7e57 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -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 diff --git a/src/Config.zig b/src/Config.zig index 43c636d..c804136 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -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); }, }; diff --git a/src/Output.zig b/src/Output.zig index 75d35dc..0ecfbf3 100644 --- a/src/Output.zig +++ b/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); diff --git a/src/Seat.zig b/src/Seat.zig index 38a460e..59613a7 100644 --- a/src/Seat.zig +++ b/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); diff --git a/src/Window.zig b/src/Window.zig index 32c7204..864c036 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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 { diff --git a/src/WindowManager.zig b/src/WindowManager.zig index e6db884..fdd4723 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -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; diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 1429b0b..3cf340e 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -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 }; } } };