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.
This commit is contained in:
Window Maker 2026-05-15 09:11:29 +02:00
parent 0e6f2f60e8
commit 245d0077b5
6 changed files with 350 additions and 103 deletions

View File

@ -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)"
]
}
}

View File

@ -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 |

View File

@ -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) */
/* ------------------------------------------------------------------ */

View File

@ -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,14 +1031,36 @@ 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
* newly managed window so pointer focus is immediately
@ -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 */
&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;
}
/* 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)
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;
/* 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 */
/* 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;
}
}
return hit;
}
/* ------------------------------------------------------------------ */
@ -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) {
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);
/* 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);
}
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,

View File

@ -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,

View File

@ -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 */