river: Implement input_method and text_input

This commit is contained in:
praschke 2023-07-11 13:22:05 +01:00 committed by Isaac Freund
parent 3aba3abbcd
commit 2abab1e9c7
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
6 changed files with 456 additions and 3 deletions

View File

@ -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});
}

218
river/InputRelay.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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);
}
}
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

129
river/TextInput.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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);
}
}