popup fallback: mount bar menus at output origin only for foreign clients

Parentless xdg_popups (menus opened by layer-shell bars such as
wf-panel-pi) must hang off the output-origin popup tree so they render
flush under the bar instead of being shifted down by the bar's reserved
edge.  But a parentless popup from the active tab's own client (an
in-tab menu whose parent surface was missed) must keep hanging under
that window's tree to preserve its old alignment.

Resolve the popup client's pid and choose the mount accordingly;
same-client popups keep the window tree, foreign clients get the origin
tree.

Also add two KICK_LOG_DEBUG signals the deterministic regression test
keys on: [layer] map pid=N when a layer surface maps (the bar is ready
for input) and [popup] out=x,y geo=w x h pcpid=N with the popup's
scene-exact output position once it maps.  test_smoke.py's
test_panel_menu_flush_under_bar waits on those lines (no pixel diffing,
no fixed sleeps) and asserts the wf-panel-pi menu's output top is flush
under the 52px bar; against a build with the fix reverted it fails with
the menu at y=109.
This commit is contained in:
Andrew Hamilton 2026-09-02 10:26:43 +10:00
parent ebae43242f
commit e539be4338
2 changed files with 116 additions and 16 deletions

73
kick.c
View file

@ -354,10 +354,11 @@ struct kick_server {
struct wlr_layer_shell_v1 *layer_shell;
struct wl_listener new_layer_surface;
struct wlr_scene_tree *layer_tree;
/* Parentless xdg_popups (menus/tooltips from a client without a live
toplevel parent, e.g. GTK layer-shell bars) hang off this dedicated
scene tree so they map at the output origin instead of inheriting a
tab tree's bar-offset. Kept on top like layer_tree. */
/* xdg_popups from foreign clients (a layer-shell bar such as wf-panel-pi)
that have no live kick-managed parent hang off this dedicated scene
tree, rooted at the output origin, so their positioner geometry maps to
real pixels instead of inheriting a tab tree's bar-offset. Kept on top
like layer_tree. */
struct wlr_scene_tree *popup_tree;
/* Layer surfaces, in reservation order (head = highest layer / lowest
overlap priority); see kick_layer. */
@ -4248,6 +4249,21 @@ static void popup_handle_commit(struct wl_listener *listener, void *data) {
popup_usable_box(&box);
wlr_xdg_popup_unconstrain_from_box(xdg_surface->popup, &box);
}
/* Once the popup maps, report its output-space top-left and size. Tests
key on this line to assert a bar's menu hung flush under the bar; the
coordinates are scene-exact (no wall-clock guesswork). */
if (xdg_surface->surface->mapped && tp->scene) {
int lx = 0, ly = 0;
wlr_scene_node_coords(&tp->scene->node, &lx, &ly);
pid_t pcpid = -1;
if (xdg_surface->client && xdg_surface->client->client)
wl_client_get_credentials(xdg_surface->client->client,
&pcpid, NULL, NULL);
kick_log(KICK_LOG_DEBUG,
"[popup] out=%d,%d geo=%dx%d pcpid=%d\n", lx, ly,
xdg_surface->geometry.width, xdg_surface->geometry.height,
pcpid);
}
}
static void popup_handle_reposition(struct wl_listener *listener, void *data) {
@ -4350,19 +4366,34 @@ found:
a window that closed, or a non-tab surface such as a layer-shell bar).
Rather than drop the popup -- which would leave the client waiting
forever for its initial configure ("Timed out waiting for initial
.configure") -- fall back to the currently-displayed window's tree so
the popup still gets rendered and configured. */
.configure") -- fall back so the popup still gets rendered and
configured, choosing the mount by which client made it:
* a parentless popup from the active tab's own client still hangs
under that window's (bar-offset) tree, keeping it aligned with the
app's content just as before; and
* a popup from any other client (a layer-shell bar such as
wf-panel-pi, or a gone/foreign surface) maps at the output origin
so its positioner geometry lands on real pixels instead of being
shifted down/right past the bar's reserved edge. */
if (!parent_tree) {
kick_log(KICK_LOG_DEBUG,
"[popup] parent %p not live; hanging popup under popup tree "
"at output origin\n",
(void *)parent_ws);
/* No live kick-managed parent. These are popups whose parent was a
layer-shell bar or a gone window. Map them under the root-level
popup_tree so their positioner geometry lands at the real output
coordinates (bars' menus would otherwise be shifted down by the
bar's reserved edge from hanging under an offset window tree). */
parent_tree = server.popup_tree;
pid_t pcpid = -1;
if (popup->base->client)
wl_client_get_credentials(popup->base->client->client,
&pcpid, NULL, NULL);
struct kick_window *cur = tab_current_window(server.active_tab);
if (cur && cur->client_pid > 0 && cur->client_pid == pcpid) {
kick_log(KICK_LOG_DEBUG,
"[popup] parent %p not live; reusing active window tree "
"for same-client popup\n",
(void *)parent_ws);
parent_tree = popup_tree_for_window(cur);
} else {
kick_log(KICK_LOG_DEBUG,
"[popup] parent %p not live; hanging popup under popup "
"tree at output origin\n",
(void *)parent_ws);
parent_tree = server.popup_tree;
}
}
if (!parent_tree) return;
create_popup(popup, parent_tree);
@ -4522,6 +4553,16 @@ static void layer_handle_commit(struct wl_listener *listener, void *data) {
(void)data;
struct kick_layer *sl = wl_container_of(listener, sl, commit);
bool mapped = sl->surface->surface->mapped;
/* Deterministic "the bar is on screen" signal for tests: fires on the
commit that maps a layer surface, including a first-commit map. */
if (mapped && !sl->mapped) {
pid_t pid = -1;
if (sl->surface->resource)
wl_client_get_credentials(
wl_resource_get_client(sl->surface->resource),
&pid, NULL, NULL);
kick_log(KICK_LOG_DEBUG, "[layer] map pid=%d\n", pid);
}
if (!sl->seen_init) {
/* First commit. wlroots' layer_surface_role_commit runs first and
sets surface->initialized, so this is the earliest point we may call