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 4f52d2f8b5278b04a2b2dde942590ac8a6ae338e
Author: Cedric BAIL <[email protected]>
AuthorDate: Wed Aug 19 16:24:11 2026 -0600
e_comp_wl - the input method's candidate window
zwp_input_popup_surface_v2, the last unimplemented piece of E-13. It is
the one place in this protocol family where a surface belonging to one
client has to be positioned against a coordinate reported by another:
the input method draws its candidate list, and only the compositor knows
where on screen the text being typed actually is, because only it sees
both clients.
**Correcting what the earlier commit said about leaving it out.** That
comment claimed fcitx5 and ibus "fall back to a normal toplevel" when
this does nothing, so an inert object was harmless. Not established, and
wrong: both libwaylandim.so and libclassicui.so reference
zwp_input_popup_surface_v2, and an inert implementation is not an
*absent* one - the object was created and never mapped, so fcitx5 had no
way to detect it would not appear and no reason to fall back. The
expected symptom was composition working with the candidate list
invisible, which is a worse failure than refusing the request would have
been.
Shaped like the input panel in wl_desktop_shell, which is the same kind
of thing - a window E draws and positions but neither decorates nor
focuses. lock_focus_in is the one property that is load-bearing rather
than tidy: focus reaching this surface would leave the text input,
deactivate the input method, and hide the very window that had just
taken the focus.
Placement is below the caret with left edges aligned, which is where
every input method on every platform puts a candidate list - it must not
cover the text it is offering completions for. It flips above when there
is no room below, and clamps to the zone. text_input_rectangle then
reports the caret in the popup's *own* coordinates, which with that
placement is (0, -height); sending the compositor-space rectangle would
look plausible and be useless, so the test checks for exactly that
mistake and names it.
Visibility is the protocol's iff, honoured in both directions: shown
only while a text input is enabled, hidden the moment it is not. Both
halves matter - a candidate list that outlives its field is a window
nobody owns, one that never appears is an input method the user cannot
see what they are typing into.
One robustness case found by the test rather than reasoned about: a
client that attaches a buffer *before* asking for the role has already
had that commit processed, and e_comp_wl.c only calls shell.map when
shell.surface is set - so the map never happened and the window could
never appear. Nothing in the protocol requires the role first, so the
role assignment catches up. The test does it in the other order, which
is what a client actually does, so both paths are exercised.
No new global: input popups hang off zwp_input_method_v2, so
globals.expected is untouched.
Measured. Full wlcs 780 passed / 14 failed, failure set identical to
9f5a42182. In-tree 36 -> 37. The test checks placement, the rectangle's
coordinate space, the visibility iff and the already-has-a-role error;
placing at the window origin instead of the caret, and sending the
rectangle in compositor coordinates, were each checked against a build
that does them and each makes it fail with the message that names the
mistake.
Still unverified against a composing input method: only fcitx5's
pass-through `keyboard` engine is installed here, and it shows no
candidates, so it never asks for a popup.
---
src/bin/e_comp_wl_text_input.c | 368 +++++++++++++++++++++++++++++++++--
src/tests/wayland/e_wl_testkit.c | 36 ++++
src/tests/wayland/e_wl_testkit.h | 17 ++
src/tests/wayland/meson.build | 1 +
src/tests/wayland/test_input_popup.c | 327 +++++++++++++++++++++++++++++++
5 files changed, 733 insertions(+), 16 deletions(-)
diff --git a/src/bin/e_comp_wl_text_input.c b/src/bin/e_comp_wl_text_input.c
index 6bb3bd092..3fc8b58f3 100644
--- a/src/bin/e_comp_wl_text_input.c
+++ b/src/bin/e_comp_wl_text_input.c
@@ -124,6 +124,10 @@ struct Input_Method
* 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;
+
+ /* Candidate windows. A list because the protocol sets no limit, and an
+ * input method showing a candidate list and a status bar is two. */
+ Eina_List *popups;
};
struct Keyboard_Grab
@@ -132,6 +136,22 @@ struct Keyboard_Grab
Input_Method *im;
};
+/* A surface the input method wants placed next to the text being typed - its
+ * candidate list. The client owns the pixels; where it goes is ours. */
+typedef struct
+{
+ struct wl_resource *res;
+ E_Client *ec;
+ Input_Method *im;
+
+ /* The text cursor's rectangle in this surface's coordinates, as last sent.
+ * Kept so that an unchanged one is not re-sent on every commit - a client
+ * that re-lays-out its candidates whenever the hint moves would otherwise
+ * do it several times a keystroke. */
+ int rx, ry, rw, rh;
+ Eina_Bool rect_sent;
+} Input_Popup;
+
static struct wl_global *_text_input_manager_global;
static struct wl_global *_input_method_manager_global;
static struct wl_global *_virtual_keyboard_manager_global;
@@ -170,6 +190,10 @@ static Eina_Bool _grabbed_keys[GRABBED_KEYS_MAX];
static struct wl_listener _focus_surface_destroy;
static Eina_Bool _focus_surface_watched;
+/* Defined with the input popup code below. Called wherever the answer to
+ * "should the candidate window be up, and where" can have changed. */
+static void _input_popups_update(void);
+
static void
_text_input_state_clear(Text_Input_State *st)
{
@@ -259,6 +283,7 @@ _im_deactivate(void)
if (!im) return;
zwp_input_method_v2_send_deactivate(im->res);
_im_done(im);
+ _input_popups_update();
/* "This event resets all state associated with previous surrounding_text,
* text_change_cause, and content_type events, as well as the state
* associated with set_preedit_string, commit_string, and
@@ -397,6 +422,9 @@ _e_text_input_cb_commit(struct wl_client *client EINA_UNUSED, struct wl_resource
}
_im_send_state(im, ti);
_im_done(im);
+ /* A commit is also how a client reports its cursor moving, so the
+ * candidate window follows the caret from here. */
+ _input_popups_update();
}
else if (was_enabled && (_active_ti == ti))
_im_deactivate();
@@ -575,29 +603,332 @@ _e_input_method_cb_commit(struct wl_client *client EINA_UNUSED, struct wl_resour
_input_method_pending_clear(im);
}
-static void
-_e_input_method_cb_input_popup_surface_get(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface EINA_UNUSED)
-{
- struct wl_resource *res;
+/*** zwp_input_popup_surface_v2 - the candidate window ***/
+
+/* The input method's candidate list, placed by the compositor next to the text
+ * being typed. It is the one part of this file where a surface belonging to
+ * one client has to be positioned against a coordinate reported by another,
+ * and the reason it can be done at all is that the compositor is the only
+ * party that knows both.
+ *
+ * "It must be visible if and only if the input method is in the active state."
+ * That is the whole visibility rule and it is not ours to soften: a candidate
+ * list left on screen after the field it belongs to has gone is a window with
+ * no owner, and one that fails to appear is an input method the user cannot
+ * see what they are typing into.
+ *
+ * The surface never takes focus. It must not: focus moving to it would leave
+ * the text input, deactivate the input method, and hide the very window that
+ * had just taken the focus. lock_focus_in below is what prevents that, and it
+ * is load-bearing rather than tidiness.
+ */
+
+/* Where the text cursor is, in compositor coordinates, and how big.
+ * EINA_FALSE when there is nothing to point at - no focus, or a client that
+ * never described its cursor area, which the protocol explicitly allows. */
+static Eina_Bool
+_cursor_rect_get(int *x, int *y, int *w, int *h)
+{
+ E_Client *ec;
+ int scale;
+
+ if ((!_focus_surface) || (!_active_ti)) return EINA_FALSE;
+ if (!_active_ti->current.enabled) return EINA_FALSE;
+
+ ec = wl_resource_get_user_data(_focus_surface);
+ if ((!ec) || e_object_is_del(E_OBJECT(ec))) return EINA_FALSE;
+
+ /* Surface-local, so it scales like every other coordinate a client sends -
+ * the same conversion the pointer's cursor hotspot gets. */
+ scale = e_comp_wl_client_scale_get(ec);
+
+ if (!_active_ti->current.cursor_rect_set)
+ {
+ /* No cursor area given. Aim at the window's own top-left rather than
+ * at nothing: an input method whose candidates appear in the corner of
+ * the right window is usable, one whose candidates appear at the
+ * screen origin looks broken. */
+ *x = ec->x;
+ *y = ec->y;
+ *w = 0;
+ *h = 0;
+ return EINA_TRUE;
+ }
+
+ *x = ec->x + (_active_ti->current.cursor_x * scale);
+ *y = ec->y + (_active_ti->current.cursor_y * scale);
+ *w = _active_ti->current.cursor_w * scale;
+ *h = _active_ti->current.cursor_h * scale;
+ return EINA_TRUE;
+}
+
+static void
+_input_popup_place(Input_Popup *ip)
+{
+ int cx, cy, cw, ch;
+ int px, py;
+ int zx, zy, zw, zh;
+ E_Zone *zone;
+
+ if ((!ip->ec) || e_object_is_del(E_OBJECT(ip->ec))) return;
+ if (!_cursor_rect_get(&cx, &cy, &cw, &ch)) return;
+
+ /* Below the cursor, left edges aligned - where every input method on every
+ * platform puts a candidate list, because it must not cover the text it is
+ * offering completions for. */
+ px = cx;
+ py = cy + ch;
+
+ zone = ip->ec->zone ?: e_zone_current_get();
+ if (zone)
+ {
+ e_zone_useful_geometry_get(zone, &zx, &zy, &zw, &zh);
+ if (px + ip->ec->w > zx + zw) px = zx + zw - ip->ec->w;
+ if (px < zx) px = zx;
+ /* No room below: flip above the cursor rather than run off the
+ * bottom. A candidate list half off the screen is unusable, and the
+ * space above is exactly as good. */
+ if (py + ip->ec->h > zy + zh)
+ {
+ py = cy - ip->ec->h;
+ if (py < zy) py = zy;
+ }
+ }
+
+ e_client_util_move_without_frame(ip->ec, px, py);
+
+ /* "Notify about the position of the area of the text input expressed as a
+ * rectangle in surface local coordinates" - local to *this* surface, so it
+ * is the cursor rectangle measured from where the popup was just put. With
+ * the placement above that is (0, -height), and the input method uses it to
+ * decide which way to lay its candidates out. */
+ {
+ int rx = cx - px, ry = cy - py;
+
+ if ((!ip->rect_sent) || (rx != ip->rx) || (ry != ip->ry) ||
+ (cw != ip->rw) || (ch != ip->rh))
+ {
+ zwp_input_popup_surface_v2_send_text_input_rectangle(ip->res, rx, ry,
+ cw, ch);
+ ip->rx = rx; ip->ry = ry; ip->rw = cw; ip->rh = ch;
+ ip->rect_sent = EINA_TRUE;
+ }
+ }
+}
+
+static void
+_input_popup_visible_update(Input_Popup *ip)
+{
+ Eina_Bool want;
+
+ if ((!ip->ec) || e_object_is_del(E_OBJECT(ip->ec))) return;
+
+ /* "visible if and only if the input method is in the active state", and
+ * only once there is something to show - a surface with no buffer yet has
+ * nothing to put on screen. */
+ want = (_active_ti && _active_ti->current.enabled &&
+ ip->ec->comp_data->mapped && e_pixmap_usable_get(ip->ec->pixmap));
+
+ if (want)
+ {
+ _input_popup_place(ip);
+ ip->ec->visible = EINA_TRUE;
+ evas_object_geometry_set(ip->ec->frame, ip->ec->x, ip->ec->y,
+ ip->ec->w, ip->ec->h);
+ evas_object_show(ip->ec->frame);
+ e_comp_object_damage(ip->ec->frame, 0, 0, ip->ec->w, ip->ec->h);
+ }
+ else
+ {
+ ip->ec->visible = EINA_FALSE;
+ evas_object_hide(ip->ec->frame);
+ }
+}
+
+/* Called wherever the answer to "should the candidates be up, and where"
+ * changes: activation, deactivation, and every text-input commit, because a
+ * commit is how a client reports its cursor moving. */
+static void
+_input_popups_update(void)
+{
+ Input_Method *im = _im_get();
+ Input_Popup *ip;
+ Eina_List *l;
+
+ if (!im) return;
+ EINA_LIST_FOREACH(im->popups, l, ip)
+ _input_popup_visible_update(ip);
+}
+
+static void
+_input_popup_shell_map(struct wl_resource *resource)
+{
+ Input_Popup *ip = wl_resource_get_user_data(resource);
+
+ if ((!ip) || (!ip->ec) || e_object_is_del(E_OBJECT(ip->ec))) return;
+ if (!e_pixmap_usable_get(ip->ec->pixmap)) return;
+ ip->ec->comp_data->mapped = EINA_TRUE;
+ _input_popup_visible_update(ip);
+}
+
+static void
+_input_popup_shell_unmap(struct wl_resource *resource)
+{
+ Input_Popup *ip = wl_resource_get_user_data(resource);
+
+ if ((!ip) || (!ip->ec) || e_object_is_del(E_OBJECT(ip->ec))) return;
+ ip->ec->comp_data->mapped = EINA_FALSE;
+ ip->ec->visible = EINA_FALSE;
+ evas_object_hide(ip->ec->frame);
+}
+
+static void
+_input_popup_shell_configure(struct wl_resource *resource, Evas_Coord x EINA_UNUSED, Evas_Coord y EINA_UNUSED, Evas_Coord w, Evas_Coord h)
+{
+ Input_Popup *ip = wl_resource_get_user_data(resource);
+
+ if ((!ip) || (!ip->ec) || e_object_is_del(E_OBJECT(ip->ec))) return;
+ /* The client chooses its own size - the compositor has no business telling
+ * a candidate list how big to be - and a size change moves it, because the
+ * placement below the cursor depends on how tall it is. */
+ e_client_util_resize_without_frame(ip->ec, w, h);
+ _input_popup_visible_update(ip);
+}
+
+static void
+_e_input_popup_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static const struct zwp_input_popup_surface_v2_interface _e_input_popup_interface =
+{
+ _e_input_popup_cb_destroy,
+};
+
+static void
+_e_input_popup_res_destroy(struct wl_resource *resource)
+{
+ Input_Popup *ip = wl_resource_get_user_data(resource);
+ E_Client *ec;
+
+ if (!ip) return;
+ if (ip->im) ip->im->popups = eina_list_remove(ip->im->popups, ip);
+
+ ec = ip->ec;
+ if (ec && (!e_object_is_del(E_OBJECT(ec))) && ec->comp_data)
+ {
+ ec->visible = EINA_FALSE;
+ if (ec->frame) evas_object_hide(ec->frame);
+ ec->comp_data->mapped = EINA_FALSE;
+ /* Give the role back. The protocol forbids destroying the wl_surface
+ * while this object lives, so the surface outlives us and could
+ * legally be given another role. */
+ ec->comp_data->shell.surface = NULL;
+ ec->comp_data->shell.configure = NULL;
+ ec->comp_data->shell.map = NULL;
+ ec->comp_data->shell.unmap = NULL;
+ }
+ free(ip);
+}
+
+static void
+_e_input_method_cb_input_popup_surface_get(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface)
+{
+ Input_Method *im = wl_resource_get_user_data(resource);
+ Input_Popup *ip;
+ struct wl_resource *res;
+ E_Client *ec;
+ E_Comp_Client_Data *cd;
+
+ ec = surface ? wl_resource_get_user_data(surface) : NULL;
+ if ((!ec) || e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data))
+ {
+ wl_resource_post_error(resource, WL_DISPLAY_ERROR_INVALID_OBJECT,
+ "no surface for input popup");
+ return;
+ }
+ cd = ec->comp_data;
+
+ /* "The surface gets assigned the 'input_popup' role. If the surface
+ * already has an assigned role, the compositor must issue a protocol
+ * error." shell.surface is what holds a role in E. */
+ if (cd->shell.surface)
+ {
+ wl_resource_post_error(resource, ZWP_INPUT_METHOD_V2_ERROR_ROLE,
+ "wl_surface already has a role");
+ return;
+ }
+
+ ip = E_NEW(Input_Popup, 1);
+ if (!ip)
+ {
+ wl_resource_post_no_memory(resource);
+ return;
+ }
- /* Not implemented: the object is created so the client stays protocol-legal
- * and keeps working, but the surface is never mapped.
- *
- * An input popup is the IME's candidate window, and placing one means
- * making a surface E does not own follow the cursor rectangle of a surface
- * in a different client. fcitx5 and ibus both fall back to a normal
- * toplevel for candidates when this does nothing, which is the reason it is
- * safe to leave here rather than refuse the request: refusing would take
- * the IME's connection down and lose the user their input method, to avoid
- * a misplaced window. */
res = wl_resource_create(client, &zwp_input_popup_surface_v2_interface,
wl_resource_get_version(resource), id);
if (!res)
{
- wl_client_post_no_memory(client);
+ free(ip);
+ wl_resource_post_no_memory(resource);
return;
}
- wl_resource_set_implementation(res, NULL, NULL, NULL);
+
+ ip->res = res;
+ ip->ec = ec;
+ wl_resource_set_implementation(res, &_e_input_popup_interface, ip,
+ _e_input_popup_res_destroy);
+
+ /* An inert input method still gets a legal object, and it stays unplaced -
+ * "any further requests and events except for the destroy request must be
+ * ignored". Not adding it to any list is what ignoring looks like here. */
+ if (im && (!im->inert))
+ {
+ ip->im = im;
+ im->popups = eina_list_append(im->popups, ip);
+ }
+
+ /* The same shape as the input panel in wl_desktop_shell: a window E draws
+ * and positions but neither decorates nor focuses. lock_focus_in is the
+ * one that matters - focus reaching this surface would leave the text
+ * input, deactivate the input method and hide this very window. */
+ EC_CHANGED(ec);
+ if (!ec->new_client)
+ {
+ ec->new_client = EINA_TRUE;
+ e_comp->new_clients++;
+ }
+ if (ec->ignored) e_client_unignore(ec);
+
+ ec->borderless = EINA_TRUE;
+ ec->argb = EINA_TRUE;
+ ec->lock_border = EINA_TRUE;
+ ec->lock_focus_in = ec->lock_focus_out = EINA_TRUE;
+ ec->netwm.state.skip_taskbar = EINA_TRUE;
+ ec->netwm.state.skip_pager = EINA_TRUE;
+ ec->no_shape_cut = EINA_TRUE;
+ ec->border_size = 0;
+ ec->netwm.type = E_WINDOW_TYPE_UTILITY;
+ /* Assigned directly, the way the drag surface in e_comp_wl_data.c does it -
+ * the same shape of thing, a client surface the compositor positions and
+ * the user never manages. */
+ ec->layer = E_LAYER_CLIENT_POPUP;
+
+ cd->shell.surface = res;
+ cd->shell.configure_send = NULL;
+ cd->shell.configure = _input_popup_shell_configure;
+ cd->shell.ping = NULL;
+ cd->shell.map = _input_popup_shell_map;
+ cd->shell.unmap = _input_popup_shell_unmap;
+
+ /* A client that attached a buffer before asking for the role has already
+ * had that commit processed, and the map it would have triggered found no
+ * role to call - e_comp_wl.c only calls shell.map if shell.surface is set.
+ * Nothing in the protocol requires the role first, so catch up here rather
+ * than leave a window that can never appear. */
+ if (e_pixmap_usable_get(ec->pixmap)) _input_popup_shell_map(res);
}
static void
@@ -771,6 +1102,11 @@ _e_input_method_res_destroy(struct wl_resource *resource)
if (!im) return;
if (_input_method == im) _input_method = NULL;
+ {
+ Input_Popup *ip;
+
+ EINA_LIST_FREE(im->popups, ip) ip->im = NULL;
+ }
_input_method_pending_clear(im);
free(im);
}
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
index 1e3c9876f..306d9a857 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -774,6 +774,42 @@ tk_pointer_button(Tk *tk, unsigned int button, int pressed)
tk_settle(tk);
}
+struct wl_surface *
+tk_surface_new(Tk *tk)
+{
+ struct wl_surface *surface = wl_compositor_create_surface(tk->compositor);
+
+ if (!surface) tk_fail(tk, "cannot create a surface");
+ return surface;
+}
+
+void
+tk_surface_paint(Tk *tk, struct wl_surface *surface, int w, int h)
+{
+ struct wl_buffer *buffer = _buffer_make(tk, w, h);
+
+ if (!buffer) tk_fail(tk, "cannot make a %dx%d buffer", w, h);
+ wl_surface_attach(surface, buffer, 0, 0);
+ wl_surface_damage(surface, 0, 0, w, h);
+ wl_surface_commit(surface);
+}
+
+struct wl_surface *
+tk_surface_new_painted(Tk *tk, int w, int h)
+{
+ struct wl_surface *surface = tk_surface_new(tk);
+
+ tk_surface_paint(tk, surface, w, h);
+ return surface;
+}
+
+void
+tk_move_surface(Tk *tk, struct wl_surface *surface, int x, int y)
+{
+ wl_test_move_surface(tk->tester, surface, x, y);
+ tk_settle(tk);
+}
+
void
tk_key(Tk *tk, const char *keyname, int pressed)
{
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
index 1c7170d4f..74381dbde 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -253,4 +253,21 @@ void tk_pointer_button(Tk *tk, unsigned int button, int pressed);
* pressed a key reached none of them. */
void tk_key(Tk *tk, const char *keyname, int pressed);
+/* A bare wl_surface with a buffer already attached and committed, for the
+ * protocols that take a surface and give it a role themselves - an input
+ * method's candidate window is the first. Not an xdg_surface: those roles come
+ * with a configure handshake, and a role assigned by another protocol does
+ * not. The kit keeps the buffer alive for the life of the connection. */
+struct wl_surface *tk_surface_new_painted(Tk *tk, int w, int h);
+
+/* The two halves of it, for when the role has to be assigned in between -
+ * which is the order a client actually uses, and the order that exercises the
+ * compositor's normal map path rather than its catch-up one. */
+struct wl_surface *tk_surface_new(Tk *tk);
+void tk_surface_paint(Tk *tk, struct wl_surface *surface, int w, int h);
+
+/* Put a window where the test wants it, so that a coordinate it asserts on is
+ * one it chose rather than one the placement policy happened to pick. */
+void tk_move_surface(Tk *tk, struct wl_surface *surface, int x, int y);
+
#endif
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 6bb7f9ca2..c60b18ff3 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -98,6 +98,7 @@ wl_protocol_tests = [
['dialog-focus', 'test_dialog_focus.c'],
['text-input', 'test_text_input.c'],
['input-method-keyboard', 'test_input_method_keyboard.c'],
+ ['input-popup', 'test_input_popup.c'],
]
# Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_input_popup.c b/src/tests/wayland/test_input_popup.c
new file mode 100644
index 000000000..24f49eab7
--- /dev/null
+++ b/src/tests/wayland/test_input_popup.c
@@ -0,0 +1,327 @@
+/* zwp_input_popup_surface_v2: the input method's candidate window.
+ *
+ * The one part of this protocol family where a surface belonging to one client
+ * has to be positioned against a coordinate reported by another. The input
+ * method draws its candidate list; only the compositor knows where the text
+ * being typed actually is on screen, because only it sees both clients.
+ *
+ * Four things are checked, and each is a different way for it to be wrong:
+ *
+ * * the popup is **placed at the cursor**, not at the origin and not at the
+ * window's corner. The test puts the application window somewhere known,
+ * gives it a cursor rectangle somewhere known inside that, and asks the
+ * compositor where the popup ended up.
+ *
+ * * `text_input_rectangle` arrives, **in the popup's own coordinates**. That
+ * is the hint the input method lays its candidates out from, and it is the
+ * easiest thing in the protocol to send in the wrong coordinate space -
+ * sending the compositor-space rectangle would look plausible and be
+ * useless.
+ *
+ * * it is **visible if and only if the input method is active**. The
+ * protocol states that as an iff, and both halves matter: a candidate list
+ * that outlives its field is a window with no owner, and one that never
+ * appears is an input method the user cannot see.
+ *
+ * * a surface that **already has a role** is refused. A wl_surface can only
+ * be one thing, and the protocol names the error.
+ *
+ * The test plays both clients itself, which is the only way to hold the
+ * application's cursor rectangle still while asking where the popup went. That
+ * it works against a real input method is what test_fcitx5.c is for.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "e_wl_testkit.h"
+#include "text-input-unstable-v3-client-protocol.h"
+#include "input-method-unstable-v2-client-protocol.h"
+
+#define PROG "test-input-popup"
+
+/* Where the application window goes, and where its text cursor is inside it.
+ * Chosen so that every number below is distinct: a popup at the window origin,
+ * at the screen origin or at the cursor are three different answers and the
+ * assertion can tell them apart. */
+#define APP_X 120
+#define APP_Y 90
+#define APP_W 400
+#define APP_H 300
+#define CUR_X 40
+#define CUR_Y 60
+#define CUR_W 2
+#define CUR_H 20
+
+/* The candidate window. Small enough that it fits below the cursor on any
+ * screen this suite runs on, so the test is not accidentally measuring the
+ * flip-above-when-there-is-no-room path. */
+#define POP_W 160
+#define POP_H 48
+
+typedef struct
+{
+ int rects;
+ int32_t rx, ry, rw, rh;
+} Popup;
+
+static void
+_rect(void *data, struct zwp_input_popup_surface_v2 *p, int32_t x, int32_t y,
+ int32_t w, int32_t h)
+{
+ Popup *pp = data;
+
+ (void)p;
+ pp->rx = x; pp->ry = y; pp->rw = w; pp->rh = h;
+ pp->rects++;
+}
+
+static const struct zwp_input_popup_surface_v2_listener _popup_listener =
+{ _rect };
+
+static int _activates, _deactivates;
+
+static void
+_im_activate(void *d, struct zwp_input_method_v2 *im)
+{ (void)d; (void)im; _activates++; }
+static void
+_im_deactivate(void *d, struct zwp_input_method_v2 *im)
+{ (void)d; (void)im; _deactivates++; }
+static void
+_im_nop_s(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_nop_u(void *d, struct zwp_input_method_v2 *im, uint32_t c)
+{ (void)d; (void)im; (void)c; }
+static void
+_im_nop_uu(void *d, struct zwp_input_method_v2 *im, uint32_t a, uint32_t b)
+{ (void)d; (void)im; (void)a; (void)b; }
+static void
+_im_nop(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_nop_s, _im_nop_u, _im_nop_uu, _im_nop,
+ _im_nop };
+
+static void
+_ti_nop_surface(void *d, struct zwp_text_input_v3 *t, struct wl_surface *s)
+{ (void)d; (void)t; (void)s; }
+static void
+_ti_nop_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_nop_str(void *d, struct zwp_text_input_v3 *t, const char *x)
+{ (void)d; (void)t; (void)x; }
+static void
+_ti_nop_uu(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_nop_u(void *d, struct zwp_text_input_v3 *t, uint32_t a)
+{ (void)d; (void)t; (void)a; }
+
+static const struct zwp_text_input_v3_listener _ti_listener =
+{ _ti_nop_surface, _ti_nop_surface, _ti_nop_preedit, _ti_nop_str, _ti_nop_uu,
+ _ti_nop_u };
+
+/* The candidate window has no app_id and no title - it is a bare wl_surface
+ * that another protocol gave a role to - so it is found by its size, which the
+ * test chose and nothing else in the session shares. */
+static Tk_Client *
+_popup_window(Tk *tk)
+{
+ static Tk_Client all[TK_MAX_CLIENTS];
+ int n, i;
+
+ n = tk_clients(tk, all, TK_MAX_CLIENTS);
+ for (i = 0; i < n; i++)
+ if ((all[i].w == POP_W) && (all[i].h == POP_H)) return &all[i];
+ return NULL;
+}
+
+static Tk_Client *
+_wait_popup(Tk *tk, int want_visible, int timeout_ms)
+{
+ Tk_Client *c;
+ int i;
+
+ for (i = 0; i * 200 < timeout_ms; i++)
+ {
+ c = _popup_window(tk);
+ if (want_visible && c && (c->states & WL_TEST_CLIENT_STATE_VISIBLE))
+ return c;
+ if ((!want_visible) &&
+ ((!c) || (!(c->states & WL_TEST_CLIENT_STATE_VISIBLE))))
+ return c;
+ tk_sync(tk);
+ }
+ return NULL;
+}
+
+int
+main(void)
+{
+ Tk *app, *ime;
+ Tk_Toplevel *top;
+ Tk_Client *w;
+ 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_popup_surface_v2 *popup;
+ struct wl_surface *psurf;
+ struct wl_seat *app_seat, *ime_seat;
+ Popup pp = { 0 };
+ uint32_t v;
+ int cur_x, cur_y;
+
+ app = tk_connect(PROG "-app");
+ ime = tk_connect(PROG "-ime");
+
+ v = tk_global_version(app, "wl_seat");
+ if (!v) tk_fail(app, "no wl_seat");
+ app_seat = tk_bind(app, &wl_seat_interface, v);
+ ime_seat = tk_bind(ime, &wl_seat_interface, v);
+
+ ti_mgr = tk_bind(app, &zwp_text_input_manager_v3_interface, 1);
+ im_mgr = tk_bind(ime, &zwp_input_method_manager_v2_interface, 1);
+ if ((!ti_mgr) || (!im_mgr)) tk_fail(app, "managers would not bind");
+
+ im = zwp_input_method_manager_v2_get_input_method(im_mgr, ime_seat);
+ zwp_input_method_v2_add_listener(im, &_im_listener, NULL);
+ tk_sync(ime);
+
+ top = tk_toplevel_new(app, "input-popup-app", "app", APP_W, APP_H);
+ tk_settle(app);
+ tk_move_surface(app, tk_toplevel_surface(top), APP_X, APP_Y);
+ tk_sync(app);
+
+ w = tk_expect(app, "input-popup-app");
+ cur_x = w->x + CUR_X;
+ cur_y = w->y + CUR_Y;
+ printf(PROG ": app window at %d,%d - cursor should be at %d,%d\n",
+ w->x, w->y, cur_x, cur_y);
+
+ /* ------------------------------------------- the candidate window */
+
+ /* Role first, then pixels - the order a client uses, and the order that
+ * exercises the compositor's ordinary map-on-commit path. */
+ psurf = tk_surface_new(ime);
+ popup = zwp_input_method_v2_get_input_popup_surface(im, psurf);
+ zwp_input_popup_surface_v2_add_listener(popup, &_popup_listener, &pp);
+ tk_surface_paint(ime, psurf, POP_W, POP_H);
+ tk_sync(ime);
+ tk_sync(app);
+
+ /* Nothing is being typed into yet, so it must not be on screen. */
+ if (_wait_popup(app, 0, 2000) && _popup_window(app) &&
+ (_popup_window(app)->states & WL_TEST_CLIENT_STATE_VISIBLE))
+ tk_fail(app, "the candidate window is visible with no text input "
+ "enabled - the protocol makes that an iff, and a candidate "
+ "list with no field behind it is a window nobody owns");
+ printf(PROG ": not shown while no field is active\n");
+
+ /* ------------------------------------------------- now type into one */
+
+ ti = zwp_text_input_manager_v3_get_text_input(ti_mgr, app_seat);
+ zwp_text_input_v3_add_listener(ti, &_ti_listener, NULL);
+ zwp_text_input_v3_enable(ti);
+ zwp_text_input_v3_set_cursor_rectangle(ti, CUR_X, CUR_Y, CUR_W, CUR_H);
+ zwp_text_input_v3_commit(ti);
+ tk_sync(app);
+ tk_sync(ime);
+ tk_sync(app);
+
+ if (!_activates)
+ tk_fail(ime, "enabling the text input did not activate the input method");
+
+ w = _wait_popup(app, 1, 5000);
+ if (!w)
+ tk_fail(app, "the candidate window never appeared once a field was being "
+ "typed into. The input method activated, so the compositor "
+ "knows there is a field; this is the placement half");
+
+ printf(PROG ": candidate window at %d,%d %dx%d\n", w->x, w->y, w->w, w->h);
+
+ /* Below the cursor, left edges aligned. */
+ if (w->x != cur_x)
+ tk_fail(app, "the candidate window is at x=%d, expected %d - the cursor "
+ "rectangle's left edge. %d would be the window's own origin "
+ "and 0 would be the screen's, which are the two ways to get "
+ "this wrong without noticing", w->x, cur_x, APP_X);
+ if (w->y != cur_y + CUR_H)
+ tk_fail(app, "the candidate window is at y=%d, expected %d - just below "
+ "the cursor. A candidate list must not cover the text it is "
+ "offering completions for", w->y, cur_y + CUR_H);
+
+ /* --------------------------------------- the rectangle it was told */
+
+ if (!pp.rects)
+ tk_fail(ime, "no text_input_rectangle was sent. It is the only thing "
+ "telling the input method where the caret is relative to "
+ "its own window, and without it candidates cannot be laid "
+ "out on the correct side");
+
+ /* Surface-local to the popup. The popup sits at the cursor's x and one
+ * cursor-height below it, so the caret is at the popup's top-left corner,
+ * one height up. */
+ if ((pp.rx != 0) || (pp.ry != -CUR_H))
+ tk_fail(ime, "text_input_rectangle put the caret at %d,%d in the popup's "
+ "coordinates, expected 0,%d. Compositor-space coordinates "
+ "would be %d,%d - which is the mistake this checks for",
+ pp.rx, pp.ry, -CUR_H, cur_x, cur_y);
+ if ((pp.rw != CUR_W) || (pp.rh != CUR_H))
+ tk_fail(ime, "text_input_rectangle reported the caret as %dx%d, expected "
+ "%dx%d", pp.rw, pp.rh, CUR_W, CUR_H);
+ printf(PROG ": text_input_rectangle %d,%d %dx%d in popup coordinates\n",
+ pp.rx, pp.ry, pp.rw, pp.rh);
+
+ /* ------------------------------------------------ and away again */
+
+ zwp_text_input_v3_disable(ti);
+ zwp_text_input_v3_commit(ti);
+ tk_sync(app);
+ tk_sync(ime);
+ tk_sync(app);
+
+ w = _wait_popup(app, 0, 5000);
+ if (w && (w->states & WL_TEST_CLIENT_STATE_VISIBLE))
+ tk_fail(app, "the candidate window is still on screen after the field "
+ "was disabled (%d deactivate). The protocol's iff has two "
+ "halves and this is the one that leaves rubbish on a user's "
+ "screen", _deactivates);
+ printf(PROG ": hidden again when the field went away\n");
+
+ /* --------------------------------- a surface can only be one thing */
+
+ {
+ Tk *rogue = tk_connect(PROG "-rogue");
+ struct zwp_input_method_manager_v2 *m;
+ struct zwp_input_method_v2 *i2;
+ struct wl_seat *s;
+ Tk_Toplevel *t2;
+
+ v = tk_global_version(rogue, "wl_seat");
+ s = tk_bind(rogue, &wl_seat_interface, v);
+ m = tk_bind(rogue, &zwp_input_method_manager_v2_interface, 1);
+ i2 = zwp_input_method_manager_v2_get_input_method(m, s);
+ /* This one loses the seat to the input method above and is inert, which
+ * does not matter: the role check happens before anything else. */
+ t2 = tk_toplevel_new(rogue, "input-popup-rogue", "rogue", 100, 100);
+ zwp_input_method_v2_get_input_popup_surface(i2, tk_toplevel_surface(t2));
+
+ if (tk_check_error(rogue) == 0)
+ tk_fail(app, "a wl_surface that is already an xdg_toplevel was "
+ "accepted as an input popup. A surface can hold one role "
+ "and the protocol names the error for this");
+ printf(PROG ": a surface with a role is refused\n");
+ tk_disconnect(rogue);
+ }
+
+ printf(PROG ": ok - placed at the caret, told where it is, and shown only "
+ "while a field is active\n");
+
+ tk_disconnect(ime);
+ tk_disconnect(app);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.