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(a)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