GDK_BACKEND/QT_QPA_PLATFORM forced to wayland stripped toolkits' fallback,
killing X11-only builds like emacs-gtk ('cannot open display' against a
healthy Xwayland). Children now self-select from WAYLAND_DISPLAY/DISPLAY,
and XAUTHORITY is cleared.
Separately, X11 membership decisions moved from window-creation time to
first map: GTK leader windows never map, yet claimed the tab slot at
creation, leaving a permanent black tab while the real frame was demoted
to a stacked member -- and Alt-cycling onto that phantom main member
crashed on wlr_seat_keyboard_notify_enter(NULL). Never-mapped windows now
free silently; keyboard-entry paths guard on live surfaces. New
tests/xleader.c probe (unmapped leader + mapped frame) locks the behaviour
in.
85 lines
2.9 KiB
C
85 lines
2.9 KiB
C
/* xleader: an X11 twin-window probe that mimics how GTK apps appear on
|
|
* X11 -- an invisible "leader" window is created first and never mapped,
|
|
* then the real frame maps. A compositor that claims windows at map time
|
|
* (instead of creation time) gives this program exactly one tab; one that
|
|
* claims at creation time steals the tab slot for the leader, which then
|
|
* renders as a permanent black tab while the real frame gets demoted to a
|
|
* tab-group member.
|
|
*
|
|
* Both windows carry _NET_WM_PID so process-group matching can claim them.
|
|
* The frame fills itself with a solid colour so pixel checks can tell it
|
|
* apart from an unrendered tab.
|
|
*
|
|
* Build: cc xleader.c -o xleader -lX11 $(pkg-config --cflags xcb xcb-icccm)
|
|
*/
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xutil.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
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_frame(Display *d, Window w, int wpx, int hpx)
|
|
{
|
|
XWMHints hints;
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.flags = InputHint | StateHint;
|
|
hints.input = True;
|
|
hints.initial_state = NormalState;
|
|
XSetWMHints(d, w, &hints);
|
|
|
|
XGCValues gcv;
|
|
GC gc = XCreateGC(d, w, 0, &gcv);
|
|
XSetForeground(d, gc, 0x306090UL); /* muted blue: visible, not black */
|
|
XFillRectangle(d, w, gc, 0, 0, wpx, hpx);
|
|
XFreeGC(d, gc);
|
|
XFlush(d);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
int wpx = argc > 1 ? atoi(argv[1]) : 640;
|
|
int hpx = argc > 2 ? atoi(argv[2]) : 480;
|
|
Display *d = XOpenDisplay(NULL);
|
|
if (!d) {
|
|
fprintf(stderr, "xleader: cannot open display\n");
|
|
return 1;
|
|
}
|
|
int scr = DefaultScreen(d);
|
|
Window root = RootWindow(d, scr);
|
|
|
|
/* The leader: created, given WM_CLASS + pid, never mapped. */
|
|
Window leader = XCreateSimpleWindow(d, root, 0, 0, 8, 8, 0, 0, 0);
|
|
XStoreName(d, leader, "xleader-leader");
|
|
set_pid(d, leader);
|
|
|
|
/* The real frame: created and mapped. The background pixel is the
|
|
probe colour so the window stays "filled" even right after a resize
|
|
clears its backing pixmap (before any Expose-driven repaint lands). */
|
|
Window frame = XCreateSimpleWindow(d, root, 0, 0, wpx, hpx, 0,
|
|
BlackPixel(d, scr),
|
|
0x306090UL);
|
|
XStoreName(d, frame, "xleader-frame");
|
|
set_pid(d, frame);
|
|
XSelectInput(d, frame, ExposureMask | StructureNotifyMask);
|
|
|
|
XMapWindow(d, frame);
|
|
fill_frame(d, frame, wpx, hpx);
|
|
XFlush(d);
|
|
|
|
for (;;) {
|
|
XEvent ev;
|
|
XNextEvent(d, &ev); /* blocks: keeps the client alive */
|
|
if (ev.type == Expose && ev.xexpose.window == frame)
|
|
fill_frame(d, frame, wpx, hpx); /* stay painted after resizes */
|
|
}
|
|
return 0;
|
|
}
|