diff --git a/README.md b/README.md index 5c3a3cb..07c43bf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Support starting programs at WM launch - [ ] Support overriding config location - [ ] Add support for multimedia/brightness keys +- [ ] Make "orelse return" bits into errors; handle gracefully - [ ] Support multiple seats +- [ ] Support clipping floating windows on edge of/between outputs - [x] Support changeable primary count - [x] Support changeable primary ratio - [x] Support multiple outputs diff --git a/examples/config.kdl b/examples/config.kdl index a3acd1e..86b9dd4 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -15,6 +15,7 @@ keybinds { send_to_next_output Mod1+Shift J send_to_prev_output Mod1+Shift K zoom Mod4 Z + toggle_float Mod4+Shift F change_ratio Mod4 H +0.05 change_ratio Mod4 L -0.05 increment_primary_count Mod4 I diff --git a/src/Config.zig b/src/Config.zig index 3e5795b..65de920 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -330,6 +330,7 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { .focus_prev_output, .send_to_next_output, .send_to_prev_output, + .toggle_float, .zoom, .reload_config, .toggle_fullscreen, diff --git a/src/Output.zig b/src/Output.zig index 8bf432a..10d1092 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -188,9 +188,24 @@ pub fn manage(output: *Output) void { } pub fn render(output: *Output) void { + const seat = output.context.wm.seats.first(); + const focused = if (seat) |s| s.focused_window else null; + var it = output.windows.iterator(.forward); while (it.next()) |window| { window.render(); + + // Make sure floating windows are above tiled windows + if (window.floating and output.tags & window.tags != 0 and window != focused) { + window.river_node_v1.placeTop(); + } + } + + // Make sure that the *focused* floating window goes above any other floating windows + if (focused) |f| { + if (f.floating and f.output == output and output.tags & f.tags != 0) { + f.river_node_v1.placeTop(); + } } } @@ -206,8 +221,12 @@ fn calculatePrimaryStackLayout(output: *Output) void { 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; + // Floating windows should be shown but not included in this layout generation + const will_float = window.pending_manage.floating orelse window.floating; + if (!will_float) { + active_count += 1; + active_list.append(&window.active_list_node); + } window.pending_render.show = true; } else { window.pending_render.show = false; diff --git a/src/Window.zig b/src/Window.zig index 864c036..53da74b 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -9,6 +9,7 @@ context: *Context, river_window_v1: *river.WindowV1, river_node_v1: *river.NodeV1, +// TODO: Could switch this to a Rect { x, y, width, height } width: u31 = 0, height: u31 = 0, x: i32 = 0, @@ -20,6 +21,12 @@ maximized: bool = false, tags: u32 = 0x0001, output: ?*Output, +floating: bool = false, +float_width: u31 = 0, +float_height: u31 = 0, +float_x: i32 = 0, +float_y: i32 = 0, + initialized: bool = false, /// State consumed in manage() phase, reset at end of manage(). @@ -44,6 +51,8 @@ pub const PendingManage = struct { tags: ?u32 = null, pending_output: ?PendingOutput = null, + floating: ?bool = null, + pub const PendingOutput = union(enum) { output: *Output, clear_output, @@ -128,6 +137,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, } pub fn manage(window: *Window) void { + const river_window_v1 = window.river_window_v1; if (!window.initialized) { // Only happens once per Window @branchHint(.unlikely); @@ -135,15 +145,48 @@ pub fn manage(window: *Window) void { // TODO: We might want to think about paying attention to the decoration_hint event // If we do, this would need to move, I think? - window.river_window_v1.useSsd(); + river_window_v1.useSsd(); - window.river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true }); - window.river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); + river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true }); + river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); } // Updating state since the last manage event defer window.pending_manage = .{}; const pending_manage = window.pending_manage; + // Floating status + if (pending_manage.floating) |floating| blk: { + // This needs to be before proposing the new dimensions since we want to save the current ones! + // Skip the rest of the block if floating matches what is already set + if (floating == window.floating) break :blk; + + window.floating = floating; + if (floating) { + // Let the window know it isn't tiled + river_window_v1.setTiled(.{}); + + if (window.float_width == 0) { + // Never floated before; use current dimensions but centered on output + if (window.output) |output| { + // Need to find center and then subtract half of the window's width/height + window.float_x = output.x + @divTrunc(output.width, 2) - @divTrunc(window.width, 2); + window.float_y = output.y + @divTrunc(output.height, 2) - @divTrunc(window.height, 2); + } + } else { + // Window has floated before; re-use its old dimensions + river_window_v1.proposeDimensions(window.float_width, window.float_height); + } + window.pending_render.x = window.float_x; + window.pending_render.y = window.float_y; + } else { + river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); + // Save floating dimensions in case the window gets floated again + window.float_width = window.width; + window.float_height = window.height; + window.float_x = window.x; + window.float_y = window.y; + } + } // Layout (pre-computed by WindowManager.calculatePrimaryStackLayout()) if (pending_manage.width) |new_width| { if (pending_manage.height) |new_height| { @@ -177,6 +220,7 @@ pub fn manage(window: *Window) void { if (pending_manage.tags) |tags| { window.tags = tags; } + // New output if (pending_manage.pending_output) |pending_output| { switch (pending_output) { .output => |output| { diff --git a/src/WindowManager.zig b/src/WindowManager.zig index fdd4723..c32287d 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -172,6 +172,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv // and focus the first one first.pending_render.focused = true; } + // We clear any orphaned_windows if an output is added output.windows.appendList(&wm.orphan_windows); }, .seat => |ev| { diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 4d85a80..afb46cd 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -13,6 +13,7 @@ pub const Command = union(enum) { send_to_next_output, send_to_prev_output, zoom, + toggle_float, // Changes the ratio on the focused output only change_ratio: f32, // Changes the primary count on the focus output only @@ -98,25 +99,36 @@ const XkbBinding = struct { .window => |window| break :blk window, } } else seat.focused_window orelse return; + + // Noop if the focused window is floating + if (current_focus.floating) return; + + // Get the first tiled window to try zoom with 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 - const next = first.link.next orelse return; - // next is the sentinel; there's only one window - if (next == &output.windows.link) { - return; + const first_tiled: *Window = blk: { + var it = output.windows.iterator(.forward); + while (it.next()) |window| { + if (window != current_focus and !window.floating) { + break :blk window; } - break :blk @fieldParentPtr("link", next); - } else { - seat.pending_manage.should_warp_pointer = true; - break :blk first; } - } else { - // If current_focus is not null, we know that first_window *must not* be null. - unreachable; + // No (or only one) tiled windows, nothing to do + return; }; - current_focus.link.swapWith(&first_window.link); + + current_focus.link.swapWith(&first_tiled.link); + // Don't warp pointer if the first was the one focused before + if (output.windows.first() == current_focus) { + seat.pending_manage.should_warp_pointer = true; + } + }, + .toggle_float => { + const seat = context.wm.seats.first() orelse return; + const window = seat.focused_window orelse return; + // Noop if the window is fullscreened + if (window.fullscreen) return; + window.pending_manage.floating = !window.floating; + context.wm.river_window_manager_v1.manageDirty(); }, .change_ratio => |diff| { const seat = context.wm.seats.first() orelse return;