Composite popups and fix sub-menu rendering

This commit is contained in:
Andrew Hamilton 2026-08-07 19:07:40 +10:00
parent efecacacb1
commit 0b6a9eef75

View file

@ -101,6 +101,7 @@ struct tabbed_server {
struct wl_listener new_output;
struct wl_listener new_toplevel;
struct wl_listener new_popup;
struct wl_listener frame;
struct wlr_xdg_toplevel *active;
@ -142,6 +143,7 @@ struct tabbed_host {
GLuint gl_program;
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc;
GLuint import_tex;
GLuint popup_tex;
};
static struct tabbed_server server;
@ -229,6 +231,14 @@ static bool gl_setup(void) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
glGenTextures(1, &host.popup_tex);
glBindTexture(GL_TEXTURE_2D, host.popup_tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
@ -546,6 +556,117 @@ static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel) {
return NULL;
}
struct tabbed_popup {
struct wlr_xdg_surface *xdg_surface;
struct wlr_scene_tree *scene;
struct wl_list link;
struct wl_listener commit;
struct wl_listener destroy;
struct wl_listener reposition;
};
/* Popups are also drawn by us (host-side) for the presented frame, because
wlroots' own premultiplied-alpha rendering of these ARGB menus produces
nothing in this nested EGL pipeline. This list tracks the live ones. */
static struct wl_list popups = { &popups, &popups };
/* Enough of the host window for popups to be positioned within; open menus
may exceed the toplevel but we keep them inside the visible area. */
static void popup_usable_box(struct wlr_box *box) {
box->x = 0;
box->y = 0;
if (server.output) {
box->width = server.output->width;
box->height = server.output->height;
} else {
box->width = host.width;
box->height = host.height;
}
}
/* Menu popups (e.g. galculator's) submit ARGB buffers that wlroots renders
with premultiplied-alpha blending, which produces nothing in this nested
EGL pipeline. We therefore draw them ourselves into the presented frame
(see composite_popups in blit_scene_buffer). */
static void popup_handle_commit(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_popup *tp = wl_container_of(listener, tp, commit);
struct wlr_xdg_surface *xdg_surface = tp->xdg_surface;
/* The client cannot map/render the popup until it receives its initial
configure, which wlroots only sends via wlr_xdg_popup_unconstrain_from
(which also adjusts geometry within the usable area). initial_commit is
true on the first commit; later commits are ignored. */
if (xdg_surface->initial_commit && xdg_surface->popup) {
struct wlr_box box;
popup_usable_box(&box);
wlr_xdg_popup_unconstrain_from_box(xdg_surface->popup, &box);
}
}
static void popup_handle_reposition(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_popup *tp = wl_container_of(listener, tp, reposition);
if (tp->xdg_surface->popup) {
struct wlr_box box;
popup_usable_box(&box);
wlr_xdg_popup_unconstrain_from_box(tp->xdg_surface->popup, &box);
}
}
static void popup_handle_destroy(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_popup *tp = wl_container_of(listener, tp, destroy);
wl_list_remove(&tp->link);
wl_list_remove(&tp->commit.link);
wl_list_remove(&tp->destroy.link);
wl_list_remove(&tp->reposition.link);
free(tp);
}
static void create_popup(struct wlr_xdg_popup *popup,
struct wlr_scene_tree *parent_tree) {
struct wlr_xdg_surface *xdg_surface = popup->base;
struct tabbed_popup *tp = calloc(1, sizeof(*tp));
if (!tp) return;
tp->xdg_surface = xdg_surface;
tp->scene = wlr_scene_xdg_surface_create(parent_tree, xdg_surface);
wl_list_insert(&popups, &tp->link);
/* wlr_scene_xdg_surface_create destroys the node when the surface is
destroyed, so we only track positioning/reposition listeners here. */
tp->commit.notify = popup_handle_commit;
wl_signal_add(&xdg_surface->surface->events.commit, &tp->commit);
tp->destroy.notify = popup_handle_destroy;
/* Listen on the popup's own destroy: wlroots emits it (then asserts its
event lists are empty) before the popup is freed. */
wl_signal_add(&popup->events.destroy, &tp->destroy);
tp->reposition.notify = popup_handle_reposition;
wl_signal_add(&popup->events.reposition, &tp->reposition);
/* remember the scene tree so a submenu popup can find its parent's tree */
xdg_surface->data = tp;
}
static void handle_new_popup(struct wl_listener *listener, void *data) {
(void)listener;
struct wlr_xdg_popup *popup = data;
/* Popups position themselves relative to their immediate parent surface,
so place each popup under its parent's scene tree. */
struct wlr_scene_tree *parent_tree = NULL;
struct wlr_xdg_surface *parent =
wlr_xdg_surface_try_from_wlr_surface(popup->parent);
if (parent) {
if (parent->role == WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
struct tabbed_app *app = app_for_toplevel(parent->toplevel);
if (app) parent_tree = app->tree;
} else if (parent->role == WLR_XDG_SURFACE_ROLE_POPUP) {
struct tabbed_popup *tp = parent->data;
if (tp) parent_tree = tp->scene;
}
}
if (!parent_tree) return;
create_popup(popup, parent_tree);
}
static void set_active_surface(struct wlr_xdg_toplevel *toplevel) {
if (server.active == toplevel) return;
@ -1119,41 +1240,58 @@ static bool import_scene_buffer(struct wlr_buffer *buf, GLuint tex) {
return true;
}
/* Dump the host window (the EGL surface we present) to a PPM, on demand.
Triggered with `kill -USR1 <pid>` so the current frame can be inspected
without any screen capture. */
static volatile sig_atomic_t dump_requested = 0;
/* Draw one popup surface buffer into the presented window at its on-screen
position. This bypasses wlroots' scene rendering for popups, whose
premultiplied-alpha blend paints nothing in our nested EGL pipeline.
static void sigusr1_handler(int sig) {
(void)sig;
dump_requested = 1;
Each popup surface is composited exactly once per frame. A sub-menu is a
popup nested under its parent menu's scene tree, so it is reachable both
from its own tree and from the parent menu's tree; the surfaces-drawn array
prevents it being drawn twice. Position uses wlr_scene_node_coords (the
true absolute offset, including every ancestor), because the iterator's
sx/sy only sums offsets from its own tree root and would place a nested
popup too high/left by the missing ancestor offsets. */
struct popup_draw_ctx {
int win_w, win_h;
struct wlr_surface **drawn;
int ndrawn;
};
static void popup_draw_cb(struct wlr_scene_buffer *buffer,
int sx, int sy, void *user_data) {
struct popup_draw_ctx *ctx = user_data;
if (!buffer->buffer) return;
struct wlr_scene_surface *ss = wlr_scene_surface_try_from_buffer(buffer);
if (!ss) return;
for (int i = 0; i < ctx->ndrawn; i++) {
if (ctx->drawn[i] == ss->surface) return;
}
ctx->drawn[ctx->ndrawn++] = ss->surface;
(void)sx; (void)sy;
int lx = 0, ly = 0;
wlr_scene_node_coords(&buffer->node, &lx, &ly);
if (!import_scene_buffer(buffer->buffer, host.popup_tex)) return;
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
draw_texture_rect(host.popup_tex, buffer->buffer->width,
buffer->buffer->height,
0, 0, buffer->buffer->width, buffer->buffer->height,
lx, ly, ctx->win_w, ctx->win_h);
glDisable(GL_BLEND);
}
static void dump_window(int seq) {
int w = host.width, h = host.height;
if (w <= 0 || h <= 0) return;
size_t npix = (size_t)w * h;
uint8_t *px = malloc(npix * 4);
if (!px) return;
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
char path[64];
snprintf(path, sizeof(path), "/tmp/window-%04d.ppm", seq);
FILE *f = fopen(path, "w");
if (f) {
fprintf(f, "P6\n%d %d\n255\n", w, h);
for (int y = h - 1; y >= 0; y--) {
const uint8_t *row = px + (size_t)y * w * 4;
for (int x = 0; x < w; x++) {
fputc(row[x * 4], f);
fputc(row[x * 4 + 1], f);
fputc(row[x * 4 + 2], f);
}
}
fclose(f);
fprintf(stderr, "[dump] wrote %s (%dx%d)\n", path, w, h);
}
free(px);
static void composite_popups(void) {
struct wlr_surface *drawn[64];
struct popup_draw_ctx ctx = {
.win_w = host.width,
.win_h = host.height,
.drawn = drawn,
.ndrawn = 0,
};
struct tabbed_popup *tp;
wl_list_for_each(tp, &popups, link)
wlr_scene_node_for_each_buffer(&tp->scene->node,
popup_draw_cb, &ctx);
}
static void blit_scene_buffer(struct wlr_buffer *buf) {
@ -1169,11 +1307,7 @@ static void blit_scene_buffer(struct wlr_buffer *buf) {
0, 0, buf->width, buf->height, 0, 0, w, h);
glDisable(GL_BLEND);
}
if (dump_requested) {
dump_requested = 0;
static int seq;
dump_window(++seq);
}
composite_popups();
eglSwapBuffers(host.egl_display, host.egl_surface);
}
@ -1258,8 +1392,6 @@ int main(int argc, char *argv[]) {
sigaction(SIGSEGV, &crash_sa, NULL);
sigaction(SIGABRT, &crash_sa, NULL);
signal(SIGCHLD, sigchld_handler);
signal(SIGUSR1, sigusr1_handler);
wlr_log_init(WLR_DEBUG, NULL);
server.wl_display = wl_display_create();
@ -1325,6 +1457,8 @@ int main(int argc, char *argv[]) {
wl_signal_add(&server.backend->events.new_output, &server.new_output);
server.new_toplevel.notify = handle_new_toplevel;
wl_signal_add(&server.xdg_shell->events.new_toplevel, &server.new_toplevel);
server.new_popup.notify = handle_new_popup;
wl_signal_add(&server.xdg_shell->events.new_popup, &server.new_popup);
/* headless output; new_output fires during backend start */
host.width = 1280;