aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Freund <ifreund@ifreund.xyz>2020-07-29 16:36:46 +0200
committerIsaac Freund <ifreund@ifreund.xyz>2020-07-31 17:06:26 +0200
commit9f3ee76e51dfe754fc6efae4c961d32fe55297e6 (patch)
tree6597eed764889a389b82411f879396efcc7f1cc8
parentac3066d8fca2479fe8f5493f69ea6ad43fefd6e8 (diff)
downloadriver-9f3ee76e51dfe754fc6efae4c961d32fe55297e6.tar.gz
river-9f3ee76e51dfe754fc6efae4c961d32fe55297e6.tar.xz
cursor: implement resize
-rw-r--r--river/Cursor.zig177
-rw-r--r--river/Output.zig30
-rw-r--r--river/View.zig31
-rw-r--r--river/VoidView.zig5
-rw-r--r--river/XdgToplevel.zig21
-rw-r--r--river/XwaylandView.zig16
6 files changed, 189 insertions, 91 deletions
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 1bee958..1e8c130 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -33,10 +33,21 @@ const Seat = @import("Seat.zig");
const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
-const Mode = enum {
- passthrough,
- move,
- resize,
+const Mode = union(enum) {
+ passthrough: void,
+ move: MoveData,
+ resize: ResizeData,
+};
+
+const MoveData = struct {
+ view: *View,
+};
+
+const ResizeData = struct {
+ view: *View,
+ /// Offset from the lower right corner of the view
+ x_offset: i32,
+ y_offset: i32,
};
const default_size = 24;
@@ -45,10 +56,11 @@ seat: *Seat,
wlr_cursor: *c.wlr_cursor,
wlr_xcursor_manager: *c.wlr_xcursor_manager,
-mode: Mode,
+/// Number of distinct buttons currently pressed
+pressed_count: u32,
-/// The target of an in progress move or resize
-target_view: ?*View,
+/// Current cursor mode as well as any state needed to implement that mode
+mode: Mode,
listen_axis: c.wl_listener,
listen_button: c.wl_listener,
@@ -71,8 +83,8 @@ pub fn init(self: *Self, seat: *Seat) !void {
return error.OutOfMemory;
try self.setTheme(null, null);
+ self.pressed_count = 0;
self.mode = .passthrough;
- self.target_view = null;
// wlr_cursor *only* displays an image on screen. It does not move around
// when the pointer moves. However, we can attach input devices to it, and
@@ -166,55 +178,59 @@ fn handleAxis(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
);
}
-fn enterCursorMode(self: *Self, event: *c.wlr_event_pointer_button, view: *View, mode: Mode) void {
- if (self.mode != .passthrough) return;
-
- switch (mode) {
- .passthrough => {},
- .resize => {},
-
- .move => {
- std.debug.assert(self.target_view == null);
-
- self.mode = .move;
- self.target_view = view;
+/// Enter move or resize mode
+fn enterCursorMode(self: *Self, event: *c.wlr_event_pointer_button, view: *View, mode: @TagType(Mode)) void {
+ std.debug.assert(self.mode == .passthrough);
+
+ log.debug(.cursor, "enter {} mode", .{@tagName(mode)});
+
+ const cur_box = &view.current.box;
+ self.mode = switch (mode) {
+ .passthrough => unreachable,
+ .move => .{ .move = .{ .view = view } },
+ .resize => .{
+ .resize = .{
+ .view = view,
+ .x_offset = cur_box.x + @intCast(i32, cur_box.width) - @floatToInt(i32, self.wlr_cursor.x),
+ .y_offset = cur_box.y + @intCast(i32, cur_box.height) - @floatToInt(i32, self.wlr_cursor.y),
+ },
+ },
+ };
- // Automatically float all views being moved by the pointer
- if (!view.current.float) {
- view.pending.float = true;
- // Start a transaction to apply the pending state of the grabbed
- // view and rearrange the layout to fill the hole.
- view.output.root.arrange();
- }
+ // Automatically float all views being moved by the pointer
+ if (!view.current.float) {
+ view.pending.float = true;
+ // Start a transaction to apply the pending state of the grabbed
+ // view and rearrange the layout to fill the hole.
+ view.output.root.arrange();
+ }
- // Clear cursor focus, so that the surface does not receive events
- c.wlr_seat_pointer_clear_focus(self.seat.wlr_seat);
+ // Clear cursor focus, so that the surface does not receive events
+ c.wlr_seat_pointer_clear_focus(self.seat.wlr_seat);
- c.wlr_xcursor_manager_set_cursor_image(self.wlr_xcursor_manager, "move", self.wlr_cursor);
- },
- }
+ c.wlr_xcursor_manager_set_cursor_image(
+ self.wlr_xcursor_manager,
+ if (mode == .move) "move" else "se-resize",
+ self.wlr_cursor,
+ );
}
+/// Return from move/resize to passthrough
fn leaveCursorMode(self: *Self, event: *c.wlr_event_pointer_button) void {
- switch (self.mode) {
- .passthrough => {},
- .resize => {},
+ std.debug.assert(self.mode != .passthrough);
- .move => {
- self.mode = .passthrough;
- self.target_view = null;
+ log.debug(.cursor, "leave {} mode", .{@tagName(self.mode)});
- // Set generic cursor image in case the application does not set one.
- c.wlr_xcursor_manager_set_cursor_image(
- self.wlr_xcursor_manager,
- "left_ptr",
- self.wlr_cursor,
- );
+ self.mode = .passthrough;
- // Cursor-Reentry by notifying surface underneath cursor.
- processMotionPassthrough(self, event.time_msec);
- },
- }
+ c.wlr_xcursor_manager_set_cursor_image(
+ self.wlr_xcursor_manager,
+ "left_ptr",
+ self.wlr_cursor,
+ );
+
+ // Cursor-Reentry by notifying surface underneath cursor.
+ processMotionPassthrough(self, event.time_msec);
}
fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
@@ -222,9 +238,20 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// event.
const self = @fieldParentPtr(Self, "listen_button", listener.?);
const event = util.voidCast(c.wlr_event_pointer_button, data.?);
+
+ if (event.state == .WLR_BUTTON_PRESSED) {
+ self.pressed_count += 1;
+ } else {
+ std.debug.assert(self.pressed_count > 0);
+ self.pressed_count -= 1;
+ if (self.pressed_count == 0 and self.mode != .passthrough) {
+ self.leaveCursorMode(event);
+ return;
+ }
+ }
+
var sx: f64 = undefined;
var sy: f64 = undefined;
-
if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| {
// If the found surface is a keyboard inteactive layer surface,
// give it keyboard focus.
@@ -244,14 +271,14 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
const view = util.voidCast(View, wlr_xdg_surface.*.data.?);
self.seat.focus(view);
- if (event.state == .WLR_BUTTON_PRESSED) {
+ if (event.state == .WLR_BUTTON_PRESSED and self.pressed_count == 1) {
// If the button is pressed and the pointer modifier is
// active, enter cursor mode or close view and return.
if (self.seat.pointer_modifier) {
switch (event.button) {
- c.BTN_LEFT => enterCursorMode(self, event, view, .move),
+ c.BTN_LEFT => self.enterCursorMode(event, view, .move),
c.BTN_MIDDLE => view.close(),
- c.BTN_RIGHT => enterCursorMode(self, event, view, .resize),
+ c.BTN_RIGHT => self.enterCursorMode(event, view, .resize),
// TODO Some mice have additional buttons. These
// could also be bound to some useful action.
@@ -259,11 +286,6 @@ fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
}
return;
}
- } else if (self.mode != .passthrough) {
- // If the button is released and the current cursor mode is
- // not passthrough, leave cursor mode and return.
- leaveCursorMode(self, event);
- return;
}
}
}
@@ -301,13 +323,15 @@ fn handleMotionAbsolute(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C)
c.wlr_cursor_warp_absolute(self.wlr_cursor, event.device, event.x, event.y);
processMotionPassthrough(self, event.time_msec);
},
- .move => {
+ .move, .resize => {
var lx: f64 = undefined;
var ly: f64 = undefined;
c.wlr_cursor_absolute_to_layout_coords(self.wlr_cursor, event.device, event.x, event.y, &lx, &ly);
- self.processMotionMove(event.device, lx - self.wlr_cursor.x, ly - self.wlr_cursor.y);
+ if (self.mode == .move)
+ self.processMotionMove(event.device, lx - self.wlr_cursor.x, ly - self.wlr_cursor.y)
+ else
+ self.processMotionResize(event.device, lx - self.wlr_cursor.x, ly - self.wlr_cursor.y);
},
- .resize => {},
}
}
@@ -327,7 +351,7 @@ fn handleMotion(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
processMotionPassthrough(self, event.time_msec);
},
.move => self.processMotionMove(event.device, event.delta_x, event.delta_y),
- .resize => {},
+ .resize => self.processMotionResize(event.device, event.delta_x, event.delta_y),
}
}
@@ -357,8 +381,7 @@ fn handleRequestSetCursor(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C
/// Move the cursor and the target view, constraining the view to the
/// dimensions of the output.
fn processMotionMove(self: *Self, device: *c.wlr_input_device, delta_x: f64, delta_y: f64) void {
- // Must be non-null if we are in move mode
- const view = self.target_view.?;
+ const view = self.mode.move.view;
const border_width = self.seat.input_manager.server.config.border_width;
var output_width: c_int = undefined;
@@ -388,6 +411,34 @@ fn processMotionMove(self: *Self, device: *c.wlr_input_device, delta_x: f64, del
view.current = view.pending;
}
+fn processMotionResize(self: *Self, device: *c.wlr_input_device, delta_x: f64, delta_y: f64) void {
+ // Must be non-null if we are in resize mode
+ const view = self.mode.resize.view;
+ const border_width = self.seat.input_manager.server.config.border_width;
+
+ 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);
+
+ // Set width/height of view, clamp to view size constraints and output dimensions
+ const box = &view.pending.box;
+ box.width = @intCast(u32, std.math.max(0, @intCast(i32, box.width) + @floatToInt(i32, delta_x)));
+ box.height = @intCast(u32, std.math.max(0, @intCast(i32, box.height) + @floatToInt(i32, delta_y)));
+ view.applyConstraints();
+ box.width = std.math.min(box.width, @intCast(u32, output_width - box.x - @intCast(i32, border_width)));
+ box.height = std.math.min(box.height, @intCast(u32, output_height - box.y - @intCast(i32, border_width)));
+
+ if (view.needsConfigure()) view.configure();
+
+ // Keep cursor locked to the original offset from the bottom right corner
+ c.wlr_cursor_warp_closest(
+ self.wlr_cursor,
+ device,
+ @intToFloat(f64, box.x + @intCast(i32, box.width) - self.mode.resize.x_offset),
+ @intToFloat(f64, box.y + @intCast(i32, box.height) - self.mode.resize.y_offset),
+ );
+}
+
fn processMotionPassthrough(self: *Self, time: u32) void {
var sx: f64 = undefined;
var sy: f64 = undefined;
diff --git a/river/Output.zig b/river/Output.zig
index 86e8514..a77efe2 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -32,14 +32,6 @@ const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const OutputStatus = @import("OutputStatus.zig");
-// Minimum width/height for surfaces.
-// This is needed, because external layouts and large padding and border sizes
-// may cause surfaces so small, that bugs in client applications are encountered,
-// or even surfaces of zero or negative size,which are a protocol error and would
-// likely cause river to crash. The value is totally arbitrary and low enough,
-// that it should never be encountered during normal usage.
-const minimum_size = 50;
-
const State = struct {
/// A bit field of focused tags
tags: u32,
@@ -187,18 +179,13 @@ fn layoutFull(self: *Self, visible_count: u32, output_tags: u32) void {
.height = self.usable_box.height - (2 * xy_offset),
};
- // Apply minimum view size
- if (full_box.width < minimum_size) {
- full_box.width = minimum_size;
- }
- if (full_box.height < minimum_size) {
- full_box.height = minimum_size;
- }
-
var it = ViewStack(View).pendingIterator(self.views.first, output_tags);
while (it.next()) |node| {
const view = &node.view;
- if (!view.pending.float and !view.pending.fullscreen) view.pending.box = full_box;
+ if (!view.pending.float and !view.pending.fullscreen) {
+ view.pending.box = full_box;
+ view.applyConstraints();
+ }
}
}
@@ -285,14 +272,6 @@ fn layoutExternal(self: *Self, visible_count: u32, output_tags: u32) !void {
box.y += self.usable_box.y + xy_offset;
box.width -= delta_size;
box.height -= delta_size;
- if (box.width < minimum_size) {
- box.width = minimum_size;
- log.notice(.layout, "window hits minimum view width.", .{});
- }
- if (box.height < minimum_size) {
- box.height = minimum_size;
- log.notice(.layout, "window hits minimum view height.", .{});
- }
try view_boxen.append(box);
}
@@ -305,6 +284,7 @@ fn layoutExternal(self: *Self, visible_count: u32, output_tags: u32) !void {
const view = &node.view;
if (!view.pending.float and !view.pending.fullscreen) {
view.pending.box = view_boxen.items[i];
+ view.applyConstraints();
i += 1;
}
}
diff --git a/river/View.zig b/river/View.zig
index 3df7e1c..2b0dea1 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -31,6 +31,21 @@ const ViewStack = @import("view_stack.zig").ViewStack;
const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandView = if (build_options.xwayland) @import("XwaylandView.zig") else @import("VoidView.zig");
+pub const Constraints = struct {
+ min_width: u32,
+ max_width: u32,
+ min_height: u32,
+ max_height: u32,
+};
+
+// Minimum width/height for surfaces.
+// This is needed, because external layouts and large padding and border sizes
+// may cause surfaces so small, that bugs in client applications are encountered,
+// or even surfaces of zero or negative size,which are a protocol error and would
+// likely cause river to crash. The value is totally arbitrary and low enough,
+// that it should never be encountered during normal usage.
+pub const min_size = 50;
+
const Impl = union(enum) {
xdg_toplevel: XdgToplevel,
xwayland_view: XwaylandView,
@@ -265,6 +280,22 @@ pub fn getTitle(self: Self) [*:0]const u8 {
};
}
+/// Clamp the width/height of the pending state to the constraints of the view
+pub fn applyConstraints(self: *Self) void {
+ const constraints = self.getConstraints();
+ const box = &self.pending.box;
+ box.width = std.math.clamp(box.width, constraints.min_width, constraints.max_width);
+ box.height = std.math.clamp(box.height, constraints.min_height, constraints.max_height);
+}
+
+/// Return bounds on the dimensions of the view
+pub fn getConstraints(self: Self) Constraints {
+ return switch (self.impl) {
+ .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getConstraints(),
+ .xwayland_view => |xwayland_view| xwayland_view.getConstraints(),
+ };
+}
+
/// Called by the impl when the surface is ready to be displayed
pub fn map(self: *Self) void {
const root = self.output.root;
diff --git a/river/VoidView.zig b/river/VoidView.zig
index d9bd3d4..784b9f0 100644
--- a/river/VoidView.zig
+++ b/river/VoidView.zig
@@ -22,6 +22,7 @@ const std = @import("std");
const c = @import("c.zig");
const Box = @import("Box.zig");
+const View = @import("View.zig");
pub fn needsConfigure(self: Self) bool {
unreachable;
@@ -58,3 +59,7 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surfa
pub fn getTitle(self: Self) [*:0]const u8 {
unreachable;
}
+
+pub fn getConstraints(self: Self) View.Constraints {
+ unreachable;
+}
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 945d68a..8636fcb 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -128,6 +128,17 @@ pub fn getTitle(self: Self) [*:0]const u8 {
return wlr_xdg_toplevel.title orelse "NULL";
}
+/// Return bounds on the dimensions of the toplevel.
+pub fn getConstraints(self: Self) View.Constraints {
+ const state = @field(self.wlr_xdg_surface, c.wlr_xdg_surface_union).toplevel.*.current;
+ return .{
+ .min_width = if (state.min_width > 0) state.min_width else View.min_size,
+ .max_width = if (state.max_width > 0) state.max_width else std.math.maxInt(u32),
+ .min_height = if (state.min_height > 0) state.min_height else View.min_size,
+ .max_height = if (state.max_height > 0) state.max_height else std.math.maxInt(u32),
+ };
+}
+
/// Called when the xdg surface is destroyed
fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
const self = @fieldParentPtr(Self, "listen_destroy", listener.?);
@@ -234,10 +245,14 @@ fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
view.surface_box = new_box;
if (s == self.wlr_xdg_surface.configure_serial) {
- // If this commit is in response to our configure, notify the
- // transaction code.
- view.output.root.notifyConfigured();
+ // If this commit is in response to our configure, either notify
+ // the transaction code or apply the pending state immediately,
+ // depending on whether or not the view is floating.
view.pending_serial = null;
+ if (view.current.float and view.pending.float)
+ view.current = view.pending
+ else
+ view.output.root.notifyConfigured();
} else {
// If the client has not yet acked our configure, we need to send a
// frame done event so that it commits another buffer. These
diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig
index 6b54e26..130ecc5 100644
--- a/river/XwaylandView.zig
+++ b/river/XwaylandView.zig
@@ -118,6 +118,22 @@ pub fn getTitle(self: Self) [*:0]const u8 {
return self.wlr_xwayland_surface.title orelse "";
}
+/// Return bounds on the dimensions of the view
+pub fn getConstraints(self: Self) View.Constraints {
+ const hints: *c.wlr_xwayland_surface_size_hints = self.wlr_xwayland_surface.size_hints orelse return .{
+ .min_width = View.min_size,
+ .max_width = std.math.maxInt(u32),
+ .min_height = View.min_size,
+ .max_height = std.math.maxInt(u32),
+ };
+ return .{
+ .min_width = if (hints.min_width > 0) @intCast(u32, hints.min_width) else View.min_size,
+ .max_width = if (hints.max_width > 0) @intCast(u32, hints.max_width) else std.math.maxInt(u32),
+ .min_height = if (hints.min_height > 0) @intCast(u32, hints.min_height) else View.min_size,
+ .max_height = if (hints.max_height > 0) @intCast(u32, hints.max_height) else std.math.maxInt(u32),
+ };
+}
+
/// Called when the xwayland surface is destroyed
fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
const self = @fieldParentPtr(Self, "listen_destroy", listener.?);