This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch wl/browser-all
in repository enlightenment.
View the commit online.
commit 035d556b3170a26e339a1820b6284f4b813af6aa
Author: Cedric BAIL <[email protected]>
AuthorDate: Wed Aug 19 10:00:55 2026 -0600
e_comp_wl - the input method keyboard grab, and the way back
An input method that composes text from keystrokes - which is every CJK
one - needs the keys before the application does. This is the half of
E-13 that was left out when the protocol pair landed, and it is two
protocols because it has to be:
zwp_input_method_v2.grab_keyboard takes the keys, and
zwp_virtual_keyboard_v1 returns the ones the input method did not turn
into text. Either alone is worse than neither. A grab with no way back
swallows every key an IME has no use for; a virtual keyboard with no
grab is only a way for one client to type into another.
virtual-keyboard is vendored from wlroots for the same reason
input-method v2 is: it has never been in wayland-protocols and there is
nowhere else to get it.
**E narrows the grab, on purpose, and this is the part to disagree with
if you are going to disagree with anything.** Read literally the grab is
exclusive over the whole seat for as long as it is held. That works
because keys come back through the virtual keyboard - and it means that
if the return path ever fails, because the IME wedges or is stopped or
has a bug, the keyboard is dead everywhere including in whatever window
you would use to fix it. Here the grab only sees keys while a text input
is actually enabled. A broken IME then costs you text fields in
applications that asked for one, and leaves terminals, bindings and
every other window working. The cost is that an IME cannot see keys with
no field focused, which only matters for toggle shortcuts pressed
outside one.
Two things fall out of where the hook goes, and both are load-bearing:
* bindings have already run. They are evaluated in _key_down, above
e_comp_wl_key_down, so an input method cannot swallow a compositor
shortcut - only the text.
* a key the grab takes is not added to kbd.keys. That array is "the
keys currently pressed", sent to a client in wl_keyboard.enter, and
a key the client was never told about is not one of them. It is also
what makes the return path work at all: the key comes back as a
fresh press, and the repeat filter would otherwise drop it as
already down. That was not theory - it is what the test caught.
Releases follow their press rather than the current state. A field
disabled between the two - exactly what happens when a key commits text
and the client disables the input - would otherwise send the press to
the input method and the release to the window, leaving the window
holding a key that never came up.
**On the security of advertising virtual-keyboard**, because the
protocol raises it itself: it says a compositor "should present an error
when an untrusted client requests a new keyboard", and E has no notion
of a trusted client, so the global is advertised to everyone - as it is
on every compositor that supports IMEs. What is done instead is to make
"arbitrary actions" as close to false as it can be: an injected key goes
to the focused surface and nowhere else. It does not run key bindings,
so it cannot invoke a compositor action, and it does not re-enter the
grab, so it cannot loop. A client using this can type into the window
you are looking at, which is inherent to the protocol; it cannot drive
the compositor. Skipping bindings is also simply correct - a key on its
way back has already been through them once, and running them again
would fire every shortcut twice.
Not implemented, still: get_input_popup_surface, the IME's candidate
window. fcitx5 and ibus fall back to a normal toplevel.
Measured. Full wlcs 780 passed / 14 failed, failure set identical to
a1b6c75be. In-tree 31 -> 32, all green including the browser tier. The
new test drives both ends from two connections and checks the four
things that can each be wrong alone: the grab gets the key, the window
does *not* also get it, the returned key arrives with its keycode
intact, and with no field enabled the grab gets nothing. Each was
checked against a build with the relevant line removed - putting the key
back into kbd.keys breaks the return path, and dropping the enabled-field
gate makes the grab take keys it should not.
---
src/bin/e_comp_wl.c | 98 ++++++
src/bin/e_comp_wl.h | 10 +
src/bin/e_comp_wl_text_input.c | 366 ++++++++++++++++++--
src/bin/e_comp_wl_text_input.h | 7 +
src/bin/generated/meson.build | 1 +
src/protocol/README | 17 +-
src/protocol/virtual-keyboard-unstable-v1.xml | 112 +++++++
src/tests/wayland/globals.expected | 1 +
src/tests/wayland/meson.build | 2 +
src/tests/wayland/test_input_method_keyboard.c | 441 +++++++++++++++++++++++++
10 files changed, 1031 insertions(+), 24 deletions(-)
diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index 8b0fec48f..795e6cbfd 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -5129,6 +5129,11 @@ e_comp_wl_output_remove(const char *id)
}
}
+/* Set only around e_comp_wl_key_inject's synchronous call, to keep a key
+ * returned by a virtual keyboard out of the input method grab it came from.
+ * See e_comp_wl_key_inject below. */
+static Eina_Bool _key_is_injected = EINA_FALSE;
+
EINTERN Eina_Bool
e_comp_wl_key_down(Ecore_Event_Key *ev, E_Client *ec)
{
@@ -5158,6 +5163,26 @@ e_comp_wl_key_down(Ecore_Event_Key *ev, E_Client *ec)
if (*k == keycode) return EINA_FALSE;
}
+ /* An input method holding a keyboard grab takes the key instead of the
+ * focused window. Bindings have already been consulted by the time we get
+ * here - they run in _key_down, above this - so an input method cannot
+ * swallow a compositor shortcut, only the text.
+ *
+ * Before kbd.keys, not after. That array is "the keys currently pressed",
+ * sent to a client in wl_keyboard.enter, and a key the client is not being
+ * told about is not one of them. Recording it there anyway also breaks the
+ * way back: the input method returns the key through
+ * zwp_virtual_keyboard_v1, that arrives here as a fresh press, and the
+ * repeat filter above would drop it as already down. */
+ if ((!_key_is_injected) &&
+ e_comp_wl_text_input_key_grab_send(keycode,
+ WL_KEYBOARD_KEY_STATE_PRESSED,
+ ev->timestamp))
+ {
+ e_comp_wl_input_keyboard_state_update(keycode, EINA_TRUE);
+ return EINA_TRUE;
+ }
+
e_comp_wl->kbd.keys.size = (const char *)end - (const char *)e_comp_wl->kbd.keys.data;
if (!(k = wl_array_add(&e_comp_wl->kbd.keys, sizeof(*k))))
{
@@ -5202,6 +5227,19 @@ e_comp_wl_key_up(Ecore_Event_Key *ev, E_Client *ec)
keycode = (ev->keycode - 8);
if (!(e_comp_wl = e_comp->wl_comp_data)) return EINA_FALSE;
+ /* The release follows the press to wherever the press went - the grab
+ * answers on the strength of having taken that keycode, not on whether a
+ * field is still enabled now. A window left holding a key that never came
+ * up repeats it for ever. */
+ if ((!_key_is_injected) &&
+ e_comp_wl_text_input_key_grab_send(keycode,
+ WL_KEYBOARD_KEY_STATE_RELEASED,
+ ev->timestamp))
+ {
+ e_comp_wl_input_keyboard_state_update(keycode, EINA_FALSE);
+ return EINA_TRUE;
+ }
+
end = (uint32_t *)e_comp_wl->kbd.keys.data + (e_comp_wl->kbd.keys.size / sizeof(*k));
for (k = e_comp_wl->kbd.keys.data; k < end; k++)
{
@@ -5234,6 +5272,66 @@ e_comp_wl_key_up(Ecore_Event_Key *ev, E_Client *ec)
return !!ec;
}
+/* A key coming back from a client rather than in from the hardware.
+ *
+ * zwp_virtual_keyboard_v1 is how an input method returns the keystrokes it did
+ * not turn into text, and this is where they re-enter. It goes through
+ * e_comp_wl_key_down / _up so that the compositor's own key state - the
+ * pressed-keys array a newly focused client is handed, and the xkb modifier
+ * state - stays right, which a direct wl_keyboard_send_key would quietly
+ * corrupt.
+ *
+ * The flag exists to keep the key out of the input method grab it just came
+ * from, which would otherwise be an immediate loop. It is not a mode: it is
+ * set and cleared around one synchronous call that posts no events, so nothing
+ * else can observe it.
+ *
+ * Bindings are not re-run, and that is deliberate rather than an omission -
+ * this key already went through them on its way in, before it was handed to
+ * the grab. Running them again would fire every shortcut twice. It is also
+ * what keeps a virtual keyboard from being able to drive the compositor: it
+ * can type into the focused window, and nothing else. */
+
+EINTERN void
+e_comp_wl_key_inject(uint32_t keycode, uint32_t state, uint32_t timestamp)
+{
+ Ecore_Event_Key ev;
+
+ if (e_comp->comp_type != E_PIXMAP_TYPE_WL) return;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.window = e_comp->ee_win;
+ ev.event_window = e_comp->ee_win;
+ ev.root_window = e_comp->ee_win;
+ ev.timestamp = timestamp;
+ /* The wire carries evdev keycodes; everything inside E is xkb, which is the
+ * same number plus eight. */
+ ev.keycode = keycode + 8;
+
+ _key_is_injected = EINA_TRUE;
+ if (state == WL_KEYBOARD_KEY_STATE_PRESSED) e_comp_wl_key_down(&ev, NULL);
+ else e_comp_wl_key_up(&ev, NULL);
+ _key_is_injected = EINA_FALSE;
+}
+
+/* The modifier half of the same path. Passed through to the focused client
+ * rather than folded into E's xkb state: the keys above already drive that,
+ * and a second writer would fight with it. */
+EINTERN void
+e_comp_wl_modifiers_inject(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group)
+{
+ struct wl_resource *res;
+ Eina_List *l;
+ uint32_t serial;
+
+ if (e_comp->comp_type != E_PIXMAP_TYPE_WL) return;
+ if (!e_comp_wl->kbd.focused) return;
+
+ serial = wl_display_next_serial(e_comp_wl->wl.disp);
+ EINA_LIST_FOREACH(e_comp_wl->kbd.focused, l, res)
+ wl_keyboard_send_modifiers(res, serial, depressed, latched, locked, group);
+}
+
E_API Eina_Bool
e_comp_wl_evas_handle_mouse_button(E_Client *ec, uint32_t timestamp, uint32_t button_id, uint32_t state)
{
diff --git a/src/bin/e_comp_wl.h b/src/bin/e_comp_wl.h
index 346422593..79ef63a9d 100644
--- a/src/bin/e_comp_wl.h
+++ b/src/bin/e_comp_wl.h
@@ -624,6 +624,16 @@ E_API int e_comp_wl_client_buffer_transform_get(const E_Client *ec);
EINTERN Eina_Bool e_comp_wl_key_down(Ecore_Event_Key *ev, E_Client *ec);
EINTERN Eina_Bool e_comp_wl_key_up(Ecore_Event_Key *ev, E_Client *ec);
+
+/* A key, or a modifier state, arriving from a client rather than from the
+ * hardware - zwp_virtual_keyboard_v1, which is how an input method returns the
+ * keystrokes it did not consume. keycode is the evdev number, as on the wire.
+ *
+ * Deliberately not a path to E's key bindings: an injected key can type into
+ * the focused window and nothing else. See the definition for why that is the
+ * correct behaviour and not only the cautious one. */
+EINTERN void e_comp_wl_key_inject(uint32_t keycode, uint32_t state, uint32_t timestamp);
+EINTERN void e_comp_wl_modifiers_inject(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group);
E_API Eina_Bool e_comp_wl_evas_handle_mouse_button(E_Client *ec, uint32_t timestamp, uint32_t button_id, uint32_t state);
EINTERN void e_comp_wl_pointer_frame_send(struct wl_resource *res);
E_API void e_comp_wl_pointer_focus_drop(E_Client *ec);
diff --git a/src/bin/e_comp_wl_text_input.c b/src/bin/e_comp_wl_text_input.c
index 87a0f1730..6bb3bd092 100644
--- a/src/bin/e_comp_wl_text_input.c
+++ b/src/bin/e_comp_wl_text_input.c
@@ -48,9 +48,11 @@
#include "e.h"
#include "text-input-unstable-v3-server-protocol.h"
#include "input-method-unstable-v2-server-protocol.h"
+#include "virtual-keyboard-unstable-v1-server-protocol.h"
typedef struct Text_Input Text_Input;
typedef struct Input_Method Input_Method;
+typedef struct Keyboard_Grab Keyboard_Grab;
/* The half of a zwp_text_input_v3 that gets latched on commit. Held twice:
* once as what the client has asked for, once as what the input method has
@@ -117,10 +119,22 @@ struct Input_Method
uint32_t delete_before, delete_after;
Eina_Bool delete_set;
} pending;
+
+ /* The keyboard grab, if this input method has taken one. At most one:
+ * grab_keyboard called a second time leaves the older object alive but
+ * inert, which is what its im back-pointer going NULL means. */
+ Keyboard_Grab *grab;
+};
+
+struct Keyboard_Grab
+{
+ struct wl_resource *res;
+ Input_Method *im;
};
static struct wl_global *_text_input_manager_global;
static struct wl_global *_input_method_manager_global;
+static struct wl_global *_virtual_keyboard_manager_global;
/* Every live zwp_text_input_v3, across all clients. */
static Eina_List *_text_inputs;
@@ -141,6 +155,16 @@ static Text_Input *_active_ti;
static struct wl_resource *_focus_surface;
static struct wl_client *_focus_client;
+/* Which keycodes the grab currently holds down.
+ *
+ * A release must go wherever its press went. Without this, a field disabled
+ * between the two - which is exactly what happens when a key commits text and
+ * the client disables the input - would send the press to the input method and
+ * the release to the window, and the window would be left holding a key that
+ * never came up. KEY_MAX is 0x2ff; anything above that is not a keyboard. */
+#define GRABBED_KEYS_MAX 0x300
+static Eina_Bool _grabbed_keys[GRABBED_KEYS_MAX];
+
/* Fires when the focused surface is destroyed, so that the leave goes out
* while there is still a surface to name in it. See _focus_surface_watch. */
static struct wl_listener _focus_surface_destroy;
@@ -576,39 +600,151 @@ _e_input_method_cb_input_popup_surface_get(struct wl_client *client, struct wl_r
wl_resource_set_implementation(res, NULL, NULL, NULL);
}
+static void
+_e_keyboard_grab_cb_release(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static const struct zwp_input_method_keyboard_grab_v2_interface _e_keyboard_grab_interface =
+{
+ _e_keyboard_grab_cb_release,
+};
+
+static void
+_e_keyboard_grab_res_destroy(struct wl_resource *resource)
+{
+ Keyboard_Grab *grab = wl_resource_get_user_data(resource);
+
+ if (!grab) return;
+ if (grab->im && (grab->im->grab == grab)) grab->im->grab = NULL;
+ /* Whatever it was holding is not held any more. The releases for those keys
+ * will now go to the window, which never saw their presses - that is a
+ * worse outcome than dropping them, so they are dropped. */
+ memset(_grabbed_keys, 0, sizeof(_grabbed_keys));
+ free(grab);
+}
+
static void
_e_input_method_cb_keyboard_grab(struct wl_client *client, struct wl_resource *resource, uint32_t id)
{
+ Input_Method *im = wl_resource_get_user_data(resource);
+ Keyboard_Grab *grab;
struct wl_resource *res;
+ int fd;
- /* Not implemented. The object is created and then stays silent: the IME
- * gets no hardware key events, and the application keeps receiving keys
- * normally through wl_keyboard. So an IME driven by anything other than the
- * keyboard still works, and one that composes from keystrokes - which is
- * every CJK input method - does not.
- *
- * Deliberately not half-done, because the half that is missing is the
- * dangerous one. Forwarding keys here also means *not* delivering them to
- * the focused application ("the compositor must not further process any
- * event after it has been forwarded to the grab holder"), and an IME then
- * hands back the keys it did not consume through
- * zwp_virtual_keyboard_v1 - which E does not have either. Implementing the
- * grab without the return path would take every unhandled keystroke away
- * from the application for as long as an input method is connected.
- *
- * There is also no way to test it yet: wl_test.key feeds evas directly
- * rather than posting an ecore key event, so a grab hung off
- * ECORE_EVENT_KEY_DOWN would see nothing in this suite and look broken when
- * it was not - or, worse, look fine when it was. That harness gap is the
- * first thing to close. */
- res = wl_resource_create(client, &zwp_input_method_keyboard_grab_v2_interface,
+ grab = E_NEW(Keyboard_Grab, 1);
+ if (!grab)
+ {
+ wl_resource_post_no_memory(resource);
+ return;
+ }
+
+ res = wl_resource_create(client,
+ &zwp_input_method_keyboard_grab_v2_interface,
wl_resource_get_version(resource), id);
if (!res)
{
- wl_client_post_no_memory(client);
+ free(grab);
+ wl_resource_post_no_memory(resource);
return;
}
- wl_resource_set_implementation(res, NULL, NULL, NULL);
+
+ grab->res = res;
+ wl_resource_set_implementation(res, &_e_keyboard_grab_interface, grab,
+ _e_keyboard_grab_res_destroy);
+
+ /* An inert input method - one that lost the race for the seat and was told
+ * unavailable - may still ask for a grab, and must not get one. The object
+ * exists and stays silent, which is what "any further requests and events
+ * except for the destroy request must be ignored" asks for. */
+ if (im && (!im->inert))
+ {
+ /* A second grab replaces the first rather than doubling it. The
+ * protocol describes an exclusive grab, so there is nothing sensible
+ * for two of them to mean. */
+ if (im->grab) im->grab->im = NULL;
+ im->grab = grab;
+ grab->im = im;
+ }
+
+ /* "This event is sent as soon as the object has been created, and is
+ * guaranteed to be received by the client before any key press event." */
+ fd = _e_comp_wl_input_keymap_fd_get();
+ if (fd >= 0)
+ {
+ zwp_input_method_keyboard_grab_v2_send_keymap(res,
+ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, fd, e_comp_wl->xkb.map_size);
+ close(fd);
+ }
+ zwp_input_method_keyboard_grab_v2_send_repeat_info(res,
+ e_config->keyboard.repeat_rate, e_config->keyboard.repeat_delay);
+}
+
+/* Should this key go to the input method instead of the focused window?
+ *
+ * Yes only while a text input is enabled - which is a deliberate narrowing of
+ * what the protocol describes, and the one place this implementation diverges
+ * from wlroots on purpose.
+ *
+ * Read literally, the grab is "an exclusive grab of the wl_keyboard interface
+ * associated with the seat": every key on the seat, for as long as the input
+ * method holds it, whether or not anything is being typed into. That works
+ * because an IME returns the keys it did not consume through
+ * zwp_virtual_keyboard_v1, so the keystrokes come back and the user notices
+ * nothing.
+ *
+ * It also means that if the return path fails for any reason - the IME wedges,
+ * or is stopped with SIGSTOP, or has a bug - the keyboard is dead everywhere,
+ * including in whatever window the user would use to fix it. Gating on an
+ * active text input confines that failure to text fields in applications that
+ * asked for an input method, and leaves terminals, key bindings and everything
+ * else working. The cost is that an IME cannot see keys with no field focused,
+ * which is only a limitation for toggle shortcuts pressed outside one.
+ *
+ * Compositor key bindings have already run by the time this is reached: they
+ * are evaluated in _key_down in e_comp_canvas.c, before e_comp_wl_key_down is
+ * called at all. So the input method cannot swallow a shortcut, which is the
+ * other half of not letting it swallow the machine. */
+EINTERN Eina_Bool
+e_comp_wl_text_input_key_grab_send(uint32_t keycode, uint32_t state, uint32_t timestamp)
+{
+ Input_Method *im = _im_get();
+ Eina_Bool held;
+ uint32_t serial;
+
+ if ((!im) || (!im->grab)) return EINA_FALSE;
+
+ held = (keycode < GRABBED_KEYS_MAX) && _grabbed_keys[keycode];
+
+ if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
+ {
+ if ((!_active_ti) || (!_active_ti->current.enabled)) return EINA_FALSE;
+ if (keycode < GRABBED_KEYS_MAX) _grabbed_keys[keycode] = EINA_TRUE;
+ }
+ else
+ {
+ /* Only the keys this grab actually took. A release for anything else
+ * belongs to the window, which is where its press went. */
+ if (!held) return EINA_FALSE;
+ _grabbed_keys[keycode] = EINA_FALSE;
+ }
+
+ serial = wl_display_next_serial(e_comp_wl->wl.disp);
+
+ /* Modifiers first, so the state the input method reads when deciding what
+ * this key means is the state as of this key. E sends them to ordinary
+ * clients the other way round - key, then the modifier update from
+ * e_comp_wl_input_keyboard_state_update - but a client applies them at a
+ * done boundary and an input method acts on the key immediately. */
+ zwp_input_method_keyboard_grab_v2_send_modifiers(im->grab->res, serial,
+ e_comp_wl->kbd.mod_depressed,
+ e_comp_wl->kbd.mod_latched,
+ e_comp_wl->kbd.mod_locked,
+ e_comp_wl->kbd.mod_group);
+ zwp_input_method_keyboard_grab_v2_send_key(im->grab->res, serial, timestamp,
+ keycode, state);
+ return EINA_TRUE;
}
static void
@@ -717,6 +853,177 @@ _e_input_method_manager_cb_bind(struct wl_client *client, void *data EINA_UNUSED
NULL);
}
+/*** zwp_virtual_keyboard_v1 - the way back ***/
+
+/* The other end of the keyboard grab, and only useful with it.
+ *
+ * An input method that grabs the keyboard receives every key while a field is
+ * being typed into, including the ones it has no interest in - a Latin letter
+ * typed while a Chinese IME is in alphabetic mode, an arrow key, Backspace.
+ * It has no way to say "not mine" through input-method, so it sends those keys
+ * back with this protocol and the compositor delivers them as if they had come
+ * from the keyboard. Without it, grabbing the keyboard would simply swallow
+ * everything the IME did not turn into text.
+ *
+ * That is why it lives in this file rather than beside the other extensions:
+ * on its own it is a way for any client to type into any other, and it is here
+ * because it completes the loop that starts at grab_keyboard.
+ *
+ * **It is worth being plain about the exposure**, because the protocol is:
+ * "If the compositor enables a keyboard to perform arbitrary actions, it
+ * should present an error when an untrusted client requests a new keyboard",
+ * and it defines an `unauthorized` error for saying so. E has no notion of a
+ * trusted client, so there is nothing to test a request against and the global
+ * is advertised to everyone - which is what every compositor supporting IMEs
+ * does today.
+ *
+ * What is done about it is to make "arbitrary actions" as close to false as it
+ * can be here: injected keys go to the focused surface and nowhere else. They
+ * do not run key bindings, so a virtual keyboard cannot invoke a compositor
+ * action, and they do not re-enter the input method grab, so they cannot loop.
+ * A client using this can type into whatever window the user is looking at,
+ * which is inherent to the protocol; it cannot drive the compositor.
+ *
+ * Skipping bindings is also the correct behaviour rather than only the safer
+ * one. A key on its way back from an IME has already been through them once,
+ * when it arrived from the hardware and before it was handed to the grab.
+ * Running them again would fire every shortcut twice.
+ */
+
+typedef struct
+{
+ struct wl_resource *res;
+ Eina_Bool keymap_set;
+} Virtual_Keyboard;
+
+static void
+_e_virtual_keyboard_cb_keymap(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t format, int32_t fd, uint32_t size EINA_UNUSED)
+{
+ Virtual_Keyboard *vk = wl_resource_get_user_data(resource);
+
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
+ {
+ if (fd >= 0) close(fd);
+ wl_resource_post_error(resource,
+ ZWP_VIRTUAL_KEYBOARD_V1_ERROR_INVALID_KEYMAP_FORMAT,
+ "keymap format %u is not xkb_v1", format);
+ return;
+ }
+
+ /* The keymap is accepted and not applied. Applying it would mean replacing
+ * the seat's keymap with one chosen by a client, which would change what
+ * every other window's keys mean.
+ *
+ * Not applying it is safe for the case this exists to serve, and the reason
+ * is worth writing down: an input method sends back keycodes it received
+ * from the grab, and those were expressed in the seat's keymap because we
+ * sent that keymap to the grab. So the numbers already mean what we think
+ * they mean. A client that sets a *different* keymap and then injects
+ * keycodes from it will have them read in the seat's keymap instead - which
+ * is a limitation, and is recorded here rather than hidden. */
+ if (fd >= 0) close(fd);
+ vk->keymap_set = EINA_TRUE;
+}
+
+static void
+_e_virtual_keyboard_cb_key(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t time, uint32_t key, uint32_t state)
+{
+ Virtual_Keyboard *vk = wl_resource_get_user_data(resource);
+
+ /* "Keymap must be set before issuing this request." */
+ if (!vk->keymap_set)
+ {
+ wl_resource_post_error(resource, ZWP_VIRTUAL_KEYBOARD_V1_ERROR_NO_KEYMAP,
+ "key sent before a keymap was set");
+ return;
+ }
+
+ e_comp_wl_key_inject(key, state, time);
+}
+
+static void
+_e_virtual_keyboard_cb_modifiers(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group)
+{
+ Virtual_Keyboard *vk = wl_resource_get_user_data(resource);
+
+ if (!vk->keymap_set)
+ {
+ wl_resource_post_error(resource, ZWP_VIRTUAL_KEYBOARD_V1_ERROR_NO_KEYMAP,
+ "modifiers sent before a keymap was set");
+ return;
+ }
+
+ e_comp_wl_modifiers_inject(depressed, latched, locked, group);
+}
+
+static void
+_e_virtual_keyboard_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static const struct zwp_virtual_keyboard_v1_interface _e_virtual_keyboard_interface =
+{
+ _e_virtual_keyboard_cb_keymap,
+ _e_virtual_keyboard_cb_key,
+ _e_virtual_keyboard_cb_modifiers,
+ _e_virtual_keyboard_cb_destroy,
+};
+
+static void
+_e_virtual_keyboard_res_destroy(struct wl_resource *resource)
+{
+ free(wl_resource_get_user_data(resource));
+}
+
+static void
+_e_virtual_keyboard_manager_cb_create(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat EINA_UNUSED, uint32_t id)
+{
+ Virtual_Keyboard *vk;
+ struct wl_resource *res;
+
+ vk = E_NEW(Virtual_Keyboard, 1);
+ if (!vk)
+ {
+ wl_resource_post_no_memory(resource);
+ return;
+ }
+
+ res = wl_resource_create(client, &zwp_virtual_keyboard_v1_interface,
+ wl_resource_get_version(resource), id);
+ if (!res)
+ {
+ free(vk);
+ wl_resource_post_no_memory(resource);
+ return;
+ }
+
+ vk->res = res;
+ wl_resource_set_implementation(res, &_e_virtual_keyboard_interface, vk,
+ _e_virtual_keyboard_res_destroy);
+}
+
+static const struct zwp_virtual_keyboard_manager_v1_interface _e_virtual_keyboard_manager_interface =
+{
+ _e_virtual_keyboard_manager_cb_create,
+};
+
+static void
+_e_virtual_keyboard_manager_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id)
+{
+ struct wl_resource *res;
+
+ res = wl_resource_create(client, &zwp_virtual_keyboard_manager_v1_interface,
+ version, id);
+ if (!res)
+ {
+ wl_client_post_no_memory(client);
+ return;
+ }
+ wl_resource_set_implementation(res, &_e_virtual_keyboard_manager_interface,
+ NULL, NULL);
+}
+
/*** focus ***/
static void _focus_leave_send(void);
@@ -865,6 +1172,16 @@ e_comp_wl_text_input_init(void)
return EINA_FALSE;
}
+ _virtual_keyboard_manager_global =
+ wl_global_create(e_comp_wl->wl.disp,
+ &zwp_virtual_keyboard_manager_v1_interface, 1, NULL,
+ _e_virtual_keyboard_manager_cb_bind);
+ if (!_virtual_keyboard_manager_global)
+ {
+ ERR("Could not add zwp_virtual_keyboard_manager_v1 to wayland globals");
+ return EINA_FALSE;
+ }
+
return EINA_TRUE;
}
@@ -877,6 +1194,9 @@ e_comp_wl_text_input_shutdown(void)
if (_input_method_manager_global)
wl_global_destroy(_input_method_manager_global);
_input_method_manager_global = NULL;
+ if (_virtual_keyboard_manager_global)
+ wl_global_destroy(_virtual_keyboard_manager_global);
+ _virtual_keyboard_manager_global = NULL;
_focus_surface_watch(NULL);
_text_inputs = eina_list_free(_text_inputs);
diff --git a/src/bin/e_comp_wl_text_input.h b/src/bin/e_comp_wl_text_input.h
index d33b9c660..9d9e8367e 100644
--- a/src/bin/e_comp_wl_text_input.h
+++ b/src/bin/e_comp_wl_text_input.h
@@ -13,5 +13,12 @@ EINTERN void e_comp_wl_text_input_shutdown(void);
EINTERN void e_comp_wl_text_input_focus_set(E_Client *ec);
EINTERN void e_comp_wl_text_input_focus_unset(E_Client *ec);
+/* Offer a key to an input method holding a keyboard grab. EINA_TRUE if it took
+ * it, in which case the focused window must not also be sent it.
+ *
+ * Called from e_comp_wl_key_down / _up, after key bindings have had their say
+ * and at the point the key would otherwise go to the client. */
+EINTERN Eina_Bool e_comp_wl_text_input_key_grab_send(uint32_t keycode, uint32_t state, uint32_t timestamp);
+
# endif
#endif
diff --git a/src/bin/generated/meson.build b/src/bin/generated/meson.build
index b07adc23b..c6666a00a 100644
--- a/src/bin/generated/meson.build
+++ b/src/bin/generated/meson.build
@@ -21,6 +21,7 @@ protos = [
# Vendored - input-method v2 has never been packaged anywhere. See
# src/protocol/README.
'../../protocol/input-method-unstable-v2.xml',
+ '../../protocol/virtual-keyboard-unstable-v1.xml',
]
proto_c = []
diff --git a/src/protocol/README b/src/protocol/README
index 8452d21c5..4894bf1d9 100644
--- a/src/protocol/README
+++ b/src/protocol/README
@@ -28,8 +28,15 @@ Vendored from upstream
https://gitlab.freedesktop.org/wlroots/wlroots
/-/raw/master/protocol/input-method-unstable-v2.xml
+ virtual-keyboard-unstable-v1.xml
+ wlroots, protocol/
+ fetched 2026-08-19
+ sha256 b4898f92c27db08c75bb82e9eeec5b179b80a4d6aeb34c26da60b485fca9c450
+ https://gitlab.freedesktop.org/wlroots/wlroots
+ /-/raw/master/protocol/virtual-keyboard-unstable-v1.xml
+
Copies of upstream files, carried because the version a build machine has is
-not ours to choose - and, for one of them, because there is no packaged version
+not ours to choose - and, for two of them, because there is no packaged version
at all.
`xdg-toplevel-drag-v1.xml` is not in wayland-protocols 1.33, which is what this
@@ -45,6 +52,14 @@ documentation fixes; the two were diffed at fetch time and every request,
event, argument and interface version is identical, so the wire protocol E
compiles against and the one wlcs tests against are the same protocol.
+`virtual-keyboard-unstable-v1.xml` is the same story and the same source. It is
+the other end of input-method's keyboard grab: an input method that takes the
+keys returns the ones it did not consume through this, so the two are useless
+apart. Note that this one carries a security decision as well as a protocol -
+it lets a client type into whatever window is focused. What E does about that
+is in `_e_virtual_keyboard_manager_cb_create` and the comment above it; read
+that before changing how the global is advertised.
+
Two rules for these:
* **Keep them byte-identical to upstream.** They are not ours to improve. If
diff --git a/src/protocol/virtual-keyboard-unstable-v1.xml b/src/protocol/virtual-keyboard-unstable-v1.xml
new file mode 100644
index 000000000..a888e5f87
--- /dev/null
+++ b/src/protocol/virtual-keyboard-unstable-v1.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="virtual_keyboard_unstable_v1">
+ <copyright>
+ Copyright © 2008-2011 Kristian Høgsberg
+ Copyright © 2010-2013 Intel Corporation
+ Copyright © 2012-2013 Collabora, Ltd.
+ Copyright © 2018 Purism SPC
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+ </copyright>
+
+ <interface name="zwp_virtual_keyboard_v1" version="1">
+ <description summary="virtual keyboard">
+ The virtual keyboard provides an application with requests which emulate
+ the behaviour of a physical keyboard.
+
+ This interface can be used by clients on its own to provide raw input
+ events, or it can accompany the input method protocol.
+ </description>
+
+ <request name="keymap">
+ <description summary="keyboard mapping">
+ Provide a file descriptor to the compositor which can be
+ memory-mapped to provide a keyboard mapping description.
+ </description>
+ <arg name="format" type="uint" enum="wl_keyboard.keymap_format" summary="keymap format"/>
+ <arg name="fd" type="fd" summary="keymap file descriptor"/>
+ <arg name="size" type="uint" summary="keymap size, in bytes"/>
+ </request>
+
+ <enum name="error">
+ <entry name="no_keymap" value="0" summary="No keymap was set"/>
+ <entry name="invalid_keymap_format" value="1" summary="Invalid keymap format"/>
+ </enum>
+
+ <request name="key">
+ <description summary="key event">
+ A key was pressed or released.
+ The time argument is a timestamp with millisecond granularity, with an
+ undefined base. All requests regarding a single object must share the
+ same clock.
+
+ Keymap must be set before issuing this request.
+
+ State carries a value from the key_state enumeration.
+ </description>
+ <arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
+ <arg name="key" type="uint" summary="key that produced the event"/>
+ <arg name="state" type="uint" summary="physical state of the key"/>
+ </request>
+
+ <request name="modifiers">
+ <description summary="modifier and group state">
+ Notifies the compositor that the modifier and/or group state has
+ changed, and it should update state.
+
+ The client should use wl_keyboard.modifiers event to synchronize its
+ internal state with seat state.
+
+ Keymap must be set before issuing this request.
+ </description>
+ <arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
+ <arg name="mods_latched" type="uint" summary="latched modifiers"/>
+ <arg name="mods_locked" type="uint" summary="locked modifiers"/>
+ <arg name="group" type="uint" summary="keyboard layout"/>
+ </request>
+
+ <request name="destroy" type="destructor" since="1">
+ <description summary="destroy the virtual keyboard keyboard object"/>
+ </request>
+ </interface>
+
+ <interface name="zwp_virtual_keyboard_manager_v1" version="1">
+ <description summary="virtual keyboard manager">
+ A virtual keyboard manager allows an application to provide keyboard
+ input events as if they came from a physical keyboard.
+ </description>
+
+ <enum name="error">
+ <entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
+ </enum>
+
+ <request name="create_virtual_keyboard">
+ <description summary="Create a new virtual keyboard">
+ Creates a new virtual keyboard associated to a seat.
+
+ If the compositor enables a keyboard to perform arbitrary actions, it
+ should present an error when an untrusted client requests a new
+ keyboard.
+ </description>
+ <arg name="seat" type="object" interface="wl_seat"/>
+ <arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
+ </request>
+ </interface>
+</protocol>
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index 7f095c412..b1a796f70 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -23,6 +23,7 @@ zwp_pointer_constraints_v1 1
zwp_primary_selection_device_manager_v1 1
zwp_relative_pointer_manager_v1 1
zwp_text_input_manager_v3 1
+zwp_virtual_keyboard_manager_v1 1
zxdg_decoration_manager_v1 1
zxdg_exporter_v1 1
zxdg_exporter_v2 1
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 1bbcf91b2..7285dd853 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -47,6 +47,7 @@ foreach p: [
'@0@/unstable/text-input/text-input-unstable-v3.xml'.format(dir_wayland_protocols),
'../../protocol/xdg-toplevel-drag-v1.xml',
'../../protocol/input-method-unstable-v2.xml',
+ '../../protocol/virtual-keyboard-unstable-v1.xml',
]
test_proto_src += gen_scanner_client.process(p)
test_proto_src += gen_scanner_impl.process(p)
@@ -96,6 +97,7 @@ wl_protocol_tests = [
['seat-capabilities', 'test_seat_capabilities.c'],
['dialog-focus', 'test_dialog_focus.c'],
['text-input', 'test_text_input.c'],
+ ['input-method-keyboard', 'test_input_method_keyboard.c'],
]
# Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_input_method_keyboard.c b/src/tests/wayland/test_input_method_keyboard.c
new file mode 100644
index 000000000..fbeb51864
--- /dev/null
+++ b/src/tests/wayland/test_input_method_keyboard.c
@@ -0,0 +1,441 @@
+/* The keyboard half of the input method: the grab, and the way back.
+ *
+ * An input method that composes text from keystrokes - which is every CJK one
+ * - needs the keys before the application does. It takes them with
+ * zwp_input_method_v2.grab_keyboard, and returns the ones it did not turn into
+ * text through zwp_virtual_keyboard_v1. Neither half is any use alone: a grab
+ * without a return path swallows everything the IME does not want, and a
+ * virtual keyboard without a grab is a way for a client to type into another
+ * client and nothing more.
+ *
+ * So this drives both, from two connections in one process - one playing the
+ * application, one playing fcitx5 - and checks the four things that can each
+ * be wrong on their own:
+ *
+ * * with a text input enabled and a grab held, a key goes to the input
+ * method **and not** to the application. Half of that is the interesting
+ * half: a compositor that forwards to the grab and also delivers to the
+ * window types every keystroke twice.
+ *
+ * * a key sent back through the virtual keyboard reaches the application,
+ * with the keycode intact. This is the whole reason the grab is safe to
+ * take at all.
+ *
+ * * with no text input enabled, the grab gets nothing and keys go straight
+ * to the window. That is E's deliberate narrowing of the protocol - see
+ * e_comp_wl_text_input_key_grab_send - and it is the difference between an
+ * input method that can only break text fields and one that can take the
+ * keyboard away from the whole session.
+ *
+ * * a virtual keyboard that sends a key before it has set a keymap is a
+ * protocol error. It needs its own connection, because being disconnected
+ * is the observation.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "e_wl_testkit.h"
+#include "text-input-unstable-v3-client-protocol.h"
+#include "input-method-unstable-v2-client-protocol.h"
+#include "virtual-keyboard-unstable-v1-client-protocol.h"
+
+#define PROG "test-input-method-keyboard"
+#define EVDEV_KEY_A 30
+
+/* ---------------------------------------------------------- the app side */
+
+typedef struct
+{
+ struct wl_surface *entered;
+ uint32_t last_key, last_state;
+ int keys;
+} App;
+
+static void
+_app_keymap(void *d, struct wl_keyboard *k, uint32_t f, int32_t fd, uint32_t s)
+{ (void)d; (void)k; (void)f; (void)s; if (fd >= 0) close(fd); }
+
+static void
+_app_enter(void *data, struct wl_keyboard *k, uint32_t se, struct wl_surface *s,
+ struct wl_array *keys)
+{ App *a = data; (void)k; (void)se; (void)keys; a->entered = s; }
+
+static void
+_app_leave(void *data, struct wl_keyboard *k, uint32_t se, struct wl_surface *s)
+{ App *a = data; (void)k; (void)se; if (a->entered == s) a->entered = NULL; }
+
+static void
+_app_key(void *data, struct wl_keyboard *k, uint32_t se, uint32_t t,
+ uint32_t key, uint32_t state)
+{
+ App *a = data;
+
+ (void)k; (void)se; (void)t;
+ a->last_key = key;
+ a->last_state = state;
+ a->keys++;
+}
+
+static void
+_app_mods(void *d, struct wl_keyboard *k, uint32_t se, uint32_t x, uint32_t y,
+ uint32_t z, uint32_t g)
+{ (void)d; (void)k; (void)se; (void)x; (void)y; (void)z; (void)g; }
+
+static void
+_app_repeat(void *d, struct wl_keyboard *k, int32_t r, int32_t delay)
+{ (void)d; (void)k; (void)r; (void)delay; }
+
+static const struct wl_keyboard_listener _app_kbd_listener =
+{ _app_keymap, _app_enter, _app_leave, _app_key, _app_mods, _app_repeat };
+
+static void
+_ti_enter(void *d, struct zwp_text_input_v3 *t, struct wl_surface *s)
+{ (void)d; (void)t; (void)s; }
+static void
+_ti_leave(void *d, struct zwp_text_input_v3 *t, struct wl_surface *s)
+{ (void)d; (void)t; (void)s; }
+static void
+_ti_preedit(void *d, struct zwp_text_input_v3 *t, const char *x, int32_t a, int32_t b)
+{ (void)d; (void)t; (void)x; (void)a; (void)b; }
+static void
+_ti_commit_string(void *d, struct zwp_text_input_v3 *t, const char *x)
+{ (void)d; (void)t; (void)x; }
+static void
+_ti_delete(void *d, struct zwp_text_input_v3 *t, uint32_t a, uint32_t b)
+{ (void)d; (void)t; (void)a; (void)b; }
+static void
+_ti_done(void *d, struct zwp_text_input_v3 *t, uint32_t s)
+{ (void)d; (void)t; (void)s; }
+
+static const struct zwp_text_input_v3_listener _ti_listener =
+{ _ti_enter, _ti_leave, _ti_preedit, _ti_commit_string, _ti_delete, _ti_done };
+
+/* ---------------------------------------------------------- the IME side */
+
+typedef struct
+{
+ int activates, deactivates, dones;
+
+ int grab_keymaps, grab_repeat_infos;
+ uint32_t grab_last_key, grab_last_state;
+ int grab_keys, grab_mods;
+} Ime;
+
+static void
+_im_activate(void *data, struct zwp_input_method_v2 *im)
+{ Ime *i = data; (void)im; i->activates++; }
+static void
+_im_deactivate(void *data, struct zwp_input_method_v2 *im)
+{ Ime *i = data; (void)im; i->deactivates++; }
+static void
+_im_surrounding(void *d, struct zwp_input_method_v2 *im, const char *t, uint32_t c, uint32_t a)
+{ (void)d; (void)im; (void)t; (void)c; (void)a; }
+static void
+_im_cause(void *d, struct zwp_input_method_v2 *im, uint32_t c)
+{ (void)d; (void)im; (void)c; }
+static void
+_im_content(void *d, struct zwp_input_method_v2 *im, uint32_t h, uint32_t p)
+{ (void)d; (void)im; (void)h; (void)p; }
+static void
+_im_done(void *data, struct zwp_input_method_v2 *im)
+{ Ime *i = data; (void)im; i->dones++; }
+static void
+_im_unavailable(void *d, struct zwp_input_method_v2 *im)
+{ (void)d; (void)im; }
+
+static const struct zwp_input_method_v2_listener _im_listener =
+{ _im_activate, _im_deactivate, _im_surrounding, _im_cause, _im_content,
+ _im_done, _im_unavailable };
+
+static void
+_grab_keymap(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
+ uint32_t format, int32_t fd, uint32_t size)
+{
+ Ime *i = data;
+
+ (void)g; (void)format; (void)size;
+ if (fd >= 0) close(fd);
+ i->grab_keymaps++;
+}
+
+static void
+_grab_key(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
+ uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
+{
+ Ime *i = data;
+
+ (void)g; (void)serial; (void)time;
+ i->grab_last_key = key;
+ i->grab_last_state = state;
+ i->grab_keys++;
+}
+
+static void
+_grab_mods(void *data, struct zwp_input_method_keyboard_grab_v2 *g, uint32_t se,
+ uint32_t a, uint32_t b, uint32_t c, uint32_t d)
+{
+ Ime *i = data;
+
+ (void)g; (void)se; (void)a; (void)b; (void)c; (void)d;
+ i->grab_mods++;
+}
+
+static void
+_grab_repeat(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
+ int32_t rate, int32_t delay)
+{
+ Ime *i = data;
+
+ (void)g; (void)rate; (void)delay;
+ i->grab_repeat_infos++;
+}
+
+static const struct zwp_input_method_keyboard_grab_v2_listener _grab_listener =
+{ _grab_keymap, _grab_key, _grab_mods, _grab_repeat };
+
+/* ---------------------------------------------------------------- glue */
+
+static struct wl_seat *
+_seat_get(Tk *tk)
+{
+ uint32_t v = tk_global_version(tk, "wl_seat");
+ struct wl_seat *seat;
+
+ if (!v) tk_fail(tk, "no wl_seat");
+ seat = tk_bind(tk, &wl_seat_interface, v);
+ if (!seat) tk_fail(tk, "wl_seat advertised but would not bind");
+ return seat;
+}
+
+static void
+_settle(Tk *app, Tk *ime)
+{
+ tk_sync(app);
+ tk_sync(ime);
+ tk_sync(app);
+}
+
+/* A keymap fd for the virtual keyboard. The contents do not matter to E - it
+ * does not apply a client keymap, and the reason is in
+ * _e_virtual_keyboard_cb_keymap - but the request carries an fd and a size and
+ * the compositor is entitled to expect both. */
+static void
+_vk_keymap_set(struct zwp_virtual_keyboard_v1 *vk)
+{
+ static const char map[] = "xkb_keymap {};";
+ char path[] = "/tmp/e-test-vk-keymap-XXXXXX";
+ int fd = mkstemp(path);
+
+ if (fd < 0) return;
+ unlink(path);
+ if (write(fd, map, sizeof(map)) != (ssize_t)sizeof(map))
+ {
+ close(fd);
+ return;
+ }
+ zwp_virtual_keyboard_v1_keymap(vk, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, fd,
+ sizeof(map));
+ close(fd);
+}
+
+int
+main(void)
+{
+ Tk *app, *ime, *rogue;
+ Tk_Toplevel *top;
+ struct zwp_text_input_manager_v3 *ti_mgr;
+ struct zwp_text_input_v3 *ti;
+ struct zwp_input_method_manager_v2 *im_mgr;
+ struct zwp_input_method_v2 *im;
+ struct zwp_input_method_keyboard_grab_v2 *grab;
+ struct zwp_virtual_keyboard_manager_v1 *vk_mgr;
+ struct zwp_virtual_keyboard_v1 *vk;
+ struct wl_keyboard *kbd;
+ App a = { 0 };
+ Ime i = { 0 };
+ int app_keys_before;
+
+ app = tk_connect(PROG "-app");
+ ime = tk_connect(PROG "-ime");
+
+ if (tk_global_version(ime, "zwp_virtual_keyboard_manager_v1") < 1)
+ tk_fail(ime, "no zwp_virtual_keyboard_manager_v1 - an input method that "
+ "grabs the keyboard has no way to hand back the keys it did "
+ "not consume, so the grab would swallow them");
+
+ ti_mgr = tk_bind(app, &zwp_text_input_manager_v3_interface, 1);
+ if (!ti_mgr) tk_fail(app, "no text input manager");
+ im_mgr = tk_bind(ime, &zwp_input_method_manager_v2_interface, 1);
+ if (!im_mgr) tk_fail(ime, "no input method manager");
+ vk_mgr = tk_bind(ime, &zwp_virtual_keyboard_manager_v1_interface, 1);
+ if (!vk_mgr) tk_fail(ime, "virtual keyboard manager would not bind");
+
+ kbd = wl_seat_get_keyboard(_seat_get(app));
+ if (!kbd) tk_fail(app, "seat has no keyboard");
+ wl_keyboard_add_listener(kbd, &_app_kbd_listener, &a);
+
+ ti = zwp_text_input_manager_v3_get_text_input(ti_mgr, _seat_get(app));
+ zwp_text_input_v3_add_listener(ti, &_ti_listener, NULL);
+
+ im = zwp_input_method_manager_v2_get_input_method(im_mgr, _seat_get(ime));
+ zwp_input_method_v2_add_listener(im, &_im_listener, &i);
+ _settle(app, ime);
+
+ top = tk_toplevel_new(app, "im-keyboard", "im keyboard", 200, 200);
+ tk_settle(app);
+ _settle(app, ime);
+
+ if (a.entered != tk_toplevel_surface(top))
+ tk_fail(app, "the window did not get keyboard focus when it mapped");
+
+ /* ------------------------------------------------------ take the grab */
+
+ grab = zwp_input_method_v2_grab_keyboard(im);
+ zwp_input_method_keyboard_grab_v2_add_listener(grab, &_grab_listener, &i);
+ _settle(app, ime);
+
+ if (i.grab_keymaps != 1)
+ tk_fail(ime, "the grab was sent %d keymaps, expected 1 - an input method "
+ "cannot interpret a keycode without one", i.grab_keymaps);
+ if (i.grab_repeat_infos != 1)
+ tk_fail(ime, "the grab was sent %d repeat_info events, expected 1; the "
+ "protocol says it arrives before any key press",
+ i.grab_repeat_infos);
+
+ /* ------------ a grab with no enabled text input must not take anything */
+
+ app_keys_before = a.keys;
+ tk_key(app, "a", 1);
+ tk_key(app, "a", 0);
+ _settle(app, ime);
+
+ if (i.grab_keys)
+ tk_fail(ime, "the grab received %d keys with no text input enabled. E "
+ "narrows the grab to an active text input on purpose: an "
+ "input method that takes every key on the seat takes the "
+ "keyboard away from the whole session if it ever stops "
+ "answering", i.grab_keys);
+ if (a.keys != app_keys_before + 2)
+ tk_fail(app, "the window received %d keys instead of 2 while no input "
+ "method was active", a.keys - app_keys_before);
+
+ /* ------------------------------------------- enable, and take the keys */
+
+ zwp_text_input_v3_enable(ti);
+ zwp_text_input_v3_commit(ti);
+ _settle(app, ime);
+
+ if (i.activates != 1)
+ tk_fail(ime, "enabling the text input did not activate the input method "
+ "(%d activates)", i.activates);
+
+ app_keys_before = a.keys;
+ tk_key(app, "a", 1);
+ _settle(app, ime);
+
+ if (i.grab_keys != 1)
+ tk_fail(ime, "the input method received %d keys, expected 1 - with a "
+ "grab held and a field enabled the key is its to compose "
+ "with", i.grab_keys);
+ if (i.grab_last_key != EVDEV_KEY_A)
+ tk_fail(ime, "the grab received keycode %u, expected %d (evdev KEY_A)",
+ i.grab_last_key, EVDEV_KEY_A);
+ if (i.grab_last_state != WL_KEYBOARD_KEY_STATE_PRESSED)
+ tk_fail(ime, "the grab received state %u for a press", i.grab_last_state);
+ if (!i.grab_mods)
+ tk_fail(ime, "the grab was sent no modifier state; an input method "
+ "cannot tell Shift+a from a without it");
+
+ if (a.keys != app_keys_before)
+ tk_fail(app, "the window also received the key the input method took "
+ "(%d extra) - forwarding to the grab and delivering to the "
+ "window types everything twice", a.keys - app_keys_before);
+
+ /* -------------------------------------------------- and give it back */
+
+ vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vk_mgr,
+ _seat_get(ime));
+ if (!vk) tk_fail(ime, "could not create a virtual keyboard");
+ _vk_keymap_set(vk);
+ zwp_virtual_keyboard_v1_key(vk, 0, EVDEV_KEY_A,
+ WL_KEYBOARD_KEY_STATE_PRESSED);
+ _settle(app, ime);
+
+ if (a.keys != app_keys_before + 1)
+ tk_fail(app, "a key returned through the virtual keyboard did not reach "
+ "the window (%d received). Without this path an input "
+ "method holding a grab swallows every key it does not turn "
+ "into text", a.keys - app_keys_before);
+ if (a.last_key != EVDEV_KEY_A)
+ tk_fail(app, "the returned key arrived as keycode %u, expected %d",
+ a.last_key, EVDEV_KEY_A);
+ if (a.last_state != WL_KEYBOARD_KEY_STATE_PRESSED)
+ tk_fail(app, "the returned key arrived with state %u", a.last_state);
+
+ /* The release follows the press to the same place. */
+ tk_key(app, "a", 0);
+ _settle(app, ime);
+ if (i.grab_keys != 2)
+ tk_fail(ime, "the release did not follow the press to the input method "
+ "(%d keys) - a window left holding a key that never came up "
+ "repeats it for ever", i.grab_keys);
+
+ zwp_virtual_keyboard_v1_key(vk, 0, EVDEV_KEY_A,
+ WL_KEYBOARD_KEY_STATE_RELEASED);
+ _settle(app, ime);
+
+ /* ------------------------------ disabling gives the keyboard back */
+
+ zwp_text_input_v3_disable(ti);
+ zwp_text_input_v3_commit(ti);
+ _settle(app, ime);
+
+ if (i.deactivates != 1)
+ tk_fail(ime, "disabling did not deactivate the input method");
+
+ app_keys_before = a.keys;
+ {
+ int grab_keys_before = i.grab_keys;
+
+ tk_key(app, "a", 1);
+ tk_key(app, "a", 0);
+ _settle(app, ime);
+
+ if (i.grab_keys != grab_keys_before)
+ tk_fail(ime, "the grab kept taking keys after the text input was "
+ "disabled (%d more)", i.grab_keys - grab_keys_before);
+ if (a.keys != app_keys_before + 2)
+ tk_fail(app, "the window got %d keys back instead of 2 once the field "
+ "was disabled", a.keys - app_keys_before);
+ }
+
+ /* ----------------------------- a key before a keymap is an error */
+
+ rogue = tk_connect(PROG "-rogue");
+ {
+ struct zwp_virtual_keyboard_manager_v1 *m;
+ struct zwp_virtual_keyboard_v1 *bad;
+
+ m = tk_bind(rogue, &zwp_virtual_keyboard_manager_v1_interface, 1);
+ if (!m) tk_fail(rogue, "virtual keyboard manager would not bind");
+ bad = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(m,
+ _seat_get(rogue));
+ zwp_virtual_keyboard_v1_key(bad, 0, EVDEV_KEY_A,
+ WL_KEYBOARD_KEY_STATE_PRESSED);
+ if (tk_check_error(rogue) == 0)
+ tk_fail(app, "a virtual keyboard sent a key before setting a keymap "
+ "and the compositor accepted it. The protocol names that "
+ "error - no_keymap - because a keycode with no keymap "
+ "means nothing");
+ }
+
+ printf(PROG ": ok - grab took %d keys, the window got %d, and the return "
+ "path works\n", i.grab_keys, a.keys);
+
+ tk_disconnect(rogue);
+ tk_disconnect(ime);
+ tk_disconnect(app);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.