308 lines
14 KiB
C
308 lines
14 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
#include <wayland-client.h>
|
|
#include "xdg-shell-client.h"
|
|
|
|
/* EXPERIMENTAL CONTRACT: every popup owns distinct XDG objects. B's actual
|
|
* parent is A's xdg_surface; logs name object IDs as well as colors. */
|
|
struct surface {
|
|
const char *name;
|
|
struct wl_surface *wl;
|
|
struct xdg_surface *xdg;
|
|
struct xdg_popup *popup;
|
|
bool configured, mapped, done;
|
|
int width, height, anchor_x, anchor_y;
|
|
uint32_t color;
|
|
};
|
|
static struct wl_display *display;
|
|
static struct wl_compositor *compositor;
|
|
static struct wl_shm *shm;
|
|
static struct xdg_wm_base *wm;
|
|
static struct wl_seat *seat;
|
|
static struct wl_pointer *pointer;
|
|
static struct xdg_toplevel *top_role;
|
|
static struct surface top = {.name="TOPLEVEL", .width=420, .height=320, .color=0x002344cc};
|
|
static struct surface a = {.name="POPUP_A", .width=300, .height=170, .color=0x00e0c020};
|
|
static struct surface b = {.name="POPUP_B", .width=300, .height=130, .color=0x0020d0a0};
|
|
static volatile sig_atomic_t running = 1;
|
|
static struct surface *entered;
|
|
|
|
/* Observe client-side SHM release separately from native GPU retirement.
|
|
* Keep protocol buffer alive until release; no backing memory is reused. */
|
|
static unsigned shm_created, shm_released;
|
|
static void buffer_release(void *data, struct wl_buffer *buffer) {
|
|
(void)data;
|
|
++shm_released;
|
|
fprintf(stderr,"SHM_RELEASE buffer=%u released=%u created=%u\n",
|
|
wl_proxy_get_id((struct wl_proxy *)buffer),shm_released,shm_created);
|
|
wl_buffer_destroy(buffer);
|
|
}
|
|
static const struct wl_buffer_listener buffer_listener = {.release=buffer_release};
|
|
static void draw(struct surface *s) {
|
|
size_t bytes = (size_t)s->width * (size_t)s->height * 4;
|
|
char path[4096];
|
|
const char *scratch = getenv("TMPDIR");
|
|
const char *lab = getenv("SMX_LAB");
|
|
/* WHY: SHM belongs to LAB, never the short socket-only runtime.
|
|
* CONTRACT: launcher supplies canonical LAB/scratch; fail before mkstemp. */
|
|
char expected[4096];
|
|
int expected_len = lab ? snprintf(expected, sizeof(expected), "%s/scratch", lab) : -1;
|
|
int path_len = scratch ? snprintf(path, sizeof(path), "%s/smx-shm-XXXXXX", scratch) : -1;
|
|
if (!lab || strncmp(lab, "/var/tmp/", 9) || strstr(lab, "/../") ||
|
|
expected_len < 0 || expected_len >= (int)sizeof(expected) ||
|
|
!scratch || strcmp(scratch, expected) || path_len < 0 ||
|
|
path_len >= (int)sizeof(path)) {
|
|
fprintf(stderr, "invalid LAB scratch for SHM\n");
|
|
exit(1);
|
|
}
|
|
int fd = mkstemp(path);
|
|
if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("buffer"); exit(1); }
|
|
unlink(path);
|
|
uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (pixels == MAP_FAILED) { perror("mmap"); exit(1); }
|
|
for (size_t i=0; i<bytes/4; ++i) pixels[i] = s->color;
|
|
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes);
|
|
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, s->width, s->height,
|
|
s->width*4, WL_SHM_FORMAT_XRGB8888);
|
|
++shm_created;
|
|
wl_buffer_add_listener(buffer,&buffer_listener,NULL);
|
|
wl_surface_attach(s->wl, buffer, 0, 0);
|
|
wl_surface_damage_buffer(s->wl, 0, 0, s->width, s->height);
|
|
wl_surface_commit(s->wl);
|
|
wl_shm_pool_destroy(pool);
|
|
munmap(pixels, bytes);
|
|
close(fd);
|
|
s->mapped = true;
|
|
fprintf(stderr, "%s MAP wl=%u xdg=%u\n", s->name,
|
|
wl_proxy_get_id((struct wl_proxy *)s->wl), wl_proxy_get_id((struct wl_proxy *)s->xdg));
|
|
}
|
|
static void on_ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
|
|
xdg_wm_base_pong(base, serial);
|
|
}
|
|
static const struct xdg_wm_base_listener wm_listener = {.ping=on_ping};
|
|
static void on_xdg_configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
|
|
struct surface *s=data;
|
|
fprintf(stderr, "%s XDG_CONFIGURE serial=%u ack\n", s->name, serial);
|
|
xdg_surface_ack_configure(xdg, serial);
|
|
s->configured=true;
|
|
if (!s->mapped && !s->done) draw(s);
|
|
}
|
|
static const struct xdg_surface_listener xdg_listener = {.configure=on_xdg_configure};
|
|
static void on_top_configure(void *data, struct xdg_toplevel *role, int32_t w,
|
|
int32_t h, struct wl_array *states) {
|
|
fprintf(stderr, "TOPLEVEL ROLE_CONFIGURE w=%d h=%d\n", w,h);
|
|
}
|
|
static void on_top_close(void *data, struct xdg_toplevel *role) {
|
|
fprintf(stderr, "TOPLEVEL CLOSE_REQUEST\n"); running=0;
|
|
}
|
|
static const struct xdg_toplevel_listener top_listener = {
|
|
.configure=on_top_configure, .close=on_top_close,
|
|
};
|
|
static void on_popup_configure(void *data, struct xdg_popup *role, int32_t x,
|
|
int32_t y, int32_t w, int32_t h) {
|
|
struct surface *s=data;
|
|
fprintf(stderr, "%s ROLE_CONFIGURE x=%d y=%d w=%d h=%d\n",s->name,x,y,w,h);
|
|
}
|
|
static void on_popup_done(void *data, struct xdg_popup *role) {
|
|
struct surface *s=data;
|
|
s->done=true;
|
|
s->mapped=false;
|
|
fprintf(stderr, "%s POPUP_DONE role=%u\n",s->name,wl_proxy_get_id((struct wl_proxy *)role));
|
|
}
|
|
static const struct xdg_popup_listener popup_listener = {
|
|
.configure=on_popup_configure, .popup_done=on_popup_done,
|
|
};
|
|
/* WHY: XDG requires child-first destruction even if the compositor sends
|
|
* dismissals for multiple levels in one dispatch batch. */
|
|
static void destroy_popup(struct surface *s) {
|
|
if (!s->popup) return;
|
|
fprintf(stderr,"%s DESTROY role=%u xdg=%u wl=%u\n",s->name,
|
|
wl_proxy_get_id((struct wl_proxy *)s->popup),
|
|
wl_proxy_get_id((struct wl_proxy *)s->xdg),
|
|
wl_proxy_get_id((struct wl_proxy *)s->wl));
|
|
xdg_popup_destroy(s->popup);
|
|
xdg_surface_destroy(s->xdg);
|
|
wl_surface_destroy(s->wl);
|
|
s->popup=NULL; s->xdg=NULL; s->wl=NULL; s->mapped=false;
|
|
if (entered==s) entered=NULL;
|
|
}
|
|
static void collect_dismissals(void) {
|
|
if (b.done) destroy_popup(&b);
|
|
if (a.done) {
|
|
destroy_popup(&b);
|
|
destroy_popup(&a);
|
|
}
|
|
}
|
|
static void open_popup(struct surface *s, struct surface *parent, uint32_t serial) {
|
|
if (s->popup || !parent->mapped || !serial) return;
|
|
s->wl=wl_compositor_create_surface(compositor);
|
|
s->xdg=xdg_wm_base_get_xdg_surface(wm,s->wl);
|
|
xdg_surface_add_listener(s->xdg,&xdg_listener,s);
|
|
struct xdg_positioner *pos=xdg_wm_base_create_positioner(wm);
|
|
xdg_positioner_set_size(pos,s->width,s->height);
|
|
xdg_positioner_set_anchor_rect(pos,s->anchor_x,s->anchor_y,1,1);
|
|
xdg_positioner_set_anchor(pos,XDG_POSITIONER_ANCHOR_TOP_LEFT);
|
|
xdg_positioner_set_gravity(pos,XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
|
|
s->popup=xdg_surface_get_popup(s->xdg,parent->xdg,pos);
|
|
xdg_popup_add_listener(s->popup,&popup_listener,s);
|
|
fprintf(stderr,"%s CREATE wl=%u xdg=%u role=%u parent=%s parent_xdg=%u\n",
|
|
s->name,wl_proxy_get_id((struct wl_proxy *)s->wl),
|
|
wl_proxy_get_id((struct wl_proxy *)s->xdg),
|
|
wl_proxy_get_id((struct wl_proxy *)s->popup),parent->name,
|
|
wl_proxy_get_id((struct wl_proxy *)parent->xdg));
|
|
fprintf(stderr,"%s GRAB_REQUEST origin=wl_pointer.button target=%s serial=%u used=%u\n",
|
|
s->name,parent->name,serial,serial);
|
|
xdg_popup_grab(s->popup,seat,serial);
|
|
wl_surface_commit(s->wl); /* Initial bufferless commit requests configure. */
|
|
xdg_positioner_destroy(pos);
|
|
}
|
|
static struct surface *which(struct wl_surface *wl) {
|
|
if (wl==top.wl) return ⊤
|
|
if (wl==a.wl && wl) return &a;
|
|
if (wl==b.wl && wl) return &b;
|
|
return NULL;
|
|
}
|
|
static void on_enter(void *data,struct wl_pointer *ptr,uint32_t serial,
|
|
struct wl_surface *wl,wl_fixed_t x,wl_fixed_t y) {
|
|
entered=which(wl);
|
|
fprintf(stderr,"POINTER_ENTER target=%s wl=%u serial=%u x=%.2f y=%.2f\n",
|
|
entered?entered->name:"OTHER",wl_proxy_get_id((struct wl_proxy *)wl),serial,
|
|
wl_fixed_to_double(x),wl_fixed_to_double(y));
|
|
}
|
|
static void on_leave(void *data,struct wl_pointer *ptr,uint32_t serial,
|
|
struct wl_surface *wl) {
|
|
struct surface *s=which(wl);
|
|
fprintf(stderr,"POINTER_LEAVE target=%s serial=%u\n",s?s->name:"OTHER",serial);
|
|
if (entered==s) entered=NULL;
|
|
}
|
|
static void on_motion(void *data,struct wl_pointer *ptr,uint32_t time,
|
|
wl_fixed_t x,wl_fixed_t y) {
|
|
fprintf(stderr,"POINTER_MOTION target=%s x=%.2f y=%.2f\n",
|
|
entered?entered->name:"OTHER",wl_fixed_to_double(x),wl_fixed_to_double(y));
|
|
}
|
|
static void on_button(void *data,struct wl_pointer *ptr,uint32_t serial,
|
|
uint32_t time,uint32_t button,uint32_t state) {
|
|
fprintf(stderr,"POINTER_BUTTON target=%s serial=%u button=%u state=%u\n",
|
|
entered?entered->name:"OTHER",serial,button,state);
|
|
if (state!=WL_POINTER_BUTTON_STATE_PRESSED || button!=272) return;
|
|
if (entered==&top && !a.popup) open_popup(&a,&top,serial);
|
|
else if (entered==&a && a.mapped && !b.popup) open_popup(&b,&a,serial);
|
|
}
|
|
static void on_axis(void *data,struct wl_pointer *ptr,uint32_t t,uint32_t axis,wl_fixed_t v) {}
|
|
static void on_frame(void *data,struct wl_pointer *ptr) {}
|
|
static const struct wl_pointer_listener pointer_listener={
|
|
.enter=on_enter,.leave=on_leave,.motion=on_motion,.button=on_button,
|
|
.axis=on_axis,.frame=on_frame,
|
|
};
|
|
static void on_caps(void *data,struct wl_seat *s,uint32_t caps) {
|
|
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) {
|
|
pointer=wl_seat_get_pointer(s);
|
|
wl_pointer_add_listener(pointer,&pointer_listener,NULL);
|
|
}
|
|
}
|
|
static void on_seat_name(void *data,struct wl_seat *s,const char *name) {}
|
|
static const struct wl_seat_listener seat_listener={.capabilities=on_caps,.name=on_seat_name};
|
|
static void on_global(void *data,struct wl_registry *reg,uint32_t name,
|
|
const char *interface,uint32_t version) {
|
|
if (!strcmp(interface,wl_compositor_interface.name))
|
|
compositor=wl_registry_bind(reg,name,&wl_compositor_interface,version<4?version:4);
|
|
else if (!strcmp(interface,wl_shm_interface.name))
|
|
shm=wl_registry_bind(reg,name,&wl_shm_interface,1);
|
|
else if (!strcmp(interface,xdg_wm_base_interface.name))
|
|
wm=wl_registry_bind(reg,name,&xdg_wm_base_interface,version<3?version:3);
|
|
else if (!strcmp(interface,wl_seat_interface.name)) {
|
|
seat=wl_registry_bind(reg,name,&wl_seat_interface,version<5?version:5);
|
|
wl_seat_add_listener(seat,&seat_listener,NULL);
|
|
}
|
|
}
|
|
static void on_removed(void *data,struct wl_registry *reg,uint32_t name) {}
|
|
static const struct wl_registry_listener registry_listener={.global=on_global,.global_remove=on_removed};
|
|
static void stop(int sig) { running=0; }
|
|
int main(int argc,char **argv) {
|
|
signal(SIGTERM,stop);
|
|
const char *placement=argc>1?argv[1]:"b_cross";
|
|
/* CONTRACT: vertical positioner anchors remain inside the toplevel's
|
|
* effective window geometry, which defaults to its buffer bounds. */
|
|
if (!strncmp(placement,"v_",2)) top.height=575;
|
|
if (!strcmp(placement,"visible")) {
|
|
a.anchor_x=50; a.anchor_y=80; a.width=250;
|
|
b.anchor_x=20; b.anchor_y=60; b.width=170;
|
|
} else if (!strcmp(placement,"v_visible")) {
|
|
a.anchor_x=50; a.anchor_y=80; a.width=250;
|
|
b.anchor_x=20; b.anchor_y=60; b.width=170;
|
|
} else if (!strcmp(placement,"v_b_cross")) {
|
|
/* WHY: only B's requested rectangle reaches below y=600 when the
|
|
* tiled toplevel content begins near y=25. Configure and pixels
|
|
* remain the authority for the actual crossing. */
|
|
a.anchor_x=50; a.anchor_y=350; a.width=250;
|
|
b.anchor_x=20; b.anchor_y=155; b.width=170;
|
|
} else if (!strcmp(placement,"v_both_cross")) {
|
|
a.anchor_x=50; a.anchor_y=480; a.width=250;
|
|
b.anchor_x=20; b.anchor_y=80; b.width=170;
|
|
} else if (!strcmp(placement,"both_cross")) {
|
|
a.anchor_x=350; a.anchor_y=80; a.width=350;
|
|
b.anchor_x=160; b.anchor_y=60; b.width=300;
|
|
} else if (!strcmp(placement,"b_cross")) {
|
|
a.anchor_x=200; a.anchor_y=80; a.width=300;
|
|
b.anchor_x=220; b.anchor_y=60; b.width=300;
|
|
} else { fprintf(stderr,"invalid placement\n"); return 2; }
|
|
display=wl_display_connect(NULL);
|
|
if (!display) { fprintf(stderr,"cannot connect Wayland\n"); return 1; }
|
|
struct wl_registry *registry=wl_display_get_registry(display);
|
|
wl_registry_add_listener(registry,®istry_listener,NULL);
|
|
wl_display_roundtrip(display); wl_display_roundtrip(display);
|
|
if (!compositor || !shm || !wm || !seat || !pointer) {
|
|
fprintf(stderr,"missing Wayland globals/pointer\n"); return 1;
|
|
}
|
|
xdg_wm_base_add_listener(wm,&wm_listener,NULL);
|
|
top.wl=wl_compositor_create_surface(compositor);
|
|
top.xdg=xdg_wm_base_get_xdg_surface(wm,top.wl);
|
|
xdg_surface_add_listener(top.xdg,&xdg_listener,&top);
|
|
top_role=xdg_surface_get_toplevel(top.xdg);
|
|
xdg_toplevel_add_listener(top_role,&top_listener,NULL);
|
|
xdg_toplevel_set_title(top_role,"Mosaic Nested Grab Probe");
|
|
xdg_toplevel_set_app_id(top_role,"org.mosaic.NestedGrabProbe");
|
|
fprintf(stderr,"TOPLEVEL CREATE wl=%u xdg=%u role=%u placement=%s\n",
|
|
wl_proxy_get_id((struct wl_proxy *)top.wl),
|
|
wl_proxy_get_id((struct wl_proxy *)top.xdg),
|
|
wl_proxy_get_id((struct wl_proxy *)top_role),placement);
|
|
wl_surface_commit(top.wl);
|
|
while (running) {
|
|
wl_display_flush(display);
|
|
struct pollfd fds[2]={{.fd=wl_display_get_fd(display),.events=POLLIN},
|
|
{.fd=STDIN_FILENO,.events=POLLIN}};
|
|
int ready=poll(fds,2,500);
|
|
if (ready<0) { if (errno==EINTR) continue; perror("poll"); break; }
|
|
if (fds[0].revents & (POLLIN|POLLERR|POLLHUP)) {
|
|
if (wl_display_dispatch(display)<0) break;
|
|
collect_dismissals();
|
|
}
|
|
if (fds[1].revents & POLLIN) {
|
|
char cmd[64];
|
|
if (fgets(cmd,sizeof(cmd),stdin)) {
|
|
if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); }
|
|
else if (!strncmp(cmd,"close_owner",11)) { fprintf(stderr,"CLOSE_RECEIVED\nCOMMAND close_owner\n"); running=0; }
|
|
}
|
|
}
|
|
}
|
|
destroy_popup(&b); destroy_popup(&a);
|
|
fprintf(stderr,"TOPLEVEL DESTROY\n");
|
|
xdg_toplevel_destroy(top_role); xdg_surface_destroy(top.xdg); wl_surface_destroy(top.wl);
|
|
wl_display_roundtrip(display);
|
|
fprintf(stderr,"SHM_CLOSURE created=%u released=%u\n",shm_created,shm_released);
|
|
wl_display_flush(display);
|
|
/* WHY/CONTRACT/INVARIANT: exit0 does not prove protocol error0. */
|
|
fprintf(stderr,"EXIT error=%d\n",wl_display_get_error(display));
|
|
wl_display_disconnect(display);
|
|
return 0;
|
|
}
|