diff --git a/river/InputManager.zig b/river/InputManager.zig
index 345f109..f069257 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -28,10 +28,12 @@ const util = @import("util.zig");
const InputConfig = @import("InputConfig.zig");
const InputDevice = @import("InputDevice.zig");
+const InputRelay = @import("InputRelay.zig");
const Keyboard = @import("Keyboard.zig");
const PointerConstraint = @import("PointerConstraint.zig");
const Seat = @import("Seat.zig");
const Switch = @import("Switch.zig");
+const TextInput = @import("TextInput.zig");
const default_seat_name = "default";
@@ -44,6 +46,8 @@ relative_pointer_manager: *wlr.RelativePointerManagerV1,
virtual_pointer_manager: *wlr.VirtualPointerManagerV1,
virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1,
pointer_constraints: *wlr.PointerConstraintsV1,
+input_method_manager: *wlr.InputMethodManagerV2,
+text_input_manager: *wlr.TextInputManagerV3,
configs: std.ArrayList(InputConfig),
devices: wl.list.Head(InputDevice, .link),
@@ -57,6 +61,10 @@ new_virtual_keyboard: wl.Listener(*wlr.VirtualKeyboardV1) =
wl.Listener(*wlr.VirtualKeyboardV1).init(handleNewVirtualKeyboard),
new_constraint: wl.Listener(*wlr.PointerConstraintV1) =
wl.Listener(*wlr.PointerConstraintV1).init(handleNewConstraint),
+new_input_method: wl.Listener(*wlr.InputMethodV2) =
+ wl.Listener(*wlr.InputMethodV2).init(handleNewInputMethod),
+new_text_input: wl.Listener(*wlr.TextInputV3) =
+ wl.Listener(*wlr.TextInputV3).init(handleNewTextInput),
pub fn init(self: *Self) !void {
const seat_node = try util.gpa.create(std.TailQueue(Seat).Node);
@@ -69,6 +77,8 @@ pub fn init(self: *Self) !void {
.virtual_pointer_manager = try wlr.VirtualPointerManagerV1.create(server.wl_server),
.virtual_keyboard_manager = try wlr.VirtualKeyboardManagerV1.create(server.wl_server),
.pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server),
+ .input_method_manager = try wlr.InputMethodManagerV2.create(server.wl_server),
+ .text_input_manager = try wlr.TextInputManagerV3.create(server.wl_server),
.configs = std.ArrayList(InputConfig).init(util.gpa),
.devices = undefined,
@@ -84,6 +94,8 @@ pub fn init(self: *Self) !void {
self.virtual_pointer_manager.events.new_virtual_pointer.add(&self.new_virtual_pointer);
self.virtual_keyboard_manager.events.new_virtual_keyboard.add(&self.new_virtual_keyboard);
self.pointer_constraints.events.new_constraint.add(&self.new_constraint);
+ self.input_method_manager.events.input_method.add(&self.new_input_method);
+ self.text_input_manager.events.text_input.add(&self.new_text_input);
}
pub fn deinit(self: *Self) void {
@@ -93,6 +105,8 @@ pub fn deinit(self: *Self) void {
self.new_virtual_pointer.link.remove();
self.new_virtual_keyboard.link.remove();
self.new_constraint.link.remove();
+ self.new_input_method.link.remove();
+ self.new_text_input.link.remove();
while (self.seats.pop()) |seat_node| {
seat_node.data.deinit();
@@ -158,3 +172,51 @@ fn handleNewConstraint(
wlr_constraint.resource.postNoMemory();
};
}
+
+fn handleNewInputMethod(
+ _: *wl.Listener(*wlr.InputMethodV2),
+ input_method: *wlr.InputMethodV2,
+) void {
+ //const self = @fieldParentPtr(Self, "new_input_method", listener);
+ const seat = @as(*Seat, @ptrFromInt(input_method.seat.data));
+ const relay = &seat.relay;
+
+ // Only one input_method can be bound to a seat.
+ if (relay.input_method != null) {
+ log.debug("attempted to connect second input method to a seat", .{});
+ input_method.sendUnavailable();
+ return;
+ }
+
+ relay.input_method = input_method;
+
+ input_method.events.commit.add(&relay.input_method_commit);
+ input_method.events.grab_keyboard.add(&relay.grab_keyboard);
+ input_method.events.destroy.add(&relay.input_method_destroy);
+
+ log.debug("new input method on seat {s}", .{relay.seat.wlr_seat.name});
+
+ const text_input = relay.getFocusableTextInput() orelse return;
+ if (text_input.pending_focused_surface) |surface| {
+ text_input.wlr_text_input.sendEnter(surface);
+ text_input.setPendingFocusedSurface(null);
+ }
+}
+
+fn handleNewTextInput(
+ _: *wl.Listener(*wlr.TextInputV3),
+ wlr_text_input: *wlr.TextInputV3,
+) void {
+ //const self = @fieldParentPtr(Self, "new_text_input", listener);
+ const seat = @as(*Seat, @ptrFromInt(wlr_text_input.seat.data));
+ const relay = &seat.relay;
+
+ const text_input_node = util.gpa.create(std.TailQueue(TextInput).Node) catch {
+ wlr_text_input.resource.postNoMemory();
+ return;
+ };
+ text_input_node.data.init(relay, wlr_text_input);
+ relay.text_inputs.append(text_input_node);
+
+ log.debug("new text input on seat {s}", .{relay.seat.wlr_seat.name});
+}
diff --git a/river/InputRelay.zig b/river/InputRelay.zig
new file mode 100644
index 0000000..8544332
--- /dev/null
+++ b/river/InputRelay.zig
@@ -0,0 +1,218 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2021 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 Self = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const mem = std.mem;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+
+const util = @import("util.zig");
+
+const TextInput = @import("TextInput.zig");
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.input_relay);
+
+/// The Relay structure manages the communication between text_inputs
+/// and input_method on a given seat.
+seat: *Seat,
+
+/// List of all TextInput bound to the relay.
+/// Multiple wlr_text_input interfaces can be bound to a relay,
+/// but only one at a time can receive events.
+text_inputs: std.TailQueue(TextInput) = .{},
+
+input_method: ?*wlr.InputMethodV2 = null,
+
+input_method_commit: wl.Listener(*wlr.InputMethodV2) =
+ wl.Listener(*wlr.InputMethodV2).init(handleInputMethodCommit),
+grab_keyboard: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) =
+ wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboard),
+input_method_destroy: wl.Listener(*wlr.InputMethodV2) =
+ wl.Listener(*wlr.InputMethodV2).init(handleInputMethodDestroy),
+
+grab_keyboard_destroy: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) =
+ wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboardDestroy),
+
+pub fn init(self: *Self, seat: *Seat) void {
+ self.* = .{ .seat = seat };
+}
+
+fn handleInputMethodCommit(
+ listener: *wl.Listener(*wlr.InputMethodV2),
+ input_method: *wlr.InputMethodV2,
+) void {
+ const self = @fieldParentPtr(Self, "input_method_commit", listener);
+ const text_input = self.getFocusedTextInput() orelse return;
+
+ assert(input_method == self.input_method);
+
+ if (input_method.current.preedit.text) |preedit_text| {
+ text_input.wlr_text_input.sendPreeditString(
+ preedit_text,
+ input_method.current.preedit.cursor_begin,
+ input_method.current.preedit.cursor_end,
+ );
+ }
+
+ if (input_method.current.commit_text) |commit_text| {
+ text_input.wlr_text_input.sendCommitString(commit_text);
+ }
+
+ if (input_method.current.delete.before_length != 0 or
+ input_method.current.delete.after_length != 0)
+ {
+ text_input.wlr_text_input.sendDeleteSurroundingText(
+ input_method.current.delete.before_length,
+ input_method.current.delete.after_length,
+ );
+ }
+
+ text_input.wlr_text_input.sendDone();
+}
+
+fn handleInputMethodDestroy(
+ listener: *wl.Listener(*wlr.InputMethodV2),
+ input_method: *wlr.InputMethodV2,
+) void {
+ const self = @fieldParentPtr(Self, "input_method_destroy", listener);
+
+ assert(input_method == self.input_method);
+ self.input_method = null;
+
+ const text_input = self.getFocusedTextInput() orelse return;
+ if (text_input.wlr_text_input.focused_surface) |surface| {
+ text_input.setPendingFocusedSurface(surface);
+ }
+ text_input.wlr_text_input.sendLeave();
+}
+
+fn handleInputMethodGrabKeyboard(
+ listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab),
+ keyboard_grab: *wlr.InputMethodV2.KeyboardGrab,
+) void {
+ const self = @fieldParentPtr(Self, "grab_keyboard", listener);
+
+ const active_keyboard = self.seat.wlr_seat.getKeyboard() orelse return;
+ keyboard_grab.setKeyboard(active_keyboard);
+ // sway says 'send modifier state to grab' but doesn't seem to do this send_modifiers
+ keyboard_grab.sendModifiers(&active_keyboard.modifiers);
+
+ keyboard_grab.events.destroy.add(&self.grab_keyboard_destroy);
+}
+
+fn handleInputMethodGrabKeyboardDestroy(
+ listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab),
+ keyboard_grab: *wlr.InputMethodV2.KeyboardGrab,
+) void {
+ const self = @fieldParentPtr(Self, "grab_keyboard_destroy", listener);
+ self.grab_keyboard_destroy.link.remove();
+
+ if (keyboard_grab.keyboard) |keyboard| {
+ keyboard_grab.input_method.seat.keyboardNotifyModifiers(&keyboard.modifiers);
+ }
+}
+
+pub fn getFocusableTextInput(self: *Self) ?*TextInput {
+ var it = self.text_inputs.first;
+ return while (it) |node| : (it = node.next) {
+ const text_input = &node.data;
+ if (text_input.pending_focused_surface != null) break text_input;
+ } else null;
+}
+
+pub fn getFocusedTextInput(self: *Self) ?*TextInput {
+ var it = self.text_inputs.first;
+ return while (it) |node| : (it = node.next) {
+ const text_input = &node.data;
+ if (text_input.wlr_text_input.focused_surface != null) break text_input;
+ } else null;
+}
+
+pub fn disableTextInput(self: *Self, text_input: *TextInput) void {
+ if (self.input_method) |im| {
+ im.sendDeactivate();
+ } else {
+ log.debug("disabling text input but input method is gone", .{});
+ return;
+ }
+
+ self.sendInputMethodState(text_input.wlr_text_input);
+}
+
+pub fn sendInputMethodState(self: *Self, wlr_text_input: *wlr.TextInputV3) void {
+ const input_method = self.input_method orelse return;
+
+ // TODO: only send each of those if they were modified
+
+ if (wlr_text_input.active_features.surrounding_text) {
+ if (wlr_text_input.current.surrounding.text) |text| {
+ input_method.sendSurroundingText(
+ text,
+ wlr_text_input.current.surrounding.cursor,
+ wlr_text_input.current.surrounding.anchor,
+ );
+ }
+ }
+
+ input_method.sendTextChangeCause(wlr_text_input.current.text_change_cause);
+
+ if (wlr_text_input.active_features.content_type) {
+ input_method.sendContentType(
+ wlr_text_input.current.content_type.hint,
+ wlr_text_input.current.content_type.purpose,
+ );
+ }
+
+ input_method.sendDone();
+ // TODO: pass intent, display popup size
+}
+
+/// Update the current focused surface. Surface must belong to the same seat.
+pub fn setSurfaceFocus(self: *Self, wlr_surface: ?*wlr.Surface) void {
+ var it = self.text_inputs.first;
+ while (it) |node| : (it = node.next) {
+ const text_input = &node.data;
+ if (text_input.pending_focused_surface) |surface| {
+ assert(text_input.wlr_text_input.focused_surface == null);
+ if (wlr_surface != surface) {
+ text_input.setPendingFocusedSurface(null);
+ }
+ } else if (text_input.wlr_text_input.focused_surface) |surface| {
+ assert(text_input.pending_focused_surface == null);
+ if (wlr_surface != surface) {
+ text_input.relay.disableTextInput(text_input);
+ text_input.wlr_text_input.sendLeave();
+ } else {
+ log.debug("IM relay setSurfaceFocus already focused", .{});
+ continue;
+ }
+ }
+ if (wlr_surface) |surface| {
+ if (text_input.wlr_text_input.resource.getClient() == surface.resource.getClient()) {
+ if (self.input_method != null) {
+ text_input.wlr_text_input.sendEnter(surface);
+ } else {
+ text_input.setPendingFocusedSurface(surface);
+ }
+ }
+ }
+ }
+}
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index 568117d..d637d54 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -116,7 +116,21 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
assert(handled);
}
- const eaten = if (released) self.eaten_keycodes.remove(event.keycode) else mapped;
+ // Handle IM grab
+ const keyboard_grab = self.getInputMethodGrab();
+ const grabbed = !mapped and (keyboard_grab != null);
+ if (grabbed) ungrab: {
+ if (!released) {
+ self.eaten_keycodes.add(event.keycode);
+ } else if (!self.eaten_keycodes.present(event.keycode)) {
+ break :ungrab;
+ }
+
+ keyboard_grab.?.setKeyboard(keyboard_grab.?.keyboard);
+ keyboard_grab.?.sendKey(event.time_msec, event.keycode, event.state);
+ }
+
+ const eaten = if (released) self.eaten_keycodes.remove(event.keycode) else (mapped or grabbed);
if (!eaten) {
// If key was not handled, we pass it along to the client.
@@ -137,8 +151,13 @@ fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void
// If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
if (wlr_keyboard.group != null) return;
- self.device.seat.wlr_seat.setKeyboard(self.device.wlr_device.toKeyboard());
- self.device.seat.wlr_seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers);
+ if (self.getInputMethodGrab()) |keyboard_grab| {
+ keyboard_grab.setKeyboard(keyboard_grab.keyboard);
+ keyboard_grab.sendModifiers(&wlr_keyboard.modifiers);
+ } else {
+ self.device.seat.wlr_seat.setKeyboard(self.device.wlr_device.toKeyboard());
+ self.device.seat.wlr_seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers);
+ }
}
/// Handle any builtin, harcoded compsitor mappings such as VT switching.
@@ -158,3 +177,17 @@ fn handleBuiltinMapping(keysym: xkb.Keysym) bool {
else => return false,
}
}
+
+/// Returns null if the keyboard is not grabbed by an input method,
+/// or if event is from virtual keyboard of the same client as grab.
+/// TODO: see https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/2322
+fn getInputMethodGrab(self: Self) ?*wlr.InputMethodV2.KeyboardGrab {
+ const input_method = self.device.seat.relay.input_method;
+ const virtual_keyboard = self.device.wlr_device.getVirtualKeyboard();
+ if (input_method == null or input_method.?.keyboard_grab == null or
+ (virtual_keyboard != null and
+ virtual_keyboard.?.resource.getClient() == input_method.?.keyboard_grab.?.resource.getClient()))
+ {
+ return null;
+ } else return input_method.?.keyboard_grab;
+}
diff --git a/river/KeycodeSet.zig b/river/KeycodeSet.zig
index a5a6b7c..b097e37 100644
--- a/river/KeycodeSet.zig
+++ b/river/KeycodeSet.zig
@@ -40,6 +40,11 @@ pub fn add(self: *Self, new: u32) void {
self.len += 1;
}
+pub fn present(self: *Self, value: u32) bool {
+ for (self.items[0..self.len]) |item| if (value == item) return true;
+ return false;
+}
+
pub fn remove(self: *Self, old: u32) bool {
for (self.items[0..self.len], 0..) |item, idx| if (old == item) {
self.len -= 1;
diff --git a/river/Seat.zig b/river/Seat.zig
index 3fe8693..fac49ae 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -31,6 +31,7 @@ const Cursor = @import("Cursor.zig");
const DragIcon = @import("DragIcon.zig");
const InputDevice = @import("InputDevice.zig");
const InputManager = @import("InputManager.zig");
+const InputRelay = @import("InputRelay.zig");
const Keyboard = @import("Keyboard.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
const KeycodeSet = @import("KeycodeSet.zig");
@@ -88,6 +89,9 @@ drag: enum {
touch,
} = .none,
+/// Relay for communication between text_input and input_method.
+relay: InputRelay = undefined,
+
request_set_selection: wl.Listener(*wlr.Seat.event.RequestSetSelection) =
wl.Listener(*wlr.Seat.event.RequestSetSelection).init(handleRequestSetSelection),
request_start_drag: wl.Listener(*wlr.Seat.event.RequestStartDrag) =
@@ -110,6 +114,7 @@ pub fn init(self: *Self, name: [*:0]const u8) !void {
self.wlr_seat.data = @intFromPtr(self);
try self.cursor.init(self);
+ self.relay.init(self);
self.wlr_seat.events.request_set_selection.add(&self.request_set_selection);
self.wlr_seat.events.request_start_drag.add(&self.request_start_drag);
@@ -260,6 +265,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
}
self.keyboardEnterOrLeave(target_surface);
+ self.relay.setSurfaceFocus(target_surface);
if (target_surface) |surface| {
const pointer_constraints = server.input_manager.pointer_constraints;
diff --git a/river/TextInput.zig b/river/TextInput.zig
new file mode 100644
index 0000000..bfd341b
--- /dev/null
+++ b/river/TextInput.zig
@@ -0,0 +1,129 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2021 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 Self = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+
+const util = @import("util.zig");
+
+const InputRelay = @import("InputRelay.zig");
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.text_input);
+
+relay: *InputRelay,
+wlr_text_input: *wlr.TextInputV3,
+
+/// Surface stored for when text-input can't receive an enter event immediately
+/// after getting focus. Cleared once text-input receive the enter event.
+pending_focused_surface: ?*wlr.Surface = null,
+
+enable: wl.Listener(*wlr.TextInputV3) =
+ wl.Listener(*wlr.TextInputV3).init(handleEnable),
+commit: wl.Listener(*wlr.TextInputV3) =
+ wl.Listener(*wlr.TextInputV3).init(handleCommit),
+disable: wl.Listener(*wlr.TextInputV3) =
+ wl.Listener(*wlr.TextInputV3).init(handleDisable),
+destroy: wl.Listener(*wlr.TextInputV3) =
+ wl.Listener(*wlr.TextInputV3).init(handleDestroy),
+
+pending_focused_surface_destroy: wl.Listener(*wlr.Surface) =
+ wl.Listener(*wlr.Surface).init(handlePendingFocusedSurfaceDestroy),
+
+pub fn init(self: *Self, relay: *InputRelay, wlr_text_input: *wlr.TextInputV3) void {
+ self.* = .{
+ .relay = relay,
+ .wlr_text_input = wlr_text_input,
+ };
+
+ wlr_text_input.events.enable.add(&self.enable);
+ wlr_text_input.events.commit.add(&self.commit);
+ wlr_text_input.events.disable.add(&self.disable);
+ wlr_text_input.events.destroy.add(&self.destroy);
+}
+
+fn handleEnable(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
+ const self = @fieldParentPtr(Self, "enable", listener);
+
+ if (self.relay.input_method) |im| {
+ im.sendActivate();
+ } else {
+ log.debug("enabling text input but input method is gone", .{});
+ return;
+ }
+
+ self.relay.sendInputMethodState(self.wlr_text_input);
+}
+
+fn handleCommit(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
+ const self = @fieldParentPtr(Self, "commit", listener);
+ if (!self.wlr_text_input.current_enabled) {
+ log.debug("inactive text input tried to commit an update", .{});
+ return;
+ }
+ log.debug("text input committed update", .{});
+ if (self.relay.input_method == null) {
+ log.debug("committed text input but input method is gone", .{});
+ return;
+ }
+ self.relay.sendInputMethodState(self.wlr_text_input);
+}
+
+fn handleDisable(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
+ const self = @fieldParentPtr(Self, "disable", listener);
+ if (self.wlr_text_input.focused_surface == null) {
+ log.debug("disabling text input, but no longer focused", .{});
+ return;
+ }
+ self.relay.disableTextInput(self);
+}
+
+fn handleDestroy(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
+ const self = @fieldParentPtr(Self, "destroy", listener);
+ const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
+
+ if (self.wlr_text_input.current_enabled) self.relay.disableTextInput(self);
+
+ node.data.setPendingFocusedSurface(null);
+
+ self.enable.link.remove();
+ self.commit.link.remove();
+ self.disable.link.remove();
+ self.destroy.link.remove();
+
+ self.relay.text_inputs.remove(node);
+ util.gpa.destroy(node);
+}
+
+fn handlePendingFocusedSurfaceDestroy(listener: *wl.Listener(*wlr.Surface), surface: *wlr.Surface) void {
+ const self = @fieldParentPtr(Self, "pending_focused_surface_destroy", listener);
+ assert(self.pending_focused_surface == surface);
+ self.pending_focused_surface = null;
+ self.pending_focused_surface_destroy.link.remove();
+}
+
+pub fn setPendingFocusedSurface(self: *Self, wlr_surface: ?*wlr.Surface) void {
+ if (self.pending_focused_surface != null) self.pending_focused_surface_destroy.link.remove();
+ self.pending_focused_surface = wlr_surface;
+ if (self.pending_focused_surface) |surface| {
+ surface.events.destroy.add(&self.pending_focused_surface_destroy);
+ }
+}