On Wayland, popup menus (WS_POPUP windows with class #32768) don't close when clicking outside because the compositor only delivers pointer events to the focused surface. X11 solves this with XGrabPointer, but Wayland requires xdg_popup with an explicit grab. Add WAYLAND_SURFACE_ROLE_POPUP that creates an xdg_popup surface with xdg_popup_grab for windows matching the POPUPMENU_CLASS_ATOM (#32768). The compositor sends popup_done when the user clicks outside, which we translate to WM_CANCELMODE to dismiss the menu. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- dlls/winewayland.drv/wayland_surface.c | 114 +++++++++++++++++++++++++ dlls/winewayland.drv/waylanddrv.h | 9 ++ dlls/winewayland.drv/window.c | 35 +++++++- 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/dlls/winewayland.drv/wayland_surface.c b/dlls/winewayland.drv/wayland_surface.c index 2f8275d..752760d 100644 --- a/dlls/winewayland.drv/wayland_surface.c +++ b/dlls/winewayland.drv/wayland_surface.c @@ -327,6 +327,103 @@ err: ERR("Failed to assign subsurface role to wayland surface\n"); } +/********************************************************************** + * xdg_popup listeners + */ +static void xdg_popup_handle_configure(void *data, struct xdg_popup *xdg_popup, + int32_t x, int32_t y, + int32_t width, int32_t height) +{ + TRACE("hwnd=%p pos=%d,%d size=%dx%d\n", data, x, y, width, height); +} + +static void xdg_popup_handle_done(void *data, struct xdg_popup *xdg_popup) +{ + HWND hwnd = data; + TRACE("hwnd=%p popup_done\n", hwnd); + NtUserPostMessage(hwnd, WM_CANCELMODE, 0, 0); +} + +static void xdg_popup_handle_repositioned(void *data, + struct xdg_popup *xdg_popup, + uint32_t token) +{ +} + +static const struct xdg_popup_listener xdg_popup_listener = +{ + xdg_popup_handle_configure, + xdg_popup_handle_done, + xdg_popup_handle_repositioned, +}; + +/********************************************************************** + * wayland_surface_make_popup + * + * Gives the popup role to a plain wayland surface. The popup is + * positioned relative to the parent surface and can use an explicit + * grab so that the compositor dismisses it on outside clicks. + */ +void wayland_surface_make_popup(struct wayland_surface *surface, + struct wayland_surface *parent, + int x, int y, int width, int height) +{ + struct xdg_positioner *positioner; + + TRACE("surface=%p parent=%p pos=%d,%d size=%dx%d\n", + surface, parent, x, y, width, height); + + assert(!surface->role || surface->role == WAYLAND_SURFACE_ROLE_POPUP); + if (surface->xdg_popup) return; + + wayland_surface_clear_role(surface); + surface->role = WAYLAND_SURFACE_ROLE_POPUP; + + /* Create xdg_surface for the popup */ + surface->popup_xdg_surface = + xdg_wm_base_get_xdg_surface(process_wayland.xdg_wm_base, + surface->wl_surface); + if (!surface->popup_xdg_surface) goto err; + xdg_surface_add_listener(surface->popup_xdg_surface, + &xdg_surface_listener, surface->hwnd); + + /* Create positioner to place popup relative to parent */ + positioner = xdg_wm_base_create_positioner(process_wayland.xdg_wm_base); + if (!positioner) goto err; + + xdg_positioner_set_size(positioner, width > 0 ? width : 1, + height > 0 ? height : 1); + xdg_positioner_set_anchor_rect(positioner, x, y, 1, 1); + xdg_positioner_set_anchor(positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + + /* Create popup */ + surface->xdg_popup = + xdg_surface_get_popup(surface->popup_xdg_surface, + parent->xdg_surface, positioner); + xdg_positioner_destroy(positioner); + if (!surface->xdg_popup) goto err; + + xdg_popup_add_listener(surface->xdg_popup, &xdg_popup_listener, + surface->hwnd); + + /* Grab the popup so compositor sends popup_done on outside click. */ + { + uint32_t serial = InterlockedCompareExchange(&process_wayland.input_serial, 0, 0); + if (serial) + xdg_popup_grab(surface->xdg_popup, process_wayland.seat.wl_seat, serial); + } + + wl_surface_commit(surface->wl_surface); + wl_display_flush(process_wayland.wl_display); + + return; + +err: + wayland_surface_clear_role(surface); + ERR("Failed to assign popup role to wayland surface\n"); +} + /********************************************************************** * wayland_surface_clear_role * @@ -375,6 +472,20 @@ void wayland_surface_clear_role(struct wayland_surface *surface) surface->toplevel_hwnd = 0; break; + + case WAYLAND_SURFACE_ROLE_POPUP: + if (surface->xdg_popup) + { + xdg_popup_destroy(surface->xdg_popup); + surface->xdg_popup = NULL; + } + + if (surface->popup_xdg_surface) + { + xdg_surface_destroy(surface->popup_xdg_surface); + surface->popup_xdg_surface = NULL; + } + break; } memset(&surface->pending, 0, sizeof(surface->pending)); @@ -733,6 +844,9 @@ BOOL wayland_surface_reconfigure(struct wayland_surface *surface) if (!surface->wl_subsurface) break; /* surface role has been cleared */ wayland_surface_reconfigure_subsurface(surface); break; + case WAYLAND_SURFACE_ROLE_POPUP: + if (!surface->xdg_popup) break; /* surface role has been cleared */ + break; } wayland_surface_reconfigure_size(surface, width, height); diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index 3b0c300..911b43c 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -82,6 +82,7 @@ enum wayland_surface_role WAYLAND_SURFACE_ROLE_NONE, WAYLAND_SURFACE_ROLE_TOPLEVEL, WAYLAND_SURFACE_ROLE_SUBSURFACE, + WAYLAND_SURFACE_ROLE_POPUP, }; struct wayland_keyboard @@ -282,6 +283,11 @@ struct wayland_surface struct wl_subsurface *wl_subsurface; HWND toplevel_hwnd; }; + struct + { + struct xdg_surface *popup_xdg_surface; + struct xdg_popup *xdg_popup; + }; }; struct wayland_surface_config pending, requested, processing, current; @@ -314,6 +320,9 @@ void wayland_surface_destroy(struct wayland_surface *surface); void wayland_surface_make_toplevel(struct wayland_surface *surface); void wayland_surface_make_subsurface(struct wayland_surface *surface, struct wayland_surface *parent); +void wayland_surface_make_popup(struct wayland_surface *surface, + struct wayland_surface *parent, + int x, int y, int width, int height); void wayland_surface_clear_role(struct wayland_surface *surface); void wayland_surface_attach_shm(struct wayland_surface *surface, struct wayland_shm_buffer *shm_buffer, diff --git a/dlls/winewayland.drv/window.c b/dlls/winewayland.drv/window.c index 2c3d1f8..c1ab090 100644 --- a/dlls/winewayland.drv/window.c +++ b/dlls/winewayland.drv/window.c @@ -192,16 +192,20 @@ static BOOL wayland_win_data_create_wayland_surface(struct wayland_win_data *dat struct wayland_surface *surface; enum wayland_surface_role role; BOOL visible; + DWORD style = NtUserGetWindowLongW(data->hwnd, GWL_STYLE); DWORD exstyle = NtUserGetWindowLongW(data->hwnd, GWL_EXSTYLE); struct wl_region *input_region; TRACE("hwnd=%p\n", data->hwnd); - visible = ((NtUserGetWindowLongW(data->hwnd, GWL_STYLE) & WS_VISIBLE) == WS_VISIBLE) && + visible = ((style & WS_VISIBLE) == WS_VISIBLE) && (!(exstyle & WS_EX_LAYERED) || data->layered_attribs_set); if (!visible) role = WAYLAND_SURFACE_ROLE_NONE; else if (toplevel_surface) role = WAYLAND_SURFACE_ROLE_SUBSURFACE; + else if ((style & WS_POPUP) && NtUserGetWindowRelative(data->hwnd, GW_OWNER) && + NtUserGetClassWord(data->hwnd, GCW_ATOM) == 32768) + role = WAYLAND_SURFACE_ROLE_POPUP; else role = WAYLAND_SURFACE_ROLE_TOPLEVEL; /* we can temporarily clear the role of a surface but cannot assign a different one after it's set */ @@ -236,6 +240,30 @@ static BOOL wayland_win_data_create_wayland_surface(struct wayland_win_data *dat case WAYLAND_SURFACE_ROLE_SUBSURFACE: wayland_surface_make_subsurface(surface, toplevel_surface); break; + case WAYLAND_SURFACE_ROLE_POPUP: + { + HWND owner = NtUserGetWindowRelative(data->hwnd, GW_OWNER); + struct wayland_win_data *owner_data = owner ? wayland_win_data_get(owner) : NULL; + if (owner_data && owner_data->wayland_surface) + { + RECT rect, owner_rect; + NtUserGetWindowRect(data->hwnd, &rect, 0); + NtUserGetWindowRect(owner, &owner_rect, 0); + wayland_surface_make_popup(surface, owner_data->wayland_surface, + rect.left - owner_rect.left, + rect.top - owner_rect.top, + rect.right - rect.left, + rect.bottom - rect.top); + wayland_win_data_release(owner_data); + } + else + { + if (owner_data) wayland_win_data_release(owner_data); + /* Fallback to toplevel if owner has no surface */ + wayland_surface_make_toplevel(surface); + } + break; + } } if (visible && client) wayland_client_surface_attach(client, data->hwnd); @@ -313,6 +341,11 @@ static void wayland_win_data_update_wayland_state(struct wayland_win_data *data) surface->processing.serial = 1; surface->processing.processed = TRUE; break; + case WAYLAND_SURFACE_ROLE_POPUP: + TRACE("hwnd=%p popup\n", surface->hwnd); + surface->processing.serial = 1; + surface->processing.processed = TRUE; + break; } wl_display_flush(process_wayland.wl_display); -- 2.53.0