Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
protocol: implement river-input-management-v1
build.zig | 2 +
build.zig.zon | 4 +-
protocol/river-input-management-v1.xml | 233 +++++++++++++++++++++++++++++++++
river/Config.zig | 9 --
river/Cursor.zig | 10 +-
river/InputDevice.zig | 202 +++++++++++++++++++++-------
river/InputManager.zig | 103 ++++++++++++---
river/Keyboard.zig | 109 ++++++++++-----
river/KeyboardGroup.zig | 10 +-
river/Output.zig | 9 ++
river/OutputManager.zig | 2 -
river/PointerBinding.zig | 13 ++
river/Seat.zig | 103 ++++++++++++---
river/Server.zig | 1 +
river/Tablet.zig | 9 +-
river/TabletTool.zig | 20 ++-
river/WindowManager.zig | 1 +
river/XkbBinding.zig | 13 ++
18 files changed, 707 insertions(+), 146 deletions(-)
diff --git a/build.zig b/build.zig
index 2cc435f..a689d0d 100644
--- a/build.zig
+++ b/build.zig
@@ -79,6 +79,7 @@ pub fn build(b: *Build) !void {
scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml"));
+ scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/wlr-output-power-management-unstable-v1.xml"));
@@ -108,6 +109,7 @@ pub fn build(b: *Build) !void {
scanner.generate("river_window_manager_v1", 2);
scanner.generate("river_xkb_bindings_v1", 1);
scanner.generate("river_layer_shell_v1", 1);
+ scanner.generate("river_input_manager_v1", 1);
scanner.generate("zwlr_output_power_manager_v1", 1);
scanner.generate("zwlr_layer_shell_v1", 4);
diff --git a/build.zig.zon b/build.zig.zon
index e27b6b6..a5a1fd2 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -14,8 +14,8 @@
.hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
},
.wayland = .{
- .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz",
- .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27",
+ .url = "git+https://codeberg.org/ifreund/zig-wayland?ref=main#52b54b71f1665b1b7ba61bf71a9319726e618fb6",
+ .hash = "wayland-0.5.0-dev-lQa1ki3YAQCEdP63PCLFnVQzfXAixkrXsPavlEl8UkPe",
},
.wlroots = .{
.url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz",
diff --git a/protocol/river-input-management-v1.xml b/protocol/river-input-management-v1.xml
new file mode 100644
index 0000000..62b04a3
--- /dev/null
+++ b/protocol/river-input-management-v1.xml
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_input_management_v1">
+ <copyright>
+ Copyright 2025 The River Developers
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ </copyright>
+
+ <description summary="configure input devices">
+ This protocol supports creating/destroying seats, assigning input devices to
+ seats, and configuring input devices (e.g. setting keyboard repeat rate).
+
+ The key words "must", "must not", "required", "shall", "shall not",
+ "should", "should not", "recommended", "may", and "optional" in this
+ document are to be interpreted as described in IETF RFC 2119.
+
+ Warning! The protocol described in this file is currently in the testing
+ phase. Backward compatible changes may be added together with the
+ corresponding interface version bump. Backward incompatible changes can only
+ be done by creating a new major version of the extension.
+ </description>
+
+ <interface name="river_input_manager_v1" version="1">
+ <description summary="input manager global interface">
+ Input manager global interface.
+ </description>
+
+ <enum name="error">
+ <entry name="invalid_destroy" value="0"/>
+ </enum>
+
+ <request name="stop">
+ <description summary="stop sending events">
+ This request indicates that the client no longer wishes to receive
+ events on this object.
+
+ The Wayland protocol is asynchronous, which means the server may send
+ further events until the stop request is processed. The client must wait
+ for a river_input_manager_v1.finished event before destroying this
+ object.
+ </description>
+ </request>
+
+ <event name="finished">
+ <description summary="the server has finished with the input manager">
+ This event indicates that the server will send no further events on this
+ object. The client should destroy the object. See
+ river_input_manager_v1.destroy for more information.
+ </description>
+ </event>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the river_input_manager_v1 object">
+ This request should be called after the finished event has been received
+ to complete destruction of the object.
+
+ It is a protocol error to make this request before the finished event
+ has been received.
+
+ If a client wishes to destroy this object it should send a
+ river_input_manager_v1.stop request and wait for a
+ river_input_manager_v1.finished event. Once the finished event is
+ received it is safe to destroy this object and any other objects created
+ through this interface.
+ </description>
+ </request>
+
+ <request name="create_seat">
+ <description summary="create a new seat">
+ Create a new seat with the given name. Has no effect if a seat with the
+ given name already exists.
+
+ The default seat with name "default" always exists and does not need to
+ be explicitly created.
+ </description>
+ <arg name="name" type="string"/>
+ </request>
+
+ <request name="destroy_seat">
+ <description summary="destroy a seat">
+ Destroy the seat with the given name. Has no effect if a seat with the
+ given name does not exist.
+
+ The default seat with name "default" cannot be destroyed and attempting
+ to destroy it will have no effect.
+
+ Any input devices assigned to the destroyed seat at the time of
+ destruction are assigned to the default seat.
+ </description>
+ <arg name="name" type="string"/>
+ </request>
+
+ <event name="input_device">
+ <description summary="new input device">
+ A new input device has been created.
+ </description>
+ <arg name="id" type="new_id" interface="river_input_device_v1"/>
+ </event>
+ </interface>
+
+ <interface name="river_input_device_v1" version="1">
+ <description summary="an input device">
+ An input device represents a physical keyboard, mouse, touchscreen, or
+ drawing tablet tool. It is assigned to exactly one seat at a time.
+ By default, all input devices are assigned to the default seat.
+ </description>
+
+ <enum name="error">
+ <entry name="invalid_repeat_info" value="0"/>
+ <entry name="invalid_scroll_factor" value="1"/>
+ <entry name="invalid_map_to_rectangle" value="2"/>
+ </enum>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the input device object">
+ This request indicates that the client will no longer use the input
+ device object and that it may be safely destroyed.
+ </description>
+ </request>
+
+ <event name="removed">
+ <description summary="the input device is removed">
+ This event indicates that the input device has been removed.
+
+ The server will send no further events on this object and ignore any
+ request (other than river_input_device_v1.destroy) made after this event is
+ sent. The client should destroy this object with the
+ river_input_device_v1.destroy request to free up resources.
+ </description>
+ </event>
+
+ <enum name="type">
+ <entry name="keyboard" value="0"/>
+ <entry name="pointer" value="1"/>
+ <entry name="touch" value="2"/>
+ <entry name="tablet" value="3"/>
+ </enum>
+
+ <event name="type">
+ <description summary="the type of the input device">
+ The type of the input device. This event is sent once when the
+ river_input_device_v1 object is created. The device type cannot
+ change during the lifetime of the object.
+ </description>
+ <arg name="type" type="uint" enum="type"/>
+ </event>
+
+ <event name="name">
+ <description summary="the name of the input device">
+ The name of the input device. This event is sent once when the
+ river_input_device_v1 object is created. The device name cannot
+ change during the lifetime of the object.
+ </description>
+ <arg name="name" type="string"/>
+ </event>
+
+ <request name="assign_to_seat">
+ <description summary="assign the input device to a seat">
+ Assign the input device to a seat. All input devices not explicitly
+ assigned to a seat are considered assigned to the default seat.
+
+ Has no effect if a seat with the given name does not exist.
+ </description>
+ <arg name="name" type="string" summary="name of the seat"/>
+ </request>
+
+ <request name="set_repeat_info">
+ <description summary="set keyboard repeat rate and delay">
+ Set repeat rate and delay for a keyboard input device. Has no effect if
+ the device is not a keyboard.
+
+ Negative values for either rate or delay are illegal. A rate of zero
+ will disable any repeating (regardless of the value of delay).
+ </description>
+ <arg name="rate" type="int" summary="rate in key repeats per second"/>
+ <arg name="delay" type="int" summary="delay in milliseconds"/>
+ </request>
+
+ <request name="set_scroll_factor">
+ <description summary="set scroll factor">
+ Set the scroll factor for a pointer input device. Has no effect if the
+ device is not a pointer.
+
+ For example, a factor of 0.5 will make scrolling twice as slow while a
+ factor of 3.0 will make scrolling 3 times as fast.
+
+ Negative values for either rate or delay are illegal. A rate of zero
+ will disable any repeating (regardless of the value of delay).
+ </description>
+ <arg name="factor" type="fixed"/>
+ </request>
+
+ <request name="map_to_output">
+ <description summary="map input device to the given output">
+ Map the input device to the given output. Has no effect if the device is
+ not a pointer, touch, or tablet device.
+
+ If mapped to both an output and a rectangle, the rectangle has priority.
+
+ Passing null clears an existing mapping.
+ </description>
+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
+ </request>
+
+ <request name="map_to_rectangle">
+ <description summary="map input device to the given rectangle">
+ Map the input device to the given rectangle in the global compositor
+ coordinate space. Has no effect if the device is not a pointer, touch,
+ or tablet device.
+
+ If mapped to both an output and a rectangle, the rectangle has priority.
+
+ Width and height must be greater than or equal to 0.
+
+ Passing 0 for width or height clears an existing mapping.
+ </description>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ <arg name="width" type="int"/>
+ <arg name="height" type="int"/>
+ </request>
+ </interface>
+</protocol>
diff --git a/river/Config.zig b/river/Config.zig
index 73fe9b4..06dc795 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -27,15 +27,6 @@ const util = @import("util.zig");
const Server = @import("Server.zig");
-/// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions)
-background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
-
-/// Keyboard repeat rate in characters per second
-repeat_rate: u31 = 25,
-
-/// Keyboard repeat delay in milliseconds
-repeat_delay: u31 = 600,
-
xkb_context: *xkb.Context,
/// The xkb keymap used for all keyboards
keymap: *xkb.Keymap,
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 6b61d4c..ad9b899 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -528,15 +528,7 @@ pub fn processAxis(cursor: *Cursor, event: *const wlr.Pointer.event.Axis) void {
event.time_msec,
event.orientation,
event.delta * device.config.scroll_factor,
- @intFromFloat(math.clamp(
- @round(@as(f32, @floatFromInt(event.delta_discrete)) * device.config.scroll_factor),
- // It seems that clamping to exactly the bounds of an i32 is insufficient to make the
- // @intFromFloat() call safe due to the max/min i32 not being exactly representable
- // by an f32. Dividing by 2 is a low effort way to ensure the value is in bounds and
- // allow users to set their scroll-factor to inf without crashing river.
- @as(f32, @floatFromInt(math.minInt(i32) / 2)),
- @as(f32, @floatFromInt(math.maxInt(i32) / 2)),
- )),
+ math.lossyCast(i32, @as(f64, @floatFromInt(event.delta_discrete)) * device.config.scroll_factor),
event.source,
event.relative_direction,
);
diff --git a/river/InputDevice.zig b/river/InputDevice.zig
index d42a6ee..0a5c66f 100644
--- a/river/InputDevice.zig
+++ b/river/InputDevice.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2022 The River Developers
+// Copyright 2022 - 2025 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
@@ -17,10 +17,11 @@
const InputDevice = @This();
const std = @import("std");
+const assert = std.debug.assert;
const mem = std.mem;
-const ascii = std.ascii;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
const c = @import("c.zig").c;
const server = &@import("main.zig").server;
@@ -34,69 +35,71 @@ const log = std.log.scoped(.input);
seat: *Seat,
wlr_device: *wlr.InputDevice,
+objects: wl.list.Head(river.InputDeviceV1, null),
-destroy: wl.Listener(*wlr.InputDevice) = .init(handleDestroy),
-
-/// Careful: The identifier is not unique! A physical input device may have
-/// multiple logical input devices with the exact same vendor id, product id
-/// and name. However identifiers of InputConfigs are unique.
-identifier: []const u8,
+remove: wl.Listener(*wlr.InputDevice) = .init(handleRemove),
config: struct {
- scroll_factor: f32 = 1.0,
+ scroll_factor: f64 = 1.0,
+ map_to_output: ?*wlr.Output = null,
+ map_to_rectangle: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
} = .{},
/// InputManager.devices
link: wl.list.Link,
-pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
- var vendor: c_uint = 0;
- var product: c_uint = 0;
-
- if (wlr_device.getLibinputDevice()) |d| {
- vendor = c.libinput_device_get_id_vendor(@ptrCast(d));
- product = c.libinput_device_get_id_product(@ptrCast(d));
- }
-
- const identifier = try std.fmt.allocPrint(
- util.gpa,
- "{s}-{}-{}-{s}",
- .{
- @tagName(wlr_device.type),
- vendor,
- product,
- mem.trim(u8, mem.sliceTo(wlr_device.name orelse "unknown", 0), &ascii.whitespace),
- },
- );
- errdefer util.gpa.free(identifier);
-
- for (identifier) |*char| {
- if (!ascii.isPrint(char.*) or ascii.isWhitespace(char.*)) {
- char.* = '_';
- }
- }
-
+pub fn init(
+ device: *InputDevice,
+ seat: *Seat,
+ wlr_device: *wlr.InputDevice,
+ virtual: bool,
+) !void {
device.* = .{
.seat = seat,
.wlr_device = wlr_device,
- .identifier = identifier,
+ .objects = undefined,
.link = undefined,
};
+ device.objects.init();
+ server.input_manager.devices.append(device);
wlr_device.data = device;
+ wlr_device.events.destroy.add(&device.remove);
- wlr_device.events.destroy.add(&device.destroy);
+ log.debug("new input device: {s}-{s}", .{
+ @tagName(wlr_device.type),
+ wlr_device.name orelse "unknown",
+ });
- server.input_manager.devices.append(device);
- seat.updateCapabilities();
+ if (!virtual) {
+ var it = server.input_manager.objects.safeIterator(.forward);
+ while (it.next()) |im_v1| device.createObject(im_v1);
+ }
+}
- log.debug("new input device: {s}", .{identifier});
+pub fn createObject(device: *InputDevice, im_v1: *river.InputManagerV1) void {
+ const device_type: river.InputDeviceV1.Type = switch (device.wlr_device.type) {
+ .keyboard => .keyboard,
+ .pointer => .pointer,
+ .touch => .touch,
+ .tablet => .tablet,
+ .@"switch", .tablet_pad => return,
+ };
+ const object = river.InputDeviceV1.create(im_v1.getClient(), im_v1.getVersion(), 0) catch {
+ log.err("out of memory", .{});
+ im_v1.postNoMemory();
+ return;
+ };
+ im_v1.sendInputDevice(object);
+ device.objects.append(object);
+ object.setHandler(*InputDevice, handleRequest, handleDestroy, device);
+ object.sendType(device_type);
+ object.sendName(device.wlr_device.name orelse "");
}
pub fn deinit(device: *InputDevice) void {
- device.destroy.link.remove();
-
- util.gpa.free(device.identifier);
+ assert(device.objects.empty());
+ device.remove.link.remove();
device.link.remove();
device.seat.updateCapabilities();
@@ -106,10 +109,31 @@ pub fn deinit(device: *InputDevice) void {
device.* = undefined;
}
-fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
- const device: *InputDevice = @fieldParentPtr("destroy", listener);
+pub fn assignToSeat(device: *InputDevice, new: *Seat) void {
+ const old = device.seat;
+ if (new == old) return;
+ old.detachDevice(device);
+ new.attachDevice(device);
+ old.updateCapabilities();
+ new.updateCapabilities();
+}
- log.debug("removed input device: {s}", .{device.identifier});
+fn handleRemove(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
+ const device: *InputDevice = @fieldParentPtr("remove", listener);
+
+ log.debug("removed input device: {s}-{s}", .{
+ @tagName(device.wlr_device.type),
+ device.wlr_device.name orelse "unknown",
+ });
+
+ {
+ var it = device.objects.iterator(.forward);
+ while (it.next()) |object| {
+ object.getLink().remove();
+ object.sendRemoved();
+ object.setHandler(?*anyopaque, handleRequestInert, null, null);
+ }
+ }
switch (device.wlr_device.type) {
.keyboard => {
@@ -127,3 +151,87 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice)
.@"switch", .tablet_pad => unreachable,
}
}
+
+fn handleRequestInert(
+ object: *river.InputDeviceV1,
+ request: river.InputDeviceV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) object.destroy();
+}
+
+fn handleDestroy(object: *river.InputDeviceV1, _: *InputDevice) void {
+ object.getLink().remove();
+}
+
+fn handleRequest(
+ object: *river.InputDeviceV1,
+ request: river.InputDeviceV1.Request,
+ device: *InputDevice,
+) void {
+ switch (request) {
+ .destroy => object.destroy(),
+ .assign_to_seat => |args| {
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (mem.orderZ(u8, args.name, seat.wlr_seat.name) == .eq) {
+ device.assignToSeat(seat);
+ }
+ }
+ log.info("client requested input device be assigned to non-existant seat '{s}'", .{args.name});
+ },
+ .set_repeat_info => |args| {
+ if (args.rate < 0 or args.delay < 0) {
+ object.postError(.invalid_repeat_info, "negative rate/delay");
+ return;
+ }
+ if (device.wlr_device.type == .keyboard) {
+ const keyboard: *Keyboard = @fieldParentPtr("device", device);
+ keyboard.setRepeatInfo(@intCast(args.rate), @intCast(args.delay));
+ }
+ },
+ .set_scroll_factor => |args| {
+ const factor = args.factor.toDouble();
+ if (factor < 0) {
+ object.postError(.invalid_scroll_factor, "negative scroll factor");
+ return;
+ }
+ device.config.scroll_factor = factor;
+ },
+ .map_to_output => |args| {
+ switch (device.wlr_device.type) {
+ .pointer, .touch, .tablet => {},
+ .keyboard, .@"switch", .tablet_pad => return,
+ }
+ if (args.output) |output| {
+ device.config.map_to_output = wlr.Output.fromWlOutput(output) orelse return;
+ } else {
+ device.config.map_to_output = null;
+ }
+ device.seat.cursor.wlr_cursor.mapInputToOutput(
+ device.wlr_device,
+ device.config.map_to_output,
+ );
+ },
+ .map_to_rectangle => |args| {
+ if (args.width < 0 or args.height < 0) {
+ object.postError(.invalid_map_to_rectangle, "negative rectangle width/height");
+ return;
+ }
+ switch (device.wlr_device.type) {
+ .pointer, .touch, .tablet => {},
+ .keyboard, .@"switch", .tablet_pad => return,
+ }
+ device.config.map_to_rectangle = .{
+ .x = args.x,
+ .y = args.y,
+ .width = args.width,
+ .height = args.height,
+ };
+ device.seat.cursor.wlr_cursor.mapInputToRegion(
+ device.wlr_device,
+ &device.config.map_to_rectangle,
+ );
+ },
+ }
+}
diff --git a/river/InputManager.zig b/river/InputManager.zig
index 8163dad..dea333e 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 - 2021 The River Developers
+// Copyright 2020 - 2025 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
@@ -22,6 +22,7 @@ const assert = std.debug.assert;
const mem = std.mem;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
const server = &@import("main.zig").server;
const util = @import("util.zig");
@@ -37,6 +38,9 @@ const default_seat_name = "default";
const log = std.log.scoped(.input);
+global: *wl.Global,
+objects: wl.list.Head(river.InputManagerV1, null),
+
new_input: wl.Listener(*wlr.InputDevice) = .init(handleNewInput),
idle_notifier: *wlr.IdleNotifierV1,
@@ -60,6 +64,7 @@ new_text_input: wl.Listener(*wlr.TextInputV3) = .init(handleNewTextInput),
pub fn init(input_manager: *InputManager) !void {
input_manager.* = .{
+ .global = try wl.Global.create(server.wl_server, river.InputManagerV1, 1, *InputManager, input_manager, bind),
// These are automatically freed when the display is destroyed
.idle_notifier = try wlr.IdleNotifierV1.create(server.wl_server),
.relative_pointer_manager = try wlr.RelativePointerManagerV1.create(server.wl_server),
@@ -71,9 +76,11 @@ pub fn init(input_manager: *InputManager) !void {
.text_input_manager = try wlr.TextInputManagerV3.create(server.wl_server),
.tablet_manager = try wlr.TabletManagerV2.create(server.wl_server),
+ .objects = undefined,
.devices = undefined,
.seats = undefined,
};
+ input_manager.objects.init();
input_manager.devices.init();
input_manager.seats.init();
@@ -94,7 +101,10 @@ pub fn init(input_manager: *InputManager) !void {
}
pub fn deinit(input_manager: *InputManager) void {
+ input_manager.global.destroy();
+
// This function must be called after the backend has been destroyed
+ assert(input_manager.objects.empty());
assert(input_manager.devices.empty());
input_manager.new_virtual_pointer.link.remove();
@@ -108,6 +118,76 @@ pub fn deinit(input_manager: *InputManager) void {
}
}
+fn bind(client: *wl.Client, im: *InputManager, version: u32, id: u32) void {
+ const im_v1 = river.InputManagerV1.create(client, version, id) catch {
+ client.postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ im_v1.setHandler(*InputManager, handleRequest, handleDestroy, im);
+ im.objects.append(im_v1);
+ {
+ var it = im.devices.iterator(.forward);
+ while (it.next()) |device| device.createObject(im_v1);
+ }
+}
+
+fn handleRequestInert(
+ im_v1: *river.InputManagerV1,
+ request: river.InputManagerV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) im_v1.destroy();
+}
+
+fn handleDestroy(im_v1: *river.InputManagerV1, _: *InputManager) void {
+ im_v1.getLink().remove();
+}
+
+fn handleRequest(
+ im_v1: *river.InputManagerV1,
+ request: river.InputManagerV1.Request,
+ im: *InputManager,
+) void {
+ switch (request) {
+ .stop => {
+ im_v1.getLink().remove();
+ im_v1.sendFinished();
+ im_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+ },
+ .destroy => {
+ im_v1.postError(.invalid_destroy, "destroy before finished event sent");
+ },
+ .create_seat => |args| {
+ var it = im.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (mem.orderZ(u8, args.name, seat.wlr_seat.name) == .eq) {
+ break;
+ }
+ } else {
+ Seat.create(args.name) catch |err| switch (err) {
+ error.OutOfMemory => {
+ im_v1.getClient().postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ },
+ };
+ }
+ },
+ .destroy_seat => |args| {
+ var it = im.seats.iterator(.forward);
+ _ = it.next(); // skip default seat
+ while (it.next()) |seat| {
+ if (mem.orderZ(u8, args.name, seat.wlr_seat.name) == .eq) {
+ seat.destroying = true;
+ server.wm.dirtyWindowing();
+ break;
+ }
+ }
+ },
+ }
+}
+
pub fn defaultSeat(input_manager: *InputManager) *Seat {
return input_manager.seats.first().?;
}
@@ -121,19 +201,10 @@ pub fn processEvents(input_manager: *InputManager) void {
}
}
-/// Reconfigures all devices' libinput configuration as well as their output mapping.
-/// This is called on outputs being added or removed and on the input configuration being changed.
-pub fn reconfigureDevices(input_manager: *InputManager) void {
- var it = input_manager.devices.iterator(.forward);
- while (it.next()) |device| {
- _ = device;
- }
-}
-
fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), wlr_device: *wlr.InputDevice) void {
const input_manager: *InputManager = @fieldParentPtr("new_input", listener);
- input_manager.defaultSeat().addDevice(wlr_device, false);
+ input_manager.defaultSeat().attachNewDevice(wlr_device, false);
}
fn handleNewVirtualPointer(
@@ -151,7 +222,7 @@ fn handleNewVirtualPointer(
log.debug("Ignoring output suggestion from virtual pointer", .{});
}
- input_manager.defaultSeat().addDevice(&event.new_pointer.pointer.base, true);
+ input_manager.defaultSeat().attachNewDevice(&event.new_pointer.pointer.base, true);
}
fn handleNewVirtualKeyboard(
@@ -175,10 +246,10 @@ fn handleNewVirtualKeyboard(
/// Yes, wlroots should probably do this for us.
const NoKeymapVirtKeyboard = struct {
virtual_keyboard: *wlr.VirtualKeyboardV1,
- destroy: wl.Listener(*wlr.InputDevice) = .init(handleDestroy),
+ destroy: wl.Listener(*wlr.InputDevice) = .init(handleVirtKeyboardDestroy),
keymap: wl.Listener(*wlr.Keyboard) = .init(handleKeymap),
- fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
+ fn handleVirtKeyboardDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("destroy", listener);
no_keymap.destroy.link.remove();
@@ -191,10 +262,10 @@ const NoKeymapVirtKeyboard = struct {
const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("keymap", listener);
const virtual_keyboard = no_keymap.virtual_keyboard;
- handleDestroy(&no_keymap.destroy, &virtual_keyboard.keyboard.base);
+ handleVirtKeyboardDestroy(&no_keymap.destroy, &virtual_keyboard.keyboard.base);
const seat: *Seat = @ptrCast(@alignCast(virtual_keyboard.seat.data));
- seat.addDevice(&virtual_keyboard.keyboard.base, true);
+ seat.attachNewDevice(&virtual_keyboard.keyboard.base, true);
}
};
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index 4d58844..c68451e 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -31,48 +31,52 @@ const Seat = @import("Seat.zig");
const log = std.log.scoped(.input);
+pub const Config = struct {
+ keymap: ?*xkb.Keymap,
+ /// Repeat rate in characters per second
+ repeat_rate: u31 = 80,
+ /// Repeat delay in milliseconds
+ repeat_delay: u31 = 300,
+
+ pub fn eql(a: *const Config, b: *const Config) bool {
+ // TODO this probably isn't a sufficient way to compare keymaps?
+ return a.keymap == b.keymap and
+ a.repeat_rate == b.repeat_rate and
+ a.repeat_delay == b.repeat_delay;
+ }
+};
+
device: InputDevice,
device_destroyed: bool = false,
queued_events: u32 = 0,
-group: *KeyboardGroup,
+virtual: bool,
+config: Config,
+/// Only null during initialization or due to allocation failure.
+group: ?*KeyboardGroup = null,
key: wl.Listener(*wlr.Keyboard.event.Key) = .init(queueKey),
modifiers: wl.Listener(*wlr.Keyboard) = .init(queueModifiers),
keymap: wl.Listener(*wlr.Keyboard) = .init(queueKeymap),
pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !*Keyboard {
+ const wlr_keyboard = wlr_device.toKeyboard();
+
const keyboard = try util.gpa.create(Keyboard);
errdefer util.gpa.destroy(keyboard);
keyboard.* = .{
+ .virtual = virtual,
+ .config = .{
+ .keymap = if (virtual) wlr_keyboard.keymap else server.config.keymap,
+ },
.device = undefined,
- .group = undefined,
};
- try keyboard.device.init(seat, wlr_device);
+ try keyboard.device.init(seat, wlr_device, virtual);
errdefer keyboard.device.deinit();
- const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
wlr_keyboard.data = keyboard;
- keyboard.group = blk: {
- if (virtual) {
- // Virtual keyboards set their own keymap and require independent modifier state.
- // Therefore, they are always placed in their own group of one.
- break :blk try KeyboardGroup.create(seat, wlr_keyboard.keymap, true);
- } else {
- var it = seat.keyboard_groups.iterator(.forward);
- while (it.next()) |group| {
- // TODO input configuration will require sorting keyboards into
- // groups based on keymap and repeat info.
- if (true) {
- break :blk group.ref();
- }
- }
- break :blk try KeyboardGroup.create(seat, server.config.keymap, false);
- }
- };
-
wlr_keyboard.events.key.add(&keyboard.key);
wlr_keyboard.events.modifiers.add(&keyboard.modifiers);
wlr_keyboard.events.keymap.add(&keyboard.keymap);
@@ -80,6 +84,47 @@ pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !*Keyboa
return keyboard;
}
+pub fn setGroup(keyboard: *Keyboard) void {
+ assert(keyboard.group == null);
+ const seat = keyboard.device.seat;
+ if (keyboard.virtual) {
+ // Virtual keyboards set their own keymap and require independent modifier state.
+ // Therefore, they are always placed in their own group of one.
+ keyboard.group = KeyboardGroup.create(seat, keyboard.config, true) catch |err| switch (err) {
+ error.OutOfMemory => blk: {
+ log.err("out of memory", .{});
+ break :blk null;
+ },
+ };
+ } else {
+ var it = seat.keyboard_groups.iterator(.forward);
+ while (it.next()) |group| {
+ if (keyboard.config.eql(&group.config)) {
+ keyboard.group = group.ref();
+ break;
+ }
+ } else {
+ keyboard.group = KeyboardGroup.create(seat, keyboard.config, false) catch |err| switch (err) {
+ error.OutOfMemory => blk: {
+ log.err("out of memory", .{});
+ break :blk null;
+ },
+ };
+ }
+ }
+}
+
+pub fn setRepeatInfo(keyboard: *Keyboard, rate: u31, delay: u31) void {
+ assert(!keyboard.virtual);
+ keyboard.config.repeat_rate = rate;
+ keyboard.config.repeat_delay = delay;
+ if (keyboard.group) |group| {
+ group.unref();
+ keyboard.group = null;
+ }
+ keyboard.setGroup();
+}
+
pub fn deviceDestroy(keyboard: *Keyboard) void {
assert(!keyboard.device_destroyed);
keyboard.device_destroyed = true;
@@ -98,28 +143,30 @@ fn maybeDestroy(keyboard: *Keyboard) void {
return;
}
- keyboard.group.unref();
+ if (keyboard.group) |group| group.unref();
util.gpa.destroy(keyboard);
}
-pub fn processKey(keyboard: *Keyboard, key: *const wlr.Keyboard.event.Key) void {
- keyboard.group.processKey(key);
+pub fn dropEvent(keyboard: *Keyboard) void {
keyboard.queued_events -= 1;
keyboard.maybeDestroy();
}
+pub fn processKey(keyboard: *Keyboard, key: *const wlr.Keyboard.event.Key) void {
+ if (keyboard.group) |group| group.processKey(key);
+ keyboard.dropEvent();
+}
+
pub fn processModifiers(keyboard: *Keyboard, modifiers: wlr.Keyboard.Modifiers) void {
- keyboard.group.processModifiers(modifiers);
- keyboard.queued_events -= 1;
- keyboard.maybeDestroy();
+ if (keyboard.group) |group| group.processModifiers(modifiers);
+ keyboard.dropEvent();
}
pub fn processKeymap(keyboard: *Keyboard, keymap: *xkb.Keymap) void {
defer keymap.unref();
- keyboard.group.processKeymap(keymap);
- keyboard.queued_events -= 1;
- keyboard.maybeDestroy();
+ if (keyboard.group) |group| group.processKeymap(keymap);
+ keyboard.dropEvent();
}
fn queueKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig
index 23ab6fa..72af126 100644
--- a/river/KeyboardGroup.zig
+++ b/river/KeyboardGroup.zig
@@ -25,6 +25,7 @@ const xkb = @import("xkbcommon");
const server = &@import("main.zig").server;
const util = @import("util.zig");
+const Keyboard = @import("Keyboard.zig");
const Seat = @import("Seat.zig");
const XkbBinding = @import("XkbBinding.zig");
@@ -66,6 +67,8 @@ link: wl.list.Link,
virtual: bool,
+config: Keyboard.Config,
+
/// This is the keyboard that actually gets passed to wlr_seat functions for
/// setting keyboard focus.
state: wlr.Keyboard,
@@ -77,12 +80,13 @@ pressed: std.AutoArrayHashMapUnmanaged(u32, Press) = .empty,
key: wl.Listener(*wlr.Keyboard.event.Key) = .init(handleKey),
modifiers: wl.Listener(*wlr.Keyboard) = .init(handleModifiers),
-pub fn create(seat: *Seat, keymap: ?*xkb.Keymap, virtual: bool) !*KeyboardGroup {
+pub fn create(seat: *Seat, config: Keyboard.Config, virtual: bool) !*KeyboardGroup {
const group = try util.gpa.create(KeyboardGroup);
errdefer util.gpa.destroy(group);
group.* = .{
.seat = seat,
.virtual = virtual,
+ .config = config,
.state = undefined,
.link = undefined,
};
@@ -99,8 +103,8 @@ pub fn create(seat: *Seat, keymap: ?*xkb.Keymap, virtual: bool) !*KeyboardGroup
group.state.data = group;
// wlroots will log an error on failure, there's not much we can do to recover unfortunately.
- _ = group.state.setKeymap(keymap);
- group.state.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay);
+ _ = group.state.setKeymap(config.keymap);
+ group.state.setRepeatInfo(config.repeat_rate, config.repeat_delay);
group.state.events.key.add(&group.key);
group.state.events.modifiers.add(&group.modifiers);
diff --git a/river/Output.zig b/river/Output.zig
index 450d67d..763939d 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -234,6 +234,15 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
}
}
}
+ {
+ var it = server.input_manager.devices.iterator(.forward);
+ while (it.next()) |device| {
+ if (device.config.map_to_output == wlr_output) {
+ device.config.map_to_output = null;
+ device.seat.cursor.wlr_cursor.mapInputToOutput(device.wlr_device, null);
+ }
+ }
+ }
output.destroy.link.remove();
output.request_state.link.remove();
diff --git a/river/OutputManager.zig b/river/OutputManager.zig
index 0e6e472..88cd1eb 100644
--- a/river/OutputManager.zig
+++ b/river/OutputManager.zig
@@ -282,8 +282,6 @@ pub fn commitOutputState(om: *OutputManager) void {
}
}
- server.input_manager.reconfigureDevices();
-
const need_modeset = blk: {
var it = wm.sent.outputs.iterator(.forward);
while (it.next()) |output| {
diff --git a/river/PointerBinding.zig b/river/PointerBinding.zig
index 4da340c..26bb395 100644
--- a/river/PointerBinding.zig
+++ b/river/PointerBinding.zig
@@ -88,6 +88,19 @@ pub fn create(
seat.pointer_bindings.append(binding);
}
+pub fn destroy(binding: *PointerBinding) void {
+ binding.object.setHandler(?*anyopaque, handleRequestInert, null, null);
+ handleDestroy(binding.object, binding);
+}
+
+fn handleRequestInert(
+ pointer_binding_v1: *river.PointerBindingV1,
+ request: river.PointerBindingV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) pointer_binding_v1.destroy();
+}
+
fn handleDestroy(_: *river.PointerBindingV1, binding: *PointerBinding) void {
if (binding.seat.cursor.pressed.getPtr(binding.button)) |value_ptr| {
// It is possible for the window manager to create duplicate pointer bindings.
diff --git a/river/Seat.zig b/river/Seat.zig
index 8f8da4c..1087d26 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -197,7 +197,6 @@ pub fn create(name: [*:0]const u8) !void {
errdefer event_queue.deinit(util.gpa);
seat.* = .{
- // This will be automatically destroyed when the display is destroyed
.wlr_seat = try wlr.Seat.create(server.wl_server, name),
.event_queue = event_queue,
.link = undefined,
@@ -229,10 +228,44 @@ pub fn create(name: [*:0]const u8) !void {
}
pub fn destroy(seat: *Seat) void {
+ while (seat.event_queue.popFront()) |event| {
+ switch (event) {
+ .keyboard_key => |data| data.keyboard.dropEvent(),
+ .keyboard_modifiers => |data| data.keyboard.dropEvent(),
+ .keyboard_keymap => |data| {
+ data.keyboard.dropEvent();
+ data.keymap.unref();
+ },
+ .pointer_motion_relative,
+ .pointer_motion_absolute,
+ .pointer_button,
+ .pointer_axis,
+ .pointer_frame,
+ .pointer_swipe_begin,
+ .pointer_swipe_update,
+ .pointer_swipe_end,
+ .pointer_pinch_begin,
+ .pointer_pinch_update,
+ .pointer_pinch_end,
+ => {},
+ }
+ }
+ {
+ var it = server.input_manager.devices.iterator(.forward);
+ while (it.next()) |device| {
+ if (device.seat == seat) {
+ device.assignToSeat(server.input_manager.defaultSeat());
+ }
+ }
+ }
{
var it = server.input_manager.devices.iterator(.forward);
while (it.next()) |device| assert(device.seat != seat);
}
+ assert(seat.keyboard_groups.empty());
+
+ while (seat.xkb_bindings.first()) |binding| binding.destroy();
+ while (seat.pointer_bindings.first()) |binding| binding.destroy();
seat.link.remove();
seat.link_sent.remove();
@@ -245,6 +278,8 @@ pub fn destroy(seat: *Seat) void {
seat.start_drag.link.remove();
if (seat.drag != .none) seat.drag_destroy.link.remove();
seat.request_set_primary_selection.link.remove();
+
+ seat.wlr_seat.destroy();
}
pub fn queueEvent(seat: *Seat, event: Event) !void {
@@ -738,36 +773,68 @@ pub fn opEnd(seat: *Seat) void {
}
}
-pub fn addDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) void {
- seat.tryAddDevice(wlr_device, virtual) catch |err| switch (err) {
- error.OutOfMemory => log.err("out of memory", .{}),
+pub fn attachNewDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) void {
+ const device = seat.createDevice(wlr_device, virtual) catch |err| switch (err) {
+ error.OutOfMemory => {
+ log.err("out of memory", .{});
+ return;
+ },
};
+ if (device) |d| {
+ seat.attachDevice(d);
+ seat.updateCapabilities();
+ }
}
-fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void {
+fn createDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !?*InputDevice {
switch (wlr_device.type) {
.keyboard => {
const keyboard = try Keyboard.create(seat, wlr_device, virtual);
-
- seat.wlr_seat.setKeyboard(&keyboard.group.state);
- if (seat.wlr_seat.keyboard_state.focused_surface) |wlr_surface| {
- seat.keyboardNotifyEnter(wlr_surface);
- }
+ return &keyboard.device;
},
.pointer, .touch => {
const device = try util.gpa.create(InputDevice);
errdefer util.gpa.destroy(device);
-
- try device.init(seat, wlr_device);
-
- seat.cursor.wlr_cursor.attachInputDevice(wlr_device);
+ try device.init(seat, wlr_device, virtual);
+ return device;
},
.tablet => {
- try Tablet.create(seat, wlr_device);
- seat.cursor.wlr_cursor.attachInputDevice(wlr_device);
+ const tablet = try Tablet.create(seat, wlr_device, virtual);
+ return &tablet.device;
+ },
+ .@"switch", .tablet_pad => return null, // unsupported
+ }
+}
+
+pub fn attachDevice(seat: *Seat, device: *InputDevice) void {
+ device.seat = seat;
+ switch (device.wlr_device.type) {
+ .keyboard => {
+ const keyboard: *Keyboard = @fieldParentPtr("device", device);
+ keyboard.setGroup();
+ if (keyboard.group) |group| {
+ seat.wlr_seat.setKeyboard(&group.state);
+ if (seat.wlr_seat.keyboard_state.focused_surface) |wlr_surface| {
+ seat.keyboardNotifyEnter(wlr_surface);
+ }
+ }
},
- // TODO Support these types of input devices.
- .@"switch", .tablet_pad => {},
+ .pointer, .touch, .tablet => {
+ seat.cursor.wlr_cursor.attachInputDevice(device.wlr_device);
+ },
+ .@"switch", .tablet_pad => unreachable, // unsupported
+ }
+}
+
+pub fn detachDevice(seat: *Seat, device: *InputDevice) void {
+ seat.cursor.wlr_cursor.detachInputDevice(device.wlr_device);
+
+ if (device.wlr_device.type == .keyboard) {
+ const keyboard: *Keyboard = @fieldParentPtr("device", device);
+ if (keyboard.group) |group| {
+ group.unref();
+ keyboard.group = null;
+ }
}
}
diff --git a/river/Server.zig b/river/Server.zig
index fa4a051..8b65066 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -335,6 +335,7 @@ fn blocklist(server: *Server, global: *const wl.Global) bool {
global == server.om.wlr_output_manager.global or
global == server.om.power_manager.global or
global == server.om.gamma_control_manager.global or
+ global == server.input_manager.global or
global == server.input_manager.idle_notifier.global or
global == server.input_manager.virtual_pointer_manager.global or
global == server.input_manager.virtual_keyboard_manager.global or
diff --git a/river/Tablet.zig b/river/Tablet.zig
index 8d3f0da..e3d97ae 100644
--- a/river/Tablet.zig
+++ b/river/Tablet.zig
@@ -25,14 +25,11 @@ const util = @import("util.zig");
const InputDevice = @import("InputDevice.zig");
const Seat = @import("Seat.zig");
-const TabletTool = @import("TabletTool.zig");
device: InputDevice,
wp_tablet: *wlr.TabletV2Tablet,
-output_mapping: ?*wlr.Output = null,
-
-pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice) !void {
+pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !*Tablet {
assert(wlr_device.type == .tablet);
const tablet = try util.gpa.create(Tablet);
@@ -44,8 +41,10 @@ pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice) !void {
.device = undefined,
.wp_tablet = try tablet_manager.createTabletV2Tablet(seat.wlr_seat, wlr_device),
};
- try tablet.device.init(seat, wlr_device);
+ try tablet.device.init(seat, wlr_device, virtual);
errdefer tablet.device.deinit();
+
+ return tablet;
}
pub fn destroy(tablet: *Tablet) void {
diff --git a/river/TabletTool.zig b/river/TabletTool.zig
index 34b106d..7abaa2b 100644
--- a/river/TabletTool.zig
+++ b/river/TabletTool.zig
@@ -126,9 +126,21 @@ fn handleSetCursor(
}
}
-pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) void {
+/// Must be called before moving/warping TabletTool.wlr_cursor
+/// Must call detach() after attach() is called before returning.
+fn attach(tool: *TabletTool, tablet: *Tablet) void {
tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
- tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
+ tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.device.config.map_to_output);
+ tool.wlr_cursor.mapInputToRegion(tablet.device.wlr_device, &tablet.device.config.map_to_rectangle);
+}
+
+fn detach(tool: *TabletTool, tablet: *Tablet) void {
+ tool.wlr_cursor.detachInputDevice(tablet.device.wlr_device);
+}
+
+pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) void {
+ tool.attach(tablet);
+ defer tool.detach(tablet);
if (event.updated_axes.x or event.updated_axes.y) {
// I don't own all these different types of tablet tools to test that this
@@ -185,8 +197,8 @@ pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) v
pub fn proximity(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Proximity) void {
switch (event.state) {
.in => {
- tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
- tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
+ tool.attach(tablet);
+ defer tool.detach(tablet);
tool.wlr_cursor.warpAbsolute(tablet.device.wlr_device, event.x, event.y);
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 92da31b..fb90d81 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -165,6 +165,7 @@ fn handleRequest(
wm_v1.sendFinished();
wm_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
},
+ // TODO send protocol error to avoid leak on race
.destroy => wm_v1.destroy(),
.manage_finish => {
if (wm.state != .manage) {
diff --git a/river/XkbBinding.zig b/river/XkbBinding.zig
index 6e0cb8a..2dcb75a 100644
--- a/river/XkbBinding.zig
+++ b/river/XkbBinding.zig
@@ -96,6 +96,19 @@ pub fn create(
seat.xkb_bindings.append(binding);
}
+pub fn destroy(binding: *XkbBinding) void {
+ binding.object.setHandler(?*anyopaque, handleRequestInert, null, null);
+ handleDestroy(binding.object, binding);
+}
+
+fn handleRequestInert(
+ xkb_binding_v1: *river.XkbBindingV1,
+ request: river.XkbBindingV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) xkb_binding_v1.destroy();
+}
+
fn handleDestroy(_: *river.XkbBindingV1, binding: *XkbBinding) void {
{
var it = binding.seat.keyboard_groups.iterator(.forward);