river: focus-view and swap by spatial direction

This commit is contained in:
Leon Henrik Plickat 2023-07-08 06:30:27 +02:00
parent 5ce2ca1bc0
commit b35d779122
9 changed files with 205 additions and 172 deletions

View File

@ -59,7 +59,8 @@ function __riverctl_completion ()
elif [ "${COMP_CWORD}" -eq 2 ] elif [ "${COMP_CWORD}" -eq 2 ]
then then
case "${COMP_WORDS[1]}" in case "${COMP_WORDS[1]}" in
"focus-output"|"focus-view"|"send-to-output"|"swap") OPTS="next previous" ;; "focus-output"|"send-to-output") OPTS="next previous" ;;
"focus-view"|"swap") OPTS="next previous up down left right" ;;
"move"|"snap") OPTS="up down left right" ;; "move"|"snap") OPTS="up down left right" ;;
"resize") OPTS="horizontal vertical" ;; "resize") OPTS="horizontal vertical" ;;
"rule-add"|"rule-del") OPTS="float no-float ssd csd tag" ;; "rule-add"|"rule-del") OPTS="float no-float ssd csd tag" ;;

View File

@ -72,12 +72,12 @@ complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'keyboard-layout'
# Subcommands # Subcommands
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-output' -a 'next previous' complete -c riverctl -x -n '__fish_seen_subcommand_from focus-output' -a 'next previous'
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-view' -a 'next previous' complete -c riverctl -x -n '__fish_seen_subcommand_from focus-view' -a 'next previous up down left right'
complete -c riverctl -x -n '__fish_seen_subcommand_from move' -a 'up down left right' complete -c riverctl -x -n '__fish_seen_subcommand_from move' -a 'up down left right'
complete -c riverctl -x -n '__fish_seen_subcommand_from resize' -a 'horizontal vertical' complete -c riverctl -x -n '__fish_seen_subcommand_from resize' -a 'horizontal vertical'
complete -c riverctl -x -n '__fish_seen_subcommand_from snap' -a 'up down left right' complete -c riverctl -x -n '__fish_seen_subcommand_from snap' -a 'up down left right'
complete -c riverctl -x -n '__fish_seen_subcommand_from send-to-output' -a 'next previous' complete -c riverctl -x -n '__fish_seen_subcommand_from send-to-output' -a 'next previous'
complete -c riverctl -x -n '__fish_seen_subcommand_from swap' -a 'next previous' complete -c riverctl -x -n '__fish_seen_subcommand_from swap' -a 'next previous up down left right'
complete -c riverctl -x -n '__fish_seen_subcommand_from map' -a '-release -repeat -layout' complete -c riverctl -x -n '__fish_seen_subcommand_from map' -a '-release -repeat -layout'
complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a '-release' complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a '-release'
complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom' complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom'

View File

@ -170,13 +170,13 @@ _riverctl()
args) args)
case "$words[1]" in case "$words[1]" in
focus-output) _alternative 'arguments:args:(next previous)' ;; focus-output) _alternative 'arguments:args:(next previous)' ;;
focus-view) _alternative 'arguments:args:(next previous)' ;; focus-view) _alternative 'arguments:args:(next previous up down left right)' ;;
input) _riverctl_input ;; input) _riverctl_input ;;
move) _alternative 'arguments:args:(up down left right)' ;; move) _alternative 'arguments:args:(up down left right)' ;;
resize) _alternative 'arguments:args:(horizontal vertical)' ;; resize) _alternative 'arguments:args:(horizontal vertical)' ;;
snap) _alternative 'arguments:args:(up down left right)' ;; snap) _alternative 'arguments:args:(up down left right)' ;;
send-to-output) _alternative 'arguments:args:(next previous)' ;; send-to-output) _alternative 'arguments:args:(next previous)' ;;
swap) _alternative 'arguments:args:(next previous)' ;; swap) _alternative 'arguments:args:(next previous up down left right)' ;;
map) _alternative 'arguments:optional:(-release -repeat -layout)' ;; map) _alternative 'arguments:optional:(-release -repeat -layout)' ;;
unmap) _alternative 'arguments:optional:(-release)' ;; unmap) _alternative 'arguments:optional:(-release)' ;;
attach-mode) _alternative 'arguments:args:(top bottom)' ;; attach-mode) _alternative 'arguments:args:(top bottom)' ;;

View File

@ -35,8 +35,9 @@ over the Wayland protocol.
Focus the next or previous output, the closest output in any direction Focus the next or previous output, the closest output in any direction
or an output by name. or an output by name.
*focus-view* *next*|*previous* *focus-view* *next*|*previous*|*up*|*down*|*left*|*right*
Focus the next or previous view in the stack. Focus the next or previous view in the stack or the closest view in
any direction.
*move* *up*|*down*|*left*|*right* _delta_ *move* *up*|*down*|*left*|*right* _delta_
Move the focused view in the specified direction by _delta_ logical Move the focused view in the specified direction by _delta_ logical
@ -62,9 +63,9 @@ over the Wayland protocol.
*spawn* only takes a single argument. To spawn a command taking *spawn* only takes a single argument. To spawn a command taking
multiple arguments, wrapping the command in quotes is recommended. multiple arguments, wrapping the command in quotes is recommended.
*swap* *next*|*previous* *swap* *next*|*previous*|*up*|*down*|*left*|*right*
Swap the focused view with the next/previous visible non-floating Swap the focused view with the next or previous non-floating view in the
view. If the first/last view in the stack is focused, wrap. stack or the closest non-floating view in any direction.
*toggle-float* *toggle-float*
Toggle the floating state of the focused view. Toggle the floating state of the focused view.

58
river/Vector.zig Normal file
View File

@ -0,0 +1,58 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2023 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const math = std.math;
const wlr = @import("wlroots");
const Vector = @This();
x: i32,
y: i32,
pub fn positionOfBox(box: wlr.Box) Vector {
return .{
.x = box.x + @divFloor(box.width, 2),
.y = box.y + @divFloor(box.height, 2),
};
}
/// Returns the difference between two vectors.
pub fn diff(a: Vector, b: Vector) Vector {
return .{
.x = b.x - a.x,
.y = b.y - a.y,
};
}
/// Returns the direction of the vector.
pub fn direction(self: Vector) ?wlr.OutputLayout.Direction {
// A zero length vector has no direction
if (self.x == 0 and self.y == 0) return null;
if ((math.absInt(self.y) catch return null) > (math.absInt(self.x) catch return null)) {
// Careful: We are operating in a Y-inverted coordinate system.
return if (self.y > 0) .down else .up;
} else {
return if (self.x > 0) .right else .left;
}
}
/// Returns the length of the vector.
pub fn length(self: Vector) u31 {
return math.sqrt(@intCast(u31, (self.x *| self.x) +| (self.y *| self.y)));
}

View File

@ -54,7 +54,7 @@ const command_impls = std.ComptimeStringMap(
.{ "focus-follows-cursor", @import("command/focus_follows_cursor.zig").focusFollowsCursor }, .{ "focus-follows-cursor", @import("command/focus_follows_cursor.zig").focusFollowsCursor },
.{ "focus-output", @import("command/output.zig").focusOutput }, .{ "focus-output", @import("command/output.zig").focusOutput },
.{ "focus-previous-tags", @import("command/tags.zig").focusPreviousTags }, .{ "focus-previous-tags", @import("command/tags.zig").focusPreviousTags },
.{ "focus-view", @import("command/focus_view.zig").focusView }, .{ "focus-view", @import("command/view_operations.zig").focusView },
.{ "hide-cursor", @import("command/cursor.zig").cursor }, .{ "hide-cursor", @import("command/cursor.zig").cursor },
.{ "input", @import("command/input.zig").input }, .{ "input", @import("command/input.zig").input },
.{ "keyboard-group-add", @import("command/keyboard_group.zig").keyboardGroupAdd }, .{ "keyboard-group-add", @import("command/keyboard_group.zig").keyboardGroupAdd },
@ -83,7 +83,7 @@ const command_impls = std.ComptimeStringMap(
.{ "snap", @import("command/move.zig").snap }, .{ "snap", @import("command/move.zig").snap },
.{ "spawn", @import("command/spawn.zig").spawn }, .{ "spawn", @import("command/spawn.zig").spawn },
.{ "spawn-tagmask", @import("command/tags.zig").spawnTagmask }, .{ "spawn-tagmask", @import("command/tags.zig").spawnTagmask },
.{ "swap", @import("command/swap.zig").swap}, .{ "swap", @import("command/view_operations.zig").swap},
.{ "toggle-float", @import("command/toggle_float.zig").toggleFloat }, .{ "toggle-float", @import("command/toggle_float.zig").toggleFloat },
.{ "toggle-focused-tags", @import("command/tags.zig").toggleFocusedTags }, .{ "toggle-focused-tags", @import("command/tags.zig").toggleFocusedTags },
.{ "toggle-fullscreen", @import("command/toggle_fullscreen.zig").toggleFullscreen }, .{ "toggle-fullscreen", @import("command/toggle_fullscreen.zig").toggleFullscreen },

View File

@ -1,80 +0,0 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const assert = std.debug.assert;
const server = &@import("../main.zig").server;
const Direction = @import("../command.zig").Direction;
const Error = @import("../command.zig").Error;
const Output = @import("../Output.zig");
const Seat = @import("../Seat.zig");
const View = @import("../View.zig");
/// Focus either the next or the previous visible view, depending on the enum
/// passed. Does nothing if there are 1 or 0 views in the stack.
pub fn focusView(
seat: *Seat,
args: []const [:0]const u8,
_: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection;
const output = seat.focused_output orelse return;
if (seat.focused != .view) return;
if (seat.focused.view.pending.fullscreen) return;
if (focusViewTarget(seat, output, direction)) |target| {
assert(!target.pending.fullscreen);
seat.focus(target);
server.root.applyPending();
}
}
fn focusViewTarget(seat: *Seat, output: *Output, direction: Direction) ?*View {
switch (direction) {
inline else => |dir| {
const it_dir = comptime switch (dir) {
.next => .forward,
.previous => .reverse,
};
var it = output.pending.wm_stack.iterator(it_dir);
while (it.next()) |view| {
if (view == seat.focused.view) break;
} else {
unreachable;
}
// Return the next view in the stack matching the tags if any.
while (it.next()) |view| {
if (output.pending.tags & view.pending.tags != 0) return view;
}
// Wrap and return the first view in the stack matching the tags if
// any is found before completing the loop back to the focused view.
while (it.next()) |view| {
if (view == seat.focused.view) return null;
if (output.pending.tags & view.pending.tags != 0) return view;
}
unreachable;
},
}
}

View File

@ -1,80 +0,0 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const assert = std.debug.assert;
const server = &@import("../main.zig").server;
const Direction = @import("../command.zig").Direction;
const Error = @import("../command.zig").Error;
const Output = @import("../Output.zig");
const Seat = @import("../Seat.zig");
const View = @import("../View.zig");
/// Swap the currently focused view with either the view higher or lower in the visible stack
pub fn swap(
seat: *Seat,
args: []const [:0]const u8,
_: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection;
const output = seat.focused_output orelse return;
if (seat.focused != .view) return;
if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return;
if (swapTarget(seat, output, direction)) |target| {
assert(!target.pending.float);
assert(!target.pending.fullscreen);
seat.focused.view.pending_wm_stack_link.swapWith(&target.pending_wm_stack_link);
server.root.applyPending();
}
}
fn swapTarget(seat: *Seat, output: *Output, direction: Direction) ?*View {
switch (direction) {
inline else => |dir| {
const it_dir = comptime switch (dir) {
.next => .forward,
.previous => .reverse,
};
var it = output.pending.wm_stack.iterator(it_dir);
while (it.next()) |view| {
if (view == seat.focused.view) break;
} else {
unreachable;
}
// Return the next view in the stack matching the tags if any.
while (it.next()) |view| {
if (output.pending.tags & view.pending.tags != 0 and !view.pending.float) return view;
}
// Wrap and return the first view in the stack matching the tags if
// any is found before completing the loop back to the focused view.
while (it.next()) |view| {
if (view == seat.focused.view) return null;
if (output.pending.tags & view.pending.tags != 0 and !view.pending.float) return view;
}
unreachable;
},
}
}

View File

@ -0,0 +1,133 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 - 2023 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const assert = std.debug.assert;
const wlr = @import("wlroots");
const server = &@import("../main.zig").server;
const Direction = @import("../command.zig").Direction;
const Error = @import("../command.zig").Error;
const Output = @import("../Output.zig");
const Seat = @import("../Seat.zig");
const View = @import("../View.zig");
const Vector = @import("../Vector.zig");
/// Focus either the next or the previous visible view, depending on the enum
/// passed. Does nothing if there are 1 or 0 views in the stack.
pub fn focusView(
seat: *Seat,
args: []const [:0]const u8,
_: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
if (try getTarget(seat, args[1], .all)) |target| {
assert(!target.pending.fullscreen);
seat.focus(target);
server.root.applyPending();
}
}
/// Swap the currently focused view with either the view higher or lower in the visible stack
pub fn swap(
seat: *Seat,
args: []const [:0]const u8,
_: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
if (try getTarget(seat, args[1], .skip_float)) |target| {
assert(!target.pending.float);
assert(!target.pending.fullscreen);
seat.focused.view.pending_wm_stack_link.swapWith(&target.pending_wm_stack_link);
server.root.applyPending();
}
}
const TargetMode = enum { all, skip_float };
fn getTarget(seat: *Seat, direction_str: []const u8, target_mode: TargetMode) !?*View {
if (seat.focused != .view) return null;
if (seat.focused.view.pending.fullscreen) return null;
if (target_mode == .skip_float and seat.focused.view.pending.float) return null;
const output = seat.focused_output orelse return null;
// If no currently view is focused, focus the first in the stack.
if (seat.focused != .view) {
var it = output.pending.wm_stack.iterator(.forward);
return it.next();
}
// Logical direction, based on the view stack.
if (std.meta.stringToEnum(Direction, direction_str)) |direction| {
switch (direction) {
inline else => |dir| {
const it_dir = comptime switch (dir) {
.next => .forward,
.previous => .reverse,
};
var it = output.pending.wm_stack.iterator(it_dir);
while (it.next()) |view| {
if (view == seat.focused.view) break;
} else {
unreachable;
}
// Return the next view in the stack matching the tags if any.
while (it.next()) |view| {
if (target_mode == .skip_float and view.pending.float) continue;
if (output.pending.tags & view.pending.tags != 0) return view;
}
// Wrap and return the first view in the stack matching the tags if
// any is found before completing the loop back to the focused view.
while (it.next()) |view| {
if (view == seat.focused.view) return null;
if (target_mode == .skip_float and view.pending.float) continue;
if (output.pending.tags & view.pending.tags != 0) return view;
}
unreachable;
},
}
}
// Spatial direction, based on view position.
if (std.meta.stringToEnum(wlr.OutputLayout.Direction, direction_str)) |direction| {
const focus_position = Vector.positionOfBox(seat.focused.view.current.box);
var target: ?*View = null;
var target_distance: usize = std.math.maxInt(usize);
var it = output.pending.wm_stack.iterator(.forward);
while (it.next()) |view| {
if (target_mode == .skip_float and view.pending.float) continue;
if (view == seat.focused.view) continue;
const view_position = Vector.positionOfBox(view.current.box);
const position_diff = focus_position.diff(view_position);
if ((position_diff.direction() orelse continue) != direction) continue;
const distance = position_diff.length();
if (distance < target_distance) {
target = view;
target_distance = distance;
}
}
return target;
}
return Error.InvalidDirection;
}