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