wayland: implement xdg_popup support for context menus

- Listen for xdg_shell new_popup signal
- Create scene tree for popups as children of parent toplevel
- Unconstrain popup geometry on initial commit
- Store xdg_surface->data for parent lookup by nested popups
- Remove redundant wl_update_pointer_focus from button handler,
  let wlr_seat manage implicit grabs internally
This commit is contained in:
lkn 2026-06-28 00:36:54 +02:00
parent 4a58fd90eb
commit 6011f4226f
4 changed files with 120 additions and 10 deletions

View File

@ -630,6 +630,11 @@ wl_display_open(const char *name)
wl_signal_add(&wl_state.xdg_shell->events.new_toplevel,
&wl_state.xdg_new_surface_listener);
extern void handle_xdg_new_popup(struct wl_listener *listener, void *data);
wl_state.xdg_new_popup_listener.notify = handle_xdg_new_popup;
wl_signal_add(&wl_state.xdg_shell->events.new_popup,
&wl_state.xdg_new_popup_listener);
#ifdef USE_XWAYLAND
/* Create XWayland server for X11 client support */
wl_state.xwayland = wlr_xwayland_create(wl_state.display,

View File

@ -350,13 +350,6 @@ handle_pointer_button(struct wl_listener *listener, void *data)
W_WL_EnqueueEvent(wmscr, &wev, target);
}
/* Refresh pointer focus before delivering button event.
* The last wl_update_pointer_focus call was during the most recent
* motion event. If no motion happened since then, the XWayland
* surface might not have pointer focus, causing
* wlr_seat_pointer_notify_button to be a no-op. */
wl_update_pointer_focus(ev->time_msec);
/* Suppress forwarding to the client when the WM modifier key is held
* on a button press over a client surface. In this case frameMouseDown
* will handle the event (move or resize), and the app must not see it. */
@ -372,9 +365,7 @@ handle_pointer_button(struct wl_listener *listener, void *data)
}
}
if (forward_to_client &&
(wl_state.seat->pointer_state.focused_surface ||
ev->state == WLR_BUTTON_RELEASED)) {
if (forward_to_client) {
wlr_seat_pointer_notify_button(wl_state.seat, ev->time_msec,
ev->button, ev->state);
wlr_seat_pointer_notify_frame(wl_state.seat);

View File

@ -174,6 +174,7 @@ struct wl_compositor_state {
struct wl_listener decoration_new_listener;
/* Phase 36: XDG toplevel tracking */
struct wl_listener xdg_new_surface_listener;
struct wl_listener xdg_new_popup_listener;
int managed_surface_count; /* surfaces seen */
/* Phase 37: periodic timer that drives client frame callbacks */
struct wl_event_source *frame_timer;

View File

@ -322,6 +322,7 @@ handle_toplevel_map(struct wl_listener *listener, void *data)
view->scene_node = wlr_scene_xdg_surface_create(
_vp, view->xdg_surface);
if (view->scene_node) {
view->xdg_surface->data = view->scene_node;
view_attach_scene_destroy_listener(view);
/* Deferred reparent: move client surface under its
* frame grouping tree now that the node exists. */
@ -684,3 +685,115 @@ handle_xdg_new_toplevel(struct wl_listener *listener, void *data)
toplevel->app_id ? toplevel->app_id : "(none)",
toplevel->title ? toplevel->title : "(none)");
}
/* ------------------------------------------------------------------ */
/* XDG Popup handling */
static void handle_popup_commit(struct wl_listener *listener, void *data);
static void handle_popup_destroy(struct wl_listener *listener, void *data);
struct wl_popup_ctx {
struct wlr_xdg_popup *popup;
struct wl_listener commit;
struct wl_listener destroy;
};
static void
handle_popup_commit(struct wl_listener *listener, void *data)
{
(void)data;
struct wl_popup_ctx *ctx = wl_container_of(listener, ctx, commit);
struct wlr_xdg_popup *popup = ctx->popup;
if (!popup->base->initialized)
return;
/* Unconstrain popup on initial configure so it fits on screen.
* The box is in the root parent toplevel's coordinate space. */
if (!popup->base->configured) {
/* Walk up to the root toplevel via parent surfaces */
struct wlr_xdg_surface *parent =
wlr_xdg_surface_try_from_wlr_surface(popup->parent);
while (parent && parent->role == WLR_XDG_SURFACE_ROLE_POPUP) {
parent = wlr_xdg_surface_try_from_wlr_surface(parent->popup->parent);
}
int toplevel_x = 0, toplevel_y = 0;
if (parent && parent->role == WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
struct wl_toplevel_view *pv =
wl_find_view_by_xdg_surface(parent);
if (pv) {
toplevel_x = pv->x;
toplevel_y = pv->y;
}
}
WScreen *scr = wScreenWithNumber(0);
struct wlr_box box = {
.x = -toplevel_x,
.y = -toplevel_y,
.width = scr ? scr->scr_width : 1920,
.height = scr ? scr->scr_height : 1080,
};
wlr_xdg_popup_unconstrain_from_box(popup, &box);
}
}
static void
handle_popup_destroy(struct wl_listener *listener, void *data)
{
(void)data;
struct wl_popup_ctx *ctx = wl_container_of(listener, ctx, destroy);
wl_list_remove(&ctx->commit.link);
wl_list_remove(&ctx->destroy.link);
free(ctx);
}
void
handle_xdg_new_popup(struct wl_listener *listener, void *data)
{
(void)listener;
struct wlr_xdg_popup *popup = data;
wwarning("wl_backend: NEW XDG POPUP parent_surface=%p base=%p",
(void*)popup->parent, (void*)popup->base);
/* Find the parent's scene tree to attach the popup to.
* Walk up the parent chain to find the toplevel view. */
struct wlr_xdg_surface *parent_xdg =
wlr_xdg_surface_try_from_wlr_surface(popup->parent);
struct wlr_scene_tree *parent_tree = NULL;
if (parent_xdg) {
if (parent_xdg->role == WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
struct wl_toplevel_view *pv =
wl_find_view_by_xdg_surface(parent_xdg);
if (pv && pv->scene_node)
parent_tree = pv->scene_node;
} else if (parent_xdg->role == WLR_XDG_SURFACE_ROLE_POPUP) {
/* Nested popup — use the parent popup's scene tree.
* wlr_scene_xdg_surface_create on the parent popup set
* data on that surface's scene tree. */
parent_tree = parent_xdg->data;
}
}
if (parent_tree) {
struct wlr_scene_tree *popup_tree =
wlr_scene_xdg_surface_create(parent_tree, popup->base);
if (popup_tree) {
popup->base->data = popup_tree;
}
} else {
wwarning("wl_backend: popup has no parent scene tree!");
}
struct wl_popup_ctx *ctx = calloc(1, sizeof(*ctx));
if (!ctx) return;
ctx->popup = popup;
ctx->commit.notify = handle_popup_commit;
wl_signal_add(&popup->base->surface->events.commit, &ctx->commit);
ctx->destroy.notify = handle_popup_destroy;
wl_signal_add(&popup->events.destroy, &ctx->destroy);
}