Merge pull request 'floating-windows' (#9) from floating-windows into main
Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/9
This commit is contained in:
commit
e5d439e27d
8 changed files with 581 additions and 48 deletions
|
|
@ -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.
|
These are in rough order of my priority, though no promises I do them in this order.
|
||||||
|
|
||||||
- [ ] Support floating windows
|
|
||||||
- [ ] Support wallpapers
|
- [ ] Support wallpapers
|
||||||
- [ ] Support a bar
|
- [ ] Support a bar
|
||||||
- [ ] Support starting programs at WM launch
|
- [ ] Support starting programs at WM launch
|
||||||
- [ ] Support overriding config location
|
- [ ] Support overriding config location
|
||||||
- [ ] Add support for multimedia/brightness keys
|
- [ ] Add support for multimedia/brightness keys
|
||||||
|
- [ ] Make "orelse return" bits into errors; handle gracefully
|
||||||
- [ ] Support multiple seats
|
- [ ] 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 ratio
|
||||||
|
- [x] Support changeable primary count
|
||||||
- [x] Support multiple outputs
|
- [x] Support multiple outputs
|
||||||
|
- [X] Support floating windows
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,68 @@
|
||||||
|
// Whether new windows should go to the top or bottom of the window stack
|
||||||
attach_mode top
|
attach_mode top
|
||||||
|
// Whether mousing over a new window should move focus
|
||||||
focus_follows_pointer #true
|
focus_follows_pointer #true
|
||||||
|
// Whether the focus should warp to the center of newly-focused windows
|
||||||
pointer_warp_on_focus_change #true
|
pointer_warp_on_focus_change #true
|
||||||
borders {
|
borders {
|
||||||
width 2
|
width 2
|
||||||
|
// 8 or 10 digit hex color
|
||||||
color_focused "0x89b4fa"
|
color_focused "0x89b4fa"
|
||||||
color_unfocused "0x1e1e2e"
|
color_unfocused "0x1e1e2e"
|
||||||
}
|
}
|
||||||
keybinds {
|
keybinds {
|
||||||
|
// Swap a window
|
||||||
spawn Mod4 T foot
|
spawn Mod4 T foot
|
||||||
|
// Move focus up or down the windows stack
|
||||||
focus_next_window Mod4 J
|
focus_next_window Mod4 J
|
||||||
focus_prev_window Mod4 K
|
focus_prev_window Mod4 K
|
||||||
focus_next_output Mod4+Shift J
|
// Move focus between windows
|
||||||
focus_prev_output Mod4+Shift K
|
focus_next_output Mod4 Period
|
||||||
send_to_next_output Mod1+Shift J
|
focus_prev_output Mod4 Comma
|
||||||
send_to_prev_output Mod1+Shift K
|
// 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
|
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_ratio Mod4 L -0.05
|
||||||
|
// Change the number of windows in the primary side
|
||||||
increment_primary_count Mod4 I
|
increment_primary_count Mod4 I
|
||||||
decrement_primary_count Mod4 D
|
decrement_primary_count Mod4 D
|
||||||
|
// Reload config file
|
||||||
reload_config Mod4+Shift R
|
reload_config Mod4+Shift R
|
||||||
|
// Toggle fullscreen on the currently-focused window
|
||||||
toggle_fullscreen Mod4 F
|
toggle_fullscreen Mod4 F
|
||||||
|
// Close the currently-focused window
|
||||||
close_window Mod4+Shift Q
|
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 set_output_tags
|
||||||
tag_bind Mod4+shift set_window_tags
|
tag_bind Mod4+Shift set_window_tags
|
||||||
tag_bind Mod4+ctrl toggle_output_tags
|
tag_bind Mod4+Ctrl toggle_output_tags
|
||||||
tag_bind Mod4+ctrl+shift toggle_window_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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
120
src/Config.zig
120
src/Config.zig
|
|
@ -21,8 +21,9 @@ focus_follows_pointer: bool = true,
|
||||||
pointer_warp_on_focus_change: bool = true,
|
pointer_warp_on_focus_change: bool = true,
|
||||||
|
|
||||||
/// Tag bind entries parsed from config (tag_bind nodes in keybinds block)
|
/// Tag bind entries parsed from config (tag_bind nodes in keybinds block)
|
||||||
tag_binds: std.ArrayListUnmanaged(Keybind) = .{},
|
tag_binds: std.ArrayList(Keybind) = .{},
|
||||||
keybinds: std.ArrayListUnmanaged(Keybind) = .{},
|
keybinds: std.ArrayList(Keybind) = .{},
|
||||||
|
pointer_binds: std.ArrayList(PointerBind) = .{},
|
||||||
|
|
||||||
pub const Keybind = struct {
|
pub const Keybind = struct {
|
||||||
modifiers: river.SeatV1.Modifiers,
|
modifiers: river.SeatV1.Modifiers,
|
||||||
|
|
@ -30,6 +31,17 @@ pub const Keybind = struct {
|
||||||
keysym: ?xkbcommon.Keysym,
|
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 {
|
pub const AttachMode = enum {
|
||||||
top,
|
top,
|
||||||
bottom,
|
bottom,
|
||||||
|
|
@ -41,6 +53,7 @@ const NodeName = enum {
|
||||||
pointer_warp_on_focus_change,
|
pointer_warp_on_focus_change,
|
||||||
borders,
|
borders,
|
||||||
keybinds,
|
keybinds,
|
||||||
|
pointer_binds,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BorderNodeName = enum {
|
const BorderNodeName = enum {
|
||||||
|
|
@ -49,6 +62,11 @@ const BorderNodeName = enum {
|
||||||
color_unfocused,
|
color_unfocused,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PointerBindNodeName = enum {
|
||||||
|
move_window,
|
||||||
|
resize_window,
|
||||||
|
};
|
||||||
|
|
||||||
// We can just directly use the tag type from Command as our node name
|
// We can just directly use the tag type from Command as our node name
|
||||||
const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?;
|
const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?;
|
||||||
|
|
||||||
|
|
@ -82,6 +100,7 @@ pub fn create() !*Config {
|
||||||
}
|
}
|
||||||
config.keybinds.clearAndFree(utils.allocator);
|
config.keybinds.clearAndFree(utils.allocator);
|
||||||
config.tag_binds.clearAndFree(utils.allocator);
|
config.tag_binds.clearAndFree(utils.allocator);
|
||||||
|
config.pointer_binds.clearAndFree(utils.allocator);
|
||||||
config.* = .{};
|
config.* = .{};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +120,7 @@ pub fn destroy(config: *Config) void {
|
||||||
}
|
}
|
||||||
config.keybinds.deinit(utils.allocator);
|
config.keybinds.deinit(utils.allocator);
|
||||||
config.tag_binds.deinit(utils.allocator);
|
config.tag_binds.deinit(utils.allocator);
|
||||||
|
config.pointer_binds.deinit(utils.allocator);
|
||||||
utils.allocator.destroy(config);
|
utils.allocator.destroy(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,6 +181,9 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
.keybinds => {
|
.keybinds => {
|
||||||
next_child_block = .keybinds;
|
next_child_block = .keybinds;
|
||||||
},
|
},
|
||||||
|
.pointer_binds => {
|
||||||
|
next_child_block = .pointer_binds;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logWarnInvalidNode(node.name);
|
logWarnInvalidNode(node.name);
|
||||||
|
|
@ -171,6 +194,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
||||||
switch (child_block) {
|
switch (child_block) {
|
||||||
.borders => try config.loadBordersChildBlock(&parser),
|
.borders => try config.loadBordersChildBlock(&parser),
|
||||||
.keybinds => try config.loadKeybindsChildBlock(&parser),
|
.keybinds => try config.loadKeybindsChildBlock(&parser),
|
||||||
|
.pointer_binds => try config.loadPointerBindsChildBlock(&parser),
|
||||||
else => {
|
else => {
|
||||||
// Nothing else should ever be marked as a next_child_block
|
// Nothing else should ever be marked as a next_child_block
|
||||||
unreachable;
|
unreachable;
|
||||||
|
|
@ -330,16 +354,36 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
|
||||||
.focus_prev_output,
|
.focus_prev_output,
|
||||||
.send_to_next_output,
|
.send_to_next_output,
|
||||||
.send_to_prev_output,
|
.send_to_prev_output,
|
||||||
|
.toggle_float,
|
||||||
.zoom,
|
.zoom,
|
||||||
.reload_config,
|
.reload_config,
|
||||||
.toggle_fullscreen,
|
.toggle_fullscreen,
|
||||||
.close_window,
|
.close_window,
|
||||||
.increment_primary_count,
|
.increment_primary_count,
|
||||||
.decrement_primary_count,
|
.decrement_primary_count,
|
||||||
|
.swap_next,
|
||||||
|
.swap_prev,
|
||||||
=> |cmd| {
|
=> |cmd| {
|
||||||
// None of these have arguments, just create the union and give it back
|
// 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 .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| {
|
inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| {
|
||||||
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
|
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
|
||||||
logWarnMissingNodeArg(name, "tags");
|
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
|
/// Skips an entire child block including any nested child blocks
|
||||||
fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void {
|
fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void {
|
||||||
log.warn("Unexpected child block. Skipping it", .{});
|
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 }),
|
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 }),
|
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 }),
|
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) ++ "\""),
|
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);
|
const node_name_type = @TypeOf(node_name);
|
||||||
switch (node_name_type) {
|
switch (node_name_type) {
|
||||||
KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
|
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) ++ "\""),
|
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,24 @@ pub fn manage(output: *Output) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(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);
|
var it = output.windows.iterator(.forward);
|
||||||
while (it.next()) |window| {
|
while (it.next()) |window| {
|
||||||
window.render();
|
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);
|
var it = output.windows.iterator(.forward);
|
||||||
while (it.next()) |window| {
|
while (it.next()) |window| {
|
||||||
if (output.tags & window.tags != 0x0000) {
|
if (output.tags & window.tags != 0x0000) {
|
||||||
active_list.append(&window.active_list_node);
|
// Floating windows should be shown but not included in this layout generation
|
||||||
active_count += 1;
|
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;
|
window.pending_render.show = true;
|
||||||
} else {
|
} else {
|
||||||
window.pending_render.show = false;
|
window.pending_render.show = false;
|
||||||
|
|
|
||||||
180
src/Seat.zig
180
src/Seat.zig
|
|
@ -11,16 +11,27 @@ river_seat_v1: *river.SeatV1,
|
||||||
focused_window: ?*Window,
|
focused_window: ?*Window,
|
||||||
focused_output: ?*Output,
|
focused_output: ?*Output,
|
||||||
|
|
||||||
|
pointer_op: PointerOp = .none,
|
||||||
|
|
||||||
/// State consumed in manage phase, reset at end of manage().
|
/// State consumed in manage phase, reset at end of manage().
|
||||||
pending_manage: PendingManage = .{},
|
pending_manage: PendingManage = .{},
|
||||||
|
|
||||||
link: wl.list.Link,
|
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 {
|
pub const PendingManage = struct {
|
||||||
window: ?PendingWindow = null,
|
window: ?PendingWindow = null,
|
||||||
output: ?PendingOutput = null,
|
output: ?PendingOutput = null,
|
||||||
should_warp_pointer: bool = false,
|
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) {
|
pub const PendingWindow = union(enum) {
|
||||||
window: *Window,
|
window: *Window,
|
||||||
clear_focus,
|
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 {
|
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
||||||
var seat = try utils.allocator.create(Seat);
|
var seat = try utils.allocator.create(Seat);
|
||||||
errdefer seat.destroy();
|
errdefer seat.destroy();
|
||||||
|
|
@ -50,6 +74,8 @@ pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(seat: *Seat) void {
|
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();
|
seat.river_seat_v1.destroy();
|
||||||
utils.allocator.destroy(seat);
|
utils.allocator.destroy(seat);
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +91,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
|
||||||
seat.setWindowFocus(ev.window);
|
seat.setWindowFocus(ev.window);
|
||||||
},
|
},
|
||||||
.window_interaction => |ev| 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| {
|
else => |ev| {
|
||||||
log.debug("unhandled event: {s}", .{@tagName(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);
|
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 {
|
pub fn render(seat: *Seat) void {
|
||||||
_ = seat;
|
_ = 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 std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
|
|
||||||
102
src/Window.zig
102
src/Window.zig
|
|
@ -9,6 +9,7 @@ context: *Context,
|
||||||
river_window_v1: *river.WindowV1,
|
river_window_v1: *river.WindowV1,
|
||||||
river_node_v1: *river.NodeV1,
|
river_node_v1: *river.NodeV1,
|
||||||
|
|
||||||
|
// TODO: Could switch this to a Rect { x, y, width, height }
|
||||||
width: u31 = 0,
|
width: u31 = 0,
|
||||||
height: u31 = 0,
|
height: u31 = 0,
|
||||||
x: i32 = 0,
|
x: i32 = 0,
|
||||||
|
|
@ -20,6 +21,12 @@ maximized: bool = false,
|
||||||
tags: u32 = 0x0001,
|
tags: u32 = 0x0001,
|
||||||
output: ?*Output,
|
output: ?*Output,
|
||||||
|
|
||||||
|
floating: bool = false,
|
||||||
|
float_width: u31 = 0,
|
||||||
|
float_height: u31 = 0,
|
||||||
|
float_x: i32 = 0,
|
||||||
|
float_y: i32 = 0,
|
||||||
|
|
||||||
initialized: bool = false,
|
initialized: bool = false,
|
||||||
|
|
||||||
/// State consumed in manage() phase, reset at end of manage().
|
/// State consumed in manage() phase, reset at end of manage().
|
||||||
|
|
@ -44,6 +51,8 @@ pub const PendingManage = struct {
|
||||||
tags: ?u32 = null,
|
tags: ?u32 = null,
|
||||||
pending_output: ?PendingOutput = null,
|
pending_output: ?PendingOutput = null,
|
||||||
|
|
||||||
|
floating: ?bool = null,
|
||||||
|
|
||||||
pub const PendingOutput = union(enum) {
|
pub const PendingOutput = union(enum) {
|
||||||
output: *Output,
|
output: *Output,
|
||||||
clear_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);
|
assert(window.river_window_v1 == river_window_v1);
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.closed => {
|
.closed => {
|
||||||
{
|
// Clear any pointer operations referencing this window
|
||||||
// If there's no output, we don't really care about focus and can skip this event
|
var seat_it = window.context.wm.seats.iterator(.forward);
|
||||||
const output = if (window.output) |output| output else return;
|
while (seat_it.next()) |seat| {
|
||||||
var it = window.context.wm.seats.iterator(.forward);
|
switch (seat.pointer_op) {
|
||||||
while (it.next()) |seat| {
|
.move => |op| if (op.window == window) {
|
||||||
if (seat.focused_window == window) {
|
seat.river_seat_v1.opEnd();
|
||||||
// Find another window to focus and warp pointer there
|
seat.pointer_op = .none;
|
||||||
if (output.prevWindow(window)) |next_focus| {
|
},
|
||||||
if (next_focus != window) {
|
.resize => |op| if (op.window == window) {
|
||||||
seat.pending_manage.window = .{ .window = next_focus };
|
seat.river_seat_v1.opEnd();
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
seat.pointer_op = .none;
|
||||||
} else {
|
},
|
||||||
// Only window in list - clear focus
|
.none => {},
|
||||||
seat.pending_manage.window = .clear_focus;
|
}
|
||||||
}
|
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 {
|
} else {
|
||||||
|
// Only window in list - clear focus
|
||||||
seat.pending_manage.window = .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 {
|
pub fn manage(window: *Window) void {
|
||||||
|
const river_window_v1 = window.river_window_v1;
|
||||||
if (!window.initialized) {
|
if (!window.initialized) {
|
||||||
// Only happens once per Window
|
// Only happens once per Window
|
||||||
@branchHint(.unlikely);
|
@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
|
// TODO: We might want to think about paying attention to the decoration_hint event
|
||||||
// If we do, this would need to move, I think?
|
// 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 });
|
river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true });
|
||||||
window.river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updating state since the last manage event
|
// Updating state since the last manage event
|
||||||
defer window.pending_manage = .{};
|
defer window.pending_manage = .{};
|
||||||
const pending_manage = 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())
|
// Layout (pre-computed by WindowManager.calculatePrimaryStackLayout())
|
||||||
if (pending_manage.width) |new_width| {
|
if (pending_manage.width) |new_width| {
|
||||||
if (pending_manage.height) |new_height| {
|
if (pending_manage.height) |new_height| {
|
||||||
|
|
@ -177,6 +241,7 @@ pub fn manage(window: *Window) void {
|
||||||
if (pending_manage.tags) |tags| {
|
if (pending_manage.tags) |tags| {
|
||||||
window.tags = tags;
|
window.tags = tags;
|
||||||
}
|
}
|
||||||
|
// New output
|
||||||
if (pending_manage.pending_output) |pending_output| {
|
if (pending_manage.pending_output) |pending_output| {
|
||||||
switch (pending_output) {
|
switch (pending_output) {
|
||||||
.output => |output| {
|
.output => |output| {
|
||||||
|
|
@ -243,5 +308,6 @@ const river = wayland.client.river;
|
||||||
const utils = @import("utils.zig");
|
const utils = @import("utils.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
const Output = @import("Output.zig");
|
const Output = @import("Output.zig");
|
||||||
|
const Seat = @import("Seat.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.Window);
|
const log = std.log.scoped(.Window);
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,28 @@ fn manage_start(wm: *WindowManager) void {
|
||||||
std.debug.assert(keybind.keysym != null);
|
std.debug.assert(keybind.keysym != null);
|
||||||
context.xkb_bindings.addBinding(river_seat_v1, keybind.keysym.?, keybind.modifiers, keybind.command);
|
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
|
// and focus the first one
|
||||||
first.pending_render.focused = true;
|
first.pending_render.focused = true;
|
||||||
}
|
}
|
||||||
|
// We clear any orphaned_windows if an output is added
|
||||||
output.windows.appendList(&wm.orphan_windows);
|
output.windows.appendList(&wm.orphan_windows);
|
||||||
},
|
},
|
||||||
.seat => |ev| {
|
.seat => |ev| {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub const Command = union(enum) {
|
||||||
send_to_next_output,
|
send_to_next_output,
|
||||||
send_to_prev_output,
|
send_to_prev_output,
|
||||||
zoom,
|
zoom,
|
||||||
|
toggle_float,
|
||||||
// Changes the ratio on the focused output only
|
// Changes the ratio on the focused output only
|
||||||
change_ratio: f32,
|
change_ratio: f32,
|
||||||
// Changes the primary count on the focus output only
|
// Changes the primary count on the focus output only
|
||||||
|
|
@ -29,6 +30,20 @@ pub const Command = union(enum) {
|
||||||
// spawn_tagmask: u32, // TODO
|
// spawn_tagmask: u32, // TODO
|
||||||
// focus_previous_tags, // TODO
|
// focus_previous_tags, // TODO
|
||||||
// send_to_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 {
|
const XkbBinding = struct {
|
||||||
|
|
@ -98,25 +113,36 @@ const XkbBinding = struct {
|
||||||
.window => |window| break :blk window,
|
.window => |window| break :blk window,
|
||||||
}
|
}
|
||||||
} else seat.focused_window orelse return;
|
} 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 output = current_focus.output orelse return;
|
||||||
const first_window: *Window = if (output.windows.first()) |first| blk: {
|
const first_tiled: *Window = blk: {
|
||||||
if (current_focus == first) {
|
var it = output.windows.iterator(.forward);
|
||||||
// Try get the second window instead
|
while (it.next()) |window| {
|
||||||
const next = first.link.next orelse return;
|
if (window != current_focus and !window.floating) {
|
||||||
// next is the sentinel; there's only one window
|
break :blk window;
|
||||||
if (next == &output.windows.link) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
break :blk @fieldParentPtr("link", next);
|
|
||||||
} else {
|
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
|
||||||
break :blk first;
|
|
||||||
}
|
}
|
||||||
} else {
|
// No (or only one) tiled windows, nothing to do
|
||||||
// If current_focus is not null, we know that first_window *must not* be null.
|
return;
|
||||||
unreachable;
|
|
||||||
};
|
};
|
||||||
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| {
|
.change_ratio => |diff| {
|
||||||
const seat = context.wm.seats.first() orelse return;
|
const seat = context.wm.seats.first() orelse return;
|
||||||
|
|
@ -202,6 +228,14 @@ const XkbBinding = struct {
|
||||||
context.wm.river_window_manager_v1.manageDirty();
|
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 };
|
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,
|
context: *Context,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue