diff --git a/contrib/config.sh b/contrib/config.sh index 3b51bbe..1e1bdc1 100755 --- a/contrib/config.sh +++ b/contrib/config.sh @@ -37,6 +37,24 @@ riverctl map normal $mod L mod-master-factor +0.05 riverctl map normal $mod+Shift H mod-master-count +1 riverctl map normal $mod+Shift L mod-master-count -1 +# Mod+Alt+{H,J,K,L} to move views +riverctl map normal $mod+Mod1 H move left 100 +riverctl map normal $mod+Mod1 J move down 100 +riverctl map normal $mod+Mod1 K move up 100 +riverctl map normal $mod+Mod1 L move right 100 + +# Mod+Alt+Control+{H,J,K,L} to snap views to screen edges +riverctl map normal $mod+Mod1+Control H snap left +riverctl map normal $mod+Mod1+Control J snap down +riverctl map normal $mod+Mod1+Control K snap up +riverctl map normal $mod+Mod1+Control L snap right + +# Mod+Alt+Shif+{H,J,K,L} to resize views +riverctl map normal $mod+Mod1+Shift H resize horizontal -100 +riverctl map normal $mod+Mod1+Shift J resize vertical 100 +riverctl map normal $mod+Mod1+Shift K resize vertical -100 +riverctl map normal $mod+Mod1+Shift L resize horizontal 100 + # Mod + Left Mouse Button to move views riverctl map-pointer normal $mod BTN_LEFT move-view diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 143d57e..14c73d7 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -53,6 +53,17 @@ used to control and configure river. negative floating point number (such as 0.05) where 1 corresponds to the whole screen. +*move* *up*|*down*|*left*|*right* _delta_ + Move the focused view in the specified direction by _delta_. The view will + be set to floating. + +*resize* *horizontal*|*vertical* _delta_ + Resize the view in the given orientation by _delta_. The view will be set to + floating. + +*snap* *up*|*down*|*left*|*right* + Snap the view to the specified screen edge. The view will be set to floating. + *send-to-output* *next*|*previous* Send the focused view to the next or the previous output. diff --git a/river/command.zig b/river/command.zig index c9935d5..92da001 100644 --- a/river/command.zig +++ b/river/command.zig @@ -24,6 +24,18 @@ pub const Direction = enum { previous, }; +pub const PhysicalDirection = enum { + up, + down, + left, + right, +}; + +pub const Orientation = enum { + horizontal, + vertical, +}; + // TODO: this could be replaced with a comptime hashmap // zig fmt: off const str_to_impl_fn = [_]struct { @@ -49,11 +61,14 @@ const str_to_impl_fn = [_]struct { .{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer }, .{ .name = "mod-master-count", .impl = @import("command/mod_master_count.zig").modMasterCount }, .{ .name = "mod-master-factor", .impl = @import("command/mod_master_factor.zig").modMasterFactor }, + .{ .name = "move", .impl = @import("command/move.zig").move }, .{ .name = "opacity", .impl = @import("command/opacity.zig").opacity }, .{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding }, + .{ .name = "resize", .impl = @import("command/move.zig").resize }, .{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput }, .{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags }, .{ .name = "set-view-tags", .impl = @import("command/tags.zig").setViewTags }, + .{ .name = "snap", .impl = @import("command/move.zig").snap }, .{ .name = "spawn", .impl = @import("command/spawn.zig").spawn }, .{ .name = "toggle-float", .impl = @import("command/toggle_float.zig").toggleFloat }, .{ .name = "toggle-focused-tags", .impl = @import("command/tags.zig").toggleFocusedTags }, @@ -73,6 +88,8 @@ pub const Error = error{ Overflow, InvalidCharacter, InvalidDirection, + InvalidPhysicalDirection, + InvalidOrientation, InvalidRgba, InvalidValue, UnknownOption, @@ -114,6 +131,8 @@ pub fn errToMsg(err: Error) [:0]const u8 { Error.Overflow => "value out of bounds", Error.InvalidCharacter => "invalid character in argument", Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", + Error.InvalidPhysicalDirection => "invalid direction. Must be 'up', 'down', 'left' or 'right'", + Error.InvalidOrientation => "invalid orientation. Must be 'horizontal', or 'vertical'", Error.InvalidRgba => "invalid color format, must be #RRGGBB or #RRGGBBAA", Error.InvalidValue => "invalid value", Error.OutOfMemory => "out of memory", diff --git a/river/command/move.zig b/river/command/move.zig new file mode 100644 index 0000000..14ef5b1 --- /dev/null +++ b/river/command/move.zig @@ -0,0 +1,192 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Leon Henrik Plickat +// +// 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 . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const PhysicalDirection = @import("../command.zig").PhysicalDirection; +const Orientation = @import("../command.zig").Orientation; +const Seat = @import("../Seat.zig"); +const View = @import("../View.zig"); +const Box = @import("../Box.zig"); + +pub fn move( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 3) return Error.NotEnoughArguments; + if (args.len > 3) return Error.TooManyArguments; + + const delta = try std.fmt.parseInt(i32, args[2], 10); + const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse + return Error.InvalidPhysicalDirection; + + const view = getView(seat) orelse return; + switch (direction) { + .up => moveVertical(view, -1 * delta), + .down => moveVertical(view, delta), + .left => moveHorizontal(view, -1 * delta), + .right => moveHorizontal(view, delta), + } + + apply(view); +} + +pub fn snap( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse + return Error.InvalidPhysicalDirection; + + const view = get_view(seat) orelse return; + const output_box = get_output_dimensions(view); + const view = getView(seat) orelse return; + const border_width = @intCast(i32, view.output.root.server.config.border_width); + switch (direction) { + .up => view.pending.box.y = border_width, + .down => view.pending.box.y = + @intCast(i32, output_box.height - view.pending.box.height) - border_width, + .left => view.pending.box.x = border_width, + .right => view.pending.box.x = + @intCast(i32, output_box.width - view.pending.box.width) - border_width, + } + + apply(view); +} + +pub fn resize( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 3) return Error.NotEnoughArguments; + if (args.len > 3) return Error.TooManyArguments; + + const delta = try std.fmt.parseInt(i32, args[2], 10); + const orientation = std.meta.stringToEnum(Orientation, args[1]) orelse + return Error.InvalidOrientation; + + const view = get_view(seat) orelse return; + const output_box = get_output_dimensions(view); + const view = getView(seat) orelse return; + const border_width = @intCast(i32, view.output.root.server.config.border_width); + switch (orientation) { + .horizontal => { + var real_delta: i32 = @intCast(i32, view.pending.box.width); + if (delta > 0) { + view.pending.box.width += @intCast(u32, delta); + } else { + // Prevent underflow + view.pending.box.width -= + std.math.min(view.pending.box.width, @intCast(u32, -1 * delta)); + } + view.applyConstraints(); + // Do not grow bigger than the output + view.pending.box.width = std.math.min( + view.pending.box.width, + output_box.width - @intCast(u32, 2 * border_width), + ); + real_delta -= @intCast(i32, view.pending.box.width); + moveHorizontal(view, @divFloor(real_delta, 2)); + }, + .vertical => { + var real_delta: i32 = @intCast(i32, view.pending.box.height); + if (delta > 0) { + view.pending.box.height += @intCast(u32, delta); + } else { + // Prevent underflow + view.pending.box.height -= + std.math.min(view.pending.box.height, @intCast(u32, -1 * delta)); + } + view.applyConstraints(); + // Do not grow bigger than the output + view.pending.box.height = std.math.min( + view.pending.box.height, + output_box.height - @intCast(u32, 2 * border_width), + ); + real_delta -= @intCast(i32, view.pending.box.height); + moveVertical(view, @divFloor(real_delta, 2)); + }, + } + + apply(view); +} + +fn apply(view: *View) void { + // Set the view to floating but keep the position and dimensions + view.pending.float = true; + view.float_box = view.pending.box; + + view.applyPending(); +} + +fn getView(seat: *Seat) ?*View { + if (seat.focused != .view) return null; + const view = seat.focused.view; + + // Do not touch fullscreen views + if (view.pending.fullscreen) return null; + + // Do not touch views which are the target of a cursor action + if (seat.input_manager.isCursorActionTarget(view)) return null; + + return view; +} + +fn get_output_dimensions(view: *View) Box { + var output_width: c_int = undefined; + var output_height: c_int = undefined; + c.wlr_output_effective_resolution(view.output.wlr_output, &output_width, &output_height); + const box: Box = .{ + .x = 0, + .y = 0, + .width = @intCast(u32, output_width), + .height = @intCast(u32, output_height), + }; + return box; +} + +fn moveVertical(view: *View, delta: i32) void { + const output_box = view.output.get_output_dimensions(view); + const border_width = @intCast(i32, view.output.root.server.config.border_width); + view.pending.box.y = std.math.clamp( + view.pending.box.y + delta, + border_width, + @intCast(i32, output_box.height - view.pending.box.height) - border_width, + ); +} + +fn moveHorizontal(view: *View, delta: i32) void { + const output_box = view.output.get_output_dimensions(view); + const border_width = @intCast(i32, view.output.root.server.config.border_width); + view.pending.box.x = std.math.clamp( + view.pending.box.x + delta, + border_width, + @intCast(i32, output_box.width - view.pending.box.width) - border_width, + ); +}