kick/tests/xpopup.c
Andrew Hamilton e1b137bf30 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.
2026-08-24 12:09:40 +10:00

131 lines
4.6 KiB
C

/* xpopup: an X11 menu-popup probe. Maps a main frame first (like any app),
* then -- after a configurable delay, mimicking a user opening a menu --
* creates and instantly maps a second window as a managed transient of the
* frame, filled with a distinct colour at known coordinates.
*
* GTK's X11 menus are regular (non-override-redirect) transients that map
* immediately on click; this reproduces that shape so a compositor bug in
* the late-association path shows up as missing popup pixels rather than
* needing a real toolkit.
*
* usage: xpopup [framew frameth popup-delay-s [unmap-after-s]]
*
* After mapping, the popup repaints continuously (as real menus do while
* animating), forcing damage so server-side offload keeps committing.
*
* The frame is muted blue (0x306090); the popup is green (0x00C000) at
* 300x200, placed at (100,100) inside the frame.
*/
#define _POSIX_C_SOURCE 200809L
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define POPUP_W 300
#define POPUP_H 200
static void set_pid(Display *d, Window w) {
pid_t pid = getpid();
Atom net_pid = XInternAtom(d, "_NET_WM_PID", False);
XChangeProperty(d, w, net_pid, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&pid, 1);
}
static void fill(Display *d, Window w, unsigned long colour, int wpx, int hpx)
{
XGCValues gcv;
GC gc = XCreateGC(d, w, 0, &gcv);
XSetForeground(d, gc, colour);
XFillRectangle(d, w, gc, 0, 0, wpx, hpx);
XFreeGC(d, gc);
}
static void repaint_frame(Display *d, Window frame, int fw, int fh)
{
/* Keep the frame painted after resizes; leave the popup rectangle to
the popup window itself (it floats above us). */
fill(d, frame, 0x306090UL, fw, fh);
}
int main(int argc, char **argv)
{
int fw = argc > 1 ? atoi(argv[1]) : 640;
int fh = argc > 2 ? atoi(argv[2]) : 480;
int delay_s = argc > 3 ? atoi(argv[3]) : 1;
int unmap_after = argc > 4 ? atoi(argv[4]) : -1; /* seconds, -1 = never */
Display *d = XOpenDisplay(NULL);
if (!d) {
fprintf(stderr, "xpopup: cannot open display\n");
return 1;
}
int scr = DefaultScreen(d);
Window root = RootWindow(d, scr);
Window frame = XCreateSimpleWindow(d, root, 0, 0, fw, fh, 0,
BlackPixel(d, scr),
0x306090UL);
XStoreName(d, frame, "xpopup-frame");
set_pid(d, frame);
Window popup = XCreateSimpleWindow(d, root, 100, 100, POPUP_W, POPUP_H, 0,
BlackPixel(d, scr),
0x00C000UL);
XStoreName(d, popup, "xpopup-menu");
set_pid(d, popup);
/* Managed-window menu shape: transient for the frame, never OR. */
XSetTransientForHint(d, popup, frame);
XSelectInput(d, frame, ExposureMask | StructureNotifyMask);
XMapWindow(d, frame);
XFlush(d);
/* Map the "menu" once things have settled. */
sleep((unsigned)delay_s);
XMapWindow(d, popup);
/* Event pump with a repaint tick: the popup refreshes continuously
(menu-like damage); the frame refills on Expose. */
time_t popup_mapped_at = time(NULL);
int popup_up = 1;
for (;;) {
while (XPending(d) > 0) {
XEvent ev;
XNextEvent(d, &ev);
if (ev.xany.window == frame && ev.type == Expose)
repaint_frame(d, frame, fw, fh);
else if (ev.xany.window == popup &&
(ev.type == ConfigureNotify || ev.type == Expose))
fill(d, popup, 0x00C000UL, POPUP_W, POPUP_H);
}
if (popup_up) {
fill(d, popup, 0x00C000UL, POPUP_W, POPUP_H);
static int checked = 0;
if (!checked && time(NULL) - popup_mapped_at > 2) {
checked = 1;
XImage *img = XGetImage(d, popup, 0, 0, 8, 8, AllPlanes,
ZPixmap);
if (img) {
unsigned long pxl = XGetPixel(img, 4, 4);
fprintf(stderr, "[xpopup] self-check pixel=%06lx (%s)\n",
pxl, pxl == 0xC000 ? "GREEN" : "NOT-green");
XDestroyImage(img);
}
}
if (unmap_after > 0 &&
(int)(time(NULL) - popup_mapped_at) >= unmap_after) {
XUnmapWindow(d, popup);
popup_up = 0;
}
}
XFlush(d);
struct timespec tick = { 0, 150 * 1000000L };
nanosleep(&tick, NULL); }
return 0;
}