diff --git a/README.md b/README.md index 5c3a3cb..0fb0537 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ SPDX-License-Identifier: GPL-3.0-or-later 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 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 -- [x] Support changeable primary count +- [ ] Support clipping floating windows on edge of/between outputs - [x] Support changeable primary ratio +- [x] Support changeable primary count - [x] Support multiple outputs +- [X] Support floating windows diff --git a/examples/config.kdl b/examples/config.kdl index a3acd1e..0a42e88 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -1,30 +1,68 @@ +// Whether new windows should go to the top or bottom of the window stack attach_mode top +// Whether mousing over a new window should move focus focus_follows_pointer #true +// Whether the focus should warp to the center of newly-focused windows pointer_warp_on_focus_change #true borders { width 2 + // 8 or 10 digit hex color color_focused "0x89b4fa" color_unfocused "0x1e1e2e" } keybinds { + // Swap a window spawn Mod4 T foot + // Move focus up or down the windows stack 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 + // Move focus between windows + focus_next_output Mod4 Period + focus_prev_output Mod4 Comma + // Move windows between outputs + send_to_next_output Mod4+Shift Period + send_to_prev_output Mod4+Shift Comma + // Swap the currently-focused window with the current primary zoom Mod4 Z - change_ratio Mod4 H +0.05 + // Float/unfloat the currently-focused window + toggle_float Mod4+Shift F + // Change the primary ratio of the current output + change_ratio Mod4 H 0.05 change_ratio Mod4 L -0.05 + // Change the number of windows in the primary side increment_primary_count Mod4 I decrement_primary_count Mod4 D + // Reload config file reload_config Mod4+Shift R + // Toggle fullscreen on the currently-focused window toggle_fullscreen Mod4 F + // Close the currently-focused window close_window Mod4+Shift Q - // Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9 + // Move windows up or down the stack + swap_next Mod4+Shift N + swap_prev Mod4+Shift P + // Move floating windows; noop on tiled windows + move_left Mod4+Shift H 100 + move_down Mod4+Shift J 100 + move_up Mod4+Shift K 100 + move_right Mod4+Shift L 100 + // Resize floating windows; noop on tiled windows + resize_width Mod4+Alt+Shift H -100 + resize_height Mod4+Alt+Shift J 100 + resize_height Mod4+Alt+Shift K -100 + resize_width Mod4+Alt+Shift L 100 + // Special command to generate keybinds for keys 1-9 and tags 1<<0 through 1<<9 tag_bind Mod4 set_output_tags - tag_bind Mod4+shift set_window_tags - tag_bind Mod4+ctrl toggle_output_tags - tag_bind Mod4+ctrl+shift toggle_window_tags + tag_bind Mod4+Shift set_window_tags + tag_bind Mod4+Ctrl toggle_output_tags + tag_bind Mod4+Ctrl+Shift toggle_window_tags } +pointer_binds { + // Mod4 + Left click to move floating windows; + // tiled windows will automatically float if moved + move_window Mod4 BTN_LEFT + // Mod4 + Right click to resize floating windows; + // tiled windows will automatically float if resized + resize_window Mod4 BTN_RIGHT +} + diff --git a/src/Config.zig b/src/Config.zig index 3e5795b..e7820fd 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -21,8 +21,9 @@ focus_follows_pointer: bool = true, pointer_warp_on_focus_change: bool = true, /// Tag bind entries parsed from config (tag_bind nodes in keybinds block) -tag_binds: std.ArrayListUnmanaged(Keybind) = .{}, -keybinds: std.ArrayListUnmanaged(Keybind) = .{}, +tag_binds: std.ArrayList(Keybind) = .{}, +keybinds: std.ArrayList(Keybind) = .{}, +pointer_binds: std.ArrayList(PointerBind) = .{}, pub const Keybind = struct { modifiers: river.SeatV1.Modifiers, @@ -30,6 +31,17 @@ pub const Keybind = struct { keysym: ?xkbcommon.Keysym, }; +pub const PointerBind = struct { + modifiers: river.SeatV1.Modifiers, + button: u32, // Linux button code (BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112) + action: PointerAction, +}; + +pub const PointerAction = enum { + move_window, + resize_window, +}; + pub const AttachMode = enum { top, bottom, @@ -41,6 +53,7 @@ const NodeName = enum { pointer_warp_on_focus_change, borders, keybinds, + pointer_binds, }; const BorderNodeName = enum { @@ -49,6 +62,11 @@ const BorderNodeName = enum { color_unfocused, }; +const PointerBindNodeName = enum { + move_window, + resize_window, +}; + // We can just directly use the tag type from Command as our node name const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?; @@ -82,6 +100,7 @@ pub fn create() !*Config { } config.keybinds.clearAndFree(utils.allocator); config.tag_binds.clearAndFree(utils.allocator); + config.pointer_binds.clearAndFree(utils.allocator); config.* = .{}; }; } @@ -101,6 +120,7 @@ pub fn destroy(config: *Config) void { } config.keybinds.deinit(utils.allocator); config.tag_binds.deinit(utils.allocator); + config.pointer_binds.deinit(utils.allocator); utils.allocator.destroy(config); } @@ -161,6 +181,9 @@ fn load(config: *Config, reader: *Io.Reader) !void { .keybinds => { next_child_block = .keybinds; }, + .pointer_binds => { + next_child_block = .pointer_binds; + }, } } else { logWarnInvalidNode(node.name); @@ -171,6 +194,7 @@ fn load(config: *Config, reader: *Io.Reader) !void { switch (child_block) { .borders => try config.loadBordersChildBlock(&parser), .keybinds => try config.loadKeybindsChildBlock(&parser), + .pointer_binds => try config.loadPointerBindsChildBlock(&parser), else => { // Nothing else should ever be marked as a next_child_block unreachable; @@ -330,16 +354,36 @@ 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, .close_window, .increment_primary_count, .decrement_primary_count, + .swap_next, + .swap_prev, => |cmd| { // None of these have arguments, just create the union and give it back break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {}); }, + inline .move_up, + .move_down, + .move_left, + .move_right, + .resize_width, + .resize_height, + => |cmd| { + const amount_str = utils.stripQuotes(node.arg(parser, 2) orelse { + logWarnMissingNodeArg(name, "amount"); + continue; + }); + const amount = fmt.parseInt(i32, amount_str, 0) catch { + logWarnInvalidNodeArg(name, amount_str); + continue; + }; + break :sw @unionInit(XkbBindings.Command, @tagName(cmd), amount); + }, inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| { const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse { logWarnMissingNodeArg(name, "tags"); @@ -374,6 +418,76 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { } } +fn loadPointerBindsChildBlock(config: *Config, parser: *kdl.Parser) !void { + while (try parser.next()) |event| { + switch (event) { + .node => |node| { + const node_name = std.meta.stringToEnum(PointerBindNodeName, node.name); + if (node_name) |name| { + // Parse modifiers (arg 0) + const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse { + logWarnMissingNodeArg(name, "modifier(s)"); + continue; + }); + const modifiers = try utils.parseModifiers(mod_str) orelse { + logWarnInvalidNodeArg(name, mod_str); + continue; + }; + + // Parse button (arg 1) + const button_str = utils.stripQuotes(node.arg(parser, 1) orelse { + logWarnMissingNodeArg(name, "button"); + continue; + }); + const button = parseButton(button_str) orelse { + logWarnInvalidNodeArg(name, button_str); + continue; + }; + + const action: PointerAction = switch (name) { + .move_window => .move_window, + .resize_window => .resize_window, + }; + + try config.pointer_binds.append(utils.allocator, .{ + .modifiers = modifiers, + .button = button, + .action = action, + }); + + log.debug("pointer_binds.{s}: {s} {s}", .{ @tagName(name), mod_str, button_str }); + } else { + logWarnInvalidNode(node.name); + } + }, + .child_block_begin => { + try config.skipChildBlock(parser); + }, + .child_block_end => { + return; + }, + } + } +} + +fn parseButton(s: []const u8) ?u32 { + // Support both numeric and named buttons + var lower_buf: [16]u8 = undefined; + const len = @min(s.len, 16); + const lower = std.ascii.lowerString(lower_buf[0..len], s[0..len]); + + if (mem.eql(u8, lower, "btn_left") or mem.eql(u8, lower, "button1")) { + return 0x110; // BTN_LEFT = 272 + } else if (mem.eql(u8, lower, "btn_right") or mem.eql(u8, lower, "button3")) { + return 0x111; // BTN_RIGHT = 273 + } else if (mem.eql(u8, lower, "btn_middle") or mem.eql(u8, lower, "button2")) { + return 0x112; // BTN_MIDDLE = 274 + } + + // Try parsing as hex or decimal + return fmt.parseInt(u32, s, 0) catch null; +} + /// Skips an entire child block including any nested child blocks fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void { log.warn("Unexpected child block. Skipping it", .{}); @@ -421,6 +535,7 @@ fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void { NodeName => log.warn("Invalid \"{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), BorderNodeName => log.warn("Invalid \"border.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), + PointerBindNodeName => log.warn("Invalid \"pointer_binds.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } @@ -429,6 +544,7 @@ fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void { const node_name_type = @TypeOf(node_name); switch (node_name_type) { KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), + PointerBindNodeName => log.warn("\"pointer_binds.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } 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/Seat.zig b/src/Seat.zig index 59613a7..77be600 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -11,16 +11,27 @@ river_seat_v1: *river.SeatV1, focused_window: ?*Window, focused_output: ?*Output, +pointer_op: PointerOp = .none, + /// State consumed in manage phase, reset at end of manage(). pending_manage: PendingManage = .{}, link: wl.list.Link, +// Pointer bindings for interactive move/resize +move_pointer_binding: ?*river.PointerBindingV1 = null, +resize_pointer_binding: ?*river.PointerBindingV1 = null, + pub const PendingManage = struct { window: ?PendingWindow = null, output: ?PendingOutput = null, should_warp_pointer: bool = false, + op_delta: ?struct { dx: i32, dy: i32 } = null, + op_released: bool = false, + pointer_move_request: ?*Window = null, + pointer_resize_request: ?struct { window: *Window, edges: river.WindowV1.Edges } = null, + pub const PendingWindow = union(enum) { window: *Window, clear_focus, @@ -32,6 +43,19 @@ pub const PendingManage = struct { }; }; +pub const PointerOp = union(enum) { + none, + move: struct { window: *Window, start_x: i32, start_y: i32 }, + resize: struct { + window: *Window, + start_width: u31, + start_height: u31, + start_x: i32, + start_y: i32, + edges: river.WindowV1.Edges, + }, +}; + pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat { var seat = try utils.allocator.create(Seat); errdefer seat.destroy(); @@ -50,6 +74,8 @@ pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat { } pub fn destroy(seat: *Seat) void { + if (seat.move_pointer_binding) |binding| binding.destroy(); + if (seat.resize_pointer_binding) |binding| binding.destroy(); seat.river_seat_v1.destroy(); utils.allocator.destroy(seat); } @@ -65,6 +91,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * seat.setWindowFocus(ev.window); }, .window_interaction => |ev| seat.setWindowFocus(ev.window), + .op_delta => |ev| { + seat.pending_manage.op_delta = .{ .dx = ev.dx, .dy = ev.dy }; + }, + .op_release => { + seat.pending_manage.op_released = true; + }, else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); }, @@ -129,12 +161,160 @@ pub fn manage(seat: *Seat) void { seat.river_seat_v1.pointerWarp(pointer_x, pointer_y); } } + + // Interactive move/resize operations + + // Start move operation + if (seat.pending_manage.pointer_move_request) |window| { + if (window.floating) { + seat.pointer_op = .{ + .move = .{ + .window = window, + .start_x = window.float_x, + .start_y = window.float_y, + }, + }; + seat.river_seat_v1.opStartPointer(); + } + } + + // Start resize operation + if (seat.pending_manage.pointer_resize_request) |req| { + if (req.window.floating) { + seat.pointer_op = .{ + .resize = .{ + .window = req.window, + .start_width = req.window.float_width, + .start_height = req.window.float_height, + .start_x = req.window.float_x, + .start_y = req.window.float_y, + .edges = req.edges, + }, + }; + seat.river_seat_v1.opStartPointer(); + req.window.river_window_v1.informResizeStart(); + } + } + + // Process pointer delta (mouse movement during operation) + if (seat.pending_manage.op_delta) |delta| { + switch (seat.pointer_op) { + .none => {}, + .move => |op| { + const output = op.window.output orelse return; + const min_x = output.x; + const max_x = output.x + output.width - @as(i32, op.window.float_width); + const min_y = output.y; + const max_y = output.y + output.height - @as(i32, op.window.float_height); + + op.window.float_x = std.math.clamp(op.start_x + delta.dx, min_x, @max(min_x, max_x)); + op.window.float_y = std.math.clamp(op.start_y + delta.dy, min_y, @max(min_y, max_y)); + op.window.pending_render.x = op.window.float_x; + op.window.pending_render.y = op.window.float_y; + }, + .resize => |op| { + var new_width: i32 = op.start_width; + var new_height: i32 = op.start_height; + var new_x: i32 = op.start_x; + var new_y: i32 = op.start_y; + + // Adjust based on which edges are being dragged + if (op.edges.right) new_width += delta.dx; + if (op.edges.left) { + new_width -= delta.dx; + new_x += delta.dx; + } + if (op.edges.bottom) new_height += delta.dy; + if (op.edges.top) { + new_height -= delta.dy; + new_y += delta.dy; + } + + // Clamp to minimum size + const min_size: i32 = 50; + if (new_width < min_size) { + if (op.edges.left) new_x -= min_size - new_width; + new_width = min_size; + } + if (new_height < min_size) { + if (op.edges.top) new_y -= min_size - new_height; + new_height = min_size; + } + + // Clamp position to output bounds + const output = op.window.output orelse return; + new_x = std.math.clamp(new_x, output.x, @max(output.x, output.x + output.width - new_width)); + new_y = std.math.clamp(new_y, output.y, @max(output.y, output.y + output.height - new_height)); + + op.window.float_width = @intCast(new_width); + op.window.float_height = @intCast(new_height); + op.window.float_x = new_x; + op.window.float_y = new_y; + + op.window.river_window_v1.proposeDimensions( + op.window.float_width, + op.window.float_height, + ); + op.window.pending_render.x = op.window.float_x; + op.window.pending_render.y = op.window.float_y; + }, + } + } + + // Process pointer release (end of operation) + if (seat.pending_manage.op_released) { + switch (seat.pointer_op) { + .none => {}, + .move => { + seat.river_seat_v1.opEnd(); + seat.pointer_op = .none; + }, + .resize => |op| { + op.window.river_window_v1.informResizeEnd(); + seat.river_seat_v1.opEnd(); + seat.pointer_op = .none; + }, + } + } } pub fn render(seat: *Seat) void { _ = seat; } +pub fn movePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void { + switch (event) { + .pressed => { + const window = seat.focused_window orelse return; + if (!window.floating) { + // Auto-float on drag + window.pending_manage.floating = true; + } + seat.pending_manage.pointer_move_request = window; + seat.context.wm.river_window_manager_v1.manageDirty(); + }, + .released => {}, + } +} + +pub fn resizePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void { + switch (event) { + .pressed => { + const window = seat.focused_window orelse return; + if (!window.floating) { + // Auto-float on drag + window.pending_manage.floating = true; + } + seat.pending_manage.pointer_resize_request = .{ + .window = window, + .edges = .{ .bottom = true, .right = true }, + }; + seat.context.wm.river_window_manager_v1.manageDirty(); + }, + .released => {}, + } +} + const std = @import("std"); const assert = std.debug.assert; diff --git a/src/Window.zig b/src/Window.zig index 864c036..5acf4ff 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, @@ -88,24 +97,43 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, assert(window.river_window_v1 == river_window_v1); switch (event) { .closed => { - { - // 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 == window) { - // Find another window to focus and warp pointer there - if (output.prevWindow(window)) |next_focus| { - if (next_focus != window) { - seat.pending_manage.window = .{ .window = next_focus }; - seat.pending_manage.should_warp_pointer = true; - } else { - // Only window in list - clear focus - seat.pending_manage.window = .clear_focus; - } + // Clear any pointer operations referencing this window + var seat_it = window.context.wm.seats.iterator(.forward); + while (seat_it.next()) |seat| { + switch (seat.pointer_op) { + .move => |op| if (op.window == window) { + seat.river_seat_v1.opEnd(); + seat.pointer_op = .none; + }, + .resize => |op| if (op.window == window) { + seat.river_seat_v1.opEnd(); + seat.pointer_op = .none; + }, + .none => {}, + } + if (seat.pending_manage.pointer_move_request == window) + seat.pending_manage.pointer_move_request = null; + if (seat.pending_manage.pointer_resize_request) |req| { + if (req.window == window) + seat.pending_manage.pointer_resize_request = null; + } + } + // 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 == window) { + // Find another window to focus and warp pointer there + if (output.prevWindow(window)) |next_focus| { + if (next_focus != window) { + seat.pending_manage.window = .{ .window = next_focus }; + seat.pending_manage.should_warp_pointer = true; } else { + // Only window in list - clear focus seat.pending_manage.window = .clear_focus; } + } else { + seat.pending_manage.window = .clear_focus; } } } @@ -128,6 +156,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 +164,50 @@ 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 + window.float_width = window.width; + window.float_height = window.height; + 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 +241,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| { @@ -243,5 +308,6 @@ const river = wayland.client.river; const utils = @import("utils.zig"); const Context = @import("Context.zig"); const Output = @import("Output.zig"); +const Seat = @import("Seat.zig"); const log = std.log.scoped(.Window); diff --git a/src/WindowManager.zig b/src/WindowManager.zig index fdd4723..1bbacd4 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -110,6 +110,28 @@ fn manage_start(wm: *WindowManager) void { std.debug.assert(keybind.keysym != null); context.xkb_bindings.addBinding(river_seat_v1, keybind.keysym.?, keybind.modifiers, keybind.command); } + + // Pointer bindings + for (context.config.pointer_binds.items) |pointer_bind| { + const binding = river_seat_v1.getPointerBinding(pointer_bind.button, pointer_bind.modifiers) catch { + log.err("Failed to create pointer binding", .{}); + continue; + }; + + switch (pointer_bind.action) { + .move_window => { + if (seat.move_pointer_binding) |old| old.destroy(); + binding.setListener(*Seat, Seat.movePointerBindingListener, seat); + seat.move_pointer_binding = binding; + }, + .resize_window => { + if (seat.resize_pointer_binding) |old| old.destroy(); + binding.setListener(*Seat, Seat.resizePointerBindingListener, seat); + seat.resize_pointer_binding = binding; + }, + } + binding.enable(); + } } { @@ -172,6 +194,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..48f2847 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 @@ -29,6 +30,20 @@ pub const Command = union(enum) { // spawn_tagmask: u32, // TODO // focus_previous_tags, // TODO // send_to_previous_tags, // TODO + + // Move floating window by pixels + move_up: i32, + move_down: i32, + move_left: i32, + move_right: i32, + + // Resize floating window by pixels + resize_width: i32, + resize_height: i32, + + // Swap window position in stack + swap_next, + swap_prev, }; const XkbBinding = struct { @@ -98,25 +113,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; @@ -202,6 +228,14 @@ const XkbBinding = struct { context.wm.river_window_manager_v1.manageDirty(); } }, + .move_up => |pixels| moveFloatingWindow(context, 0, -pixels), + .move_down => |pixels| moveFloatingWindow(context, 0, pixels), + .move_left => |pixels| moveFloatingWindow(context, -pixels, 0), + .move_right => |pixels| moveFloatingWindow(context, pixels, 0), + .resize_width => |delta| resizeFloatingWindow(context, delta, 0), + .resize_height => |delta| resizeFloatingWindow(context, 0, delta), + .swap_next => swapWindow(context, .next), + .swap_prev => swapWindow(context, .prev), } } @@ -294,6 +328,61 @@ const XkbBinding = struct { window.pending_manage.pending_output = .{ .output = output }; } } + + fn moveFloatingWindow(context: *Context, dx: i32, dy: i32) void { + const seat = context.wm.seats.first() orelse return; + const window = seat.focused_window orelse return; + if (!window.floating) return; + const output = window.output orelse return; + + const min_x = output.x; + const max_x = output.x + output.width - @as(i32, window.float_width); + const min_y = output.y; + const max_y = output.y + output.height - @as(i32, window.float_height); + + window.float_x = std.math.clamp(window.float_x + dx, min_x, @max(min_x, max_x)); + window.float_y = std.math.clamp(window.float_y + dy, min_y, @max(min_y, max_y)); + window.pending_render.x = window.float_x; + window.pending_render.y = window.float_y; + context.wm.river_window_manager_v1.manageDirty(); + } + + fn resizeFloatingWindow(context: *Context, dw: i32, dh: i32) void { + const seat = context.wm.seats.first() orelse return; + const window = seat.focused_window orelse return; + if (!window.floating) return; + const output = window.output orelse return; + + const new_width: i32 = @as(i32, window.float_width) + dw; + const new_height: i32 = @as(i32, window.float_height) + dh; + window.float_width = @intCast(@max(50, new_width)); + window.float_height = @intCast(@max(50, new_height)); + + // Clamp position to keep window on screen after resize + const max_x = output.x + output.width - @as(i32, window.float_width); + const max_y = output.y + output.height - @as(i32, window.float_height); + window.float_x = std.math.clamp(window.float_x, output.x, @max(output.x, max_x)); + window.float_y = std.math.clamp(window.float_y, output.y, @max(output.y, max_y)); + + window.pending_render.x = window.float_x; + window.pending_render.y = window.float_y; + window.river_window_v1.proposeDimensions(window.float_width, window.float_height); + context.wm.river_window_manager_v1.manageDirty(); + } + + fn swapWindow(context: *Context, comptime direction: enum { next, prev }) void { + const seat = context.wm.seats.first() orelse return; + const window = seat.focused_window orelse return; + const output = window.output orelse return; + const target = switch (direction) { + .next => output.nextWindow(window), + .prev => output.prevWindow(window), + } orelse return; + if (target != window) { + window.link.swapWith(&target.link); + context.wm.river_window_manager_v1.manageDirty(); + } + } }; context: *Context,