From 245d0077b5efd5c8475f0e8020992862a0b8bd41 Mon Sep 17 00:00:00 2001 From: Window Maker Date: Fri, 15 May 2026 09:11:29 +0200 Subject: [PATCH] wayland: implement correct stacking model with scene-graph level trees Three interlocking problems were present in the Wayland stacking implementation. 1. Wrong scene-graph nodes targeted by stacking operations Each managed window groups its frame pixels and client surface under a single wlr_scene_tree (fb->tree): fb->tree fb->scene_buf (frame decoration pixels) child_frame_bufs (titlebar / resizebar sub-frames) v->scene_node (client surface, reparented in) wl_stacking_restack was raising fb->scene_buf -- a child inside fb->tree -- instead of fb->tree itself, a no-op for inter-window ordering. Three redundant loops then tried to separately raise the client surface and child frames, also to no effect. wl_stacking_lower had the same bug. Fix: raise/lower fb->tree in both functions so the entire window assembly moves as one atomic unit. 2. Flat scene graph -- no structural level ordering All nodes were direct children of scene->tree, so creation order and raise_to_top calls determined Z-order. A normal window could accidentally render above a floating window after any CommitStacking call. Fix: introduce 9 wlr_scene_tree level buckets created at scene init time in bottom-to-top order: 0 WL_LAYER_BACKGROUND background rect / wallpaper 1 WL_LAYER_DESKTOP WMDesktopLevel 2 WL_LAYER_SUNKEN WMSunkenLevel 3 WL_LAYER_NORMAL WMNormalLevel (default for new frames) 4 WL_LAYER_FLOATING WMFloatingLevel / WINGs dialogs 5 WL_LAYER_MENU WMMainMenuWindowLevel / OR-redirect windows 6 WL_LAYER_DOCK WMStatusWindowLevel / dock panel surface 7 WL_LAYER_FULLSCREEN WMFullscreenLevel 8 WL_LAYER_OVERLAY WMModalPanel / WMPopUp / XOR overlay Every scene node is assigned to the correct bucket at creation. A new stacking_set_level vtable slot called from AddToStackList moves fb->tree to the right bucket whenever a window is added or its level changes via ChangeStackingLevel. X11 backend implements it as a no-op. 3. XDG client surface not reparented into fb->tree For XDG toplevels the map handler called wManageWindow before creating view->scene_node, so the reparent inside wl_client_reparent was a no-op. The client surface then lived as a flat sibling of fb->tree in WL_LAYER_NORMAL, never moving with its frame during stacking operations. Fix: after creating view->scene_node, check if frame_id is already set and do the deferred reparent immediately. 4. Hit-testing broken by level-tree hierarchy frame_buf_at walked up from the hit scene node to the first direct child of scene->tree. With level buckets that stopped at the level_tree node instead of fb->tree, making every window in the same bucket appear to be the same window. Client surfaces were not blocking clicks on frames beneath them, and titlebar sub-frames were not distinguished from the top-level frame, breaking move and resize initiation. Fix: rewrite frame_buf_at with a two-pass approach: Pass 1 -- exact match: check if the topmost node is a child frame_buf scene_buf such as titlebar, resizebar, or button; return it directly. Pass 2 -- walk up stopping when parent is a level_tree bucket, then match that window-root node to a top-level frame_buf tree. Return NULL if the hit was on a client surface. ROADMAP: mark A1 done. --- .claude/settings.local.json | 61 +++++ WAYLAND_ROADMAP.md | 2 +- src/backend/backend.h | 10 + src/backend/wayland/wl_backend.c | 371 ++++++++++++++++++++++--------- src/backend/x11/x11_backend.c | 8 + src/stacking.c | 1 + 6 files changed, 350 insertions(+), 103 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..246f0c1 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,61 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -n 'wFrameWindowCreate' /home/lkn/src/WindowMaker-0.96.0/src/*.c)", + "Bash(typeb /home/lkn/src/WindowMaker-0.96.0/src/window.c /home/lkn/src/WindowMaker-0.96.0/src/winspector.c)", + "Bash(grep -n \"wm_hints->input\\\\|wm_hints->icon_x\\\\|wm_hints->icon_y\" /home/lkn/src/WindowMaker-0.96.0/src/*.c)", + "Bash(grep -n \"^\\\\s*return\\\\s*0;\\\\s*$\\\\|^\\\\s*return\\\\s*NULL;\\\\s*$\\\\|^\\\\s*return\\\\s*1;\\\\s*$\\\\|^\\\\s*return;\\\\s*$\" /home/lkn/src/WindowMaker-0.96.0/src/backend/wayland/wl_backend.c)", + "Bash(grep *)", + "Bash(make -C src/backend/wayland wl_backend.lo)", + "Bash(make -C /home/lkn/src/WindowMaker-0.96.0/src)", + "Bash(pkg-config --exists \"x11\")", + "Bash(pkg-config --modversion \"x11\")", + "Bash(pkg-config --modversion wlroots-0.19 wlroots-0.20)", + "Bash(pkg-config --variable=pcfiledir wlroots-0.19)", + "Read(//usr/local/**)", + "Read(//usr/**)", + "Bash(pkg-config --exists libgif)", + "Bash(pkg-config --modversion libgif)", + "Bash(make -C /home/lkn/src/WindowMaker-0.96.0/wrlib)", + "Bash(pkg-config --libs wlroots)", + "Bash(pkg-config --libs wlroots-0.19)", + "Bash(pkg-config --modversion wlroots-0.19)", + "Bash(sed -n '560,570p' /usr/include/wlroots-0.19/wlr/types/wlr_compositor.h)", + "Bash(sed -n '344,350p' /usr/include/wlroots-0.19/wlr/types/wlr_xdg_shell.h)", + "Bash(sed -n '122,128p' /usr/include/wlroots-0.19/wlr/types/wlr_layer_shell_v1.h)", + "Bash(sed -n '59,65p' /usr/include/wlroots-0.19/wlr/types/wlr_presentation_time.h)", + "Bash(sed -n '76,92p' /usr/include/wlroots-0.19/wlr/types/wlr_linux_dmabuf_v1.h)", + "Bash(tar -tzf /var/cache/distfiles/wlroots-0.19.2.tar.gz)", + "Bash(tar -xOzf /var/cache/distfiles/wlroots-0.19.2.tar.gz \"wlroots-0.19.2/render/allocator/allocator.c\")", + "Bash(tar -xOzf /var/cache/distfiles/wlroots-0.19.2.tar.gz \"wlroots-0.19.2/types/output/output.c\")", + "Bash(nm -D /usr/lib64/wlroots-0.19/libwlroots-0.19.so)", + "Bash(tar xJf /var/cache/distfiles/libX11-1.8.13.tar.xz -C /tmp libX11-1.8.13/src/XlibInt.c)", + "Read(//tmp/libX11-1.8.13/src/**)", + "Bash(tar xJf /var/cache/distfiles/libX11-1.8.13.tar.xz -C /tmp libX11-1.8.13/src/ErrHndlr.c)", + "Bash(nm -D /usr/lib64/libX11.so.6)", + "Bash(objdump -d /usr/lib64/libX11.so.6)", + "Bash(readelf -d /usr/lib64/libX11.so.6)", + "Bash(readelf -s /usr/lib64/libX11.so.6)", + "Bash(xargs -I{} basename {})", + "Bash(pkg-config --exists pangoft2)", + "Bash(pkg-config --cflags --libs pangoft2)", + "Bash(pkg-config --exists pixman-1)", + "Bash(awk 'NR>=1700 && NR<=1900 && /^[a-zA-Z]/ {print NR\": \"$0}' /home/lkn/src/WindowMaker-0.96.0/src/menu.c)", + "Bash(awk -F: '$2>1400 && $2<1420 || $2>1730 && $2<1745 || $2>2075 && $2<2090 || $2>2328 && $2<2340 || $2>2408 && $2<2420 {print}')", + "Bash(awk *)", + "Bash(sed -n '2325,2345p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '2408,2420p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '930,934p' /home/lkn/src/WindowMaker-0.96.0/src/menu.c)", + "Bash(sed -n '1631,1635p' /home/lkn/src/WindowMaker-0.96.0/src/menu.c)", + "Bash(sed -n '1875,1879p' /home/lkn/src/WindowMaker-0.96.0/src/menu.c)", + "Bash(sed -n '2233,2237p' /home/lkn/src/WindowMaker-0.96.0/src/menu.c)", + "Bash(sed -n '1408,1413p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '1735,1742p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '2081,2086p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '2332,2336p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(sed -n '2413,2417p' /home/lkn/src/WindowMaker-0.96.0/src/moveres.c)", + "Bash(make -C WINGs)", + "Bash(make -C src)" + ] + } +} diff --git a/WAYLAND_ROADMAP.md b/WAYLAND_ROADMAP.md index 3b0d6e6..a61a0f7 100644 --- a/WAYLAND_ROADMAP.md +++ b/WAYLAND_ROADMAP.md @@ -80,7 +80,7 @@ Priority: HIGH — needed for daily-driver use. | # | Task | Key Files | Complexity | |---|------|-----------|------------| -| A1 | Stacking (raise/lower via scene graph) | stacking.c, wlr_scene_node_raise/lower | Low | +| A1 | ~~Stacking (raise/lower via scene graph)~~ | stacking.c, wlr_scene_node_raise/lower | ✅ Done | | A2 | ~~Window maximize~~ | actions.c, usable area calc | ✅ Done | | A3 | ~~Window shade (titlebar collapse)~~ | actions.c, frame buffer resize | ✅ Done | | A4 | Workspace switching (create/destroy/switch) | workspace.c, scene tree reparenting | Medium | diff --git a/src/backend/backend.h b/src/backend/backend.h index 96f7063..2acdb01 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -369,6 +369,16 @@ typedef struct WMBackend { int (*stacking_query)(WNativeWindow root, WNativeWindow **out, unsigned int *count); + /* + * Notify the backend that a frame's WM stacking level has changed. + * The backend may use this to place the frame in the correct scene-graph + * layer so that level ordering is structurally enforced without requiring + * a full CommitStacking reorder. + * level is one of the WM*Level constants from WindowMaker.h. + * No-op on backends that do not have per-level scene trees (e.g. X11). + */ + void (*stacking_set_level)(WNativeWindow win, int level); + /* ------------------------------------------------------------------ */ /* Event queue operations (Phase 3) */ /* ------------------------------------------------------------------ */ diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 80920b6..85971f0 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -491,6 +491,39 @@ struct wl_compositor_state { int icon_w, icon_h; } dock_apps[WM_DOCK_MAX_APPS]; int dock_app_count; + /* Stacking level trees — one wlr_scene_tree per layer, ordered + * bottom-to-top (index 0 rendered first = furthest back). + * + * Every scene node that participates in the compositor output is + * assigned to exactly one bucket so that creation order within the + * root tree never matters and there are no Z-order surprises. + * + * Named constants (WL_LAYER_*) map to bucket indices: + * + * 0 WL_LAYER_BACKGROUND background rect / wallpaper + * 1 WL_LAYER_DESKTOP WMDesktopLevel (level < -1) + * 2 WL_LAYER_SUNKEN WMSunkenLevel (level == -1) + * 3 WL_LAYER_NORMAL WMNormalLevel (0 <= level < 3) + * 4 WL_LAYER_FLOATING WMFloatingLevel (3 <= level < 20) + * 5 WL_LAYER_MENU WMMainMenuWindowLevel (20), OR-redirect + * 6 WL_LAYER_DOCK WMStatusWindowLevel / WMDockWindowLevel (21-49) + * also the dock panel wm_shm_surface + * 7 WL_LAYER_FULLSCREEN WMFullscreenLevel (50-99) + * 8 WL_LAYER_OVERLAY WMModalPanel / WMPopUp (>=100) + * also the XOR animation overlay surface + */ +#define WL_LAYER_BACKGROUND 0 +#define WL_LAYER_DESKTOP 1 +#define WL_LAYER_SUNKEN 2 +#define WL_LAYER_NORMAL 3 +#define WL_LAYER_FLOATING 4 +#define WL_LAYER_MENU 5 +#define WL_LAYER_DOCK 6 +#define WL_LAYER_FULLSCREEN 7 +#define WL_LAYER_OVERLAY 8 +#define WL_NUM_LEVEL_BUCKETS 9 + struct wlr_scene_tree *level_trees[WL_NUM_LEVEL_BUCKETS]; + /* Phase 81: full-screen overlay surface for XOR animation drawing */ struct wm_shm_surface *overlay_surf; /* NULL until first draw */ /* Focus sink: small off-screen InputOnly X window that receives focus @@ -513,6 +546,11 @@ struct wl_compositor_state { static struct wl_compositor_state wl_state; +/* Forward declarations for helpers used before their definition. */ +static int level_to_bucket(int level); +static struct wlr_scene_tree *level_tree_for(int level); +static struct wl_frame_buf *frame_buf_find(WNativeWindow id); + /* ------------------------------------------------------------------ */ /* Phase A: WINGs event backend wiring helpers */ /* ------------------------------------------------------------------ */ @@ -993,13 +1031,35 @@ handle_toplevel_map(struct wl_listener *listener, void *data) * (auto_focus=1 set in wl_screen_create_internals); the * direct wl_focus_set() call is no longer needed here. */ - /* Phase 18: add XDG surface to the scene graph. */ + /* Phase 18: add XDG surface to the scene graph. + * + * ORDERING: wManageWindow (above) calls wl_client_reparent + * which sets view->frame_id but cannot reparent the scene + * node because it does not exist yet. We create it here + * and immediately do the deferred reparent if frame_id is + * already set, so the client surface ends up inside + * fb->tree and moves with it during stacking operations. + * Without this the client surface would sit as a flat + * sibling of fb->tree in WL_LAYER_NORMAL and CommitStacking + * raises would leave it behind the frame decorations of + * subsequently-opened windows. */ if (wl_state.scene && !view->scene_node) { + struct wlr_scene_tree *_vp = wl_state.level_trees[WL_LAYER_NORMAL] + ? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree; view->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, view->xdg_surface->surface); - if (view->scene_node) + _vp, view->xdg_surface->surface); + if (view->scene_node) { + /* Deferred reparent: move client surface under its + * frame grouping tree now that the node exists. */ + if (view->frame_id) { + struct wl_frame_buf *_pfb = frame_buf_find(view->frame_id); + if (_pfb && _pfb->tree) + wlr_scene_node_reparent(&view->scene_node->node, + _pfb->tree); + } wlr_scene_node_set_position(&view->scene_node->node, view->x, view->y); + } } /* Phase 13: warp virtual pointer to the centre of the @@ -1176,8 +1236,9 @@ handle_or_map(struct wl_listener *listener, void *data) return; if (wl_state.scene && !ov->scene_node && xw->surface) { - ov->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, xw->surface); + struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU] + ? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree; + ov->scene_node = wlr_scene_subsurface_tree_create(_op, xw->surface); if (ov->scene_node) wlr_scene_node_set_position(&ov->scene_node->node, xw->x, xw->y); } @@ -1259,8 +1320,9 @@ handle_xwayland_map(struct wl_listener *listener, void *data) * rather than xw->x,y which may not yet reflect the configure we * sent; the set_geometry listener keeps them in sync thereafter. */ if (wl_state.scene && !view->scene_node && xw->surface) { - view->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, xw->surface); + struct wlr_scene_tree *_vp = wl_state.level_trees[WL_LAYER_NORMAL] + ? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree; + view->scene_node = wlr_scene_subsurface_tree_create(_vp, xw->surface); if (view->scene_node) wlr_scene_node_set_position(&view->scene_node->node, view->x, view->y); } else if (!xw->surface) { @@ -1460,8 +1522,9 @@ handle_xwayland_new_surface(struct wl_listener *listener, void *data) /* If already associated (surface exists), add to scene now. */ if (xw->surface) { if (wl_state.scene) { - ov->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, xw->surface); + struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU] + ? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree; + ov->scene_node = wlr_scene_subsurface_tree_create(_op, xw->surface); if (ov->scene_node) wlr_scene_node_set_position(&ov->scene_node->node, xw->x, xw->y); @@ -1964,7 +2027,7 @@ handle_backend_new_output(struct wl_listener *listener, void *data) * surfaces. Default WindowMaker desktop colour: rgb:50/50/75. */ { float bg[4] = { 0x50/255.0f, 0x50/255.0f, 0x75/255.0f, 1.0f }; - wlr_scene_rect_create(&wl_state.scene->tree, + wlr_scene_rect_create(wl_state.level_trees[WL_LAYER_BACKGROUND], (int)output->width, (int)output->height, bg); wwarning("wl_backend: root background created (%dx%d)", (int)output->width, (int)output->height); @@ -2221,9 +2284,17 @@ wl_display_open(const char *name) extern void wl_view_commit_backing_impl(unsigned long id); W_ViewCommitBackingFn = wl_view_commit_backing_impl; } - if (wl_state.scene) + if (wl_state.scene) { wwarning("wl_backend: scene graph created"); - else + /* Create the 9 level-bucket trees in bottom-to-top order + * (WL_LAYER_BACKGROUND first, WL_LAYER_OVERLAY last). + * Every subsequent scene node is parented to one of these + * trees, never directly to scene->tree, so Z-ordering is + * determined entirely by bucket index + intra-bucket order. */ + int _li; + for (_li = 0; _li < WL_NUM_LEVEL_BUCKETS; _li++) + wl_state.level_trees[_li] = wlr_scene_tree_create(&wl_state.scene->tree); + } else wwarning("wl_backend: wlr_scene_create failed (no compositing)"); /* Phase 20: wire new_input BEFORE starting the backend so that real @@ -3067,8 +3138,9 @@ wl_event_loop_run(void) struct wl_or_view *ov; wl_list_for_each(ov, &wl_state.or_list, link) { if (!ov->scene_node && ov->xw_surface->surface && wl_state.scene) { - ov->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, ov->xw_surface->surface); + struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU] + ? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree; + ov->scene_node = wlr_scene_subsurface_tree_create(_op, ov->xw_surface->surface); if (ov->scene_node) wlr_scene_node_set_position(&ov->scene_node->node, ov->xw_surface->x, ov->xw_surface->y); @@ -3456,7 +3528,14 @@ wm_dock_create(WScreen *scr) } wm_dock_render(s, icon_sz, n_slots); - wm_shm_surface_commit(s, &wl_state.scene->tree, sw - dw, 0); + /* Dock panel belongs in WL_LAYER_DOCK so it renders at the same + * depth as the dock icon frame_bufs (which wl_stacking_set_level + * places there for WMStatusWindowLevel=21). */ + { + struct wlr_scene_tree *_dp = wl_state.level_trees[WL_LAYER_DOCK] + ? wl_state.level_trees[WL_LAYER_DOCK] : &wl_state.scene->tree; + wm_shm_surface_commit(s, _dp, sw - dw, 0); + } wl_state.dock_surface = s; wl_state.dock_width = dw; @@ -3761,54 +3840,73 @@ frame_buf_find(WNativeWindow id) return NULL; } -/* Hit-test: find the topmost frame_buf containing (px, py). */ +/* Hit-test: find the frame_buf under (px, py) that should receive the event. + * + * Returns the most-specific (innermost) frame_buf whose rendered pixels + * are topmost at (px, py), so the WM event layer receives the correct + * sub-window ID (titlebar, resizebar, button) needed to trigger move/resize. + * + * Scene-graph structure (level-tree model): + * + * scene->tree + * level_trees[N] <- level bucket + * fb->tree <- top-level frame grouping tree + * fb->scene_buf <- full-frame decoration pixels + * child_fb->scene_buf <- titlebar / resizebar sub-frames + * v->scene_node <- client surface (returns NULL) + * + * Strategy: + * 1. wlr_scene_node_at gives the topmost rendered node. + * 2. First check if that exact node is a child frame_buf's scene_buf + * (titlebar, resizebar, button) — if so, return it directly. + * 3. Walk up to the fb->tree level (parent is a level_tree bucket). + * 4. Match that window-root node to a top-level frame_buf's tree. + * 5. If it was a client surface (v->scene_node), return NULL. */ static struct wl_frame_buf * frame_buf_at(int px, int py) { - /* Find the topmost frame_buf at this position, but only if no client - * surface from a DIFFERENT window is above it in the scene graph. */ - struct wl_frame_buf *hit = NULL; - struct wl_frame_buf *fb; - wl_list_for_each(fb, &wl_state.frame_list, link) { - if (!fb->scene_buf || !fb->scene_buf->node.enabled) - continue; - if (px >= fb->x && px < fb->x + fb->width && - py >= fb->y && py < fb->y + fb->height) { - /* Prefer the smallest (most specific) frame_buf */ - if (!hit || (fb->width * fb->height < hit->width * hit->height)) - hit = fb; - } - } - if (!hit) return NULL; + if (!wl_state.scene) return NULL; - /* Check if a client surface from a DIFFERENT window is above us */ - if (wl_state.scene) { - double sx, sy; - struct wlr_scene_node *node = wlr_scene_node_at( - &wl_state.scene->tree.node, px, py, &sx, &sy); - if (node) { - /* Find which tree this node belongs to */ - struct wlr_scene_node *n = node; - while (n->parent && &n->parent->node != &wl_state.scene->tree.node) - n = &n->parent->node; - /* Find which tree our hit frame_buf belongs to */ - struct wlr_scene_node *h = &hit->scene_buf->node; - while (h->parent && &h->parent->node != &wl_state.scene->tree.node) - h = &h->parent->node; - /* Same tree = same window, allow the hit */ - if (n == h) - return hit; - /* Different tree: check if topmost is a frame_buf */ - struct wl_frame_buf *fb2; - wl_list_for_each(fb2, &wl_state.frame_list, link) { - if (fb2->scene_buf && &fb2->scene_buf->node == node) - return fb2; - } - /* Topmost node is a client surface from another window */ - return NULL; - } + double sx, sy; + struct wlr_scene_node *node = wlr_scene_node_at( + &wl_state.scene->tree.node, (double)px, (double)py, &sx, &sy); + if (!node) return NULL; + + struct wl_frame_buf *fb; + + /* Step 2: exact match — is the topmost node a child frame_buf's + * scene_buf (no tree wrapper)? This covers titlebars, resizebars, + * and button sub-frames, which are what the WM needs for move/resize. */ + wl_list_for_each(fb, &wl_state.frame_list, link) { + if (!fb->tree && fb->scene_buf && &fb->scene_buf->node == node) + return fb; } - return hit; + + /* Step 3: walk up until the parent is a level_tree bucket. + * The node we stop at is the fb->tree of a top-level frame, or + * a v->scene_node sitting directly in a level tree (unframed). */ + struct wlr_scene_node *n = node; + while (n->parent + && &n->parent->node != &wl_state.scene->tree.node) { + int _at_level = 0, _li; + for (_li = 0; _li < WL_NUM_LEVEL_BUCKETS; _li++) { + if (wl_state.level_trees[_li] && + n->parent == wl_state.level_trees[_li]) { + _at_level = 1; break; + } + } + if (_at_level) break; + n = &n->parent->node; + } + + /* Step 4: match window-root node to a top-level frame_buf. */ + wl_list_for_each(fb, &wl_state.frame_list, link) { + if (fb->tree && &fb->tree->node == n) + return fb; + } + + /* Step 5: topmost node was a client surface — not a WM frame. */ + return NULL; } /* ------------------------------------------------------------------ */ @@ -3847,9 +3945,13 @@ wl_frame_create_toplevel(WScreen *scr, fb->width = width; fb->height = height; - /* Create scene buffer node under a per-window tree. */ + /* Create scene buffer node under a per-window tree. + * Default to WL_LAYER_NORMAL; wl_stacking_set_level() will reparent + * to the correct bucket when AddToStackList() fires immediately after. */ if (wl_state.scene) { - fb->tree = wlr_scene_tree_create(&wl_state.scene->tree); + struct wlr_scene_tree *_parent = wl_state.level_trees[WL_LAYER_NORMAL] + ? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree; + fb->tree = wlr_scene_tree_create(_parent); if (fb->tree) { wlr_scene_node_set_position(&fb->tree->node, 0, 0); fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); @@ -4835,10 +4937,24 @@ handle_layer_shell_new_surface(struct wl_listener *listener, void *data) } lv->layer_surface = ls; - /* Add to scene graph if available. */ + /* Add to scene graph in the correct level bucket. + * Layer-shell layer enum maps to WL_LAYER_* buckets: + * BACKGROUND -> WL_LAYER_BACKGROUND + * BOTTOM -> WL_LAYER_DESKTOP + * TOP -> WL_LAYER_DOCK + * OVERLAY -> WL_LAYER_OVERLAY */ if (wl_state.scene) { - lv->scene_node = wlr_scene_subsurface_tree_create( - &wl_state.scene->tree, ls->surface); + int _lb; + switch (ls->pending.layer) { + case ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND: _lb = WL_LAYER_BACKGROUND; break; + case ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM: _lb = WL_LAYER_DESKTOP; break; + case ZWLR_LAYER_SHELL_V1_LAYER_TOP: _lb = WL_LAYER_DOCK; break; + case ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY: _lb = WL_LAYER_OVERLAY; break; + default: _lb = WL_LAYER_NORMAL; break; + } + struct wlr_scene_tree *_lp = wl_state.level_trees[_lb] + ? wl_state.level_trees[_lb] : &wl_state.scene->tree; + lv->scene_node = wlr_scene_subsurface_tree_create(_lp, ls->surface); } /* Compute and send initial configure. */ @@ -5419,12 +5535,16 @@ overlay_flush(void) struct wm_shm_surface *s = wl_state.overlay_surf; if (!s || !wl_state.scene) return; /* First call creates the scene_buffer node; subsequent calls reposition. */ - wm_shm_surface_commit(s, &wl_state.scene->tree, 0, 0); + /* Overlay surface lives in WL_LAYER_OVERLAY so it is structurally + * above every window and dock node without needing raise_to_top. */ + { + struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + wm_shm_surface_commit(s, _op, 0, 0); + } if (s->scene_buf) { /* Notify wlroots that the pixel data has changed. */ wlr_scene_buffer_set_buffer(s->scene_buf, &s->wlr_buf); - /* Keep the overlay above all client windows. */ - wlr_scene_node_raise_to_top(&s->scene_buf->node); } /* Commit the scene immediately so the animation step is visible * before the caller's wusleep() delay. */ @@ -5443,6 +5563,50 @@ static void wl_server_ungrab(void) { overlay_flush(); } /* Stacking */ /* ------------------------------------------------------------------ */ +/* Map a WM window level integer to a level_trees[] bucket index. + * Buckets are ordered 0=lowest (WL_LAYER_BACKGROUND) to 8=highest + * (WL_LAYER_OVERLAY), matching the bottom-to-top creation order of + * level_trees[]. Does not map WL_LAYER_BACKGROUND (no WM level). */ +static int +level_to_bucket(int level) +{ + if (level < -1) return WL_LAYER_DESKTOP; + if (level < 0) return WL_LAYER_SUNKEN; + if (level < 3) return WL_LAYER_NORMAL; + if (level < 20) return WL_LAYER_FLOATING; + if (level < 21) return WL_LAYER_MENU; + if (level < 50) return WL_LAYER_DOCK; + if (level < 100) return WL_LAYER_FULLSCREEN; + return WL_LAYER_OVERLAY; +} + +/* Return the level_trees[] entry for a given WM level integer. + * Falls back to the root scene tree only if the scene is not yet + * initialised, which cannot happen during normal window management. */ +static struct wlr_scene_tree * +level_tree_for(int level) +{ + int b = level_to_bucket(level); + if (wl_state.level_trees[b]) + return wl_state.level_trees[b]; + /* Scene not yet initialised — this window will be invisible. */ + return &wl_state.scene->tree; +} + +static void +wl_stacking_set_level(WNativeWindow win, int level) +{ + /* Move the frame's grouping tree into the correct level bucket so + * that the scene graph structurally enforces level ordering. All + * other nodes (background rect, dock panel, OR views, overlays) + * are already placed in their fixed buckets at creation time. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (!fb || !fb->tree) return; + struct wlr_scene_tree *target = level_tree_for(level); + if (fb->tree->node.parent != target) + wlr_scene_node_reparent(&fb->tree->node, target); +} + static void wl_stacking_raise(WNativeWindow win) { @@ -5475,57 +5639,54 @@ wl_stacking_raise(WNativeWindow win) static void wl_stacking_lower(WNativeWindow win) { + /* Frame-buf path: operate on the grouping tree, not the inner buffer + * node. Lowering fb->tree moves the whole window (frame pixels + + * reparented client surface) together, within its level bucket. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + if (fb->tree) + wlr_scene_node_lower_to_bottom(&fb->tree->node); + else if (fb->scene_buf) + wlr_scene_node_lower_to_bottom(&fb->scene_buf->node); + return; + } + /* Unframed toplevel view (no frame_buf wrapper). */ struct wl_toplevel_view *v = wl_find_view_by_id(win); if (v) { wl_list_remove(&v->link); wl_list_insert(wl_state.toplevel_list.prev, &v->link); if (v->scene_node) wlr_scene_node_lower_to_bottom(&v->scene_node->node); - return; - } - struct wl_frame_buf *fb = frame_buf_find(win); - if (fb && fb->scene_buf) { - wlr_scene_node_lower_to_bottom(&fb->scene_buf->node); - return; } } static void wl_stacking_restack(WNativeWindow *windows, int count) { - /* Reorder scene graph to match the WM's stacking order. - * Array is top-to-bottom. Process bottom-to-top so raise_to_top - * puts the first element on top last. */ + /* Reorder scene graph to match the WM stacking order. + * + * windows[] is ordered top-to-bottom (index 0 = topmost). + * Process bottom-to-top: last raise_to_top call wins, so the + * window at index 0 ends up on top within its level bucket. + * + * Each window is represented by its toplevel frame_buf ID. We + * raise fb->tree — the grouping tree that owns both the frame + * pixel buffer and the reparented client surface — so the entire + * window moves as one atomic unit. Child frame_bufs (titlebar, + * resizebar) are already children of fb->tree and move with it. */ int i; for (i = count - 1; i >= 0; i--) { WNativeWindow win = windows[i]; struct wl_frame_buf *fb = frame_buf_find(win); - if (fb && fb->scene_buf) { - wlr_scene_node_raise_to_top(&fb->scene_buf->node); - /* Raise client surface inside this frame */ - struct wl_toplevel_view *cv; - wl_list_for_each(cv, &wl_state.toplevel_list, link) { - if (cv->frame_id == win && cv->scene_node) { - wlr_scene_node_raise_to_top(&cv->scene_node->node); - break; - } - } - /* Also try via WM context */ - WObjDescriptor *desc = wm_backend->context_find(win, WM_CTX_CLIENT_WIN); - if (desc && desc->parent_type == WCLASS_WINDOW) { - WWindow *wwin = (WWindow *)desc->parent; - struct wl_toplevel_view *v = wl_find_view_by_id(wwin->client_win); - if (v && v->scene_node) - wlr_scene_node_raise_to_top(&v->scene_node->node); - } - /* Raise children (titlebar, resizebar) on top */ - 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_raise_to_top(&child->scene_buf->node); - } + if (fb) { + /* Raise the grouping tree: moves frame + client together. */ + if (fb->tree) + wlr_scene_node_raise_to_top(&fb->tree->node); + else if (fb->scene_buf) + wlr_scene_node_raise_to_top(&fb->scene_buf->node); continue; } + /* Fallback: unframed toplevel view (no frame_buf). */ struct wl_toplevel_view *v = wl_find_view_by_id(win); if (v) { wl_list_remove(&v->link); @@ -6227,8 +6388,11 @@ wl_client_reparent(WNativeWindow client, WNativeWindow new_parent, int x, int y) fb->height = h; if (wl_state.scene) { - fb->scene_buf = wlr_scene_buffer_create(&wl_state.scene->tree, - &fb->base); + /* WINGs dialog box: place in WL_LAYER_FLOATING so it renders + * above normal windows but below menus and dock. */ + struct wlr_scene_tree *_wp = wl_state.level_trees[WL_LAYER_FLOATING] + ? wl_state.level_trees[WL_LAYER_FLOATING] : &wl_state.scene->tree; + fb->scene_buf = wlr_scene_buffer_create(_wp, &fb->base); if (fb->scene_buf) { wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); wlr_scene_node_set_enabled(&fb->scene_buf->node, false); @@ -6432,7 +6596,9 @@ static WNativeWindow wl_fake_leader_create(WScreen *scr) wlr_buffer_init(&fb->base, &frame_buf_impl, 1, 1); if (wl_state.scene) { - fb->scene_buf = wlr_scene_buffer_create(&wl_state.scene->tree, &fb->base); + struct wlr_scene_tree *_pp = wl_state.level_trees[WL_LAYER_NORMAL] + ? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree; + fb->scene_buf = wlr_scene_buffer_create(_pp, &fb->base); if (fb->scene_buf) wlr_scene_node_set_enabled(&fb->scene_buf->node, false); } @@ -7625,6 +7791,7 @@ const WMBackend wl_backend = { .stacking_lower = wl_stacking_lower, .stacking_restack = wl_stacking_restack, .stacking_query = wl_stacking_query, + .stacking_set_level = wl_stacking_set_level, .event_flush = wl_event_flush, .event_flush_expose = wl_event_flush_expose, diff --git a/src/backend/x11/x11_backend.c b/src/backend/x11/x11_backend.c index aabe1f5..02fd2cc 100644 --- a/src/backend/x11/x11_backend.c +++ b/src/backend/x11/x11_backend.c @@ -766,6 +766,13 @@ x11_stacking_lower(WNativeWindow win) XLowerWindow(dpy, WNW_TO_XID(win)); } +static void +x11_stacking_set_level(WNativeWindow win, int level) +{ + /* X11: the server owns z-order; no per-level scene trees needed. */ + (void)win; (void)level; +} + static int x11_stacking_query(WNativeWindow root, WNativeWindow **out, unsigned int *count) { @@ -4252,6 +4259,7 @@ const WMBackend x11_backend = { .stacking_raise = x11_stacking_raise, .stacking_lower = x11_stacking_lower, .stacking_query = x11_stacking_query, + .stacking_set_level = x11_stacking_set_level, .event_flush = x11_event_flush, .event_sync = x11_event_sync, .event_pending = x11_event_pending, diff --git a/src/stacking.c b/src/stacking.c index c1efbbd..613ea60 100644 --- a/src/stacking.c +++ b/src/stacking.c @@ -399,6 +399,7 @@ void AddToStackList(WCoreWindow * frame) frame->screen_ptr->window_count++; wm_backend->context_save(frame->window, WM_CTX_STACK, frame); + wm_backend->stacking_set_level(frame->window, frame->stacking->window_level); curtop = WMGetFromBag(scr->stacking_list, index); /* first window in this level */