wayland: fix close/iconify buttons and miniwindow rendering

Three bugs fixed:

1. Close and iconify buttons broken after frame rendering model change
   Buttons are composited into the titlebar buffer, so their scene_buf
   nodes carry no committed buffer and are invisible to wlr_scene_node_at.
   frame_buf_at() returned the titlebar fb for all button-area clicks,
   routing them to titlebarMouseDown instead of buttonMouseDown.

   Fix: add frame_buf_button_at() -- coordinate walk of frame_list entries
   with scene_buf->buffer == NULL -- and frame_buf_hit() which tries button
   resolution first then falls back to the scene graph. Replace all
   frame_buf_at() calls in button and motion handlers with frame_buf_hit().
   Also emit synthetic WME_LEAVE/WME_ENTER events on sub-window boundary
   crossings so buttonMouseDown's press-track loop works correctly.

2. Iconify crash (SIGSEGV in update_icon_title)
   WMDrawString() was passed icon->core->window (a frame_buf synthetic ID,
   0x10000000+) as the drawable. wl_font_resolve_drawable() only recognised
   WINGs view IDs (0x20000000+), so it cast the frame_buf ID directly as a
   pixman_image_t* -- wild pointer -- SIGSEGV.

   Fix: add W_RegisterBacking()/W_UnregisterBacking() to wview_wl.c.
   Register each frame_buf's pixman image in the backing table on creation
   and unregister on destroy. Extend the ID range check in wfont_wl.c and
   wrender_wl.c from 0x20000000 down to 0x10000000 so frame_buf IDs resolve
   correctly via W_GetViewBacking().

3. Resizebar visible after iconify + miniwindow not rendering
   wl_window_unmap() disabled fb->scene_buf->node individually, but a
   subsequent wl_frame_paint() could re-enable and re-commit it while the
   window was still iconified.

   Fix: for top-level frames, disable fb->tree->node -- hides the entire
   scene tree (frame buffer, titlebar, resizebar, client surface) atomically.
   Symmetric enable in wl_window_map().

   Miniwindow icons were invisible: icon_paint was a noop so the frame_buf
   image was never committed to the scene graph. Add wl_icon_paint() mirroring
   wl_appicon_paint(): tile + file_image composite + title text, committed
   via wlr_scene_buffer_set_buffer(). Wire into wIconPaint() in icon.c so
   the backend finalises the surface after policy-layer setup.
This commit is contained in:
lkn 2026-05-15 09:49:06 +02:00
parent a961c86d4b
commit d29f2a29c9
5 changed files with 256 additions and 25 deletions

View File

@ -23,7 +23,11 @@
extern void *W_GetViewBacking(unsigned long id);
static inline pixman_image_t *wl_font_resolve_drawable(WMDrawable d)
{
if ((unsigned long)d >= 0x20000000 && (unsigned long)d < 0x30000000)
/* Synthetic IDs: 0x10000000-0x1FFFFFFF = WM frame_buf windows,
* 0x20000000-0x2FFFFFFF = WINGs view windows.
* Both are registered via W_RegisterBacking so W_GetViewBacking
* handles both ranges uniformly. */
if ((unsigned long)d >= 0x10000000 && (unsigned long)d < 0x30000000)
return (pixman_image_t *)W_GetViewBacking((unsigned long)d);
return (pixman_image_t *)(uintptr_t)d;
}

View File

@ -23,12 +23,14 @@ render_wl_screen_fini(void *rctx)
}
/* Resolve a WMDrawable to a pixman_image_t*.
* If it's a WINGs view ID (0x20000000..0x2FFFFFFF), look up backing image. */
* Synthetic IDs: 0x10000000-0x1FFFFFFF = WM frame_buf windows,
* 0x20000000-0x2FFFFFFF = WINGs view windows.
* Both registered via W_RegisterBacking. */
extern void *W_GetViewBacking(unsigned long id);
static inline pixman_image_t *
resolve_drawable(WMDrawable d)
{
if ((unsigned long)d >= 0x20000000 && (unsigned long)d < 0x30000000)
if ((unsigned long)d >= 0x10000000 && (unsigned long)d < 0x30000000)
return (pixman_image_t *)W_GetViewBacking((unsigned long)d);
return (pixman_image_t *)d;
}

View File

@ -91,6 +91,24 @@ static void view_table_remove(Window id)
}
}
/*
* W_RegisterBacking - register a pixman_image_t backing for any synthetic
* window ID (WINGs views OR WM frame_buf windows). Used by wl_backend.c so
* that WMDrawString() can resolve a WM window ID to a pixman_image_t when
* drawing text into frame_buf images (e.g. miniwindow title).
*/
void W_RegisterBacking(Window id, void *img)
{
view_table_put(id, NULL);
W_SetViewBacking(id, img);
}
/* W_UnregisterBacking - remove a backing registration (on frame_buf destroy). */
void W_UnregisterBacking(Window id)
{
view_table_remove(id);
}
static void *wl_screen_init(W_Screen *scr)
{
(void)scr;

View File

@ -213,6 +213,8 @@ void W_WL_SetLoopDispatch(struct W_Screen *scr,
int loop_fd,
void (*dispatch_fn)(int timeout_ms, void *ud),
void *userdata);
void W_RegisterBacking(unsigned long id, void *img);
void W_UnregisterBacking(unsigned long id);
/* ------------------------------------------------------------------ */
/* Forward declarations */
@ -542,6 +544,11 @@ struct wl_compositor_state {
RImage *btn_close_img;
RImage *btn_iconify_img;
RImage *btn_kill_img;
/* Tracks the frame_buf the pointer was last over (after button resolution),
* so motion handlers can emit synthetic WME_LEAVE/WME_ENTER events when
* the pointer crosses between WM frame sub-windows (titlebar, resizebar,
* buttons). buttonMouseDown's inner press-track loop depends on these. */
WNativeWindow last_hovered_id;
};
static struct wl_compositor_state wl_state;
@ -3909,6 +3916,50 @@ frame_buf_at(int px, int py)
return NULL;
}
/*
* frame_buf_button_at — coordinate-based hit-test for virtual button frame_bufs.
*
* Buttons are composited into the titlebar buffer: their scene_buf nodes are
* in the scene graph but carry no committed buffer (scene_buf->buffer == NULL)
* and are therefore invisible to wlr_scene_node_at. We find them by walking
* the frame_list and comparing absolute screen coordinates.
*
* Returns the button fb whose absolute rect encloses (px,py) with the
* smallest area (most specific), or NULL if no button is under the pointer.
*/
static struct wl_frame_buf *
frame_buf_button_at(int px, int py)
{
struct wl_frame_buf *best = NULL;
struct wl_frame_buf *fb;
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (!fb->parent_id) continue; /* top-level root */
if (!fb->scene_buf) continue; /* no scene node */
if (fb->scene_buf->buffer) continue; /* has active pixels — titlebar/resizebar */
if (px < fb->x || px >= fb->x + fb->width) continue;
if (py < fb->y || py >= fb->y + fb->height) continue;
/* Prefer the smallest (most specific) enclosing fb. */
if (!best || (fb->width * fb->height) < (best->width * best->height))
best = fb;
}
return best;
}
/*
* frame_buf_hit — resolve the WM sub-window at (px,py).
*
* Tries button hit-testing first (buttons are invisible to the scene graph
* because their pixels are composited into the titlebar buffer), then falls
* back to the scene-graph-based frame_buf_at for titlebars, resizebars, and
* top-level frames.
*/
static struct wl_frame_buf *
frame_buf_hit(int px, int py)
{
struct wl_frame_buf *fb = frame_buf_button_at(px, py);
return fb ? fb : frame_buf_at(px, py);
}
/* ------------------------------------------------------------------ */
/* Frame / window lifecycle */
/* ------------------------------------------------------------------ */
@ -3964,6 +4015,10 @@ wl_frame_create_toplevel(WScreen *scr,
}
wl_list_insert(&wl_state.frame_list, &fb->link);
/* Register this frame_buf image in the WINGs backing table so
* WMDrawString() can resolve the window ID to a pixman_image_t
* (needed for miniwindow title drawing in update_icon_title). */
W_RegisterBacking(fb->id, fb->image);
return fb->id;
}
@ -4016,6 +4071,7 @@ wl_frame_create_child(WNativeWindow parent_win, WScreen *scr,
}
wl_list_insert(&wl_state.frame_list, &fb->link);
W_RegisterBacking(fb->id, fb->image);
return fb->id;
}
@ -4025,6 +4081,7 @@ wl_frame_destroy(WScreen *scr, WNativeWindow win)
(void)scr;
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb) return;
W_UnregisterBacking(fb->id);
if (fb->scene_buf)
wlr_scene_node_destroy(&fb->scene_buf->node);
wl_list_remove(&fb->link);
@ -4442,19 +4499,23 @@ handle_pointer_motion(struct wl_listener *listener, void *data)
if (wl_state.cursor)
wlr_cursor_warp(wl_state.cursor, NULL,
(double)wl_state.pointer_x, (double)wl_state.pointer_y);
/* Phase A: enqueue motion event for WINGs */
/* Phase A: enqueue motion event for WINGs.
* Use frame_buf_hit so button sub-windows (which have no active scene_buf
* buffer and are invisible to wlr_scene_node_at) are resolved correctly. */
WMScreen *wmscr = wl_get_wmscreen();
if (wmscr) {
WMEvent wev;
memset(&wev, 0, sizeof(wev));
wev.type = WME_MOTION;
struct wl_frame_buf *fb = frame_buf_at(wl_state.pointer_x,
wl_state.pointer_y);
struct wl_frame_buf *fb = frame_buf_hit(wl_state.pointer_x,
wl_state.pointer_y);
WNativeWindow new_id;
if (fb) {
wev.window = fb->id;
wev.u.motion.window = fb->id;
wev.u.motion.x = wl_state.pointer_x - fb->x;
wev.u.motion.y = wl_state.pointer_y - fb->y;
new_id = fb->id;
} else {
WScreen *scr0 = wScreenWithNumber(0);
WNativeWindow root = scr0 ? scr0->root_win : (WNativeWindow)0x0FFFFFFF;
@ -4462,11 +4523,39 @@ handle_pointer_motion(struct wl_listener *listener, void *data)
wev.u.motion.window = root;
wev.u.motion.x = wl_state.pointer_x;
wev.u.motion.y = wl_state.pointer_y;
new_id = root;
}
wev.u.motion.time = ev->time_msec;
wev.u.motion.x_root = wl_state.pointer_x;
wev.u.motion.y_root = wl_state.pointer_y;
W_WL_EnqueueEvent(wmscr, &wev, NULL);
/* Emit synthetic LEAVE/ENTER when pointer crosses WM sub-window
* boundaries (titlebar ↔ button, etc.). These feed the inner
* press-track loop in framewin.c:buttonMouseDown so the cancel-
* on-leave behaviour works correctly. */
if (new_id != wl_state.last_hovered_id) {
WMEvent xev;
memset(&xev, 0, sizeof(xev));
if (wl_state.last_hovered_id) {
xev.type = WME_LEAVE;
xev.window = wl_state.last_hovered_id;
xev.u.crossing.window = wl_state.last_hovered_id;
xev.u.crossing.x_root = wl_state.pointer_x;
xev.u.crossing.y_root = wl_state.pointer_y;
xev.u.crossing.mode = NotifyNormal;
xev.u.crossing.detail = NotifyAncestor;
W_WL_EnqueueEvent(wmscr, &xev, NULL);
}
xev.type = WME_ENTER;
xev.window = new_id;
xev.u.crossing.window = new_id;
xev.u.crossing.x_root = wl_state.pointer_x;
xev.u.crossing.y_root = wl_state.pointer_y;
xev.u.crossing.mode = NotifyNormal;
xev.u.crossing.detail = NotifyAncestor;
W_WL_EnqueueEvent(wmscr, &xev, NULL);
wl_state.last_hovered_id = new_id;
}
}
/* Phase 22: apply grab before updating normal pointer focus. */
if (wl_state.grab_type) { wl_grab_apply_motion(); return; }
@ -4495,19 +4584,23 @@ handle_pointer_motion_absolute(struct wl_listener *listener, void *data)
if (wl_state.pointer_x >= ow) wl_state.pointer_x = ow - 1;
if (wl_state.pointer_y >= oh) wl_state.pointer_y = oh - 1;
}
/* Phase A: enqueue motion event for WINGs */
/* Phase A: enqueue motion event for WINGs.
* Use frame_buf_hit so button sub-windows (which have no active scene_buf
* buffer and are invisible to wlr_scene_node_at) are resolved correctly. */
WMScreen *wmscr2 = wl_get_wmscreen();
if (wmscr2) {
WMEvent wev;
memset(&wev, 0, sizeof(wev));
wev.type = WME_MOTION;
struct wl_frame_buf *fb = frame_buf_at(wl_state.pointer_x,
wl_state.pointer_y);
struct wl_frame_buf *fb = frame_buf_hit(wl_state.pointer_x,
wl_state.pointer_y);
WNativeWindow new_id;
if (fb) {
wev.window = fb->id;
wev.u.motion.window = fb->id;
wev.u.motion.x = wl_state.pointer_x - fb->x;
wev.u.motion.y = wl_state.pointer_y - fb->y;
new_id = fb->id;
} else {
WScreen *scr0 = wScreenWithNumber(0);
WNativeWindow root = scr0 ? scr0->root_win : (WNativeWindow)0x0FFFFFFF;
@ -4515,11 +4608,36 @@ handle_pointer_motion_absolute(struct wl_listener *listener, void *data)
wev.u.motion.window = root;
wev.u.motion.x = wl_state.pointer_x;
wev.u.motion.y = wl_state.pointer_y;
new_id = root;
}
wev.u.motion.time = ev->time_msec;
wev.u.motion.x_root = wl_state.pointer_x;
wev.u.motion.y_root = wl_state.pointer_y;
W_WL_EnqueueEvent(wmscr2, &wev, NULL);
/* Emit synthetic LEAVE/ENTER on sub-window boundary crossings. */
if (new_id != wl_state.last_hovered_id) {
WMEvent xev;
memset(&xev, 0, sizeof(xev));
if (wl_state.last_hovered_id) {
xev.type = WME_LEAVE;
xev.window = wl_state.last_hovered_id;
xev.u.crossing.window = wl_state.last_hovered_id;
xev.u.crossing.x_root = wl_state.pointer_x;
xev.u.crossing.y_root = wl_state.pointer_y;
xev.u.crossing.mode = NotifyNormal;
xev.u.crossing.detail = NotifyAncestor;
W_WL_EnqueueEvent(wmscr2, &xev, NULL);
}
xev.type = WME_ENTER;
xev.window = new_id;
xev.u.crossing.window = new_id;
xev.u.crossing.x_root = wl_state.pointer_x;
xev.u.crossing.y_root = wl_state.pointer_y;
xev.u.crossing.mode = NotifyNormal;
xev.u.crossing.detail = NotifyAncestor;
W_WL_EnqueueEvent(wmscr2, &xev, NULL);
wl_state.last_hovered_id = new_id;
}
}
/* Phase 22: apply grab before updating normal pointer focus. */
if (wl_state.grab_type) { wl_grab_apply_motion(); return; }
@ -5059,9 +5177,13 @@ handle_pointer_button(struct wl_listener *listener, void *data)
wev.type = (ev->state == WLR_BUTTON_PRESSED)
? WME_BUTTON_PRESS : WME_BUTTON_RELEASE;
/* Hit-test: determine which WM window was clicked */
struct wl_frame_buf *fb = frame_buf_at(wl_state.pointer_x,
wl_state.pointer_y);
/* Hit-test: determine which WM window was clicked.
* Use frame_buf_hit so button sub-windows (invisible to the scene
* graph because their pixels are composited into the titlebar buffer)
* are resolved to their own window IDs, letting handleButtonPress
* route the event to buttonMouseDown instead of titlebarMouseDown. */
struct wl_frame_buf *fb = frame_buf_hit(wl_state.pointer_x,
wl_state.pointer_y);
if (fb) {
wev.window = fb->id;
wev.u.button.window = fb->id;
@ -5975,9 +6097,10 @@ wl_window_map(WNativeWindow win)
}
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
/* Raise the tree (raises entire window assembly together) */
if (fb->tree)
if (fb->tree) {
wlr_scene_node_set_enabled(&fb->tree->node, true);
wlr_scene_node_raise_to_top(&fb->tree->node);
else if (!fb->parent_id)
} else if (!fb->parent_id)
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
/* Also enable children (and grandchildren) */
struct wl_frame_buf *child;
@ -6036,16 +6159,20 @@ wl_window_unmap(WNativeWindow win)
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb && fb->scene_buf) {
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
/* Also hide children */
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
if (child->parent_id == win && child->scene_buf)
wlr_scene_node_set_enabled(&child->scene_buf->node, false);
}
if (!fb) return;
/* For top-level frames, disable the whole tree. This hides the frame
* buffer, all child scene_bufs (titlebar, resizebar, buttons), and the
* client surface reparented into fb->tree — atomically and irrevocably
* until the frame is explicitly mapped again. Disabling individual
* child nodes is insufficient because a subsequent wl_frame_paint can
* re-enable them while the window is still iconified. */
if (fb->tree) {
wlr_scene_node_set_enabled(&fb->tree->node, false);
return;
}
/* Child frame_buf: disable its scene_buf node only. */
if (fb->scene_buf)
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
static void wl_window_move(WNativeWindow win, int x, int y)
{
@ -7235,7 +7362,85 @@ static char *wl_get_command_for_window(WNativeWindow win, int elements)
}
static void wl_noop_icon_update_pixmap(WIcon *icon) { (void)icon; }
static void wl_noop_icon_paint(WIcon *icon) { (void)icon; }
/*
* wl_icon_paint - render a miniwindow (iconified window) icon into its
* frame_buf and commit it to the scene graph.
*
* Mirrors wl_appicon_paint: tile background + optional image + title text.
* Called at the end of wIconPaint() in icon.c so the backend finalises the
* surface after the policy layer has set up icon->file_image etc.
*/
static void
wl_icon_paint(WIcon *icon)
{
if (!icon || !icon->core) return;
struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)icon->core->window);
if (!fb || !fb->image) return;
WScreen *scr = icon->core->screen_ptr;
if (!scr) return;
int size = wPreferences.icon_size;
/* --- Tile background --- */
RImage *tile = scr->icon_tile ? RCloneImage(scr->icon_tile) : NULL;
if (!tile) {
tile = RCreateImage(size, size, 1);
if (!tile) return;
RColor grey = {0xAE, 0xAA, 0xAE, 0xFF};
RClearImage(tile, &grey);
}
/* --- Composite icon image (file_image from database / NET_WM_ICON) --- */
RImage *img = icon->file_image;
if (img) {
int theight = icon->show_title ? WMFontHeight(scr->icon_title_font) : 0;
unsigned w = ((unsigned)img->width > (unsigned)size) ? (unsigned)size : (unsigned)img->width;
unsigned h = ((unsigned)img->height + (unsigned)theight > (unsigned)size)
? (unsigned)(size - theight) : (unsigned)img->height;
int x = (size - (int)w) / 2;
int y = theight + (size - theight - (int)h) / 2;
int sx = ((int)img->width - (int)w) / 2;
int sy = ((int)img->height - (int)h) / 2;
RCombineArea(tile, img, sx, sy, w, h, x, y);
}
/* --- Blit RImage (RGBA/RGB) -> pixman ARGB32 --- */
uint32_t *dst = (uint32_t *)pixman_image_get_data(fb->image);
int stride = pixman_image_get_stride(fb->image) / 4;
unsigned char *tsrc = tile->data;
int has_alpha = (tile->format == RRGBAFormat);
for (int row = 0; row < size && row < fb->height; row++) {
for (int col = 0; col < size && col < fb->width; col++) {
int si = (row * tile->width + col) * (has_alpha ? 4 : 3);
unsigned char r = tsrc[si], g = tsrc[si+1], b = tsrc[si+2];
unsigned char a = has_alpha ? tsrc[si+3] : 0xFF;
dst[row * stride + col] = ((uint32_t)a << 24) | ((uint32_t)r << 16)
| ((uint32_t)g << 8) | (uint32_t)b;
}
}
RReleaseImage(tile);
/* --- Title text --- */
if (icon->show_title && icon->icon_name && scr->icon_title_font) {
WMDrawable d = (WMDrawable)(uintptr_t)fb->image;
int tw = WMWidthOfString(scr->icon_title_font, icon->icon_name,
strlen(icon->icon_name));
int tx = (size - tw) / 2;
if (tx < 2) tx = 2;
WMColor *color = scr->icon_title_color;
if (color)
WMDrawString(scr->wmscreen, d, color, scr->icon_title_font,
tx, 0, icon->icon_name, strlen(icon->icon_name));
}
/* --- Commit to scene graph --- */
wlr_scene_buffer_set_buffer(fb->scene_buf, &fb->base);
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
if (fb->tree)
wlr_scene_node_set_enabled(&fb->tree->node, true);
}
static void wl_noop_appicon_show(WApplication *wapp) { (void)wapp; }
static void wl_appicon_paint(WAppIcon *aicon)
{
@ -7954,7 +8159,7 @@ const WMBackend wl_backend = {
.get_shortcut_string = wl_noop_get_shortcut_string,
.get_command_for_window = wl_get_command_for_window,
.icon_update_pixmap = wl_noop_icon_update_pixmap,
.icon_paint = wl_noop_icon_paint,
.icon_paint = wl_icon_paint,
.appicon_show = wl_noop_appicon_show,
.appicon_paint = wl_appicon_paint,
.restore_desktop = wl_noop_restore_desktop,

View File

@ -817,7 +817,6 @@ void wIconPaint(WIcon *icon)
if (!icon || !icon->core || !icon->core->screen_ptr)
return;
/* Wayland backend: icon windows have ID=None; skip X11 drawing. */
if (!icon->core->window)
return;
@ -830,6 +829,9 @@ void wIconPaint(WIcon *icon)
if (icon->selected)
wm_backend->icon_select_paint(icon, 0);
/* Allow the backend to composite the full icon (tile + image + title)
* and commit it to its rendering surface. */
wm_backend->icon_paint(icon);
}
/******************************************************************/