diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index fec47dd..0826850 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -288,6 +288,15 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ If the view to be focused is on an output that does not have focus, focus is switched to that output. +*hide-cursor* *timeout* _timeout_ + Hide the cursor if it wasn't moved in the last _timeout_ milliseconds + until it is moved again. The default value is 0, which disables + automatically hiding the cursor. Show the cursor again on any movement. + +*hide-cursor* *when-typing* *enabled*|*disabled* + Hide the cursor when pressing any non-modifier key. Show the cursor + again on any movement. + *set-cursor-warp* *disabled*|*on-output-change* Set the cursor warp mode. There are two available modes: diff --git a/river/Config.zig b/river/Config.zig index e69177c..2da717c 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -36,6 +36,11 @@ pub const WarpCursorMode = enum { @"on-output-change", }; +pub const HideCursorWhenTypingMode = enum { + disabled, + enabled, +}; + /// Color of background in RGBA (alpha should only affect nested sessions) background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03 @@ -85,6 +90,12 @@ repeat_rate: u31 = 25, /// Keyboard repeat delay in milliseconds repeat_delay: u31 = 600, +/// Cursor hide timeout in milliseconds +cursor_hide_timeout: u31 = 0, + +/// Hide the cursor while typing +cursor_hide_when_typing: HideCursorWhenTypingMode = .disabled, + pub fn init() !Self { var self = Self{ .mode_to_id = std.StringHashMap(usize).init(util.gpa), diff --git a/river/Cursor.zig b/river/Cursor.zig index fec0764..04c0ee3 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -102,6 +102,10 @@ constraint: ?*wlr.PointerConstraintV1 = null, /// Number of distinct buttons currently pressed pressed_count: u32 = 0, +hide_cursor_timer: *wl.EventSource, + +hidden: bool = false, + axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(handleAxis), frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(handleFrame), button: wl.Listener(*wlr.Pointer.event.Button) = @@ -136,12 +140,16 @@ pub fn init(self: *Self, seat: *Seat) !void { const xcursor_manager = try wlr.XcursorManager.create(null, default_size); errdefer xcursor_manager.destroy(); + const event_loop = server.wl_server.getEventLoop(); self.* = .{ .seat = seat, .wlr_cursor = wlr_cursor, .pointer_gestures = try wlr.PointerGesturesV1.create(server.wl_server), .xcursor_manager = xcursor_manager, + .hide_cursor_timer = try event_loop.addTimer(*Self, handleHideCursorTimeout, self), }; + errdefer self.hide_cursor_timer.remove(); + try self.hide_cursor_timer.timerUpdate(server.config.cursor_hide_timeout); try self.setTheme(null, null); // wlr_cursor *only* displays an image on screen. It does not move around @@ -165,6 +173,7 @@ pub fn init(self: *Self, seat: *Seat) !void { } pub fn deinit(self: *Self) void { + self.hide_cursor_timer.remove(); self.xcursor_manager.destroy(); self.wlr_cursor.destroy(); } @@ -250,6 +259,7 @@ fn handleAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Point const self = @fieldParentPtr(Self, "axis", listener); self.seat.handleActivity(); + self.unhide(); // Notify the client with pointer focus of the axis event. self.seat.wlr_seat.pointerNotifyAxis( @@ -265,6 +275,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P const self = @fieldParentPtr(Self, "button", listener); self.seat.handleActivity(); + self.unhide(); if (event.state == .released) { assert(self.pressed_count > 0); @@ -487,6 +498,32 @@ fn handleRequestSetCursor( } } +pub fn hide(self: *Self) void { + if (self.pressed_count > 0) return; + self.hidden = true; + self.wlr_cursor.setImage(null, 0, 0, 0, 0, 0, 0); + self.image = .unknown; + self.seat.wlr_seat.pointerNotifyClearFocus(); + self.hide_cursor_timer.timerUpdate(0) catch { + log.err("failed to update cursor hide timeout", .{}); + }; +} + +pub fn unhide(self: *Self) void { + self.hide_cursor_timer.timerUpdate(server.config.cursor_hide_timeout) catch { + log.err("failed to update cursor hide timeout", .{}); + }; + if (!self.hidden) return; + self.hidden = false; + self.updateState(); +} + +fn handleHideCursorTimeout(self: *Self) callconv(.C) c_int { + log.debug("hide cursor timeout", .{}); + self.hide(); + return 0; +} + const SurfaceAtResult = struct { surface: *wlr.Surface, sx: f64, @@ -762,6 +799,8 @@ fn leaveMode(self: *Self, event: *wlr.Pointer.event.Button) void { } fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void { + self.unhide(); + server.input_manager.relative_pointer_manager.sendRelativeMotion( self.seat.wlr_seat, @as(u64, time) * 1000, diff --git a/river/Keyboard.zig b/river/Keyboard.zig index 087e303..ac10234 100644 --- a/river/Keyboard.zig +++ b/river/Keyboard.zig @@ -91,8 +91,12 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa var handled = false; + var non_modifier_pressed = false; + // First check translated keysyms as xkb reports them for (wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| { + if (!released and !isModifier(sym)) non_modifier_pressed = true; + // Handle builtin mapping only when keys are pressed if (!released and handleBuiltinMapping(sym)) { handled = true; @@ -125,6 +129,14 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa wlr_seat.setKeyboard(self.input_device); wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state); } + + if (non_modifier_pressed and server.config.cursor_hide_when_typing == .enabled) { + self.seat.cursor.hide(); + } +} + +fn isModifier(keysym: xkb.Keysym) bool { + return @enumToInt(keysym) >= xkb.Keysym.Shift_L and @enumToInt(keysym) <= xkb.Keysym.Hyper_R; } /// Simply pass modifiers along to the client diff --git a/river/command.zig b/river/command.zig index 1606f06..199ec0c 100644 --- a/river/command.zig +++ b/river/command.zig @@ -59,6 +59,7 @@ const command_impls = std.ComptimeStringMap( .{ "focus-output", @import("command/output.zig").focusOutput }, .{ "focus-previous-tags", @import("command/tags.zig").focusPreviousTags }, .{ "focus-view", @import("command/focus_view.zig").focusView }, + .{ "hide-cursor", @import("command/cursor.zig").cursor }, .{ "input", @import("command/input.zig").input }, .{ "list-input-configs", @import("command/input.zig").listInputConfigs}, .{ "list-inputs", @import("command/input.zig").listInputs }, diff --git a/river/command/cursor.zig b/river/command/cursor.zig new file mode 100644 index 0000000..efa2b2c --- /dev/null +++ b/river/command/cursor.zig @@ -0,0 +1,50 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2022 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 . + +const std = @import("std"); + +const util = @import("../util.zig"); + +const server = &@import("../main.zig").server; + +const Config = @import("../Config.zig"); +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +pub fn cursor( + _: *Seat, + args: []const [:0]const u8, + _: *?[]const u8, +) Error!void { + if (std.mem.eql(u8, "timeout", args[1])) { + if (args.len < 3) return Error.NotEnoughArguments; + if (args.len > 3) return Error.TooManyArguments; + server.config.cursor_hide_timeout = try std.fmt.parseInt(u31, args[2], 10); + var seat_it = server.input_manager.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + const seat = &seat_node.data; + seat.cursor.unhide(); + } + } else if (std.mem.eql(u8, "when-typing", args[1])) { + if (args.len < 3) return Error.NotEnoughArguments; + if (args.len > 3) return Error.TooManyArguments; + server.config.cursor_hide_when_typing = std.meta.stringToEnum(Config.HideCursorWhenTypingMode, args[2]) orelse + return Error.UnknownOption; + } else { + return Error.UnknownOption; + } +}