fix X11 menus and popups: replay late association, respect transient geometry
X11 windows now pair their Wayland surface after membership decisions run (membership starts at first map), so instantly-mapped transients -- GTK menu dropdowns -- attached without ever wiring their scene surface: emacs menus did nothing, silently. The associate handlers now replay the map when the surface is already mapped, members join the owner's list before that replay (show_current walks the list to pick visibility), X11 members keep their own geometry instead of being stretched maximized, and late attaches force whole-scene damage so the first frame is never black. Members render through wlr_scene_subsurface_tree_create; the ctl test feed gains CTL_CMD_PTR_BUTTON so tests can click for real; tests/xpopup.c probes the exact menu shape (instantly-mapped managed transient), and a regression test locks in the attach/replay/focus lifecycle.
This commit is contained in:
parent
08202dc6c3
commit
e1b137bf30
4 changed files with 272 additions and 24 deletions
118
kick.c
118
kick.c
|
|
@ -2817,6 +2817,7 @@ static struct kick_tab_group_member *top_mapped_member(struct kick_app *app) {
|
|||
no kind gets special treatment). Only the shown window's scene node
|
||||
stays enabled -- hidden layers are neither drawn nor hit-tested, and a
|
||||
member's popups (parented on its own tree) hide with it. */
|
||||
|
||||
static void show_current(struct kick_app *app) {
|
||||
if (!app || !app->stack) return;
|
||||
struct kick_tab_group_member *cur =
|
||||
|
|
@ -2824,6 +2825,19 @@ static void show_current(struct kick_app *app) {
|
|||
struct kick_tab_group_member *m;
|
||||
wl_list_for_each(m, &app->group_members, link)
|
||||
wlr_scene_node_set_enabled(&m->tree->node, m == cur);
|
||||
{
|
||||
fprintf(stderr, "[DBG-show] app=%d cur=%s\n", app_index(app),
|
||||
cur ? (cur->is_main ? "main" : "sub") : "none");
|
||||
wl_list_for_each(m, &app->group_members, link)
|
||||
fprintf(stderr, "[DBG-show] m=%s mapped=%d en=%d pos=(%d,%d)\n",
|
||||
m->is_main ? "main" : "sub", (int)m->mapped,
|
||||
(int)m->tree->node.enabled,
|
||||
(int)m->xsurface ? (int)m->xsurface->x : -1,
|
||||
(int)m->xsurface ? (int)m->xsurface->y : -1);
|
||||
fprintf(stderr, "[DBG-show] stack_en=%d main_surf=%p\n",
|
||||
(int)app->stack->node.enabled,
|
||||
(void *)app_surface(app));
|
||||
}
|
||||
}
|
||||
|
||||
/* Give the keyboard to a stacked tab-group member of the active tab, demoting
|
||||
|
|
@ -2951,12 +2965,8 @@ static void handle_sub_commit(struct wl_listener *listener, void *data) {
|
|||
}
|
||||
#if HAVE_XWAYLAND
|
||||
else if (sub->is_x11 && sub->xsurface && sub->tree) {
|
||||
/* Keep the window's content at the tab origin, cropping any
|
||||
self-placement offset (mirrors x11_sync_position). */
|
||||
struct wlr_xwayland_surface *xs = sub->xsurface;
|
||||
int nx = (xs->x > 0 && xs->x < server.output->width) ? -xs->x : 0;
|
||||
int ny = (xs->y > 0 && xs->y < server.output->height) ? -xs->y : 0;
|
||||
wlr_scene_node_set_position(&sub->tree->node, nx, ny);
|
||||
wlr_scene_node_set_position(&sub->tree->node,
|
||||
sub->xsurface->x, sub->xsurface->y);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
@ -2966,16 +2976,23 @@ static void handle_sub_map(struct wl_listener *listener, void *data) {
|
|||
struct kick_tab_group_member *sub = wl_container_of(listener, sub, map);
|
||||
sub->mapped = true;
|
||||
#if HAVE_XWAYLAND
|
||||
/* X11 tab-group members have no initial_commit; configure them to the
|
||||
maximized output size on every map (idempotent). */
|
||||
if (sub->is_x11 && sub->xsurface && sub->xsurface->surface) {
|
||||
wlr_xwayland_surface_configure(sub->xsurface, 0, 0,
|
||||
(uint16_t)server.output->width,
|
||||
(uint16_t)server.output->height);
|
||||
wlr_xwayland_surface_set_maximized(sub->xsurface, true, true);
|
||||
if (server.clients_fullscreen)
|
||||
wlr_xwayland_surface_set_fullscreen(sub->xsurface, true);
|
||||
}
|
||||
/* X11 tab-group members are transients -- menus, dialogs, palettes --
|
||||
not workspaces: they keep their own geometry and float at the
|
||||
absolute position their client chose. Never configure them to the
|
||||
output size (that reallocates their backing store to garbage and
|
||||
stretches menus across the screen); just place the node. */
|
||||
if (sub->is_x11 && sub->xsurface && sub->tree)
|
||||
wlr_scene_node_set_position(&sub->tree->node,
|
||||
sub->xsurface->x, sub->xsurface->y);
|
||||
#endif
|
||||
#if HAVE_XWAYLAND
|
||||
/* Late-attached X11 members (menus) join with their surface already
|
||||
mapped and committed, outside the normal commit-damage flow: the
|
||||
enable/position surgery above doesn't reliably produce output damage,
|
||||
so the first frame shows black until an unrelated repaint. Force a
|
||||
full repaint whenever such a member appears on the shown tab. */
|
||||
if (sub->is_x11 && server.active_app == sub->owner && server.scene_output)
|
||||
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
||||
#endif
|
||||
/* A tab-group member never steals focus: it appears with its owner tab and
|
||||
takes the keyboard only when that tab is already the one shown. */
|
||||
|
|
@ -2992,6 +3009,7 @@ static void handle_sub_map(struct wl_listener *listener, void *data) {
|
|||
static void handle_sub_unmap(struct wl_listener *listener, void *data) {
|
||||
(void)data;
|
||||
struct kick_tab_group_member *sub = wl_container_of(listener, sub, unmap);
|
||||
fprintf(stderr, "[DBG] SUB UNMAP is_x11=%d\n", (int)sub->is_x11);
|
||||
sub->mapped = false;
|
||||
if (sub->owner->focus_member == sub)
|
||||
sub_refocus_owner(sub->owner);
|
||||
|
|
@ -3324,7 +3342,10 @@ static void handle_sub_x_associate(struct wl_listener *listener, void *data) {
|
|||
(void)data;
|
||||
struct kick_tab_group_member *sub = wl_container_of(listener, sub, xs_associate);
|
||||
if (!sub->xsurface || !sub->xsurface->surface || !sub->tree) return;
|
||||
wlr_scene_surface_create(sub->tree, sub->xsurface->surface);
|
||||
/* Full subsurface-tree wrapper rather than a bare scene surface: it
|
||||
carries its own commit/position sync, which late association (our
|
||||
map-time membership) needs to light up correctly. */
|
||||
wlr_scene_subsurface_tree_create(sub->tree, sub->xsurface->surface);
|
||||
sub->map.notify = handle_sub_map;
|
||||
wl_signal_add(&sub->xsurface->surface->events.map, &sub->map);
|
||||
sub->unmap.notify = handle_sub_unmap;
|
||||
|
|
@ -3332,6 +3353,15 @@ static void handle_sub_x_associate(struct wl_listener *listener, void *data) {
|
|||
sub->commit.notify = handle_sub_commit;
|
||||
wl_signal_add(&sub->xsurface->surface->events.commit, &sub->commit);
|
||||
sub->xs_surface_linked = true;
|
||||
bool mapped = sub->xsurface->surface->mapped;
|
||||
/* Membership starts at first map, so association can arrive after the
|
||||
window already mapped and committed (an instantly-shown menu popup):
|
||||
the map event fired before this listener existed. Replay it, or the
|
||||
member stays an invisible phantom with live content behind it. */
|
||||
kick_log(KICK_LOG_DEBUG, "[x11] member associate app=%d%s\n",
|
||||
app_index(sub->owner), mapped ? " (late: already mapped)" : "");
|
||||
if (mapped)
|
||||
handle_sub_map(&sub->map, NULL);
|
||||
}
|
||||
|
||||
static void handle_sub_x_destroy(struct wl_listener *listener, void *data) {
|
||||
|
|
@ -3364,17 +3394,21 @@ static void attach_group_member_x11(struct kick_app *owner,
|
|||
free(sub);
|
||||
return;
|
||||
}
|
||||
/* show_current() picks the visible window once it maps; start hidden. */
|
||||
wlr_scene_node_set_enabled(&sub->tree->node, false);
|
||||
/* Join the owner's member list and wire up the surface BEFORE hiding
|
||||
anything: association may already have happened (membership starts
|
||||
at first map), and a scene surface added to a currently-DISABLED
|
||||
tree can be missed by the scene's visibility bookkeeping when the
|
||||
tree is re-enabled -- the subtree then never paints (the invisible
|
||||
menu bug). Attach live-first, let the replayed map decide
|
||||
visibility through the normal show_current() path. */
|
||||
wl_list_insert(owner->group_members.prev, &sub->link);
|
||||
if (xsurface->surface) {
|
||||
/* Defensive: association normally happens after new_surface. */
|
||||
handle_sub_x_associate(&sub->xs_associate, NULL);
|
||||
}
|
||||
sub->xs_associate.notify = handle_sub_x_associate;
|
||||
wl_signal_add(&xsurface->events.associate, &sub->xs_associate);
|
||||
sub->destroy.notify = handle_sub_x_destroy;
|
||||
wl_signal_add(&xsurface->events.destroy, &sub->destroy);
|
||||
wl_list_insert(owner->group_members.prev, &sub->link);
|
||||
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", app_index(owner));
|
||||
}
|
||||
|
||||
|
|
@ -3604,6 +3638,14 @@ static void or_handle_associate(struct wl_listener *listener, void *data) {
|
|||
wl_signal_add(&orw->xsurface->surface->events.commit, &orw->commit);
|
||||
orw->surface_linked = true;
|
||||
or_window_position(orw);
|
||||
/* Same late-map replay as the member path: an override-redirect popup
|
||||
can map and commit before its surface pairing reaches us. */
|
||||
bool mapped = orw->xsurface->surface->mapped;
|
||||
kick_log(KICK_LOG_DEBUG, "[x11] or associate class=%s%s\n",
|
||||
orw->xsurface->class ? orw->xsurface->class : "?",
|
||||
mapped ? " (late: already mapped)" : "");
|
||||
if (mapped)
|
||||
or_handle_map(&orw->map, NULL);
|
||||
}
|
||||
|
||||
static void or_handle_dissociate(struct wl_listener *listener, void *data) {
|
||||
|
|
@ -3621,6 +3663,10 @@ static void or_handle_map(struct wl_listener *listener, void *data) {
|
|||
struct kick_or_window *orw = wl_container_of(listener, orw, map);
|
||||
wlr_scene_node_set_enabled(&orw->tree->node, true);
|
||||
or_window_position(orw);
|
||||
/* Late-associated OR popups join already-mapped, outside the commit
|
||||
damage flow -- same first-frame-black hazard as X11 members. */
|
||||
if (server.scene_output)
|
||||
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
||||
if (wlr_xwayland_surface_override_redirect_wants_focus(orw->xsurface)) {
|
||||
if (!server.or_focus_owner && server.active_app)
|
||||
server.or_focus_owner = server.active_app;
|
||||
|
|
@ -3698,6 +3744,10 @@ static void or_window_create(struct wlr_xwayland_surface *xsurface) {
|
|||
wl_list_insert(server.or_windows.prev, &orw->link);
|
||||
kick_log(KICK_LOG_INFO, "[x11] or window class=%s\n",
|
||||
xsurface->class ? xsurface->class : "?");
|
||||
kick_log(KICK_LOG_DEBUG, "[x11] or create wid=%u %dx%d@%d,%d\n",
|
||||
(unsigned)xsurface->window_id,
|
||||
(int)xsurface->width, (int)xsurface->height,
|
||||
(int)xsurface->x, (int)xsurface->y);
|
||||
}
|
||||
|
||||
static void classify_xwayland_window(struct wlr_xwayland_surface *xsurface);
|
||||
|
|
@ -3739,6 +3789,8 @@ static void x11_pending_handle_destroy(struct wl_listener *listener,
|
|||
void *data) {
|
||||
(void)data;
|
||||
struct kick_x11_pending *p = wl_container_of(listener, p, destroy);
|
||||
kick_log(KICK_LOG_DEBUG, "[x11] pending wid=%u destroyed unmapped\n",
|
||||
(unsigned)p->xsurface->window_id);
|
||||
x11_pending_teardown(p); /* never mapped: no tab, no member, no trace */
|
||||
}
|
||||
|
||||
|
|
@ -3773,6 +3825,8 @@ static void handle_xwayland_new_surface(struct wl_listener *listener,
|
|||
p->destroy.notify = x11_pending_handle_destroy;
|
||||
wl_signal_add(&xsurface->events.destroy, &p->destroy);
|
||||
p->associate.notify = x11_pending_handle_associate;
|
||||
kick_log(KICK_LOG_DEBUG, "[x11] pending wid=%u pid=%d\n",
|
||||
(unsigned)xsurface->window_id, (int)xsurface->pid);
|
||||
wl_signal_add(&xsurface->events.associate, &p->associate);
|
||||
wl_list_insert(server.x11_pendings.prev, &p->link);
|
||||
}
|
||||
|
|
@ -6650,17 +6704,21 @@ static void handle_new_input(struct wl_listener *listener, void *data) {
|
|||
}
|
||||
|
||||
/* ============ stdin event feed (test/driver channel) ============
|
||||
Reads raw keyboard events from stdin and dispatches them into the
|
||||
same host_kbd_key / host_kbd_modifiers handlers a real host would
|
||||
deliver, so the key handler itself stays under test. Two records:
|
||||
Reads raw input events from stdin and dispatches them into the same
|
||||
handlers a real device would drive, so the input paths stay under test.
|
||||
Records (after the u8 command byte, u32s little-endian):
|
||||
|
||||
1 | u32 key | u32 state (WL_KEYBOARD_KEY_STATE_*)
|
||||
2 | u32 mods | u32 latched | u32 locked | u32 group
|
||||
3 | u32 x | u32 y (absolute output pixels)
|
||||
4 | i32 dx | i32 dy (relative motion)
|
||||
5 | u32 btn | u32 state (evdev button; 1 press / 0 release)
|
||||
*/
|
||||
#define CTL_CMD_KEY 1
|
||||
#define CTL_CMD_MODS 2
|
||||
#define CTL_CMD_WARP 3
|
||||
#define CTL_CMD_PTR_MOTION 4
|
||||
#define CTL_CMD_PTR_BUTTON 5
|
||||
|
||||
#define CTL_MAX_MSG 20 /* 1 + 4 + 4 (or 1 + 16) after padding */
|
||||
#define CTL_BUF_SIZE 512
|
||||
|
|
@ -6718,6 +6776,18 @@ static void ctl_dispatch(const uint8_t *msg, size_t len) {
|
|||
if (server.standalone)
|
||||
standalone_overlay_damage();
|
||||
break;
|
||||
case CTL_CMD_PTR_BUTTON:
|
||||
if (len < 9) return;
|
||||
memcpy(u32, msg + 1, 8);
|
||||
ctl_time += 1;
|
||||
/* Button press/release at the current pointer position, through the
|
||||
same seat entry real devices use. Button codes are evdev-style
|
||||
(BTN_LEFT = 0x110); state 1 = press, 0 = release. */
|
||||
if (!server.seat) return;
|
||||
wlr_seat_pointer_notify_button(server.seat, ctl_time,
|
||||
u32[0], u32[1]);
|
||||
wlr_seat_pointer_notify_frame(server.seat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue