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:
parent
ebae43242f
commit
e539be4338
2 changed files with 116 additions and 16 deletions
73
kick.c
73
kick.c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3081,3 +3081,62 @@ def test_layer_zero_exclusive_does_not_resize():
|
|||
layer_proc.wait(timeout=5)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
# -- wf-panel-pi menu placement ---------------------------------------
|
||||
|
||||
def test_panel_menu_flush_under_bar():
|
||||
"""wf-panel-pi opens its menus as (transiently) parentless xdg_popups.
|
||||
They must hang off the output origin so the menu renders flush under the
|
||||
top bar -- not shifted down by the bar's reserved edge (the menu-offset
|
||||
regression). Deterministic: waits on kick's own log signals ([layer] map,
|
||||
then the panel client's [popup] out= line), no pixel diffing or sleeps."""
|
||||
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||
panel = shutil.which("wf-panel-pi")
|
||||
if not panel:
|
||||
pytest.skip("wf-panel-pi not installed")
|
||||
s = _nested_session(f"{WL} '{RED}'", extra_args=("--panel", panel))
|
||||
try:
|
||||
s.wait_for(r"\[diag\] map app=0", timeout=20.0)
|
||||
|
||||
# The panel's layer surface must map before it can take clicks.
|
||||
s.wait_for(r"\[layer\] map pid=\d+", timeout=30.0)
|
||||
pm = re.findall(r"\[layer\] map pid=(\d+)", s.read_log())
|
||||
assert pm, "no [layer] map pid= line"
|
||||
panel_pid = int(pm[-1])
|
||||
|
||||
# Open the wf-panel-pi left menu (smenu button near the top-left).
|
||||
# The menu popup is created by the panel's own client, so its [popup]
|
||||
# out= line is tagged with the panel pid.
|
||||
out_rx = re.compile(
|
||||
r"\[popup\] out=-?\d+,(-?\d+) geo=\d+x\d+ pcpid=%d\b" % panel_pid)
|
||||
|
||||
s.warp(25, 26)
|
||||
last_y = None
|
||||
since = time.monotonic()
|
||||
while time.monotonic() - since < 30.0:
|
||||
s.click()
|
||||
# Poll long enough for the gtk menu to map, and re-click only if a
|
||||
# whole poll window passes with no popup (an already-open menu
|
||||
# re-click only toggles it closed; the mapped out= line is still
|
||||
# in the log, so we still detect it).
|
||||
poll_since = time.monotonic()
|
||||
while time.monotonic() - poll_since < 2.0:
|
||||
for m in out_rx.findall(s.read_log()):
|
||||
last_y = int(m)
|
||||
# The bar is 52px tall; the menu's output-space top must
|
||||
# sit flush under it. y ~109+ is the regression (menu
|
||||
# shifted down by the bar's reserved edge because it hung
|
||||
# under the active window's tree instead of the origin);
|
||||
# y <= 80 stays comfortably away from that.
|
||||
if 48 <= int(m) <= 80:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
tail = s.read_log()[-1200:]
|
||||
pytest.fail(
|
||||
f"panel menu popup never mapped flush under the bar "
|
||||
f"(pcpid={panel_pid}, outermost y seen={last_y}, bar is 52px);\n"
|
||||
f"{tail}")
|
||||
finally:
|
||||
s.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue