wayland: fix frame/client desync and titlebar offset

Three issues fixed:

1. Frame and client surface moved independently during interactive
   window move. The client surface (wl_toplevel_view) and frame
   decorations (wl_frame_buf) are independent scene graph nodes —
   unlike X11 where reparenting creates an implicit parent-child
   relationship. Added frame_id/frame_rel_x/frame_rel_y to
   wl_toplevel_view so wl_window_move keeps them in sync.

2. window_move(client_win, x, y) treated coordinates as absolute,
   but the WM core passes relative-to-frame coordinates (matching
   X11 child window semantics). Now detected via frame_id and
   converted to absolute scene positions.

3. Titlebar scene node was offset by left_button_width because
   updateTitlebar() configures it for X11 layout (separate
   overlapping windows). Since the Wayland backend composites
   buttons directly into the titlebar buffer, override the
   titlebar position to frame origin during wl_frame_paint.
This commit is contained in:
Levi Neely 2026-05-14 12:59:39 +02:00
parent 4be05c799c
commit 6dfdc5a108
2 changed files with 88 additions and 3 deletions

View File

@ -2,6 +2,35 @@
Last updated: 2026-05-14
## Governing Principles
1. **No guards in the middle** — the core (src/, wrlib/, WINGs/) never asks "am I on
X11 or Wayland?" Backend-specific behavior lives exclusively in the backend that
owns it. Zero `#ifdef WAYLAND` or runtime backend checks in shared code.
2. **Duplication over coupling** — if both backends need similar-but-not-identical
logic, each gets its own copy. Shared abstractions that leak backend details are
forbidden. Clarity and independence beat DRY.
3. **Fail fast, fail loudly** — no defensive null checks, no silent fallbacks, no
"just in case" guards. If a precondition is violated, crash immediately with a
clear signal. Defensive coding for the sake of being defensive is not acceptable.
4. **Perfect architecture over fewer lines** — the right structure wins even if it
means more files or duplicated code. Clean separation now buys simple reasoning
forever.
```
┌─────────────┐ ┌─────────────────────────┐ ┌─────────────────┐
│ X11 Backend │◄────┤ Core / wrlib / WINGs ├────►│ Wayland Backend │
└─────────────┘ └─────────────────────────┘ └─────────────────┘
│ │ │
backend-specific pure policy, backend-specific
special cases zero backend guards special cases
```
---
## Architecture
WindowMaker 0.96.0 uses a vtable-based backend abstraction (`WMBackend` struct in

View File

@ -250,6 +250,8 @@ struct wl_toplevel_view {
/* Phase 18: scene graph tree for this surface (NULL if scene unavailable). */
struct wlr_scene_tree *scene_node;
WNativeWindow id; /* synthetic handle, >= 0x10000000 */
WNativeWindow frame_id; /* frame_buf that owns this view (0 = none) */
int frame_rel_x, frame_rel_y; /* offset within frame */
int x, y; /* Phase 55: WM-assigned position */
long *wm_state; /* Phase 56: saved WM state or NULL */
int wm_state_n; /* Phase 56: element count */
@ -3880,6 +3882,19 @@ wl_frame_paint(struct WFrameWindow *fwin)
int tb_w = total_w; /* always full frame width */
int tb_h = fwin->top_width;
/* In Wayland we composite buttons into the titlebar buffer, so
* the titlebar must be full-width and positioned at the frame origin
* regardless of what updateTitlebar() configured for X11 layout. */
if (tb_fb) {
tb_fb->rel_x = 0;
tb_fb->rel_y = 0;
tb_fb->x = fb->x;
tb_fb->y = fb->y;
if (tb_fb->scene_buf)
wlr_scene_node_set_position(&tb_fb->scene_buf->node,
tb_fb->x, tb_fb->y);
}
/* Ensure titlebar buffer is full width */
if (tb_fb && tb_fb->width < tb_w) {
pixman_image_unref(tb_fb->image);
@ -5620,12 +5635,24 @@ static void wl_window_move(WNativeWindow win, int x, int y)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
v->x = x; v->y = y;
if (v->frame_id) {
/* Client is a child of a frame — coords are relative to frame */
v->frame_rel_x = x;
v->frame_rel_y = y;
struct wl_frame_buf *fb = frame_buf_find(v->frame_id);
if (fb) {
v->x = fb->x + x;
v->y = fb->y + y;
}
} else {
v->x = x; v->y = y;
}
if (v->scene_node)
wlr_scene_node_set_position(&v->scene_node->node, x, y);
wlr_scene_node_set_position(&v->scene_node->node, v->x, v->y);
if (v->xw_surface)
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)x, (int16_t)y, v->xw_surface->width, v->xw_surface->height);
(int16_t)v->x, (int16_t)v->y,
v->xw_surface->width, v->xw_surface->height);
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
@ -5633,6 +5660,18 @@ static void wl_window_move(WNativeWindow win, int x, int y)
fb->x = x; fb->y = y;
if (fb->scene_buf)
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
/* Move the client surface (toplevel view) that belongs to this frame */
struct wl_toplevel_view *cv;
wl_list_for_each(cv, &wl_state.toplevel_list, link) {
if (cv->frame_id == win) {
cv->x = x + cv->frame_rel_x;
cv->y = y + cv->frame_rel_y;
if (cv->scene_node)
wlr_scene_node_set_position(&cv->scene_node->node,
cv->x, cv->y);
break;
}
}
/* Move children and grandchildren */
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
@ -5858,6 +5897,23 @@ wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask)
static void
wl_client_reparent(WNativeWindow client, WNativeWindow new_parent, int x, int y)
{
/* Toplevel view reparented into a frame_buf — record the association
* so wl_window_move can keep them in sync. */
struct wl_toplevel_view *v = wl_find_view_by_id(client);
if (v) {
v->frame_id = new_parent;
v->frame_rel_x = x;
v->frame_rel_y = y;
/* Position the client surface relative to the frame */
struct wl_frame_buf *parent_fb = frame_buf_find(new_parent);
if (parent_fb) {
v->x = parent_fb->x + x;
v->y = parent_fb->y + y;
if (v->scene_node)
wlr_scene_node_set_position(&v->scene_node->node, v->x, v->y);
}
return;
}
/* For WINGs internal windows: create a child frame_buf so the dialog
* content is visible in the scene graph. */