From d29f2a29c9852874aa534bea3ca241d23a1880d3 Mon Sep 17 00:00:00 2001 From: lkn Date: Fri, 15 May 2026 09:49:06 +0200 Subject: [PATCH] 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. --- WINGs/wfont_wl.c | 6 +- WINGs/wrender_wl.c | 6 +- WINGs/wview_wl.c | 18 +++ src/backend/wayland/wl_backend.c | 247 ++++++++++++++++++++++++++++--- src/icon.c | 4 +- 5 files changed, 256 insertions(+), 25 deletions(-) diff --git a/WINGs/wfont_wl.c b/WINGs/wfont_wl.c index 9dd549c..a9d517a 100644 --- a/WINGs/wfont_wl.c +++ b/WINGs/wfont_wl.c @@ -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; } diff --git a/WINGs/wrender_wl.c b/WINGs/wrender_wl.c index cf2b14f..27ff27a 100644 --- a/WINGs/wrender_wl.c +++ b/WINGs/wrender_wl.c @@ -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; } diff --git a/WINGs/wview_wl.c b/WINGs/wview_wl.c index 9d51019..e75965b 100644 --- a/WINGs/wview_wl.c +++ b/WINGs/wview_wl.c @@ -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; diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index fc3f031..815eb06 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -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, diff --git a/src/icon.c b/src/icon.c index a950ef2..2adbb83 100644 --- a/src/icon.c +++ b/src/icon.c @@ -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); } /******************************************************************/