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 wlr_layer_shell_v1 *layer_shell;
|
||||||
struct wl_listener new_layer_surface;
|
struct wl_listener new_layer_surface;
|
||||||
struct wlr_scene_tree *layer_tree;
|
struct wlr_scene_tree *layer_tree;
|
||||||
/* Parentless xdg_popups (menus/tooltips from a client without a live
|
/* xdg_popups from foreign clients (a layer-shell bar such as wf-panel-pi)
|
||||||
toplevel parent, e.g. GTK layer-shell bars) hang off this dedicated
|
that have no live kick-managed parent hang off this dedicated scene
|
||||||
scene tree so they map at the output origin instead of inheriting a
|
tree, rooted at the output origin, so their positioner geometry maps to
|
||||||
tab tree's bar-offset. Kept on top like layer_tree. */
|
real pixels instead of inheriting a tab tree's bar-offset. Kept on top
|
||||||
|
like layer_tree. */
|
||||||
struct wlr_scene_tree *popup_tree;
|
struct wlr_scene_tree *popup_tree;
|
||||||
/* Layer surfaces, in reservation order (head = highest layer / lowest
|
/* Layer surfaces, in reservation order (head = highest layer / lowest
|
||||||
overlap priority); see kick_layer. */
|
overlap priority); see kick_layer. */
|
||||||
|
|
@ -4248,6 +4249,21 @@ static void popup_handle_commit(struct wl_listener *listener, void *data) {
|
||||||
popup_usable_box(&box);
|
popup_usable_box(&box);
|
||||||
wlr_xdg_popup_unconstrain_from_box(xdg_surface->popup, &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) {
|
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).
|
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
|
Rather than drop the popup -- which would leave the client waiting
|
||||||
forever for its initial configure ("Timed out waiting for initial
|
forever for its initial configure ("Timed out waiting for initial
|
||||||
.configure") -- fall back to the currently-displayed window's tree so
|
.configure") -- fall back so the popup still gets rendered and
|
||||||
the popup still gets rendered and configured. */
|
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) {
|
if (!parent_tree) {
|
||||||
kick_log(KICK_LOG_DEBUG,
|
pid_t pcpid = -1;
|
||||||
"[popup] parent %p not live; hanging popup under popup tree "
|
if (popup->base->client)
|
||||||
"at output origin\n",
|
wl_client_get_credentials(popup->base->client->client,
|
||||||
(void *)parent_ws);
|
&pcpid, NULL, NULL);
|
||||||
/* No live kick-managed parent. These are popups whose parent was a
|
struct kick_window *cur = tab_current_window(server.active_tab);
|
||||||
layer-shell bar or a gone window. Map them under the root-level
|
if (cur && cur->client_pid > 0 && cur->client_pid == pcpid) {
|
||||||
popup_tree so their positioner geometry lands at the real output
|
kick_log(KICK_LOG_DEBUG,
|
||||||
coordinates (bars' menus would otherwise be shifted down by the
|
"[popup] parent %p not live; reusing active window tree "
|
||||||
bar's reserved edge from hanging under an offset window tree). */
|
"for same-client popup\n",
|
||||||
parent_tree = server.popup_tree;
|
(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;
|
if (!parent_tree) return;
|
||||||
create_popup(popup, parent_tree);
|
create_popup(popup, parent_tree);
|
||||||
|
|
@ -4522,6 +4553,16 @@ static void layer_handle_commit(struct wl_listener *listener, void *data) {
|
||||||
(void)data;
|
(void)data;
|
||||||
struct kick_layer *sl = wl_container_of(listener, sl, commit);
|
struct kick_layer *sl = wl_container_of(listener, sl, commit);
|
||||||
bool mapped = sl->surface->surface->mapped;
|
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) {
|
if (!sl->seen_init) {
|
||||||
/* First commit. wlroots' layer_surface_role_commit runs first and
|
/* First commit. wlroots' layer_surface_role_commit runs first and
|
||||||
sets surface->initialized, so this is the earliest point we may call
|
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)
|
layer_proc.wait(timeout=5)
|
||||||
finally:
|
finally:
|
||||||
s.close()
|
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