Implement wmaker-popup-v1 Wayland protocol for client-driven popup positioning

Standard xdg_shell does not allow Wayland clients to position toplevels —
the compositor decides placement.  WINGs UI patterns require client-driven
positioning (e.g., a dropdown popup must appear directly below its trigger
button; menu editor items must be placed by the client at specific screen
coordinates).

This protocol lets the client wrap an xdg_toplevel with a wmaker_popup
object and call set_position(x, y) before the surface is mapped.  The
compositor honours the requested position when the surface becomes visible
and on subsequent set_position calls.

Files:
- protocols/wmaker-popup-v1.xml: protocol definition
- src/Makefile.am, src/Makefile: server-protocol.h + protocol.c generation
- WINGs/Makefile.am, WINGs/Makefile: client-protocol.h + protocol.c generation
- src/backend/wayland/wl_backend.c: wmaker_popup_manager global, get_popup
  handler that wraps an xdg_toplevel; set_position handler that updates
  view->popup_x/y and calls wmaker_popup_apply to move the frame_buf
  scene tree.  Apply runs at toplevel_map time too for pre-map positions.
- WINGs/wclient_wl.c: bind manager in registry, wrap popup toplevels with
  wmaker_popup, expose W_WLClientPopupSetPosition.
- WINGs/wview_wl.c: wl_move calls W_WLClientPopupSetPosition for popup
  toplevels so WMMoveWidget propagates to the compositor.
- WINGs/wevent_wl.c: wl_move_window also routes to popup positioning.
This commit is contained in:
Window Maker 2026-05-24 18:27:55 +02:00
parent fd73e94a58
commit 60348da411
8 changed files with 367 additions and 7 deletions

View File

@ -104,9 +104,13 @@ endif
if USE_WAYLAND_CLIENT
nodist_libWINGs_la_SOURCES = \
xdg-shell-protocol.c \
xdg-shell-client-protocol.h
BUILT_SOURCES = xdg-shell-client-protocol.h xdg-shell-protocol.c
CLEANFILES = xdg-shell-client-protocol.h xdg-shell-protocol.c
xdg-shell-client-protocol.h \
wmaker-popup-v1-protocol.c \
wmaker-popup-v1-client-protocol.h
BUILT_SOURCES = xdg-shell-client-protocol.h xdg-shell-protocol.c \
wmaker-popup-v1-client-protocol.h wmaker-popup-v1-protocol.c
CLEANFILES = xdg-shell-client-protocol.h xdg-shell-protocol.c \
wmaker-popup-v1-client-protocol.h wmaker-popup-v1-protocol.c
xdg-shell-client-protocol.h:
$(WAYLAND_SCANNER) client-header \
@ -115,6 +119,13 @@ xdg-shell-client-protocol.h:
xdg-shell-protocol.c: xdg-shell-client-protocol.h
$(WAYLAND_SCANNER) private-code \
$(WAYLAND_PROTOCOLS_DIR)/stable/xdg-shell/xdg-shell.xml $@
wmaker-popup-v1-client-protocol.h: $(top_srcdir)/protocols/wmaker-popup-v1.xml
$(WAYLAND_SCANNER) client-header $< $@
wmaker-popup-v1-protocol.c: $(top_srcdir)/protocols/wmaker-popup-v1.xml \
wmaker-popup-v1-client-protocol.h
$(WAYLAND_SCANNER) private-code $< $@
endif
libWUtil_la_SOURCES = \

View File

@ -37,6 +37,7 @@
#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"
#include "wmaker-popup-v1-client-protocol.h"
#include <xkbcommon/xkbcommon.h>
@ -63,6 +64,8 @@ typedef struct WLClientCtx {
struct wl_shm *shm;
struct wl_seat *seat;
struct xdg_wm_base *wm_base;
/* wmaker-popup-v1: client-driven popup positioning */
struct wmaker_popup_manager_v1 *wmaker_popup_mgr;
struct wl_pointer *pointer;
struct wl_keyboard *keyboard;
@ -221,6 +224,8 @@ typedef struct WLClientToplevel {
struct wl_surface *surface;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
/* wmaker-popup-v1 wrapper for client-driven positioning (NULL if not popup) */
struct wmaker_popup_v1 *wmaker_popup;
WLClientShmBuffer *buf; /* current backing */
@ -412,6 +417,7 @@ wl_client_view_destroy_backing(unsigned long id)
{
WLClientToplevel *top = wlc_top_find((WNativeWindow)id);
if (!top) return;
if (top->wmaker_popup) wmaker_popup_v1_destroy(top->wmaker_popup);
if (top->xdg_toplevel) xdg_toplevel_destroy(top->xdg_toplevel);
if (top->xdg_surface) xdg_surface_destroy(top->xdg_surface);
if (top->surface) wl_surface_destroy(top->surface);
@ -453,6 +459,16 @@ wl_client_toplevel_realize(struct W_View *view, int width, int height)
if (view->flags.popup) {
xdg_toplevel_set_app_id(top->xdg_toplevel, "wings-popup");
xdg_toplevel_set_title(top->xdg_toplevel, "");
/* Wrap with wmaker-popup-v1 so we can drive position. */
if (ctx->wmaker_popup_mgr) {
top->wmaker_popup = wmaker_popup_manager_v1_get_popup(
ctx->wmaker_popup_mgr, top->xdg_toplevel);
/* Apply any position recorded in view->pos before realize. */
if (top->wmaker_popup && (view->pos.x || view->pos.y)) {
wmaker_popup_v1_set_position(top->wmaker_popup,
view->pos.x, view->pos.y);
}
}
} else {
xdg_toplevel_set_app_id(top->xdg_toplevel, "WINGs");
xdg_toplevel_set_title(top->xdg_toplevel, "WINGs Application");
@ -515,6 +531,14 @@ W_WLClientSetMinMaxSize(W_View *view)
wl_surface_commit(top->surface);
}
void
W_WLClientPopupSetPosition(WNativeWindow window, int x, int y)
{
WLClientToplevel *top = wlc_top_find(window);
if (!top || !top->wmaker_popup) return;
wmaker_popup_v1_set_position(top->wmaker_popup, x, y);
}
Bool
W_WLClientBeginMove(WNativeWindow window)
{
@ -1036,6 +1060,9 @@ registry_handle_global(void *data, struct wl_registry *registry,
ctx->wm_base = wl_registry_bind(registry, name,
&xdg_wm_base_interface, v);
xdg_wm_base_add_listener(ctx->wm_base, &wm_base_listener, ctx);
} else if (strcmp(interface, wmaker_popup_manager_v1_interface.name) == 0) {
ctx->wmaker_popup_mgr = wl_registry_bind(registry, name,
&wmaker_popup_manager_v1_interface, 1);
}
}

View File

@ -216,8 +216,13 @@ static Bool wl_get_window_geometry(void *ectx, WNativeWindow w,
if (width) *width = 0; if (height) *height = 0;
return False; }
extern void W_WLClientPopupSetPosition(WNativeWindow w, int x, int y);
static void wl_move_window(void *ectx, WNativeWindow w, int x, int y)
{ (void)ectx; (void)w; (void)x; (void)y; }
{
(void)ectx;
W_WLClientPopupSetPosition(w, x, y);
}
/* Implemented in wclient_wl.c — calls xdg_toplevel_move on the client
* connection so the compositor handles the drag. */

View File

@ -0,0 +1,72 @@
/* Generated by wayland-scanner 1.24.0 */
/*
* Copyright © 2026 Window Maker Team
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
#ifndef __has_attribute
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
#endif
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
#else
#define WL_PRIVATE
#endif
extern const struct wl_interface wmaker_popup_v1_interface;
extern const struct wl_interface xdg_toplevel_interface;
static const struct wl_interface *wmaker_popup_v1_types[] = {
NULL,
NULL,
&wmaker_popup_v1_interface,
&xdg_toplevel_interface,
};
static const struct wl_message wmaker_popup_manager_v1_requests[] = {
{ "destroy", "", wmaker_popup_v1_types + 0 },
{ "get_popup", "no", wmaker_popup_v1_types + 2 },
};
WL_PRIVATE const struct wl_interface wmaker_popup_manager_v1_interface = {
"wmaker_popup_manager_v1", 1,
2, wmaker_popup_manager_v1_requests,
0, NULL,
};
static const struct wl_message wmaker_popup_v1_requests[] = {
{ "destroy", "", wmaker_popup_v1_types + 0 },
{ "set_position", "ii", wmaker_popup_v1_types + 0 },
};
WL_PRIVATE const struct wl_interface wmaker_popup_v1_interface = {
"wmaker_popup_v1", 1,
2, wmaker_popup_v1_requests,
0, NULL,
};

View File

@ -316,6 +316,14 @@ static void wl_move(void *vctx, W_View *view, int x, int y)
(void)vctx;
view->pos.x = x;
view->pos.y = y;
/* For popup-style toplevels, drive position via wmaker-popup-v1.
* The client function is a no-op if the toplevel hasn't been
* realized yet — pos.x/y is captured into view->pos for the
* realize-time call. */
if (view->flags.topLevel && view->flags.popup && view->flags.realized) {
extern void W_WLClientPopupSetPosition(WNativeWindow w, int x, int y);
W_WLClientPopupSetPosition((WNativeWindow)view->window, x, y);
}
if (view->parent && view->flags.mapped)
wl_redisplay(vctx, view->parent);
}

View File

@ -1,11 +1,27 @@
AUTOMAKE_OPTIONS = subdir-objects
BUILT_SOURCES = wconfig.h
BUILT_SOURCES = wconfig.h \
wmaker-popup-v1-server-protocol.h \
wmaker-popup-v1-protocol.c
bin_PROGRAMS = wmaker
EXTRA_DIST =
nodist_wmaker_SOURCES = \
wmaker-popup-v1-protocol.c \
wmaker-popup-v1-server-protocol.h
CLEANFILES = wmaker-popup-v1-server-protocol.h \
wmaker-popup-v1-protocol.c
wmaker-popup-v1-server-protocol.h: $(top_srcdir)/protocols/wmaker-popup-v1.xml
$(WAYLAND_SCANNER) server-header $< $@
wmaker-popup-v1-protocol.c: $(top_srcdir)/protocols/wmaker-popup-v1.xml \
wmaker-popup-v1-server-protocol.h
$(WAYLAND_SCANNER) private-code $< $@
wmaker_SOURCES = \
GNUstep.h \
WindowMaker.h \
@ -148,10 +164,10 @@ if USE_NESTED_FUNC
wmaker_SOURCES += misc.c \
xmodifier.c
else
nodist_wmaker_SOURCES = misc.hack_nf.c \
nodist_wmaker_SOURCES += misc.hack_nf.c \
xmodifier.hack_nf.c
CLEANFILES = $(nodist_wmaker_SOURCES)
CLEANFILES += misc.hack_nf.c xmodifier.hack_nf.c
misc.hack_nf.c: misc.c $(top_srcdir)/script/nested-func-to-macro.sh
$(AM_V_GEN)$(top_srcdir)/script/nested-func-to-macro.sh \

View File

@ -132,6 +132,7 @@
#include <wlr/types/wlr_seat.h>
#include <wlr/types/wlr_keyboard.h>
#include <wlr/types/wlr_xdg_shell.h>
#include "wmaker-popup-v1-server-protocol.h"
#include <wlr/types/wlr_xdg_decoration_v1.h>
#include <wlr/xwayland.h> /* Phase 17: XWayland support */
#include <X11/Xproto.h> /* X_SetInputFocus and other request codes */
@ -273,6 +274,9 @@ struct wl_toplevel_view {
WNativeWindow frame_id; /* frame_buf that owns this view (0 = none) */
int frame_rel_x, frame_rel_y; /* offset within frame */
int x, y; /* Phase 55: WM-assigned position */
/* wmaker-popup-v1: client-driven popup position (only valid if popup != NULL) */
struct wl_resource *popup; /* wmaker_popup_v1 resource or NULL */
int popup_x, popup_y;
long *wm_state; /* Phase 56: saved WM state or NULL */
int wm_state_n; /* Phase 56: element count */
char **wm_command; /* Phase 57: WM_COMMAND argv or NULL */
@ -362,6 +366,8 @@ struct wl_compositor_state {
struct wlr_seat *seat;
struct wlr_data_device_manager *data_device_mgr;
struct wlr_xdg_shell *xdg_shell;
/* wmaker-popup-v1 protocol global — client-driven popup positioning */
struct wl_global *wmaker_popup_manager;
/* Phase 64: XDG decoration manager — tell clients to use CSD */
struct wlr_xdg_decoration_manager_v1 *decoration_manager;
struct wl_listener decoration_new_listener;
@ -681,6 +687,8 @@ static int wl_client_get_pid(WNativeWindow win);
/* Phase 13: forward declarations — defined in pointer section below. */
static void wl_update_pointer_focus(uint32_t time_msec);
static void wl_pointer_warp_to(int x, int y);
/* wmaker-popup-v1 helper — defined in protocol section below. */
static void wmaker_popup_apply(struct wl_toplevel_view *view);
/* Phase 14: pointer event handler forward declarations. */
static void handle_pointer_motion(struct wl_listener *listener, void *data);
/* Phase 24: click-to-focus helper — defined in Phase 13 pointer section. */
@ -1165,6 +1173,10 @@ handle_toplevel_map(struct wl_listener *listener, void *data)
/* Rebuild frame without decorations */
extern void wWindowConfigureBorders(WWindow *wwin);
wWindowConfigureBorders(wwin);
/* If the client requested a position via wmaker-popup-v1,
* apply it now that the frame is set up. */
if (view->popup)
wmaker_popup_apply(view);
}
int pid = wl_client_get_pid(view->id);
/* Phase 62: focus delivered by wManageWindow via wSetFocusTo
@ -1366,6 +1378,136 @@ handle_new_toplevel_decoration(struct wl_listener *listener, void *data)
}
}
/* ------------------------------------------------------------------ */
/* wmaker-popup-v1 protocol — client-driven popup positioning */
/* ------------------------------------------------------------------ */
static void
wmaker_popup_handle_destroy(struct wl_client *client,
struct wl_resource *resource)
{
(void)client;
wl_resource_destroy(resource);
}
static void
wmaker_popup_handle_set_position(struct wl_client *client,
struct wl_resource *resource,
int32_t x, int32_t y)
{
(void)client;
struct wl_toplevel_view *view = wl_resource_get_user_data(resource);
if (!view) return;
view->popup_x = (int)x;
view->popup_y = (int)y;
wmaker_popup_apply(view);
}
static const struct wmaker_popup_v1_interface wmaker_popup_impl = {
.destroy = wmaker_popup_handle_destroy,
.set_position = wmaker_popup_handle_set_position,
};
static void
wmaker_popup_resource_destroy(struct wl_resource *resource)
{
struct wl_toplevel_view *view = wl_resource_get_user_data(resource);
if (view && view->popup == resource)
view->popup = NULL;
}
static void
wmaker_popup_apply(struct wl_toplevel_view *view)
{
if (!view || !view->popup) return;
/* Update the canonical view position so subsequent compositor logic
* (frame moves, scene-node positioning) sees the new placement. */
int dx = view->popup_x - view->x;
int dy = view->popup_y - view->y;
view->x = view->popup_x;
view->y = view->popup_y;
/* If a frame_buf wraps this view, move it via the WM's window-move
* path so the frame and its tree node update consistently. */
if (view->frame_id) {
struct wl_frame_buf *fb = frame_buf_find(view->frame_id);
if (fb) {
fb->x += dx;
fb->y += dy;
if (fb->tree)
wlr_scene_node_set_position(&fb->tree->node, fb->x, fb->y);
else if (fb->scene_buf)
wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y);
view->frame_rel_x = 0;
view->frame_rel_y = 0;
}
} else if (view->scene_node) {
wlr_scene_node_set_position(&view->scene_node->node,
view->x, view->y);
}
}
static void
wmaker_popup_manager_handle_destroy(struct wl_client *client,
struct wl_resource *resource)
{
(void)client;
wl_resource_destroy(resource);
}
static void
wmaker_popup_manager_handle_get_popup(struct wl_client *client,
struct wl_resource *manager_resource,
uint32_t id,
struct wl_resource *toplevel_resource)
{
(void)manager_resource;
struct wlr_xdg_toplevel *xdg_top =
wlr_xdg_toplevel_from_resource(toplevel_resource);
if (!xdg_top) {
wl_resource_post_error(manager_resource, 0,
"wmaker_popup: invalid xdg_toplevel resource");
return;
}
struct wl_toplevel_view *view =
wl_find_view_by_xdg_surface(xdg_top->base);
struct wl_resource *resource = wl_resource_create(client,
&wmaker_popup_v1_interface, 1, id);
if (!resource) {
wl_client_post_no_memory(client);
return;
}
wl_resource_set_implementation(resource, &wmaker_popup_impl,
view, wmaker_popup_resource_destroy);
if (view)
view->popup = resource;
}
static const struct wmaker_popup_manager_v1_interface wmaker_popup_manager_impl = {
.destroy = wmaker_popup_manager_handle_destroy,
.get_popup = wmaker_popup_manager_handle_get_popup,
};
static void
wmaker_popup_manager_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *resource = wl_resource_create(client,
&wmaker_popup_manager_v1_interface,
version > 1 ? 1 : (int)version, id);
if (!resource) {
wl_client_post_no_memory(client);
return;
}
wl_resource_set_implementation(resource, &wmaker_popup_manager_impl,
NULL, NULL);
}
/* ------------------------------------------------------------------ */
/* Phase 17: XWayland surface lifecycle */
/* ------------------------------------------------------------------ */
@ -2788,6 +2930,13 @@ wl_display_open(const char *name)
wl_state.xdg_shell = wlr_xdg_shell_create(wl_state.display, 3);
/* Advertise wmaker-popup-v1 — clients use it to drive popup positioning. */
wl_state.wmaker_popup_manager = wl_global_create(wl_state.display,
&wmaker_popup_manager_v1_interface, 1, NULL,
wmaker_popup_manager_bind);
if (wl_state.wmaker_popup_manager)
wwarning("wl_backend: wmaker-popup-v1 manager created");
/* Phase 64: advertise zxdg_decoration_manager_v1 so clients that
* support server-side decorations can negotiate; we respond with
* CLIENT_SIDE since the Wayland path has no X11 frame rendering. */

View File

@ -0,0 +1,72 @@
/* Generated by wayland-scanner 1.24.0 */
/*
* Copyright © 2026 Window Maker Team
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
#ifndef __has_attribute
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
#endif
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
#else
#define WL_PRIVATE
#endif
extern const struct wl_interface wmaker_popup_v1_interface;
extern const struct wl_interface xdg_toplevel_interface;
static const struct wl_interface *wmaker_popup_v1_types[] = {
NULL,
NULL,
&wmaker_popup_v1_interface,
&xdg_toplevel_interface,
};
static const struct wl_message wmaker_popup_manager_v1_requests[] = {
{ "destroy", "", wmaker_popup_v1_types + 0 },
{ "get_popup", "no", wmaker_popup_v1_types + 2 },
};
WL_PRIVATE const struct wl_interface wmaker_popup_manager_v1_interface = {
"wmaker_popup_manager_v1", 1,
2, wmaker_popup_manager_v1_requests,
0, NULL,
};
static const struct wl_message wmaker_popup_v1_requests[] = {
{ "destroy", "", wmaker_popup_v1_types + 0 },
{ "set_position", "ii", wmaker_popup_v1_types + 0 },
};
WL_PRIVATE const struct wl_interface wmaker_popup_v1_interface = {
"wmaker_popup_v1", 1,
2, wmaker_popup_v1_requests,
0, NULL,
};