river/river/Cursor.zig
Isaac Freund 6bdb152808
cursor: make xcursor theme configurable
- add a new command to set the theme
- export the theme of the default seat through environment variables
2020-07-14 17:34:29 +02:00

558 lines
22 KiB
Zig

// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 Isaac Freund
// 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 <https://www.gnu.org/licenses/>.
const Self = @This();
const build_options = @import("build_options");
const std = @import("std");
const c = @import("c.zig");
const log = @import("log.zig");
const util = @import("util.zig");
const Box = @import("Box.zig");
const Config = @import("Config.zig");
const LayerSurface = @import("LayerSurface.zig");
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const CursorMode = enum {
Passthrough,
Move,
Resize,
};
const default_size = 24;
seat: *Seat,
wlr_cursor: *c.wlr_cursor,
wlr_xcursor_manager: *c.wlr_xcursor_manager,
mode: CursorMode,
grabbed_view: *View,
/// Distance between cursor and top-left corner of grabbed view
grab_delta_x: f64,
grab_delta_y: f64,
/// Dimensions of the output the grabbed view is on
grab_output_width: u64,
grab_output_height: u64,
const CursorPosition = struct {
x: f64,
y: f64,
};
listen_axis: c.wl_listener,
listen_button: c.wl_listener,
listen_frame: c.wl_listener,
listen_motion_absolute: c.wl_listener,
listen_motion: c.wl_listener,
listen_request_set_cursor: c.wl_listener,
pub fn init(self: *Self, seat: *Seat) !void {
self.seat = seat;
// Creates a wlroots utility for tracking the cursor image shown on screen.
self.wlr_cursor = c.wlr_cursor_create() orelse return error.OutOfMemory;
c.wlr_cursor_attach_output_layout(self.wlr_cursor, seat.input_manager.server.root.wlr_output_layout);
// This is here so that self.wlr_xcursor_manager doesn't need to be an
// optional pointer. This isn't optimal as it does a needless allocation,
// but this is not a hot path.
self.wlr_xcursor_manager = c.wlr_xcursor_manager_create(null, default_size) orelse
return error.OutOfMemory;
try self.setTheme(null, null);
self.mode = CursorMode.Passthrough;
self.grabbed_view = undefined;
self.grab_delta_x = 0.0;
self.grab_delta_y = 0.0;
// 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
// it will generate aggregate events for all of them. In these events, we
// can choose how we want to process them, forwarding them to clients and
// moving the cursor around. See following post for more detail:
// https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html
self.listen_axis.notify = handleAxis;
c.wl_signal_add(&self.wlr_cursor.events.axis, &self.listen_axis);
self.listen_button.notify = handleButton;
c.wl_signal_add(&self.wlr_cursor.events.button, &self.listen_button);
self.listen_frame.notify = handleFrame;
c.wl_signal_add(&self.wlr_cursor.events.frame, &self.listen_frame);
self.listen_motion_absolute.notify = handleMotionAbsolute;
c.wl_signal_add(&self.wlr_cursor.events.motion_absolute, &self.listen_motion_absolute);
self.listen_motion.notify = handleMotion;
c.wl_signal_add(&self.wlr_cursor.events.motion, &self.listen_motion);
self.listen_request_set_cursor.notify = handleRequestSetCursor;
c.wl_signal_add(&self.seat.wlr_seat.events.request_set_cursor, &self.listen_request_set_cursor);
}
pub fn deinit(self: *Self) void {
c.wlr_xcursor_manager_destroy(self.wlr_xcursor_manager);
c.wlr_cursor_destroy(self.wlr_cursor);
}
/// Set the cursor theme for the given seat, as well as the xwayland theme if
/// this is the default seat.
pub fn setTheme(self: *Self, theme: ?[*:0]const u8, size: ?u32) !void {
const server = self.seat.input_manager.server;
c.wlr_xcursor_manager_destroy(self.wlr_xcursor_manager);
self.wlr_xcursor_manager = c.wlr_xcursor_manager_create(theme, size orelse default_size) orelse
return error.OutOfMemory;
// For each output, ensure a theme of the proper scale is loaded
var it = server.root.outputs.first;
while (it) |node| : (it = node.next) {
const wlr_output = node.data.wlr_output;
if (c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, wlr_output.scale) != 0)
log.err(.cursor, "failed to load xcursor theme '{}' at scale {}", .{ theme, wlr_output.scale });
}
// If this cursor belongs to the default seat, set the xcursor environment
// variables and the xwayland cursor theme.
if (self.seat == self.seat.input_manager.default_seat) {
const size_str = try std.fmt.allocPrint0(util.gpa, "{}", .{size});
defer util.gpa.free(size_str);
if (c.setenv("XCURSOR_SIZE", size_str, 1) < 0) return error.OutOfMemory;
if (theme) |t| if (c.setenv("XCURSOR_THEME", t, 1) < 0) return error.OutOfMemory;
if (build_options.xwayland) {
if (c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, 1) == 0) {
const wlr_xcursor = c.wlr_xcursor_manager_get_xcursor(self.wlr_xcursor_manager, "left_ptr", 1).?;
const image: *c.wlr_xcursor_image = wlr_xcursor.*.images[0];
c.wlr_xwayland_set_cursor(
server.wlr_xwayland,
image.buffer,
image.width * 4,
image.width,
image.height,
@intCast(i32, image.hotspot_x),
@intCast(i32, image.hotspot_y),
);
} else log.err(.cursor, "failed to load xcursor theme '{}' at scale 1", .{theme});
}
}
}
fn handleAxis(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is forwarded by the cursor when a pointer emits an axis event,
// for example when you move the scroll wheel.
const cursor = @fieldParentPtr(Self, "listen_axis", listener.?);
const event = util.voidCast(c.wlr_event_pointer_axis, data.?);
// Notify the client with pointer focus of the axis event.
c.wlr_seat_pointer_notify_axis(
cursor.seat.wlr_seat,
event.time_msec,
event.orientation,
event.delta,
event.delta_discrete,
event.source,
);
}
fn enterCursorMode(self: *Self, event: *c.wlr_event_pointer_button, view: *View, mode: CursorMode) void {
if (self.mode != CursorMode.Passthrough) return;
switch (mode) {
.Passthrough => {},
.Resize => {},
.Move => {
self.grabbed_view = view;
// Automatically float alll views being moved by the pointer
if (!self.grabbed_view.current.float) {
self.grabbed_view.pending.float = true;
// Start a transaction to apply the pending state of the grabbed
// view and rearrange the layout to fill the hole.
self.grabbed_view.output.root.arrange();
}
// Enter moving mode
self.mode = CursorMode.Move;
self.grab_delta_x = @fabs(self.wlr_cursor.x - @intToFloat(f64, self.grabbed_view.pending.box.x));
self.grab_delta_y = @fabs(self.wlr_cursor.y - @intToFloat(f64, self.grabbed_view.pending.box.y));
// 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);
// Get dimension of output the grabbed view is on
var output_width_c: c_int = undefined;
var output_height_c: c_int = undefined;
c.wlr_output_effective_resolution(
self.grabbed_view.output.wlr_output,
&output_width_c,
&output_height_c,
);
self.grab_output_width = @intCast(u64, output_width_c);
self.grab_output_height = @intCast(u64, output_height_c);
},
}
}
fn leaveCursorMode(self: *Self, event: *c.wlr_event_pointer_button) void {
switch (self.mode) {
.Passthrough => {},
.Resize => {},
.Move => {
self.mode = CursorMode.Passthrough;
// 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,
);
// Cursor-Reentry by notifying surface underneath cursor.
processMotionPassthrough(self, event.time_msec);
},
}
}
fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is forwarded by the cursor when a pointer emits a button
// event.
const self = @fieldParentPtr(Self, "listen_button", listener.?);
const event = util.voidCast(c.wlr_event_pointer_button, data.?);
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.
if (c.wlr_surface_is_layer_surface(wlr_surface)) {
const wlr_layer_surface = c.wlr_layer_surface_v1_from_wlr_surface(wlr_surface);
if (wlr_layer_surface.*.current.keyboard_interactive) {
const layer_surface = util.voidCast(LayerSurface, wlr_layer_surface.*.data.?);
self.seat.setFocusRaw(.{ .layer = layer_surface });
}
}
// If the found surface is an xdg toplevel surface, send keyboard
// focus to the view.
if (c.wlr_surface_is_xdg_surface(wlr_surface)) {
const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(wlr_surface);
if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
const view = util.voidCast(View, wlr_xdg_surface.*.data.?);
self.seat.focus(view);
if (event.state == .WLR_BUTTON_PRESSED) {
// 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, CursorMode.Move),
c.BTN_MIDDLE => view.close(),
c.BTN_RIGHT => {}, // TODO Resize
// TODO Some mice have additional buttons. These
// could also be bound to some useful action.
else => {},
}
return;
}
} else if (self.mode != CursorMode.Passthrough) {
// If the button is released and the current cursor mode is
// not passthrough, leave cursor mode and return.
leaveCursorMode(self, event);
return;
}
}
}
_ = c.wlr_seat_pointer_notify_button(
self.seat.wlr_seat,
event.time_msec,
event.button,
event.state,
);
}
}
fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is forwarded by the cursor when a pointer emits an frame
// event. Frame events are sent after regular pointer events to group
// multiple events together. For instance, two axis events may happen at the
// same time, in which case a frame event won't be sent in between.
const self = @fieldParentPtr(Self, "listen_frame", listener.?);
// Notify the client with pointer focus of the frame event.
c.wlr_seat_pointer_notify_frame(self.seat.wlr_seat);
}
fn handleMotionAbsolute(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is forwarded by the cursor when a pointer emits an _absolute_
// motion event, from 0..1 on each axis. This happens, for example, when
// wlroots is running under a Wayland window rather than KMS+DRM, and you
// move the mouse over the window. You could enter the window from any edge,
// so we have to warp the mouse there. There is also some hardware which
// emits these events.
const self = @fieldParentPtr(Self, "listen_motion_absolute", listener.?);
const event = util.voidCast(c.wlr_event_pointer_motion_absolute, data.?);
switch (self.mode) {
CursorMode.Passthrough => {
c.wlr_cursor_warp_absolute(self.wlr_cursor, event.device, event.x, event.y);
processMotionPassthrough(self, event.time_msec);
},
CursorMode.Move => {
var x_layout: f64 = undefined;
var y_layout: f64 = undefined;
c.wlr_cursor_absolute_to_layout_coords(self.wlr_cursor, event.device, event.x, event.y, &x_layout, &y_layout);
var cursor: CursorPosition = processMotionMove(self, x_layout, y_layout);
_ = c.wlr_cursor_warp(self.wlr_cursor, event.device, cursor.x, cursor.y);
},
CursorMode.Resize => {},
}
}
fn handleMotion(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is forwarded by the cursor when a pointer emits a _relative_
// pointer motion event (i.e. a delta)
const self = @fieldParentPtr(Self, "listen_motion", listener.?);
const event = util.voidCast(c.wlr_event_pointer_motion, data.?);
switch (self.mode) {
CursorMode.Passthrough => {
// The cursor doesn't move unless we tell it to. The cursor automatically
// handles constraining the motion to the output layout, as well as any
// special configuration applied for the specific input device which
// generated the event. You can pass NULL for the device if you want to move
// the cursor around without any input.
c.wlr_cursor_move(self.wlr_cursor, event.device, event.delta_x, event.delta_y);
processMotionPassthrough(self, event.time_msec);
},
CursorMode.Move => {
var cursor: CursorPosition = processMotionMove(self, event.delta_x + self.wlr_cursor.x, event.delta_y + self.wlr_cursor.y);
_ = c.wlr_cursor_warp(self.wlr_cursor, event.device, cursor.x, cursor.y);
},
CursorMode.Resize => {},
}
}
fn handleRequestSetCursor(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
// This event is rasied by the seat when a client provides a cursor image
const self = @fieldParentPtr(Self, "listen_request_set_cursor", listener.?);
const event = util.voidCast(c.wlr_seat_pointer_request_set_cursor_event, data.?);
const focused_client = self.seat.wlr_seat.pointer_state.focused_client;
// This can be sent by any client, so we check to make sure this one is
// actually has pointer focus first.
if (focused_client == event.seat_client) {
// Once we've vetted the client, we can tell the cursor to use the
// provided surface as the cursor image. It will set the hardware cursor
// on the output that it's currently on and continue to do so as the
// cursor moves between outputs.
log.debug(.cursor, "focused client set cursor", .{});
c.wlr_cursor_set_surface(
self.wlr_cursor,
event.surface,
event.hotspot_x,
event.hotspot_y,
);
}
}
fn cursorMoveConstraints(self: *Self, _position: f64, _output: u64, _view: u64) i32 {
const position: i32 = @floatToInt(i32, _position);
const view: i32 = @intCast(i32, _view);
const border_width = @intCast(i32, self.grabbed_view.output.root.server.config.border_width);
const useable: i32 = @intCast(i32, _output - _view) - border_width;
var new: i32 = position;
if (position > useable) {
new = useable;
} else if (position < border_width) {
new = border_width;
}
return new;
}
/// Moves grabbed view and returns new cursor position
fn processMotionMove(self: *Self, x_in: f64, y_in: f64) CursorPosition {
// Get new X and Y of cursor and view.
// Width and height of surface will stay the same.
self.grabbed_view.pending.box.x = cursorMoveConstraints(
self,
x_in - self.grab_delta_x,
self.grab_output_width,
self.grabbed_view.pending.box.width,
);
self.grabbed_view.pending.box.y = cursorMoveConstraints(
self,
y_in - self.grab_delta_y,
self.grab_output_height,
self.grabbed_view.pending.box.height,
);
// Apply new pending state (no need for a transaction as size didn't change)
self.grabbed_view.current = self.grabbed_view.pending;
// This function returns the cursor position so that the calling function
// can do the cursor movement.
return .{
.x = @intToFloat(f64, self.grabbed_view.current.box.x) + self.grab_delta_x,
.y = @intToFloat(f64, self.grabbed_view.current.box.y) + self.grab_delta_y,
};
}
fn processMotionPassthrough(self: *Self, time: u32) void {
var sx: f64 = undefined;
var sy: f64 = undefined;
if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| {
// "Enter" the surface if necessary. This lets the client know that the
// cursor has entered one of its surfaces.
//
// Note that this gives the surface "pointer focus", which is distinct
// from keyboard focus. You get pointer focus by moving the pointer over
// a window.
if (self.seat.input_manager.inputAllowed(wlr_surface)) {
const wlr_seat = self.seat.wlr_seat;
const focus_change = wlr_seat.pointer_state.focused_surface != wlr_surface;
if (focus_change) {
log.debug(.cursor, "pointer notify enter at ({},{})", .{ sx, sy });
c.wlr_seat_pointer_notify_enter(wlr_seat, wlr_surface, sx, sy);
} else {
// The enter event contains coordinates, so we only need to notify
// on motion if the focus did not change.
c.wlr_seat_pointer_notify_motion(wlr_seat, time, sx, sy);
}
return;
}
}
// There is either no surface under the cursor or input is disallowed
// Reset the cursor image to the default
c.wlr_xcursor_manager_set_cursor_image(
self.wlr_xcursor_manager,
"left_ptr",
self.wlr_cursor,
);
// Clear pointer focus so future button events and such are not sent to
// the last client to have the cursor over it.
c.wlr_seat_pointer_clear_focus(self.seat.wlr_seat);
}
/// Find the topmost surface under the output layout coordinates lx/ly
/// returns the surface if found and sets the sx/sy parametes to the
/// surface coordinates.
fn surfaceAt(self: Self, lx: f64, ly: f64, sx: *f64, sy: *f64) ?*c.wlr_surface {
// Find the output to check
const root = self.seat.input_manager.server.root;
const wlr_output = c.wlr_output_layout_output_at(root.wlr_output_layout, lx, ly) orelse return null;
const output = util.voidCast(Output, wlr_output.*.data orelse return null);
// Get output-local coords from the layout coords
var ox = lx;
var oy = ly;
c.wlr_output_layout_output_coords(root.wlr_output_layout, wlr_output, &ox, &oy);
// Check layers and views from top to bottom
const layer_idxs = [_]usize{
c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
c.ZWLR_LAYER_SHELL_V1_LAYER_TOP,
c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM,
c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND,
};
// Check overlay layer incl. popups
if (layerSurfaceAt(output.*, output.layers[layer_idxs[0]], ox, oy, sx, sy, false)) |s| return s;
// Check top-background popups only
for (layer_idxs[1..4]) |idx|
if (layerSurfaceAt(output.*, output.layers[idx], ox, oy, sx, sy, true)) |s| return s;
// Check top layer
if (layerSurfaceAt(output.*, output.layers[layer_idxs[1]], ox, oy, sx, sy, false)) |s| return s;
// Check views
if (viewSurfaceAt(output.*, ox, oy, sx, sy)) |s| return s;
// Check the bottom-background layers
for (layer_idxs[2..4]) |idx|
if (layerSurfaceAt(output.*, output.layers[idx], ox, oy, sx, sy, false)) |s| return s;
return null;
}
/// Find the topmost surface on the given layer at ox,oy. Will only check
/// popups if popups_only is true.
fn layerSurfaceAt(
output: Output,
layer: std.TailQueue(LayerSurface),
ox: f64,
oy: f64,
sx: *f64,
sy: *f64,
popups_only: bool,
) ?*c.wlr_surface {
var it = layer.first;
while (it) |node| : (it = node.next) {
const layer_surface = &node.data;
const surface = c.wlr_layer_surface_v1_surface_at(
layer_surface.wlr_layer_surface,
ox - @intToFloat(f64, layer_surface.box.x),
oy - @intToFloat(f64, layer_surface.box.y),
sx,
sy,
);
if (surface) |found| {
if (!popups_only) {
return found;
} else if (c.wlr_surface_is_xdg_surface(found)) {
const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(found);
if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_POPUP) {
return found;
}
}
}
}
return null;
}
/// Find the topmost visible view surface (incl. popups) at ox,oy.
fn viewSurfaceAt(output: Output, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface {
// Focused views are rendered on top, so look for them first.
var it = ViewStack(View).iterator(output.views.first, output.current.tags);
while (it.next()) |node| {
if (!node.view.focused) continue;
if (node.view.surfaceAt(ox, oy, sx, sy)) |found| return found;
}
it = ViewStack(View).iterator(output.views.first, output.current.tags);
while (it.next()) |node| {
if (node.view.surfaceAt(ox, oy, sx, sy)) |found| return found;
}
return null;
}