diff --git a/src/Makefile.am b/src/Makefile.am index 0c0b195..860523f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -158,7 +158,10 @@ if USE_WAYLAND_BACKEND wmaker_SOURCES += \ backend/wayland/wl_backend.h \ backend/wayland/wl_backend.c \ - backend/wayland/wl_context.c + backend/wayland/wl_context.c \ + backend/wayland/wl_event.c \ + backend/wayland/wl_stacking.c \ + backend/wayland/wl_overlay.c endif if USE_NESTED_FUNC diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 790e684..42a21e8 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -238,9 +238,9 @@ static void wm_dock_app_remove(WNativeWindow id); 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); +int level_to_bucket(int level); +struct wlr_scene_tree *level_tree_for(int level); +struct wl_frame_buf *frame_buf_find(WNativeWindow id); /* ------------------------------------------------------------------ */ /* Phase A: WINGs event backend wiring helpers */ @@ -384,7 +384,7 @@ static void handle_pointer_button(struct wl_listener *listener, void *data); static void handle_pointer_frame(struct wl_listener *listener, void *data); /* Phase 41: look up the view for a given synthetic Window ID. */ -static struct wl_toplevel_view * +struct wl_toplevel_view * wl_find_view_by_id(WNativeWindow id) { struct wl_toplevel_view *v; @@ -3439,19 +3439,7 @@ wl_event_loop_terminate(void) * once the dock and title-bar code drives it. */ -struct wm_shm_surface { - /* wlr_buffer interface — MUST be first so wl_container_of works */ - struct wlr_buffer wlr_buf; - /* Pixel store */ - uint32_t *data; /* ARGB8888, row-major */ - int width; - int height; - size_t stride; /* bytes per row = width * 4 */ - /* pixman drawing surface wrapping data[] */ - pixman_image_t *image; - /* scene graph presence */ - struct wlr_scene_buffer *scene_buf; /* NULL until first commit */ -}; +/* struct wm_shm_surface now in wl_types.h */ /* ---------- wlr_buffer implementation -------------------------------- */ @@ -3491,7 +3479,7 @@ static const struct wlr_buffer_impl wm_shm_buf_impl = { /* ---------- surface API ---------------------------------------------- */ -static struct wm_shm_surface * +struct wm_shm_surface * wm_shm_surface_create(int width, int height) { if (width <= 0 || height <= 0) @@ -3531,7 +3519,7 @@ wm_shm_surface_create(int width, int height) * Fill a rectangle with a solid ARGB colour. * argb: 0xAARRGGBB — e.g. 0xFF4080C0 for opaque steel-blue. */ -static void +void wm_shm_surface_fill(struct wm_shm_surface *s, int x, int y, int w, int h, uint32_t argb) @@ -3570,7 +3558,7 @@ wm_shm_surface_fill(struct wm_shm_surface *s, * reposition it (pixel updates are uploaded lazily by wlroots on the * next wlr_scene_output_commit). */ -static void +void wm_shm_surface_commit(struct wm_shm_surface *s, struct wlr_scene_tree *parent, int x, int y) @@ -3594,7 +3582,7 @@ wm_shm_surface_commit(struct wm_shm_surface *s, * After this call s is invalid — the memory is freed asynchronously * by shm_buf_destroy once the scene node releases its lock. */ -static void +void wm_shm_surface_destroy(struct wm_shm_surface *s) { if (!s) @@ -4020,7 +4008,7 @@ static const struct wlr_buffer_impl frame_buf_impl = { .end_data_ptr_access = frame_buf_end_data_ptr_access, }; -static struct wl_frame_buf * +struct wl_frame_buf * frame_buf_find(WNativeWindow id) { struct wl_frame_buf *fb; @@ -6261,454 +6249,11 @@ wl_keyboard_ungrab(void) wl_state.keyboard_grabbed = false; } -/* ------------------------------------------------------------------ */ -/* Phase 81: Overlay animation surface — XOR pixel engine + bitmap */ -/* font for geometry display (miniaturize zoom, transparent */ -/* move/resize frames, position/size readouts). */ -/* ------------------------------------------------------------------ */ - -/* - * XOR mask: 0xFFFFFFFF XOR 0x00000000 (transparent) = 0xFFFFFFFF (opaque - * white); XOR 0xFFFFFFFF again = 0x00000000 (transparent, i.e. erased). - * The overlay scene buffer is composited OVER all client windows via - * standard alpha blending, so transparent pixels show through to the - * desktop and opaque-white pixels appear on top. - */ -#define OVL_MASK 0xFFFFFFFFu - -/* - * 5-wide × 9-tall bitmap font for printable ASCII (0x20–0x7E). - * Encoding: bit 7 = column 0 (left), bit 3 = column 4 (right), bits 2–0 - * unused. Cell width = 6 px (glyph 5 + 1 gap); cell height = 10 px. - */ -#define OVL_GW 5 /* glyph pixel width */ -#define OVL_GH 9 /* glyph pixel height */ -#define OVL_CW 6 /* cell width (px) */ -#define OVL_CH 10 /* cell height (px) */ - -static const uint8_t ovl_font[96][OVL_GH] = { - /*20 ' '*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, - /*21 '!'*/{0x20,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x00}, - /*22 '"'*/{0x50,0x50,0x50,0x00,0x00,0x00,0x00,0x00,0x00}, - /*23 '#'*/{0x50,0x50,0xF8,0x50,0xF8,0x50,0x50,0x00,0x00}, - /*24 '$'*/{0x20,0x70,0xA0,0x70,0x28,0x70,0x20,0x00,0x00}, - /*25 '%'*/{0xC0,0xC8,0x10,0x20,0x40,0x98,0x18,0x00,0x00}, - /*26 '&'*/{0x60,0x90,0x60,0xA0,0x98,0x90,0x68,0x00,0x00}, - /*27'\''*/ {0x60,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00}, - /*28 '('*/{0x10,0x20,0x40,0x40,0x40,0x20,0x10,0x00,0x00}, - /*29 ')'*/{0x40,0x20,0x10,0x10,0x10,0x20,0x40,0x00,0x00}, - /*2A '*'*/{0x00,0x88,0x50,0xF8,0x50,0x88,0x00,0x00,0x00}, - /*2B '+'*/{0x00,0x20,0x20,0xF8,0x20,0x20,0x00,0x00,0x00}, - /*2C ','*/{0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00}, - /*2D '-'*/{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00}, - /*2E '.'*/{0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00}, - /*2F '/'*/{0x08,0x08,0x10,0x20,0x40,0x80,0x80,0x00,0x00}, - /*30 '0'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, - /*31 '1'*/{0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, - /*32 '2'*/{0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00,0x00}, - /*33 '3'*/{0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00,0x00}, - /*34 '4'*/{0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00,0x00}, - /*35 '5'*/{0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00,0x00}, - /*36 '6'*/{0x38,0x40,0x80,0xF0,0x88,0x88,0x70,0x00,0x00}, - /*37 '7'*/{0xF8,0x08,0x10,0x20,0x20,0x20,0x20,0x00,0x00}, - /*38 '8'*/{0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00,0x00}, - /*39 '9'*/{0x70,0x88,0x88,0x78,0x08,0x08,0x70,0x00,0x00}, - /*3A ':'*/{0x00,0x60,0x60,0x00,0x60,0x60,0x00,0x00,0x00}, - /*3B ';'*/{0x00,0x60,0x60,0x00,0x60,0x20,0x40,0x00,0x00}, - /*3C '<'*/{0x10,0x20,0x40,0x80,0x40,0x20,0x10,0x00,0x00}, - /*3D '='*/{0x00,0x00,0xF8,0x00,0xF8,0x00,0x00,0x00,0x00}, - /*3E '>'*/{0x40,0x20,0x10,0x08,0x10,0x20,0x40,0x00,0x00}, - /*3F '?'*/{0x70,0x88,0x08,0x10,0x20,0x00,0x20,0x00,0x00}, - /*40 '@'*/{0x70,0x88,0x98,0xA8,0x98,0x80,0x70,0x00,0x00}, - /*41 'A'*/{0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00,0x00}, - /*42 'B'*/{0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00,0x00}, - /*43 'C'*/{0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00,0x00}, - /*44 'D'*/{0xE0,0x90,0x88,0x88,0x88,0x90,0xE0,0x00,0x00}, - /*45 'E'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00,0x00}, - /*46 'F'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00,0x00}, - /*47 'G'*/{0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00,0x00}, - /*48 'H'*/{0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00,0x00}, - /*49 'I'*/{0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, - /*4A 'J'*/{0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x00}, - /*4B 'K'*/{0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00,0x00}, - /*4C 'L'*/{0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00}, - /*4D 'M'*/{0x88,0xD8,0xA8,0x88,0x88,0x88,0x88,0x00,0x00}, - /*4E 'N'*/{0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00,0x00}, - /*4F 'O'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, - /*50 'P'*/{0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00,0x00}, - /*51 'Q'*/{0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00,0x00}, - /*52 'R'*/{0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00,0x00}, - /*53 'S'*/{0x70,0x88,0x80,0x70,0x08,0x88,0x70,0x00,0x00}, - /*54 'T'*/{0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x00}, - /*55 'U'*/{0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, - /*56 'V'*/{0x88,0x88,0x88,0x88,0x50,0x50,0x20,0x00,0x00}, - /*57 'W'*/{0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00,0x00}, - /*58 'X'*/{0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00,0x00}, - /*59 'Y'*/{0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00,0x00}, - /*5A 'Z'*/{0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00,0x00}, - /*5B '['*/{0x70,0x40,0x40,0x40,0x40,0x40,0x70,0x00,0x00}, - /*5C'\\'*/ {0x80,0x80,0x40,0x20,0x10,0x08,0x08,0x00,0x00}, - /*5D ']'*/{0x70,0x10,0x10,0x10,0x10,0x10,0x70,0x00,0x00}, - /*5E '^'*/{0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00}, - /*5F '_'*/{0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x00}, - /*60 '`'*/{0x40,0x20,0x10,0x00,0x00,0x00,0x00,0x00,0x00}, - /*61 'a'*/{0x00,0x00,0x70,0x08,0x78,0x88,0x78,0x00,0x00}, - /*62 'b'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0xF0,0x00,0x00}, - /*63 'c'*/{0x00,0x00,0x70,0x88,0x80,0x88,0x70,0x00,0x00}, - /*64 'd'*/{0x08,0x08,0x78,0x88,0x88,0x88,0x78,0x00,0x00}, - /*65 'e'*/{0x00,0x00,0x70,0x88,0xF8,0x80,0x70,0x00,0x00}, - /*66 'f'*/{0x30,0x48,0x40,0xE0,0x40,0x40,0x40,0x00,0x00}, - /*67 'g'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x70,0x00}, - /*68 'h'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, - /*69 'i'*/{0x20,0x00,0x60,0x20,0x20,0x20,0x70,0x00,0x00}, - /*6A 'j'*/{0x10,0x00,0x30,0x10,0x10,0x10,0x90,0x60,0x00}, - /*6B 'k'*/{0x80,0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00,0x00}, - /*6C 'l'*/{0x60,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, - /*6D 'm'*/{0x00,0x00,0xD8,0xA8,0xA8,0xA8,0x88,0x00,0x00}, - /*6E 'n'*/{0x00,0x00,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, - /*6F 'o'*/{0x00,0x00,0x70,0x88,0x88,0x88,0x70,0x00,0x00}, - /*70 'p'*/{0x00,0x00,0xF0,0x88,0x88,0xF0,0x80,0x80,0x00}, - /*71 'q'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x08,0x00}, - /*72 'r'*/{0x00,0x00,0xB0,0xC8,0x80,0x80,0x80,0x00,0x00}, - /*73 's'*/{0x00,0x00,0x70,0x80,0x70,0x08,0xF0,0x00,0x00}, - /*74 't'*/{0x40,0x40,0xF0,0x40,0x40,0x48,0x30,0x00,0x00}, - /*75 'u'*/{0x00,0x00,0x88,0x88,0x88,0x98,0x68,0x00,0x00}, - /*76 'v'*/{0x00,0x00,0x88,0x88,0x88,0x50,0x20,0x00,0x00}, - /*77 'w'*/{0x00,0x00,0x88,0x88,0xA8,0xD8,0x88,0x00,0x00}, - /*78 'x'*/{0x00,0x00,0x88,0x50,0x20,0x50,0x88,0x00,0x00}, - /*79 'y'*/{0x00,0x00,0x88,0x88,0x78,0x08,0x70,0x00,0x00}, - /*7A 'z'*/{0x00,0x00,0xF8,0x10,0x20,0x40,0xF8,0x00,0x00}, - /*7B '{'*/{0x18,0x20,0x20,0x40,0x20,0x20,0x18,0x00,0x00}, - /*7C '|'*/{0x20,0x20,0x20,0x00,0x20,0x20,0x20,0x00,0x00}, - /*7D '}'*/{0x60,0x10,0x10,0x08,0x10,0x10,0x60,0x00,0x00}, - /*7E '~'*/{0x40,0xA8,0x18,0x00,0x00,0x00,0x00,0x00,0x00}, -}; - -/* Lazily allocate (or resize) the full-screen overlay buffer. */ -static void -overlay_ensure(WScreen *scr) -{ - int w, h; - if (scr && scr->scr_width > 0 && scr->scr_height > 0) { - w = scr->scr_width; - h = scr->scr_height; - } else if (wl_state.output) { - w = (int)wl_state.output->width; - h = (int)wl_state.output->height; - } else { - w = 1920; h = 1080; - } - if (wl_state.overlay_surf && - wl_state.overlay_surf->width == w && - wl_state.overlay_surf->height == h) - return; - if (wl_state.overlay_surf) { - wm_shm_surface_destroy(wl_state.overlay_surf); - wl_state.overlay_surf = NULL; - } - wl_state.overlay_surf = wm_shm_surface_create(w, h); -} - -/* XOR a single pixel on the overlay buffer (clipped). */ -static inline void -ovl_plot(struct wm_shm_surface *s, int x, int y) -{ - if ((unsigned)x < (unsigned)s->width && (unsigned)y < (unsigned)s->height) - s->data[y * (int)(s->stride >> 2) + x] ^= OVL_MASK; -} - -/* XOR a horizontal run of `len` pixels starting at (x, y). */ -static void -ovl_hline(struct wm_shm_surface *s, int x, int y, int len) -{ - if ((unsigned)y >= (unsigned)s->height || len <= 0) return; - int x0 = x < 0 ? 0 : x; - int x1 = x + len; if (x1 > s->width) x1 = s->width; - if (x0 >= x1) return; - uint32_t *p = s->data + y * (int)(s->stride >> 2) + x0; - for (int i = x0; i < x1; i++) *p++ ^= OVL_MASK; -} - -/* XOR a vertical run of `len` pixels starting at (x, y). */ -static void -ovl_vline(struct wm_shm_surface *s, int x, int y, int len) -{ - if ((unsigned)x >= (unsigned)s->width || len <= 0) return; - int y0 = y < 0 ? 0 : y; - int y1 = y + len; if (y1 > s->height) y1 = s->height; - int stride = (int)(s->stride >> 2); - for (int j = y0; j < y1; j++) - s->data[j * stride + x] ^= OVL_MASK; -} - -/* Bresenham line (with fast h/v paths). */ -static void -ovl_line(struct wm_shm_surface *s, int x0, int y0, int x1, int y1) -{ - if (x0 == x1) { - int y = y0 < y1 ? y0 : y1; - ovl_vline(s, x0, y, abs(y1 - y0) + 1); - return; - } - if (y0 == y1) { - int x = x0 < x1 ? x0 : x1; - ovl_hline(s, x, y0, abs(x1 - x0) + 1); - return; - } - int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; - int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; - int err = dx + dy; - for (;;) { - ovl_plot(s, x0, y0); - if (x0 == x1 && y0 == y1) break; - int e2 = 2 * err; - if (e2 >= dy) { err += dy; x0 += sx; } - if (e2 <= dx) { err += dx; y0 += sy; } - } -} - -/* Render one glyph at cell origin (cx, cy) using XOR. */ -static void -ovl_draw_glyph(struct wm_shm_surface *s, int cx, int cy, unsigned char ch) -{ - if (ch < 0x20 || ch > 0x7E) ch = '?'; - const uint8_t *g = ovl_font[ch - 0x20]; - for (int row = 0; row < OVL_GH; row++) { - uint8_t bits = g[row]; - if (!bits) continue; - int py = cy + row; - if (py < 0 || py >= s->height) continue; - for (int col = 0; col < OVL_GW; col++) { - if (bits & (0x80u >> col)) - ovl_plot(s, cx + col, py); - } - } -} - -/* Push the modified overlay buffer to the scene and render a frame. */ -static void -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. */ - /* 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) { - /* Ensure the node is visible for the animation frame. */ - wlr_scene_node_set_enabled(&s->scene_buf->node, true); - /* Notify wlroots that the pixel data has changed. */ - wlr_scene_buffer_set_buffer_with_damage(s->scene_buf, &s->wlr_buf, NULL); - } - /* Commit the scene immediately so the animation step is visible - * before the caller's wusleep() delay. */ - if (wl_state.scene_output && wl_state.allocator) - wlr_scene_output_commit(wl_state.scene_output, NULL); -} - -/* ------------------------------------------------------------------ */ -/* Server grab */ -/* ------------------------------------------------------------------ */ - -static void wl_server_grab(void) {} /* no-op: Wayland double-buffers for us */ -static void wl_server_ungrab(void) -{ - overlay_flush(); - /* Disable the overlay scene node so it doesn't intercept hit-testing. - * The XOR animation has already erased its pixels; leaving the node - * enabled blocks frame_buf_at (wlr_scene_node_at hits the overlay - * before any frame_buf). */ - if (wl_state.overlay_surf && wl_state.overlay_surf->scene_buf) - wlr_scene_node_set_enabled(&wl_state.overlay_surf->scene_buf->node, false); -} - -/* ------------------------------------------------------------------ */ -/* 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) -{ - 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, &v->link); - if (v->frame_id) { - struct wl_frame_buf *fb = frame_buf_find(v->frame_id); - if (fb && fb->tree) { - wlr_scene_node_raise_to_top(&fb->tree->node); - return; - } - } - if (v->scene_node) - wlr_scene_node_raise_to_top(&v->scene_node->node); - return; - } - struct wl_frame_buf *fb = frame_buf_find(win); - if (fb) { - 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); - } - return; - } -} - -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); - } -} - -static void -wl_stacking_restack(WNativeWindow *windows, int count) -{ - /* 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) { - /* 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); - wl_list_insert(&wl_state.toplevel_list, &v->link); - if (v->scene_node) - wlr_scene_node_raise_to_top(&v->scene_node->node); - } - } -} - -static int -wl_stacking_query(WNativeWindow root, - WNativeWindow **out, unsigned int *count) -{ - /* Phase 58: return synthetic IDs in Z-order (top to bottom). - * RemakeStackList will find no WM_CTX_STACK entries for them - * and set window_count=0, but returning 1 avoids the "could not - * get window list" warning. */ - struct wl_toplevel_view *v; - unsigned int n = 0, i = 0; - WNativeWindow *wins; - (void)root; - - wl_list_for_each(v, &wl_state.toplevel_list, link) n++; - if (n == 0) { *out = NULL; *count = 0; return 1; } - - wins = wmalloc(n * sizeof(WNativeWindow)); - wl_list_for_each(v, &wl_state.toplevel_list, link) wins[i++] = v->id; - *out = wins; *count = n; - return 1; -} /* ------------------------------------------------------------------ */ /* Events */ /* ------------------------------------------------------------------ */ -static void wl_event_flush(void) -{ - /* Flush pending Xlib requests to XWayland so drawing becomes visible. */ - if (wl_state.x_display) - XFlush(wl_state.x_display); -} -static void wl_event_flush_expose(void) {} -static void wl_event_sync(int discard) -{ - if (wl_state.x_display) - XSync(wl_state.x_display, discard ? True : False); -} -static int wl_event_pending(void) { return 0; } -static void wl_event_drain_motion(void) {} -static void wl_event_discard_enter_leave_until(unsigned long timestamp) { (void)timestamp; } -static void wl_event_handle_extensions(WMEvent *event) { (void)event; } -static void wl_event_randr_update(WMEvent *event) { (void)event; } - /* ------------------------------------------------------------------ */ /* Properties */ /* ------------------------------------------------------------------ */ @@ -7240,75 +6785,7 @@ static void wl_window_add_events(WNativeWindow win, unsigned long extra_mask) { static void wl_window_select_events(WNativeWindow win, unsigned long mask) { (void)win; (void)mask; } static void wl_window_shape_clear(WWindow *wwin) { (void)wwin; } -static void wl_window_shape_set(WWindow *wwin) { (void)wwin; } - -/* ------------------------------------------------------------------ */ -/* Overlays */ -/* ------------------------------------------------------------------ */ - -static void -wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2) -{ - overlay_ensure(scr); - if (wl_state.overlay_surf) { - ovl_line(wl_state.overlay_surf, x1, y1, x2, y2); - overlay_flush(); - } -} - -static void -wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n) -{ - overlay_ensure(scr); - struct wm_shm_surface *s = wl_state.overlay_surf; - if (!s || n < 2) return; - for (int i = 0; i < n - 1; i++) - ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y); -} - -static void -wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h) -{ - overlay_ensure(scr); - struct wm_shm_surface *s = wl_state.overlay_surf; - if (!s || w <= 0 || h <= 0) return; - /* Matching XDrawRectangle semantics: corners are shared between - * adjacent edges and therefore XOR'd twice (become transparent). */ - ovl_hline(s, x, y, w + 1); /* top */ - ovl_hline(s, x, y + h, w + 1); /* bottom */ - ovl_vline(s, x, y, h + 1); /* left */ - ovl_vline(s, x + w, y, h + 1); /* right */ - overlay_flush(); -} - -static void -wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n) -{ - overlay_ensure(scr); - struct wm_shm_surface *s = wl_state.overlay_surf; - if (!s) return; - for (int i = 0; i < n; i++) - ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2); -} - -static void -wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len) -{ - overlay_ensure(scr); - struct wm_shm_surface *s = wl_state.overlay_surf; - if (!s || !str) return; - /* y is the text baseline; render from the top of the cell. */ - int top = y - (OVL_GH - 2); - for (int i = 0; i < len; i++) - ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]); -} - -static int -wl_overlay_text_width(WScreen *scr, const char *str, int n) -{ - (void)scr; (void)str; - return n * OVL_CW; -} +void wl_window_shape_set(WWindow *wwin) { (void)wwin; } /* ------------------------------------------------------------------ */ /* Client */ diff --git a/src/backend/wayland/wl_backend.c.bak b/src/backend/wayland/wl_backend.c.bak new file mode 100644 index 0000000..5f4d501 --- /dev/null +++ b/src/backend/wayland/wl_backend.c.bak @@ -0,0 +1,9981 @@ +/* + * wl_backend.c -- wlroots-based Wayland compositor backend for Window Maker + * + * Phase 31: complete stub vtable. Every slot compiles and returns a safe + * zero/null/false value. Subsequent phases will replace stubs with real + * wlroots implementations one functional area at a time. + * Phase 32: display_open/close/screen_count implemented with wlroots headless + * backend. + * Phase 34: Wayland compositor socket created (clients can connect); + * wlr_output_layout added; monitors_query returns real head geometry. + * Phase 35: Core Wayland protocol globals advertised (wl_compositor, + * wl_subcompositor, wl_seat, wl_data_device_manager, xdg_wm_base). + * Phase 36: wl_shm and wl_output advertised; XDG toplevel listener wired. + * Phase 37: frame callbacks driven by output frame event; XDG configure + * sent on new toplevel so clients complete their first frame. + * Phase 38: per-toplevel view struct with map/unmap/destroy lifecycle; + * synthetic Window IDs; context_save/find/delete implemented. + * Phase 39: handle_toplevel_map wires XDG toplevel map to wManageWindow so + * Wayland surfaces appear in scr->focused_window list. + * Phase 40: handle_toplevel_unmap wires XDG toplevel unmap to wUnmanageWindow. + * Phase 41: prop_get_wm_class and window_fetch_name return XDG app_id/title. + * Phase 42: window_get_client_attrs returns WCLIENT_VIEWABLE + surface + * geometry for mapped XDG toplevels. + * Phase 43: focus_set delivers keyboard focus via wlr_seat; headless keyboard + * device created and attached to seat; mapped toplevels auto-focused. + * Phase 44: client_synth_configure, window_resize send XDG configure events + * so clients receive size changes from the WM. + * Phase 45: pointer_query returns 1 (always on-screen); wWindowFocus/Unfocus + * guarded on frame!=NULL so wSetFocusTo works without a frame window. + * Phase 46: client_close vtable slot (wlr_xdg_toplevel_send_close); + * title_listener updates wwin->wm_instance/wm_class on set_title signal; + * wClientSendProtocol/wClientKill guarded for Wayland. + * Phase 47: prop_get_normal_hints returns XDG toplevel min/max/current size. + * Phase 53: headless pointer device added to seat; seat advertises both + * WL_SEAT_CAPABILITY_KEYBOARD and WL_SEAT_CAPABILITY_POINTER. + * Phase 54: virtual pointer position tracking; pointer_warp/warp_to update + * state; pointer_query/query_full return tracked coordinates. + * Phase 55: window_get_rect returns real geometry for synthetic toplevel IDs; + * window_move records WM-assigned x,y in the view struct. + * Phase 56: prop_set/get_wm_state persist WM state (workspace, miniaturized, + * shaded, etc.) in the view struct so wWindowGetSavedState works correctly. + * Phase 57: icccm_set/get/free_command store WM_COMMAND in the view struct; + * misc.c getCommandForWindow uses the vtable on the Wayland path. + * Phase 58: stacking_raise/lower/restack manipulate toplevel_list Z-order; + * stacking_query enumerates IDs in order (returns 1 to suppress warning). + * Phase 62: wl_screen_create_internals creates a real off-screen InputOnly X + * window (no_focus_xwin) as the focus sink and sets scr->no_focus_win to it. + * wl_focus_set recognises WNATIVE_NONE or no_focus_xwin as "clear focus" and + * calls wlr_seat_keyboard_clear_focus instead of activating an XWayland surface. + * Phase 63: PropGetProtocols sets DELETE_WINDOW=1 for all Wayland windows + * (all XDG toplevels support close); wClientSendProtocol dispatches + * client_close in the Wayland path so MC_CLOSE is enabled and kill_close=0. + * wManageWindow succeeded log now includes kill_close= field. + * Phase 64: zxdg_decoration_manager_v1 advertised; new_toplevel_decoration + * listener responds immediately with CLIENT_SIDE mode so clients draw their + * own title bars (no X11 frame rendering in the Wayland path). + * Phase 65: prop_get_transient_for implemented — walks xdg_toplevel->parent + * chain and returns the synthetic Window ID of the parent view so + * dialog windows are correctly identified as transients by the policy layer. + * Phase 66: XDG popup initial configure — schedule configure for + * WLR_XDG_SURFACE_ROLE_POPUP so context menus and autocomplete dropdowns + * can commit their first buffer without stalling on the compositor. + * Phase 17: XWayland support — wlr_xwayland started alongside the headless + * backend; X11 windows (e.g. GNUstep apps) appear via new_surface events, + * receive synthetic Window IDs equal to their xcb_window_t, and + * are managed through the same wl_toplevel_view / wManageWindow lifecycle + * as XDG toplevels. DISPLAY env var is set so X11 clients can connect. + * Phase 37: zwlr_data_control_manager_v1, zwlr_input_inhibitor_v1, + * zwlr_virtual_pointer_manager_v1, org_kde_kwin_idle, zxdg_exporter/importer_v2 + * advertised. Input inhibitor activate/deactivate wired. + * Phase 36: zwlr_layer_shell_v1 advertised; new_surface handler creates a + * wl_layer_view, assigns output, computes anchor/margin geometry, sends + * configure, and positions the scene node. Map/unmap/destroy wired. + * Phase 35: zwlr_foreign_toplevel_management_v1 advertised; handle created + * on map, destroyed on unmap, with title/app_id/output/activated state set. + * xdg_activation_v1 advertised; request_activate delivers WM focus + raise. + * Phase 34: zwp_text_input_manager_v3, zwp_input_method_manager_v2, and + * zwp_virtual_keyboard_manager_v1 advertised for IME / on-screen keyboard. + * Phase 33: zwlr_output_management_v1, zwlr_output_power_management_v1, + * zwlr_gamma_control_manager_v1, and wp_presentation advertised. + * Output management apply/test requests acknowledged (single-output no-op). + * Output power set_mode applies DPMS on/off via wlr_output_enable/commit. + * Phase 32: zwp_linux_dmabuf_v1, zwlr_screencopy_manager_v1, and + * zwlr_export_dmabuf_manager_v1 advertised as passive globals so GPU + * zero-copy buffers, screenshot tools, and screen recorders work. + * Phase 18: nested Wayland compositor + wlr_scene rendering. + * If WMAKER_WAYLAND_NESTED=1 is set, a wlr_wl_backend is used alongside the + * headless backend (via wlr_multi_backend) so the compositor appears as a + * Wayland window on the host desktop. wlr_scene provides a scene graph for + * compositing client surfaces; wlr_scene_output_commit renders each frame. + * A per-view scene node is created for each XDG/XWayland surface and kept + * in sync with the WM-assigned position. + * Phase 20: real input device forwarding for nested mode. + * handle_backend_new_input wires listeners for the host compositor's real + * pointer/keyboard devices so key presses and mouse motion are forwarded to + * focused Wayland clients. A grey wlr_scene_rect root background is added + * so the compositor output shows a proper desktop instead of solid black. + * Pointer bounds clamping updated to use actual output dimensions. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_INOTIFY +#include +#include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "wmaker-popup-v1-server-protocol.h" +#include +#include /* Phase 17: XWayland support */ +#include /* X_SetInputFocus and other request codes */ +#include /* Phase 18: nested Wayland backend */ +#include /* Phase 18: multi-backend */ +#include /* Phase 18: scene graph compositing */ +#include /* Phase 18: buffer allocator */ +#include /* Phase 29+: WM-owned SHM surface buffers */ +#include /* wlr_buffer_init/wlr_buffer_impl */ +#include /* Phase 29+: DRM_FORMAT_ARGB8888 */ +#include /* Phase 29+: software pixel rendering */ +#include /* Phase 23: hardware cursor tracking */ +#include /* Phase 23: xcursor theme */ +#include /* Phase 25: clipboard / selection */ +#include /* Phase 26: primary selection (middle-click) */ +#include /* Phase 26: zwp_primary_selection_device_manager_v1 */ +#include /* Phase 27: xdg-output for panel geometry */ +#include /* Phase 28: idle inhibit for video/fullscreen */ +#include /* Phase 29: wp_viewporter for video scaling */ +#include /* Phase 30: relative pointer for games */ +/* Generated protocol header required by wlr_pointer_constraints_v1.h */ +#include "protocols/pointer-constraints-unstable-v1-protocol.h" +#include /* Phase 30: pointer lock for games */ +#include /* Phase 31: shortcut inhibit for games */ +#include /* Phase 32: zwp_linux_dmabuf_v1 for GPU zero-copy */ +#include /* Phase 32: zwlr_screencopy_manager_v1 for screenshots */ +#include /* Phase 32: zwlr_export_dmabuf_manager_v1 for screen recorders */ +#include /* Phase 33: zwlr_output_management_v1 (randr equivalent) */ +/* Generated protocol header required by wlr_output_power_management_v1.h */ +#include "protocols/wlr-output-power-management-unstable-v1-protocol.h" +#include /* Phase 33: zwlr_output_power_management_v1 (DPMS) */ +#include /* Phase 33: zwlr_gamma_control_manager_v1 (Night Light) */ +#include /* Phase 33: wp_presentation (smooth frame timing) */ +#include /* Phase 34: zwp_text_input_manager_v3 (IME client) */ +#include /* Phase 34: zwp_input_method_manager_v2 (IME server) */ +#include /* Phase 34: zwp_virtual_keyboard_manager_v1 */ +#include /* Phase 35: zwlr_foreign_toplevel_management_v1 */ +#include /* Phase 35: xdg_activation_v1 (focus tokens) */ +/* Generated protocol header required by wlr_layer_shell_v1.h */ +#include "protocols/wlr-layer-shell-unstable-v1-protocol.h" +#include /* Phase 36: zwlr_layer_shell_v1 (panels, docks) */ +#include /* Phase 37: zwlr_data_control_manager_v1 (clipboard mgr) */ +#include /* Phase 37: zwlr_virtual_pointer_manager_v1 (remote desktop) */ +/* wlr_input_inhibitor and wlr_idle (org_kde_kwin_idle) were removed in + * wlroots 0.18 — use wlr_session_lock_v1 / wlr_idle_notify_v1 instead. */ +#include /* Phase 37: xdg-foreign registry (shared state) */ +#include /* Phase 37: zxdg_exporter_v2 / zxdg_importer_v2 */ +#include +/* Generated xdg-shell protocol implementation (Wayland glue code) */ +#include "protocols/xdg-shell-protocol.c" +#include +/* Phase 77: XKB for keysym→keycode translation and modifier mask setup */ +#include + +#include "../../WindowMaker.h" +#include "../../screen.h" +#include "../../window.h" +#include "../../framewin.h" /* WFrameWindow for frame_paint */ +#include "../../texture.h" /* WTexture, wTextureRenderImage */ +#include "../../misc.h" /* ShrinkString */ +#include "../../actions.h" /* Phase 16: wMaximizeWindow, wIconifyWindow, etc. */ +#include "../../stacking.h" /* Phase 24: wRaiseFrame for click-to-raise */ +#include "../../xinerama.h" /* Phase 16: wGetHeadForWindow */ +#include "../../xmodifier.h" /* Phase 77: wSetModifierMasks */ +#include "../../event.h" /* Phase 78: wWaylandKeyPress */ +#include "../x11/wmspec.h" /* Phase 30+: get_window_image_from_x11 */ +#include "../../icon.h" /* WIcon, TILE_* */ +#include "../../workspace.h" /* WWorkspace */ +#include "../../dock.h" /* WDock (for clip->collapsed) */ +#include "../../appicon.h" /* WAppIcon */ +#include "../../defaults.h" /* wDefaultsCheckDomains */ +#include "../../resources.h" /* wGetColor */ +#include "../../menu.h" /* WMenu, WMenuEntry */ +#include "../../balloon.h" /* WBalloon */ +#include "../../dialog.h" /* WMRestart, WMAbort */ +#include "../backend.h" +#include "wl_backend.h" + +/* ------------------------------------------------------------------ */ +/* WINGs private header — needed for W_EventBackend, W_Screen fields */ +#include +/* ------------------------------------------------------------------ */ +#include +#include "../../def_pixmaps.h" +struct W_Screen; +struct W_View; +void W_WL_EnqueueEvent(struct W_Screen *scr, const WMEvent *ev, struct W_View *target); +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 */ +/* ------------------------------------------------------------------ */ + +#include "wl_types.h" +static void wm_dock_app_add(struct wl_toplevel_view *v); +static void wm_dock_app_remove(WNativeWindow id); + +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); +struct wl_frame_buf *frame_buf_find(WNativeWindow id); + +/* ------------------------------------------------------------------ */ +/* Phase A: WINGs event backend wiring helpers */ +/* ------------------------------------------------------------------ */ + +/* + * Get the WMScreen (W_Screen) for screen 0. Returns NULL if StartUp() + * hasn't initialised the screen yet. + */ +static WMScreen * +wl_get_wmscreen(void) +{ + if (w_global.screen_count == 0) + return NULL; + WScreen *scr = wScreenWithNumber(0); + return scr ? scr->wmscreen : NULL; +} + +/* + * Dispatch callback for W_WL_SetLoopDispatch — ticks the Wayland compositor + * event loop so clients stay responsive during WINGs modal loops. + */ +static void +wl_wings_dispatch(int timeout_ms, void *ud) +{ + (void)ud; + if (!wl_state.display) + return; + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + wl_display_flush_clients(wl_state.display); + wl_event_loop_dispatch(loop, timeout_ms); +} + +/* ------------------------------------------------------------------ */ +/* Phase 36 / 37 / 38: XDG surface lifecycle, frame loop, view mgmt */ +/* ------------------------------------------------------------------ */ + +/* + * Phase 37: Wayland event loop timer — drives client frame callbacks. + */ +/* Called from within the event loop when the shutdown self-pipe becomes + * readable. Calling wl_display_terminate() here is safe because we are + * inside wl_event_loop_dispatch(), not in a signal handler. */ +static int +wl_shutdown_pipe_handler(int fd, uint32_t mask, void *data) +{ + char buf[16]; + (void)mask; (void)data; + /* drain the pipe so the fd does not stay readable */ + while (read(fd, buf, sizeof(buf)) > 0) + ; + wl_state.terminate = 1; + return 0; +} + +static int +frame_timer_cb(void *data) +{ + (void)data; + + if (!wl_state.startup_pump_running) { + if (!wl_state.is_nested && wl_state.output) + wlr_output_send_frame(wl_state.output); + } + + if (wl_state.frame_timer) + wl_event_source_timer_update(wl_state.frame_timer, 16); + return 0; +} + +/* Helper: recover the view from one of its embedded listeners. */ +#define VIEW_FROM_LISTENER(ptr, field) \ + ((struct wl_toplevel_view *)((char *)(ptr) - offsetof(struct wl_toplevel_view, field))) + +/* X error handler — defined near the vtable at the bottom of this file. */ +static int wl_catch_xwayland_error(Display *edpy, XErrorEvent *error); + +/* Phase 43: forward declaration — defined in the focus section below. */ +static void wl_focus_set(WNativeWindow win, int revert_to, unsigned long timestamp); +/* Phase 50: forward declaration — defined in client section below. */ +int wl_client_get_pid(WNativeWindow win); +/* Phase 13: forward declarations — defined in pointer section below. */ +static void wl_update_pointer_focus(uint32_t time_msec); +static void wl_pointer_warp_to(int x, int y); +/* wmaker-popup-v1 helper — defined in protocol section below. */ +static void wmaker_popup_apply(struct wl_toplevel_view *view); +/* Phase 14: pointer event handler forward declarations. */ +static void handle_pointer_motion(struct wl_listener *listener, void *data); +/* Phase 24: click-to-focus helper — defined in Phase 13 pointer section. */ +static struct wlr_surface *view_surface_at(int px, int py, double *sx_out, + double *sy_out, struct wl_toplevel_view **view_out); +/* Phase 24: client cursor-surface request handler. */ +static void handle_seat_request_cursor(struct wl_listener *listener, void *data); +/* Phase 25: client clipboard selection request handler. */ +static void handle_seat_request_set_selection(struct wl_listener *listener, void *data); +/* Phase 25b: drag-and-drop request handler. */ +static void handle_seat_request_start_drag(struct wl_listener *listener, void *data); +/* Phase 26: primary selection request handler. */ +static void handle_seat_request_primary_selection(struct wl_listener *listener, void *data); +/* Compositor-internal clipboard hooks for WINGs. */ +static void wl_clipboard_store_to_seat(const char *text, int len); +static char *wl_clipboard_fetch_from_seat(int *len_out); + +/* Drag icon overlay for WINGs compositor-internal DND. */ +static void wl_drag_icon_show(struct W_View *view, int x, int y); +static void wl_drag_icon_move(int x, int y); +static void wl_drag_icon_hide(void); +static void wl_drag_cursor_feedback(Bool accepted); +/* Phase 28: idle inhibitor created/destroyed handler. */ +static void handle_idle_new_inhibitor(struct wl_listener *listener, void *data); +/* Phase 29: scroll-wheel axis event handler. */ +static void handle_pointer_axis(struct wl_listener *listener, void *data); +/* Phase 30: new pointer constraint handler. */ +static void handle_new_pointer_constraint(struct wl_listener *listener, void *data); +/* Phase 31: new keyboard shortcuts inhibitor handler. */ +static void handle_new_shortcuts_inhibitor(struct wl_listener *listener, void *data); +/* Phase 33: output management apply/test + output power set_mode handlers. */ +static void handle_output_mgr_apply(struct wl_listener *listener, void *data); +static void handle_output_mgr_test(struct wl_listener *listener, void *data); +static void handle_output_power_set_mode(struct wl_listener *listener, void *data); +/* Phase 34: text input, input method, virtual keyboard new-instance handlers. */ +static void handle_text_input_new(struct wl_listener *listener, void *data); +static void handle_input_method_new(struct wl_listener *listener, void *data); +static void handle_virtual_keyboard_new(struct wl_listener *listener, void *data); +/* Phase 35: xdg_activation request_activate handler. */ +static void handle_xdg_activation_request(struct wl_listener *listener, void *data); +/* Phase 36: layer shell new_surface handler. */ +static void handle_layer_shell_new_surface(struct wl_listener *listener, void *data); +/* Phase 37: virtual pointer handler. */ +static void handle_virtual_pointer_new(struct wl_listener *listener, void *data); +/* Phase 17: XWayland new surface forward declaration. */ +static void handle_xwayland_new_surface(struct wl_listener *listener, void *data); +static void wm_dock_create(WScreen *scr); +/* Phase 20: nested backend real input device forward declaration. */ +static void handle_backend_new_input(struct wl_listener *listener, void *data); +/* Phase 18: output lifecycle and frame rendering forward declarations. */ +static void handle_backend_new_output(struct wl_listener *listener, void *data); +static void handle_output_frame(struct wl_listener *listener, void *data); +static void handle_pointer_motion_absolute(struct wl_listener *listener, void *data); +static void handle_pointer_button(struct wl_listener *listener, void *data); +static void handle_pointer_frame(struct wl_listener *listener, void *data); + +/* Phase 41: look up the view for a given synthetic Window ID. */ +struct wl_toplevel_view * +wl_find_view_by_id(WNativeWindow id) +{ + struct wl_toplevel_view *v; + wl_list_for_each(v, &wl_state.toplevel_list, link) { + if (v->id == id) + return v; + } + return NULL; +} + +/* Phase 65: look up the view for a given wlr_xdg_surface pointer. */ +struct wl_toplevel_view * +wl_find_view_by_xdg_surface(struct wlr_xdg_surface *surf) +{ + struct wl_toplevel_view *v; + wl_list_for_each(v, &wl_state.toplevel_list, link) { + if (v->xdg_surface == surf) + return v; + } + return NULL; +} + +/* + * Phase 46: XDG toplevel title changed — update wwin->wm_instance. + */ +static void +handle_toplevel_title(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, title_listener); + (void)data; + const char *new_title = view->xdg_surface->toplevel->title; + + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + + if (wwin->wm_instance) + wfree(wwin->wm_instance); + wwin->wm_instance = new_title ? strdup(new_title) : NULL; + + if (wwin->wm_class) + wfree(wwin->wm_class); + const char *app_id = (view->xdg_surface->toplevel->app_id) + ? view->xdg_surface->toplevel->app_id : "wayland-client"; + wwin->wm_class = strdup(app_id); + + wWindowUpdateName(wwin, new_title); +} + +/* + * Phase 23: set the cursor image by name from the loaded xcursor theme. + * No-op if cursor or xcursor_mgr was not initialised. + */ +static void +wl_set_cursor_image(const char *name) +{ + if (!wl_state.cursor || !wl_state.xcursor_mgr) + return; + wlr_cursor_set_xcursor(wl_state.cursor, wl_state.xcursor_mgr, name); +} + +/* + * Phase 23: map WL_SHELL_SURFACE_RESIZE_* edge bitmask to an xcursor name. + */ +static const char * +resize_edges_to_cursor(uint32_t edges) +{ + switch (edges & (WL_SHELL_SURFACE_RESIZE_TOP | WL_SHELL_SURFACE_RESIZE_BOTTOM | + WL_SHELL_SURFACE_RESIZE_LEFT | WL_SHELL_SURFACE_RESIZE_RIGHT)) { + case WL_SHELL_SURFACE_RESIZE_TOP: return "n-resize"; + case WL_SHELL_SURFACE_RESIZE_BOTTOM: return "s-resize"; + case WL_SHELL_SURFACE_RESIZE_LEFT: return "w-resize"; + case WL_SHELL_SURFACE_RESIZE_RIGHT: return "e-resize"; + case WL_SHELL_SURFACE_RESIZE_TOP_LEFT: return "nw-resize"; + case WL_SHELL_SURFACE_RESIZE_TOP_RIGHT: return "ne-resize"; + case WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT: return "sw-resize"; + case WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT: return "se-resize"; + default: return "se-resize"; + } +} + +/* + * Phase 22: interactive grab helpers. + * + * wl_grab_begin_move — called from handle_toplevel_request_move to start + * a compositor-side move grab. Records the pointer position and window + * origin so that subsequent motion events can compute the displacement. + * + * wl_grab_begin_resize — same for resize; additionally records the window + * size and the edge bitmask so we know which sides to stretch. + * + * wl_grab_end — clears the grab state (called on button release). + * + * wl_grab_apply_motion — called from both pointer motion handlers to + * update the window position or size while the grab is active. + */ +static void +wl_grab_end(void) +{ + wl_state.grab_type = 0; + wl_state.grab_view = NULL; + /* Phase 23: restore default arrow cursor when grab ends. */ + wl_set_cursor_image("left_ptr"); +} + +static void +wl_grab_begin_move(struct wl_toplevel_view *view) +{ + int w, h; + if (!view) return; + + /* Retrieve current window geometry. */ + if (view->xw_surface) { + w = view->xw_surface->width; + h = view->xw_surface->height; + } else if (view->xdg_surface && view->xdg_surface->surface) { + w = view->xdg_surface->surface->current.width; + h = view->xdg_surface->surface->current.height; + } else { w = 1; h = 1; } + + wl_state.grab_type = 1; /* MOVE */ + wl_state.grab_view = view; + wl_state.grab_start_ptr_x = wl_state.pointer_x; + wl_state.grab_start_ptr_y = wl_state.pointer_y; + wl_state.grab_start_win_x = view->x; + wl_state.grab_start_win_y = view->y; + wl_state.grab_start_win_w = w; + wl_state.grab_start_win_h = h; + wwarning("wl_backend: move grab started for id=0x%lx at ptr=%d,%d win=%d,%d", + view->id, wl_state.pointer_x, wl_state.pointer_y, view->x, view->y); + /* Clear seat pointer focus so motion events are not delivered to the + * client while we are repositioning it. */ + if (wl_state.seat) + wlr_seat_pointer_clear_focus(wl_state.seat); + /* Phase 23: show a move cursor during the grab. */ + wl_set_cursor_image("fleur"); +} + +static void +wl_grab_begin_resize(struct wl_toplevel_view *view, uint32_t edges) +{ + int w, h; + if (!view) return; + + if (view->xw_surface) { + w = view->xw_surface->width; + h = view->xw_surface->height; + } else if (view->xdg_surface && view->xdg_surface->surface) { + w = view->xdg_surface->surface->current.width; + h = view->xdg_surface->surface->current.height; + } else { w = 1; h = 1; } + + wl_state.grab_type = 2; /* RESIZE */ + wl_state.grab_view = view; + wl_state.grab_start_ptr_x = wl_state.pointer_x; + wl_state.grab_start_ptr_y = wl_state.pointer_y; + wl_state.grab_start_win_x = view->x; + wl_state.grab_start_win_y = view->y; + wl_state.grab_start_win_w = w; + wl_state.grab_start_win_h = h; + wl_state.grab_resize_edges = edges; + wwarning("wl_backend: resize grab started for id=0x%lx edges=0x%x size=%dx%d", + view->id, edges, w, h); + if (wl_state.seat) + wlr_seat_pointer_clear_focus(wl_state.seat); + /* Phase 23: show an appropriate resize cursor during the grab. */ + wl_set_cursor_image(resize_edges_to_cursor(edges)); +} + +/* Called from both pointer-motion handlers (relative + absolute). */ +static void +wl_grab_apply_motion(void) +{ + if (!wl_state.grab_type || !wl_state.grab_view) + return; + + struct wl_toplevel_view *v = wl_state.grab_view; + int dx = wl_state.pointer_x - wl_state.grab_start_ptr_x; + int dy = wl_state.pointer_y - wl_state.grab_start_ptr_y; + + if (wl_state.grab_type == 1) { + /* MOVE: simply reposition the window. */ + int nx = wl_state.grab_start_win_x + dx; + int ny = wl_state.grab_start_win_y + dy; + v->x = nx; v->y = ny; + if (v->frame_id) { + /* Framed window: move the frame tree. The client scene_node + * is a child of the tree and moves with it automatically. */ + struct wl_frame_buf *fb = frame_buf_find(v->frame_id); + if (fb && fb->tree) { + int frame_x = nx - v->frame_rel_x; + int frame_y = ny - v->frame_rel_y; + fb->x = frame_x; + fb->y = frame_y; + wlr_scene_node_set_position(&fb->tree->node, frame_x, frame_y); + /* Update child absolute tracking */ + struct wl_frame_buf *child; + wl_list_for_each(child, &wl_state.frame_list, link) { + if (child->parent_id == v->frame_id) { + child->x = fb->x + child->rel_x; + child->y = fb->y + child->rel_y; + } + } + } + } else if (v->scene_node) { + wlr_scene_node_set_position(&v->scene_node->node, nx, ny); + } + if (v->xw_surface) + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)nx, (int16_t)ny, + v->xw_surface->width, v->xw_surface->height); + + } else if (wl_state.grab_type == 2) { + /* RESIZE: compute new size (and origin for left/top edges). */ + uint32_t edges = wl_state.grab_resize_edges; + int nx = wl_state.grab_start_win_x; + int ny = wl_state.grab_start_win_y; + int nw = wl_state.grab_start_win_w; + int nh = wl_state.grab_start_win_h; + + if (edges & WL_SHELL_SURFACE_RESIZE_RIGHT) + nw = wl_state.grab_start_win_w + dx; + if (edges & WL_SHELL_SURFACE_RESIZE_LEFT) { + nw = wl_state.grab_start_win_w - dx; + nx = wl_state.grab_start_win_x + dx; + } + if (edges & WL_SHELL_SURFACE_RESIZE_BOTTOM) + nh = wl_state.grab_start_win_h + dy; + if (edges & WL_SHELL_SURFACE_RESIZE_TOP) { + nh = wl_state.grab_start_win_h - dy; + ny = wl_state.grab_start_win_y + dy; + } + + if (nw < 32) { nw = 32; } + if (nh < 32) { nh = 32; } + + /* Use wWindowConfigure to update the WM frame (titlebar/resizebar) + * along with the client surface. */ + WObjDescriptor *desc = wm_backend->context_find(v->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + /* Ensure client scene_node is parented under frame tree */ + if (v->scene_node && v->frame_id) { + struct wl_frame_buf *ffb = frame_buf_find(v->frame_id); + if (ffb && ffb->tree) { + if (v->scene_node->node.parent != ffb->tree) { + wwarning("REPARENT FIX: scene_node parent=%p tree=%p", + (void*)v->scene_node->node.parent, (void*)ffb->tree); + wlr_scene_node_reparent(&v->scene_node->node, ffb->tree); + wlr_scene_node_set_position(&v->scene_node->node, + v->frame_rel_x, v->frame_rel_y); + } + } + } + int frame_x = nx; + int frame_y = ny; + if (wwin->frame) + frame_y = ny - wwin->frame->top_width; + wWindowConfigure(wwin, frame_x, frame_y, nw, nh); + } else { + /* Fallback for unmanaged surfaces: direct scene manipulation. */ + v->x = nx; v->y = ny; + if (v->scene_node) + wlr_scene_node_set_position(&v->scene_node->node, nx, ny); + if (v->xw_surface) { + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)nx, (int16_t)ny, (uint16_t)nw, (uint16_t)nh); + } else if (v->xdg_surface) { + wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)nw, (uint32_t)nh); + wlr_xdg_surface_schedule_configure(v->xdg_surface); + } + } + } +} + +/* + * Phase 15/22: xdg_toplevel request_move — client requests interactive move. + * Phase 22: starts a compositor-side move grab. + */ +static void +handle_toplevel_request_move(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_move_listener); + (void)data; + wl_grab_begin_move(view); +} + +/* + * Phase 15/22: xdg_toplevel request_resize — client requests interactive resize. + * Phase 22: starts a compositor-side resize grab. + */ +static void +handle_toplevel_request_resize(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_resize_listener); + struct wlr_xdg_toplevel_resize_event *ev = data; + wl_grab_begin_resize(view, ev ? ev->edges : WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT); +} + +/* + * Phase 16: xdg_toplevel request_maximize — toggle maximize state. + */ +static void +handle_toplevel_request_maximize(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_maximize_listener); + (void)data; + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + if (wwin->flags.maximized) + wUnmaximizeWindow(wwin); + else + wMaximizeWindow(wwin, MAX_VERTICAL | MAX_HORIZONTAL, wGetHeadForWindow(wwin)); +} + +/* + * Phase 16: xdg_toplevel request_fullscreen. + */ +static void +handle_toplevel_request_fullscreen(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_fullscreen_listener); + (void)data; + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + if (!wwin->flags.fullscreen) + wFullscreenWindow(wwin); +} + +/* + * Phase 16: xdg_toplevel request_minimize — iconify the window. + */ +static void +handle_toplevel_request_minimize(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_minimize_listener); + (void)data; + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + if (!wwin->flags.miniaturized) + wIconifyWindow(wwin); +} + +/* + * Phase 16: xdg_toplevel set_app_id — update wm_class. + */ +static void +handle_toplevel_set_app_id(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, set_app_id_listener); + (void)data; + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + const char *app_id = view->xdg_surface->toplevel->app_id; + if (!app_id) + return; + if (wwin->wm_class) + wfree(wwin->wm_class); + wwin->wm_class = strdup(app_id); +} + +/* + * Phase 38: toplevel map — surface is ready to be managed. + */ +static void +handle_toplevel_scene_destroy(struct wl_listener *listener, void *data) +{ + (void)data; + struct wl_toplevel_view *view = + VIEW_FROM_LISTENER(listener, scene_destroy_listener); + /* wlroots is destroying view->scene_node (typically because its + * parent tree — the WMaker frame's fb->tree — is being destroyed + * as part of unmanage). NULL the pointer so the later + * handle_toplevel_destroy doesn't try to redestroy it. */ + view->scene_node = NULL; + wl_list_remove(&view->scene_destroy_listener.link); +} + +static void +view_attach_scene_destroy_listener(struct wl_toplevel_view *view) +{ + if (!view || !view->scene_node) return; + view->scene_destroy_listener.notify = handle_toplevel_scene_destroy; + wl_signal_add(&view->scene_node->node.events.destroy, + &view->scene_destroy_listener); +} + +static void +handle_toplevel_map(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, map_listener); + (void)data; + + wwarning("wl_backend: toplevel mapped id=0x%lx app_id=%s title=%s", + view->id, + view->xdg_surface->toplevel->app_id ? view->xdg_surface->toplevel->app_id : "(none)", + view->xdg_surface->toplevel->title ? view->xdg_surface->toplevel->title : "(none)"); + + /* Detect popup-style WINGs windows (menu editor items, etc.) — these + * need borderless presentation, no taskbar entry, and free positioning. */ + const char *app_id_check = view->xdg_surface->toplevel->app_id; + int is_popup = (app_id_check && strcmp(app_id_check, "wings-popup") == 0); + + /* Phase 39/41: register the surface as a managed window. */ + { + WScreen *scr = wScreenWithNumber(0); + if (scr) { + WWindow *wwin = wManageWindow(scr, view->id); + if (wwin) { + if (is_popup) { + /* Set borderless + skip-taskbar attributes */ + wwin->client_flags.no_border = 1; + wwin->client_flags.no_titlebar = 1; + wwin->client_flags.no_closable = 1; + wwin->client_flags.no_miniaturizable = 1; + wwin->client_flags.no_resizable = 1; + wwin->client_flags.no_close_button = 1; + wwin->client_flags.no_miniaturize_button = 1; + wwin->client_flags.no_resizebar = 1; + wwin->client_flags.skip_window_list = 1; + wwin->client_flags.skip_switchpanel = 1; + wwin->user_flags = wwin->client_flags; + /* Rebuild frame without decorations */ + extern void wWindowConfigureBorders(WWindow *wwin); + wWindowConfigureBorders(wwin); + /* If the client requested a position via wmaker-popup-v1, + * apply it now that the frame is set up. */ + if (view->popup) + wmaker_popup_apply(view); + } + int pid = wl_client_get_pid(view->id); + /* Phase 62: focus delivered by wManageWindow via wSetFocusTo + * (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. + * + * 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( + _vp, view->xdg_surface->surface); + if (view->scene_node) { + view_attach_scene_destroy_listener(view); + /* 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); + /* Position at relative offset within frame tree */ + wlr_scene_node_set_position(&view->scene_node->node, + view->frame_rel_x, view->frame_rel_y); + } else { + 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 + * delivered to its surface. The warp calls + * wl_update_pointer_focus() internally. */ + { + int cx = view->x + wwin->client.width / 2; + int cy = view->y + wwin->client.height / 2; + wl_pointer_warp_to(cx, cy); + } + + /* Phase 35: create a foreign toplevel handle so taskbars + * and docks (e.g. wlr-taskbar, waybar) can enumerate and + * control this window. */ + if (wl_state.foreign_toplevel_mgr && !view->ft_handle) { + view->ft_handle = + wlr_foreign_toplevel_handle_v1_create( + wl_state.foreign_toplevel_mgr); + if (view->ft_handle) { + const char *title = wwin->wm_class ? wwin->wm_class : ""; + const char *app_id = wwin->wm_instance ? wwin->wm_instance : ""; + wlr_foreign_toplevel_handle_v1_set_title(view->ft_handle, title); + wlr_foreign_toplevel_handle_v1_set_app_id(view->ft_handle, app_id); + if (wl_state.output) + wlr_foreign_toplevel_handle_v1_output_enter( + view->ft_handle, wl_state.output); + wlr_foreign_toplevel_handle_v1_set_activated(view->ft_handle, true); + } + } + + /* Phase 30+: add to dock icon list */ + wm_dock_app_add(view); + } + } + } +} + +/* + * Phase 40: toplevel unmap — surface hidden or destroyed; unmanage the window. + */ +static void +handle_toplevel_unmap(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, unmap_listener); + (void)data; + + wwarning("wl_backend: toplevel unmapped id=0x%lx", view->id); + + /* Phase 35: destroy the foreign toplevel handle so taskbars stop + * showing this window in their list. */ + if (view->ft_handle) { + wlr_foreign_toplevel_handle_v1_destroy(view->ft_handle); + view->ft_handle = NULL; + } + + /* Phase 30+: remove from dock icon list */ + wm_dock_app_remove(view->id); + + /* Phase 40: remove the WWindow from the WM's managed list. */ + WScreen *scr = wScreenWithNumber(0); + if (scr) { + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + wUnmanageWindow(wwin, False, True); + } + } +} + +/* + * Phase 38: toplevel destroy — free the view. + */ +static void +handle_toplevel_destroy(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener); + (void)data; + + /* Remove all listeners registered with xdg_surface / xdg_toplevel signals. + * Failure to remove a listener before freeing the view leaves a dangling + * pointer in wlroots' signal list; when wlroots later emits or cleans up + * those signals (e.g. during wl_display_destroy) it corrupts the heap. */ + wl_list_remove(&view->map_listener.link); + wl_list_remove(&view->unmap_listener.link); + wl_list_remove(&view->destroy_listener.link); + wl_list_remove(&view->title_listener.link); + wl_list_remove(&view->request_move_listener.link); + wl_list_remove(&view->request_resize_listener.link); + wl_list_remove(&view->request_maximize_listener.link); + wl_list_remove(&view->request_fullscreen_listener.link); + wl_list_remove(&view->request_minimize_listener.link); + wl_list_remove(&view->set_app_id_listener.link); + wl_list_remove(&view->commit_listener.link); + wl_list_remove(&view->surface_commit_listener.link); + wl_list_remove(&view->link); + /* Phase 18: remove scene node. */ + if (view->scene_node) { + /* Remove our destroy listener BEFORE destroying so the listener + * doesn't fire and try to remove itself from a list we're + * already cleaning up. */ + wl_list_remove(&view->scene_destroy_listener.link); + wlr_scene_node_destroy(&view->scene_node->node); + view->scene_node = NULL; + } + wl_state.managed_surface_count--; + if (view->wm_state) wfree(view->wm_state); /* Phase 56 */ + if (view->wm_command) { /* Phase 57 */ + int ci; + for (ci = 0; ci < view->wm_command_n; ci++) + wfree(view->wm_command[ci]); + wfree(view->wm_command); + } + wfree(view); +} + +/* + * Phase 64: new_toplevel_decoration listener. + * Respond with CLIENT_SIDE so clients draw their own decorations. + * Must defer until the surface is initialized (wlroots 0.19). + */ +struct wl_deferred_decoration { + struct wl_listener commit_listener; + struct wlr_xdg_toplevel_decoration_v1 *deco; +}; + +static void +handle_deferred_decoration_commit(struct wl_listener *listener, void *data) +{ + struct wl_deferred_decoration *dd = + wl_container_of(listener, dd, commit_listener); + struct wlr_xdg_surface *surface = dd->deco->toplevel->base; + (void)data; + + if (!surface->initial_commit) + return; + + wl_list_remove(&dd->commit_listener.link); + wlr_xdg_toplevel_decoration_v1_set_mode(dd->deco, + WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + wfree(dd); +} + +static void +handle_new_toplevel_decoration(struct wl_listener *listener, void *data) +{ + struct wlr_xdg_toplevel_decoration_v1 *deco = data; + (void)listener; + + struct wlr_xdg_surface *surface = deco->toplevel->base; + if (surface->initialized) { + wlr_xdg_toplevel_decoration_v1_set_mode(deco, + WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + } else { + struct wl_deferred_decoration *dd = wmalloc(sizeof(*dd)); + dd->deco = deco; + dd->commit_listener.notify = handle_deferred_decoration_commit; + wl_signal_add(&surface->surface->events.commit, &dd->commit_listener); + } +} + +/* ------------------------------------------------------------------ */ +/* wmaker-popup-v1 protocol — client-driven popup positioning */ +/* ------------------------------------------------------------------ */ + +static void +wmaker_popup_handle_destroy(struct wl_client *client, + struct wl_resource *resource) +{ + (void)client; + wl_resource_destroy(resource); +} + +static void +wmaker_popup_handle_set_parent(struct wl_client *client, + struct wl_resource *resource, + struct wl_resource *parent_resource) +{ + (void)client; + struct wl_toplevel_view *view = wl_resource_get_user_data(resource); + if (!view) return; + if (parent_resource) { + view->popup_parent = wlr_xdg_toplevel_from_resource(parent_resource); + } else { + view->popup_parent = NULL; + } + wwarning("wmaker_popup: set_parent view=%p parent=%p", (void*)view, + (void*)view->popup_parent); +} + +static void +wmaker_popup_handle_set_position(struct wl_client *client, + struct wl_resource *resource, + int32_t x, int32_t y) +{ + (void)client; + struct wl_toplevel_view *view = wl_resource_get_user_data(resource); + wwarning("wmaker_popup: set_position view=%p (%d,%d)", (void*)view, x, y); + if (!view) return; + view->popup_x = (int)x; + view->popup_y = (int)y; + wmaker_popup_apply(view); +} + +static const struct wmaker_popup_v1_interface wmaker_popup_impl = { + .destroy = wmaker_popup_handle_destroy, + .set_parent = wmaker_popup_handle_set_parent, + .set_position = wmaker_popup_handle_set_position, +}; + +static void +wmaker_popup_resource_destroy(struct wl_resource *resource) +{ + struct wl_toplevel_view *view = wl_resource_get_user_data(resource); + if (view && view->popup == resource) + view->popup = NULL; +} + +static void +wmaker_popup_apply(struct wl_toplevel_view *view) +{ + if (!view || !view->popup) return; + + /* If a parent was set, translate the requested position into screen + * coordinates by adding the parent toplevel's current position. */ + int target_x = view->popup_x; + int target_y = view->popup_y; + if (view->popup_parent) { + struct wl_toplevel_view *parent_view = + wl_find_view_by_xdg_surface(view->popup_parent->base); + if (parent_view) { + target_x += parent_view->x; + target_y += parent_view->y; + } + } + + int dx = target_x - view->x; + int dy = target_y - view->y; + wwarning("wmaker_popup_apply: view=%p (%d,%d)->(%d,%d) frame_id=0x%lx scene_node=%p", + (void*)view, view->x, view->y, target_x, target_y, + (unsigned long)view->frame_id, (void*)view->scene_node); + + view->x = target_x; + view->y = target_y; + + /* If a frame_buf wraps this view, move it via the WM's window-move + * path so the frame and its tree node update consistently. */ + if (view->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(view->frame_id); + if (fb) { + fb->x += dx; + fb->y += dy; + if (fb->tree) { + wlr_scene_node_set_position(&fb->tree->node, fb->x, fb->y); + /* Raise above siblings so popups appear over their parent */ + wlr_scene_node_raise_to_top(&fb->tree->node); + wwarning("popup_apply: raised frame tree id=0x%lx to top, parent=%p", + (unsigned long)view->id, (void*)fb->tree->node.parent); + } else if (fb->scene_buf) { + wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); + wlr_scene_node_raise_to_top(&fb->scene_buf->node); + } + view->frame_rel_x = 0; + view->frame_rel_y = 0; + } + } else if (view->scene_node) { + wlr_scene_node_set_position(&view->scene_node->node, + view->x, view->y); + wlr_scene_node_raise_to_top(&view->scene_node->node); + wwarning("popup_apply: raised scene_node id=0x%lx to top", + (unsigned long)view->id); + } +} + +static void +wmaker_popup_manager_handle_destroy(struct wl_client *client, + struct wl_resource *resource) +{ + (void)client; + wl_resource_destroy(resource); +} + +static void +wmaker_popup_manager_handle_get_popup(struct wl_client *client, + struct wl_resource *manager_resource, + uint32_t id, + struct wl_resource *toplevel_resource) +{ + (void)manager_resource; + struct wlr_xdg_toplevel *xdg_top = + wlr_xdg_toplevel_from_resource(toplevel_resource); + if (!xdg_top) { + wl_resource_post_error(manager_resource, 0, + "wmaker_popup: invalid xdg_toplevel resource"); + return; + } + + struct wl_toplevel_view *view = + wl_find_view_by_xdg_surface(xdg_top->base); + + struct wl_resource *resource = wl_resource_create(client, + &wmaker_popup_v1_interface, 1, id); + if (!resource) { + wl_client_post_no_memory(client); + return; + } + wl_resource_set_implementation(resource, &wmaker_popup_impl, + view, wmaker_popup_resource_destroy); + + if (view) + view->popup = resource; +} + +static const struct wmaker_popup_manager_v1_interface wmaker_popup_manager_impl = { + .destroy = wmaker_popup_manager_handle_destroy, + .get_popup = wmaker_popup_manager_handle_get_popup, +}; + +static void +wmaker_popup_manager_bind(struct wl_client *client, void *data, + uint32_t version, uint32_t id) +{ + (void)data; + struct wl_resource *resource = wl_resource_create(client, + &wmaker_popup_manager_v1_interface, + version > 1 ? 1 : (int)version, id); + if (!resource) { + wl_client_post_no_memory(client); + return; + } + wl_resource_set_implementation(resource, &wmaker_popup_manager_impl, + NULL, NULL); +} + +/* ------------------------------------------------------------------ */ +/* Phase 17: XWayland surface lifecycle */ +/* ------------------------------------------------------------------ */ + +/* --- Override-redirect (unmanaged) XWayland surface handlers --- */ + +static void +handle_or_map(struct wl_listener *listener, void *data) +{ + struct wl_or_view *ov = wl_container_of(listener, ov, map_listener); + struct wlr_xwayland_surface *xw = ov->xw_surface; + (void)data; + + /* Skip scene operations while the startup pump thread is running — + * wlroots is not thread-safe. The scene node will be created when + * the surface is next committed after the pump thread exits. */ + if (wl_state.startup_pump_running) + return; + + if (wl_state.scene && !ov->scene_node && 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); + } +} + +static void +handle_or_unmap(struct wl_listener *listener, void *data) +{ + struct wl_or_view *ov = wl_container_of(listener, ov, unmap_listener); + (void)data; + + if (ov->scene_node) { + wlr_scene_node_destroy(&ov->scene_node->node); + ov->scene_node = NULL; + } +} + +static void +handle_or_destroy(struct wl_listener *listener, void *data) +{ + struct wl_or_view *ov = wl_container_of(listener, ov, destroy_listener); + (void)data; + + wl_list_remove(&ov->map_listener.link); + wl_list_remove(&ov->unmap_listener.link); + wl_list_remove(&ov->destroy_listener.link); + wl_list_remove(&ov->set_geometry_listener.link); + wl_list_remove(&ov->link); + if (ov->scene_node) + wlr_scene_node_destroy(&ov->scene_node->node); + wfree(ov); +} + +static void +handle_or_set_geometry(struct wl_listener *listener, void *data) +{ + struct wl_or_view *ov = wl_container_of(listener, ov, set_geometry_listener); + struct wlr_xwayland_surface *xw = ov->xw_surface; + (void)data; + if (ov->scene_node) + wlr_scene_node_set_position(&ov->scene_node->node, xw->x, xw->y); +} + +/* --- Managed XWayland surface handlers --- */ + +/* + * Path 1: handle_xwayland_set_geometry — X11 window moved/resized. + * Keeps the scene node in sync with the X11-authoritative position. + */ +static void +handle_xwayland_set_geometry(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = + wl_container_of(listener, view, xw_geometry_listener); + struct wlr_xwayland_surface *xw = view->xw_surface; + (void)data; + if (view->scene_node) { + if (view->frame_id) { + /* Framed: position relative to frame tree */ + wlr_scene_node_set_position(&view->scene_node->node, + view->frame_rel_x, view->frame_rel_y); + } else { + wlr_scene_node_set_position(&view->scene_node->node, xw->x, xw->y); + } + } +} + +/* + * handle_xwayland_surface_map — wlr_surface has mapped (committed first buffer). + * Now we can add to scene graph and manage as a WM client. + */ +static void +handle_xwayland_surface_map(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = + wl_container_of(listener, view, xw_surface_map_listener); + (void)data; + struct wlr_xwayland_surface *xw = view->xw_surface; + + wwarning("wl_backend: xwayland surface mapped id=0x%lx class=%s title=%s", + view->id, + xw->class ? xw->class : "(none)", + xw->title ? xw->title : "(none)"); + + /* Add to scene graph for compositing. */ + if (wl_state.scene && !view->scene_node && 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) { + view_attach_scene_destroy_listener(view); + wlr_scene_node_set_position(&view->scene_node->node, view->x, view->y); + } + } + + /* Manage as WM client. */ + if (w_global.screen_count == 0) + return; + WScreen *scr = wScreenWithNumber(0); + if (scr) { + WWindow *wwin = wManageWindow(scr, view->id); + if (wwin) { + wwarning("wl_backend: wManageWindow(xwayland) succeeded id=0x%lx " + "class=%s name=%s geo=%dx%d pid=%d", + view->id, + wwin->wm_class ? wwin->wm_class : "(none)", + wwin->wm_instance ? wwin->wm_instance : "(none)", + wwin->client.width, wwin->client.height, (int)xw->pid); + int cx = view->x + wwin->client.width / 2; + int cy = view->y + wwin->client.height / 2; + wl_pointer_warp_to(cx, cy); + + wm_dock_app_add(view); + } + } +} + +/* + * handle_xwayland_associate — wlr_surface attached to XWayland surface. + * Hook surface->events.map to know when it actually has content. + */ +static void +handle_xwayland_associate(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, map_listener); + (void)data; + struct wlr_xwayland_surface *xw = view->xw_surface; + + wwarning("wl_backend: xwayland associate id=0x%lx surface=%p", + view->id, (void*)xw->surface); + + if (xw->surface) { + view->xw_surface_map_listener.notify = handle_xwayland_surface_map; + wl_signal_add(&xw->surface->events.map, &view->xw_surface_map_listener); + } +} + +/* + * handle_xwayland_unmap — X11 window hidden. + * Path 1: just remove the scene node; WindowMaker handles unmanagement. + * Pure Wayland: unmanage via wUnmanageWindow. + */ +static void +handle_xwayland_unmap(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, unmap_listener); + (void)data; + + wwarning("wl_backend: xwayland unmapped id=0x%lx", view->id); + + /* Remove surface map listener since the surface is being dissociated */ + if (view->xw_surface_map_listener.link.next && + view->xw_surface_map_listener.link.next != &view->xw_surface_map_listener.link) + wl_list_remove(&view->xw_surface_map_listener.link); + wl_list_init(&view->xw_surface_map_listener.link); + + /* Phase 30+: remove from dock icon list */ + wm_dock_app_remove(view->id); + + if (view->scene_node) { + wlr_scene_node_destroy(&view->scene_node->node); + view->scene_node = NULL; + } + + if (w_global.screen_count == 0) + return; + WScreen *scr = wScreenWithNumber(0); + if (scr) { + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + wUnmanageWindow(wwin, False, True); + } + } +} + +/* + * handle_xwayland_destroy — X11 window destroyed; free the view. + */ +static void +handle_xwayland_destroy(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener); + (void)data; + + wl_list_remove(&view->map_listener.link); + wl_list_remove(&view->unmap_listener.link); + wl_list_remove(&view->destroy_listener.link); + wl_list_remove(&view->title_listener.link); + if (view->xw_surface_map_listener.link.next && + view->xw_surface_map_listener.link.next != &view->xw_surface_map_listener.link) + wl_list_remove(&view->xw_surface_map_listener.link); + if (view->xw_geometry_listener.link.next) + wl_list_remove(&view->xw_geometry_listener.link); + wl_list_remove(&view->link); + /* Phase 18: remove scene node (may already be gone from unmap). */ + if (view->scene_node) { + wlr_scene_node_destroy(&view->scene_node->node); + view->scene_node = NULL; + } + wl_state.managed_surface_count--; + if (view->wm_state) wfree(view->wm_state); + if (view->wm_command) { + int ci; + for (ci = 0; ci < view->wm_command_n; ci++) + wfree(view->wm_command[ci]); + wfree(view->wm_command); + } + wfree(view); +} + +/* + * handle_xwayland_set_class — X11 window WM_CLASS changed. + */ +static void +handle_xwayland_set_class(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, title_listener); + (void)data; + struct wlr_xwayland_surface *xw = view->xw_surface; + + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (!desc || desc->parent_type != WCLASS_WINDOW) + return; + WWindow *wwin = (WWindow *)desc->parent; + if (wwin->wm_class) wfree(wwin->wm_class); + if (wwin->wm_instance) wfree(wwin->wm_instance); + wwin->wm_class = xw->class ? strdup(xw->class) : NULL; + wwin->wm_instance = xw->instance ? strdup(xw->instance) : NULL; +} + +/* + * Path 1: handle_xwayland_server_destroy — fired when the XWayland server + * process exits before or without firing the ready signal (startup failure). + * Set the failure flag so the pump loop can exit early and clean up. + */ +static void +handle_xwayland_server_destroy(struct wl_listener *listener, void *data) +{ + (void)listener; + (void)data; + wwarning("wl_backend: XWayland server exited"); + wl_state.xwayland_ready = true; /* break the wait loop */ +} + +static void +handle_xwayland_ready(struct wl_listener *listener, void *data) +{ + (void)listener; + (void)data; + + if (!wl_state.xwayland || !wl_state.xwayland->display_name) { + wwarning("wl_backend: XWayland ready but display_name unavailable"); + wl_state.xwayland_ready = true; + return; + } + + /* Advertise the XWayland display so X11 client apps can connect. */ + setenv("DISPLAY", wl_state.xwayland->display_name, 1); + wwarning("wl_backend: XWayland ready on %s — opening WM Xlib connection", + wl_state.xwayland->display_name); + + /* Open the WM's own Xlib connection to its XWayland instance. + * This Display* is returned from wl_display_open so WINGs, title bars, + * root menus, and the dock all work via the X11 code path. */ + wl_state.x_display = XOpenDisplay(wl_state.xwayland->display_name); + if (!wl_state.x_display) + wwarning("wl_backend: XOpenDisplay(%s) failed — WM UI limited to native Wayland", + wl_state.xwayland->display_name); + + wl_state.xwayland_ready = true; +} + +/* + * handle_xwayland_new_surface — called when a new X11 window connects. + * Allocates a wl_toplevel_view, uses window_id as the Window ID, + * and wires the lifecycle listeners to XWayland signals. + */ +static void +handle_xwayland_new_surface(struct wl_listener *listener, void *data) +{ + struct wlr_xwayland_surface *xw = data; + (void)listener; + + /* Override_redirect windows (WM frames, dock icons, menus, tooltips): + * not managed as ICCCM clients but must be composited. Track them in + * or_list and add to the scene graph on map. */ + if (xw->override_redirect) { + /* Skip the rcontext drawable — it's an offscreen pixmap holder. */ + if (wl_state.rcontext_xwin && + xw->window_id == (xcb_window_t)wl_state.rcontext_xwin) + return; + + struct wl_or_view *ov = wmalloc(sizeof(*ov)); + memset(ov, 0, sizeof(*ov)); + ov->xw_surface = xw; + + ov->map_listener.notify = handle_or_map; + ov->unmap_listener.notify = handle_or_unmap; + ov->destroy_listener.notify = handle_or_destroy; + ov->set_geometry_listener.notify = handle_or_set_geometry; + + wl_signal_add(&xw->events.destroy, &ov->destroy_listener); + wl_signal_add(&xw->events.associate, &ov->map_listener); + wl_signal_add(&xw->events.dissociate, &ov->unmap_listener); + wl_signal_add(&xw->events.set_geometry, &ov->set_geometry_listener); + + wl_list_insert(&wl_state.or_list, &ov->link); + + /* If already associated (surface exists), add to scene now. */ + if (xw->surface) { + if (wl_state.scene) { + 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); + } + } + return; + } + + /* Skip the wrlib RContext drawable window. It is created by RCreateContext + * without override_redirect, but is purely an internal offscreen drawable + * with no WM_CLASS and no backing wl_surface. XWM managing it triggers + * RENDER picture operations that fail (BadDrawable/RenderBadPicture) and + * corrupt XWM state → broken pipe. This check must come before the + * screen_count guard because we record rcontext_xwin in screen_alloc_gcs + * (called during wScreenInit at screen_count==0), and XWM may emit the + * new_surface event for it after screen_count becomes 1. */ + if (wl_state.rcontext_xwin && xw->window_id == (xcb_window_t)wl_state.rcontext_xwin) { + wwarning("wl_backend: xwayland rcontext drawable 0x%x skipped", + (unsigned)xw->window_id); + return; + } + + /* Guard: during startup the pump thread dispatches Wayland events while + * WMCreateScreenWithRContext runs on the main thread. WINGs creates + * internal X windows then, triggering this callback from the pump thread. + * wlroots is not thread-safe — calling wlr_xwayland_surface_configure + * from a non-main thread crashes. Real app windows arrive only after + * screen_count > 0. */ + if (w_global.screen_count == 0) { + wwarning("wl_backend: xwayland new surface 0x%x during startup — skipped", + (unsigned)xw->window_id); + return; + } + + /* Skip windows with no WM_CLASS and no title: these are internal + * WINGs/wrlib helper windows that XWM has processed late (after + * screen_count > 0 due to pump-thread timing). Managing them causes + * frame-window creation on a null WNativeWindow → BadDrawable cascade + * that corrupts XWayland state and eventually triggers an XIO error. + * Real application windows always have WM_CLASS set before they map. + * If WM_CLASS is set later, the set_class signal will NOT re-trigger + * management — apps that misbehave this way simply won't be managed, + * which is better than a broken-pipe crash. */ + if (!xw->class && !xw->title) { + wwarning("wl_backend: xwayland surface 0x%x has no class/title yet — deferring", + (unsigned)xw->window_id); + } + + struct wl_toplevel_view *view = wmalloc(sizeof(*view)); + memset(view, 0, sizeof(*view)); + view->xw_surface = xw; + view->id = (WNativeWindow)xw->window_id; + + view->map_listener.notify = handle_xwayland_associate; + view->unmap_listener.notify = handle_xwayland_unmap; + view->destroy_listener.notify = handle_xwayland_destroy; + view->title_listener.notify = handle_xwayland_set_class; + view->xw_geometry_listener.notify = handle_xwayland_set_geometry; + + wl_signal_add(&xw->events.associate, &view->map_listener); + wl_signal_add(&xw->events.dissociate, &view->unmap_listener); + wl_signal_add(&xw->events.destroy, &view->destroy_listener); + wl_signal_add(&xw->events.set_class, &view->title_listener); + wl_signal_add(&xw->events.set_geometry, &view->xw_geometry_listener); + + wl_list_insert(&wl_state.toplevel_list, &view->link); + wl_state.managed_surface_count++; + + view->x = (wl_state.output ? (int)wl_state.output->width : 1920) / 4; + view->y = (wl_state.output ? (int)wl_state.output->height : 1080) / 4; + + /* If the surface is already associated (has a wlr_surface), we missed + * the associate event. Hook surface->events.map directly. */ + if (xw->surface) { + view->xw_surface_map_listener.notify = handle_xwayland_surface_map; + wl_signal_add(&xw->surface->events.map, &view->xw_surface_map_listener); + } + + wwarning("wl_backend: new XWayland surface id=0x%lx class=%s title=%s pos=%d,%d", + view->id, + xw->class ? xw->class : "(none)", + xw->title ? xw->title : "(none)", + xw->x, xw->y); + + /* Send initial configure so the X11 client gets a position and size. + * Use the client's requested size if reasonable, otherwise default. */ + { + int cw = (xw->width >= 64) ? xw->width : 640; + int ch = (xw->height >= 64) ? xw->height : 480; + wlr_xwayland_surface_configure(xw, + (int16_t)view->x, (int16_t)view->y, cw, ch); + } +} + +/* + * handle_xdg_toplevel_initial_commit -- wlroots 0.19 requires that the + * initial configure is sent in response to the client's first (empty) commit, + * not at new_surface time. The surface is only "initialized" after this + * commit, so schedule_configure is safe here. + */ +static void +handle_xdg_toplevel_initial_commit(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = + wl_container_of(listener, view, commit_listener); + (void)data; + + if (!view->xdg_surface || !view->xdg_surface->initial_commit) + return; + + /* Only need this once — remove ourselves. */ + wl_list_remove(&view->commit_listener.link); + wl_list_init(&view->commit_listener.link); + + /* Send initial configure so the client enters the configure/ack cycle. + * Size 0,0 means "choose your own size" per the XDG spec. */ + wlr_xdg_toplevel_set_activated(view->xdg_surface->toplevel, true); + wlr_xdg_toplevel_set_size(view->xdg_surface->toplevel, 0, 0); + wlr_xdg_surface_schedule_configure(view->xdg_surface); +} + +static void +handle_toplevel_commit(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = + wl_container_of(listener, view, surface_commit_listener); + (void)data; + + /* Ensure client scene_node is parented under frame tree */ + if (view->scene_node && view->frame_id) { + struct wl_frame_buf *ffb = frame_buf_find(view->frame_id); + if (ffb && ffb->tree && view->scene_node->node.parent != ffb->tree) { + wlr_scene_node_reparent(&view->scene_node->node, ffb->tree); + wlr_scene_node_set_position(&view->scene_node->node, + view->frame_rel_x, view->frame_rel_y); + } + } + + /* When the client commits a buffer at a new size (e.g. after interactive + * resize), update the WM frame to match. */ + if (view->xdg_surface && view->xdg_surface->surface && view->frame_id) { + int cw = view->xdg_surface->surface->current.width; + int ch = view->xdg_surface->surface->current.height; + if (cw > 0 && ch > 0) { + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + if (wwin->frame && + (cw != wwin->client.width || ch != wwin->client.height)) { + wwin->client.width = cw; + wwin->client.height = ch; + int h = ch + wwin->frame->top_width + wwin->frame->bottom_width; + wFrameWindowResize(wwin->frame, cw, h); + } + } + } + } + + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); +} + +/* + * Phase 36 / 38: new XDG toplevel listener (wlroots 0.19+). + * Fires when the client calls get_toplevel on an xdg_surface. + * Allocates a wl_toplevel_view, assigns a synthetic Window ID, + * wires lifecycle listeners, and sets up the initial commit handler. + */ +static void +handle_xdg_new_toplevel(struct wl_listener *listener, void *data) +{ + struct wlr_xdg_toplevel *toplevel = data; + struct wlr_xdg_surface *xdg_surface = toplevel->base; + struct wl_toplevel_view *view; + (void)listener; + + view = wmalloc(sizeof(*view)); + memset(view, 0, sizeof(*view)); + view->xdg_surface = xdg_surface; + view->id = wl_state.next_synthetic_id++; + + view->map_listener.notify = handle_toplevel_map; + view->unmap_listener.notify = handle_toplevel_unmap; + view->destroy_listener.notify = handle_toplevel_destroy; + view->title_listener.notify = handle_toplevel_title; + view->request_move_listener.notify = handle_toplevel_request_move; + view->request_resize_listener.notify = handle_toplevel_request_resize; + view->request_maximize_listener.notify = handle_toplevel_request_maximize; + view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen; + view->request_minimize_listener.notify = handle_toplevel_request_minimize; + view->set_app_id_listener.notify = handle_toplevel_set_app_id; + + wl_signal_add(&xdg_surface->surface->events.map, &view->map_listener); + wl_signal_add(&xdg_surface->surface->events.unmap, &view->unmap_listener); + wl_signal_add(&toplevel->events.destroy, &view->destroy_listener); + wl_signal_add(&toplevel->events.set_title, &view->title_listener); + wl_signal_add(&toplevel->events.request_move, &view->request_move_listener); + wl_signal_add(&toplevel->events.request_resize, &view->request_resize_listener); + wl_signal_add(&toplevel->events.request_maximize, &view->request_maximize_listener); + wl_signal_add(&toplevel->events.request_fullscreen, &view->request_fullscreen_listener); + wl_signal_add(&toplevel->events.request_minimize, &view->request_minimize_listener); + wl_signal_add(&toplevel->events.set_app_id, &view->set_app_id_listener); + + /* wlroots 0.19: send initial configure on the client's first commit, + * when the surface becomes initialized. */ + view->commit_listener.notify = handle_xdg_toplevel_initial_commit; + wl_signal_add(&xdg_surface->surface->events.commit, &view->commit_listener); + + view->surface_commit_listener.notify = handle_toplevel_commit; + wl_signal_add(&xdg_surface->surface->events.commit, &view->surface_commit_listener); + + wl_list_insert(&wl_state.toplevel_list, &view->link); + wl_state.managed_surface_count++; + + wwarning("wl_backend: new XDG toplevel id=0x%lx app_id=%s title=%s", + view->id, + toplevel->app_id ? toplevel->app_id : "(none)", + toplevel->title ? toplevel->title : "(none)"); +} + +/* ------------------------------------------------------------------ */ +/* Phase 78: keyboard event handlers */ +/* ------------------------------------------------------------------ */ + +/* + * handle_keyboard_key — fires for every key press/release on the headless + * keyboard. On key-press, dispatches WM shortcuts via wWaylandKeyPress(); + * then notifies the focused Wayland client via wlr_seat_keyboard_notify_key. + * (The headless backend never actually fires these in practice, but the + * listener is wired so it works immediately when a real backend is used.) + */ +static void +handle_keyboard_key(struct wl_listener *listener, void *data) +{ + struct wlr_keyboard_key_event *event = data; + (void)listener; + + if (!wl_state.seat) + return; + + struct wlr_keyboard *kb = wl_state.seat->keyboard_state.keyboard; + if (!kb) return; + + /* Update modifier state */ + wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers); + + uint32_t keycode = event->keycode + 8; /* evdev → XKB keycode */ + uint32_t modifiers = wlr_keyboard_get_modifiers(kb); + + /* VT switching: Ctrl+Alt+Fn */ + if (wl_state.session && event->state == WL_KEYBOARD_KEY_STATE_PRESSED && + (modifiers & (WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT)) == + (WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT)) { + xkb_keysym_t sym = xkb_state_key_get_one_sym(kb->xkb_state, keycode); + if (sym >= XKB_KEY_XF86Switch_VT_1 && sym <= XKB_KEY_XF86Switch_VT_12) { + wlr_session_change_vt(wl_state.session, sym - XKB_KEY_XF86Switch_VT_1 + 1); + return; + } + } + + if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + /* Try WM shortcut dispatch first */ + if (!wl_state.keyboard_grabbed && wWaylandKeyPress((unsigned int)keycode, (unsigned int)modifiers)) + return; /* consumed — do not forward to client */ + } + + /* Phase A: enqueue native WMEvent for WINGs widgets */ + WMScreen *wmscr = wl_get_wmscreen(); + if (wmscr) { + WMEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) + ? WME_KEY_PRESS : WME_KEY_RELEASE; + + /* Route to focused view */ + W_FocusInfo *finfo = wmscr->focusInfo; + W_View *target = (finfo && finfo->focused) ? finfo->focused + : (finfo && finfo->toplevel) ? finfo->toplevel : NULL; + if (!target && wmscr->modalView) + target = wmscr->modalView; + ev.window = target ? (WNativeWindow)target->window : WNATIVE_NONE; + + ev.u.key.keycode = keycode; + ev.u.key.state = modifiers; + ev.u.key.time = event->time_msec; + ev.u.key.x = wl_state.pointer_x; + ev.u.key.y = wl_state.pointer_y; + ev.u.key.x_root = wl_state.pointer_x; + ev.u.key.y_root = wl_state.pointer_y; + + /* Translate keycode → keysym + UTF-8 via xkb */ + if (kb->xkb_state) { + ev.u.key.keysym = xkb_state_key_get_one_sym(kb->xkb_state, keycode); + ev.u.key.utf8_len = xkb_state_key_get_utf8(kb->xkb_state, keycode, + ev.u.key.utf8, sizeof(ev.u.key.utf8)); + if (ev.u.key.utf8_len >= (int)sizeof(ev.u.key.utf8)) + ev.u.key.utf8_len = 0; + } else { + wwarning("handle_keyboard_key: kb->xkb_state is NULL, dropping key event (keycode=%u)", keycode); + return; + } + + W_WL_EnqueueEvent(wmscr, &ev, NULL); + } + + /* Forward non-shortcut key events to focused Wayland client (if any), + * but not during a keyboard grab (Alt-Tab cycling). */ + if (!wl_state.keyboard_grabbed && wl_state.seat->keyboard_state.focused_surface) + wlr_seat_keyboard_notify_key(wl_state.seat, + event->time_msec, event->keycode, event->state); +} + +static void +handle_keyboard_modifiers(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + if (!wl_state.seat) return; + struct wlr_keyboard *kb = wl_state.seat->keyboard_state.keyboard; + if (!kb) return; + wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers); +} + +/* ------------------------------------------------------------------ */ +/* Phase 20: real input device forwarding for nested mode */ +/* ------------------------------------------------------------------ */ + +/* + * handle_backend_new_input — fired for every input device added to any + * sub-backend. Headless synthetic devices (which never generate events) + * are skipped; the explicit setup code in wl_display_open handles those. + * Real devices from wlr_wl_backend (the nested compositor path) carry + * events from the host compositor's seat and must be wired so key presses + * and mouse motion reach focused Wayland clients. + */ +static void +handle_keyboard_destroy(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wl_list_remove(&wl_state.real_keyboard_key_listener.link); + wl_list_remove(&wl_state.real_keyboard_modifiers_listener.link); + wl_list_remove(&wl_state.real_keyboard_destroy_listener.link); + wl_list_init(&wl_state.real_keyboard_key_listener.link); + wl_list_init(&wl_state.real_keyboard_modifiers_listener.link); + wl_list_init(&wl_state.real_keyboard_destroy_listener.link); + wl_state.real_keyboard_dev = NULL; + wl_state.keyboard_dev = NULL; +} + +static void +handle_pointer_destroy(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wl_list_remove(&wl_state.pointer_motion_listener.link); + wl_list_remove(&wl_state.real_pointer_motion_abs_listener.link); + wl_list_remove(&wl_state.real_pointer_button_listener.link); + wl_list_remove(&wl_state.real_pointer_frame_listener.link); + wl_list_remove(&wl_state.real_pointer_axis_listener.link); + wl_list_remove(&wl_state.real_pointer_destroy_listener.link); + wl_list_init(&wl_state.pointer_motion_listener.link); + wl_list_init(&wl_state.real_pointer_motion_abs_listener.link); + wl_list_init(&wl_state.real_pointer_button_listener.link); + wl_list_init(&wl_state.real_pointer_frame_listener.link); + wl_list_init(&wl_state.real_pointer_axis_listener.link); + wl_list_init(&wl_state.real_pointer_destroy_listener.link); + wl_state.real_pointer_dev = NULL; + wl_state.pointer_dev = NULL; +} + +static void +handle_backend_new_input(struct wl_listener *listener, void *data) +{ + struct wlr_input_device *dev = data; + (void)listener; + + /* In wlroots 0.19 the headless backend has no input devices, so every + * device that arrives here is a real device from the nested Wayland + * backend (or a virtual device). No need to skip headless devices. */ + switch (dev->type) { + case WLR_INPUT_DEVICE_KEYBOARD: { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(dev); + if (wl_state.xkb_keymap && !kb->xkb_state) + wlr_keyboard_set_keymap(kb, wl_state.xkb_keymap); + wl_state.real_keyboard_dev = dev; + wl_state.keyboard_dev = dev; + if (wl_state.seat) + wlr_seat_set_keyboard(wl_state.seat, kb); + wl_state.real_keyboard_key_listener.notify = handle_keyboard_key; + wl_signal_add(&kb->events.key, + &wl_state.real_keyboard_key_listener); + wl_state.real_keyboard_modifiers_listener.notify = handle_keyboard_modifiers; + wl_signal_add(&kb->events.modifiers, + &wl_state.real_keyboard_modifiers_listener); + wl_state.real_keyboard_destroy_listener.notify = handle_keyboard_destroy; + wl_signal_add(&dev->events.destroy, + &wl_state.real_keyboard_destroy_listener); + wwarning("wl_backend: real keyboard wired"); + break; + } + case WLR_INPUT_DEVICE_POINTER: { + struct wlr_pointer *ptr = wlr_pointer_from_input_device(dev); + wl_state.real_pointer_dev = dev; + wl_state.pointer_dev = dev; + wl_state.pointer_motion_listener.notify = handle_pointer_motion; + wl_signal_add(&ptr->events.motion, + &wl_state.pointer_motion_listener); + wl_state.real_pointer_motion_abs_listener.notify = + handle_pointer_motion_absolute; + wl_signal_add(&ptr->events.motion_absolute, + &wl_state.real_pointer_motion_abs_listener); + wl_state.real_pointer_button_listener.notify = handle_pointer_button; + wl_signal_add(&ptr->events.button, + &wl_state.real_pointer_button_listener); + wl_state.real_pointer_frame_listener.notify = handle_pointer_frame; + wl_signal_add(&ptr->events.frame, + &wl_state.real_pointer_frame_listener); + wl_state.real_pointer_axis_listener.notify = handle_pointer_axis; + wl_signal_add(&ptr->events.axis, + &wl_state.real_pointer_axis_listener); + wl_state.real_pointer_destroy_listener.notify = handle_pointer_destroy; + wl_signal_add(&dev->events.destroy, + &wl_state.real_pointer_destroy_listener); + wwarning("wl_backend: real pointer wired"); + break; + } + default: + break; + } +} + +/* ------------------------------------------------------------------ */ +/* Phase 18: output frame rendering and backend new-output handler */ +/* ------------------------------------------------------------------ */ + +/* + * handle_output_frame — fires on every output vsync/frame event. + * Calls wlr_scene_output_commit which renders the scene graph (all mapped + * surfaces at their current positions) and sends Wayland frame callbacks + * to clients so they know they can submit the next buffer. + */ +static void +handle_output_frame(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + if (wl_state.startup_pump_running) + return; + if (wl_state.session && !wl_state.session->active) + return; + if (!wl_state.scene_output) + return; + static int frame_count = 0; + ++frame_count; + if (wl_state.allocator) { + /* Full scene commit — renders client surfaces and fires + * wl_surface.frame callbacks so clients can submit next buffer. + * Also send frame_done unconditionally so clients aren't stalled. */ + wlr_scene_output_commit(wl_state.scene_output, NULL); + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + wlr_scene_output_send_frame_done(wl_state.scene_output, &now); + } else { + /* No GPU allocator available (e.g. headless without DRI access). + * Skip rendering but still fire wl_surface.frame callbacks so + * clients are not stalled waiting for frame pacing. Without + * this, weston-simple-shm would never submit its second buffer. */ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + wlr_scene_output_send_frame_done(wl_state.scene_output, &now); + } +} + +/* + * handle_backend_new_output — fires when a new output is added to the backend. + * For the headless backend this fires synchronously after wlr_headless_add_output; + * for the nested Wayland backend it fires when the host compositor creates the + * output window. Initialises the render pipeline and wires the frame handler. + */ +static void +handle_backend_new_output(struct wl_listener *listener, void *data) +{ + struct wlr_output *output = data; + (void)listener; + + if (wl_state.output) + return; /* we only manage one output */ + + wl_state.output = output; + + /* Initialise the render pipeline for this output. */ + if (wl_state.allocator && wl_state.renderer) + wlr_output_init_render(output, wl_state.allocator, wl_state.renderer); + + /* Enable the output: select preferred mode, then commit via state API. + * Nested Wayland outputs: sway's initial xdg-shell configure sends 0×0 + * ("compositor defers to client"), so output->width/height stay 0 after + * the configure busy-loop. Without an explicit mode the swapchain tries + * to allocate a 0×0 GBM buffer which crashes. Fall back to 1920×1080 so + * the commit succeeds; sway will send a real configure once the window is + * visible and we handle it in the frame/configure signal path. */ + { + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_enabled(&state, true); + struct wlr_output_mode *mode = wlr_output_preferred_mode(output); + if (mode) { + wlr_output_state_set_mode(&state, mode); + } else { + /* Default size for nested mode (or headless with no preferred mode). */ + wlr_output_state_set_custom_mode(&state, 1920, 1080, 0); + } + if (!wlr_output_commit_state(output, &state)) + wwarning("wl_backend: initial output commit failed"); + wlr_output_state_finish(&state); + } + + /* Register with the output layout. */ + if (wl_state.output_layout) + wlr_output_layout_add_auto(wl_state.output_layout, output); + + /* Phase 23: apply cursor image now that this output is in the layout. + * wlr_xcursor_manager_set_cursor_image iterates the output layout and + * sets the hardware cursor on each output; for the nested backend this + * sends wl_pointer.set_cursor to the host compositor, making the cursor + * visible. Also warp to the current virtual pointer position so the + * cursor appears at the right location from the first frame. */ + if (wl_state.cursor && wl_state.xcursor_mgr) { + wlr_xcursor_manager_load(wl_state.xcursor_mgr, output->scale); + wlr_cursor_set_xcursor(wl_state.cursor, wl_state.xcursor_mgr, "left_ptr"); + wlr_cursor_warp(wl_state.cursor, NULL, + (double)wl_state.pointer_x, + (double)wl_state.pointer_y); + wwarning("wl_backend: cursor image set to 'left_ptr' on output %s", + output->name); + } + + /* Set window title when running as a nested compositor. */ + if (wl_state.is_nested && wl_state.wl_backend_inner && + wlr_backend_is_wl(wl_state.wl_backend_inner)) + wlr_wl_output_set_title(output, "Window Maker"); + + /* Create scene output and wire the frame renderer. */ + if (wl_state.scene) { + wl_state.scene_output = wlr_scene_output_create(wl_state.scene, output); + wl_state.output_frame_listener.notify = handle_output_frame; + wl_signal_add(&output->events.frame, &wl_state.output_frame_listener); + wwarning("wl_backend: scene output created for %s (%dx%d)", + output->name, output->width, output->height); + + /* Phase 20: root background rectangle. + * Inserted first in the scene tree so it is always below client + * surfaces. Default WindowMaker desktop colour: rgb:50/50/75. */ + { + float bg[4] = { 0x50/255.0f, 0x50/255.0f, 0x75/255.0f, 1.0f }; + wl_state.bg_rect = 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); + } + } + + wwarning("wl_backend: new output %s (%dx%d)", + output->name, output->width, output->height); +} + +/* ------------------------------------------------------------------ */ +/* Display lifecycle (Phase 32 / Phase 34) */ +/* ------------------------------------------------------------------ */ + +/* Startup pump thread: keeps the Wayland event loop running while the main + * thread initialises WINGs/RContext during StartUp(). XWayland needs the + * Wayland compositor event loop to be responsive so it can answer X11 + * round-trips like XGetModifierMapping (which queries keyboard state from + * wlroots via Wayland protocol). The thread does NOT touch Xlib — the + * x11_event_source fd watcher is registered AFTER this thread exits in + * wl_event_loop_run(), preventing any concurrent Xlib access. */ +static void * +wl_startup_pump_thread(void *arg) +{ + (void)arg; + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + while (wl_state.startup_pump_running) { + wl_display_flush_clients(wl_state.display); + wl_event_loop_dispatch(loop, 1); + } + return NULL; +} + +/* Called by the Wayland event loop whenever the X11 connection fd is readable. + * Drains all pending X11 events and dispatches them through WINGs so that + * menus, dialogs, and other WINGs widgets work during wl_display_run(). */ +static int +wl_handle_x11_events(int fd, uint32_t mask, void *data) +{ + (void)fd; (void)mask; (void)data; + if (!wl_state.x_display) + return 0; + XEvent event; + while (XPending(wl_state.x_display)) { + XNextEvent(wl_state.x_display, &event); + /* Guard: WINGs screen is not initialised until StartUp() completes. */ + if (w_global.screen_count > 0) { + /* Translate XEvent → WMEvent at the XWayland boundary. + * _x11_raw carries the original XEvent so WMHandleEvent can + * do X11-specific operations (view lookup, XFilterEvent, etc.) */ + WMEvent wev; + memset(&wev, 0, sizeof(wev)); + wev._x11_raw = &event; + wev.window = (WNativeWindow)event.xany.window; + switch (event.type) { + case KeyPress: wev.type = WME_KEY_PRESS; break; + case KeyRelease: wev.type = WME_KEY_RELEASE; break; + case ButtonPress: wev.type = WME_BUTTON_PRESS; break; + case ButtonRelease: wev.type = WME_BUTTON_RELEASE; break; + case MotionNotify: wev.type = WME_MOTION; break; + case EnterNotify: wev.type = WME_ENTER; break; + case LeaveNotify: wev.type = WME_LEAVE; break; + case Expose: wev.type = WME_EXPOSE; break; + case ConfigureNotify: wev.type = WME_CONFIGURE; break; + case MapNotify: wev.type = WME_MAP; break; + case UnmapNotify: wev.type = WME_UNMAP; break; + case DestroyNotify: wev.type = WME_DESTROY; break; + case FocusIn: wev.type = WME_FOCUS_IN; break; + case FocusOut: wev.type = WME_FOCUS_OUT; break; + case PropertyNotify: wev.type = WME_PROPERTY; break; + case ClientMessage: wev.type = WME_CLIENT_MESSAGE; break; + case SelectionNotify: wev.type = WME_SELECTION; break; + case SelectionRequest: wev.type = WME_SELECTION_REQUEST; break; + case SelectionClear: wev.type = WME_SELECTION_CLEAR; break; + case MappingNotify: wev.type = WME_MAPPING; break; + case ColormapNotify: wev.type = WME_COLORMAP; break; + case VisibilityNotify: wev.type = WME_VISIBILITY; break; + case ReparentNotify: wev.type = WME_REPARENT; break; + case ConfigureRequest: wev.type = WME_CONFIGURE_REQUEST; break; + case MapRequest: wev.type = WME_MAP_REQUEST; break; + default: wev.type = WME_NONE; break; + } + WMHandleEvent(&wev); + } + } + return 0; +} + +/* Public accessor for the XWayland Display* — used by screen.c to create + * an RContext for texture rendering without exposing wl_state globally. */ +Display *wl_get_x_display(void) +{ + return wl_state.x_display; +} + +static void * +wl_display_open(const char *name) +{ + const char *sock; + + (void)name; /* Wayland compositor picks its own socket name */ + + /* Initialize listener links so wl_list_remove is safe before first add */ + wl_list_init(&wl_state.real_keyboard_key_listener.link); + wl_list_init(&wl_state.real_keyboard_modifiers_listener.link); + wl_list_init(&wl_state.real_keyboard_destroy_listener.link); + wl_list_init(&wl_state.pointer_motion_listener.link); + wl_list_init(&wl_state.real_pointer_motion_abs_listener.link); + wl_list_init(&wl_state.real_pointer_button_listener.link); + wl_list_init(&wl_state.real_pointer_frame_listener.link); + wl_list_init(&wl_state.real_pointer_axis_listener.link); + wl_list_init(&wl_state.real_pointer_destroy_listener.link); + + XInitThreads(); + + /* Install X error handler immediately: Mesa/EGL may open a connection to + * DISPLAY (e.g. the host compositor's XWayland) during wlr_backend_start, + * which happens before StartUp() calls register_error_handlers(). + * Installing here prevents Xlib's default handler from aborting the + * process on the benign BadMatch that XWayland generates on SetInputFocus + * during WM startup. */ + XSetErrorHandler(wl_catch_xwayland_error); + + wlr_log_init(WLR_ERROR, NULL); + + wl_state.display = wl_display_create(); + if (!wl_state.display) { + wwarning("wl_backend: failed to create wl_display"); + return NULL; + } + + /* Phase 18: choose backend — nested Wayland if WMAKER_WAYLAND_NESTED=1 + * is set (run inside an existing Wayland compositor), otherwise headless. + * Both modes always include a headless backend (headless_inner) so that + * wlr_headless_add_input_device works regardless of nesting. */ + /* Auto-detect nested mode: if WAYLAND_DISPLAY is set we are running inside + * an existing Wayland session and should use wlr_wl_backend so the + * compositor appears as a window on the host desktop. + * WMAKER_WAYLAND_NESTED=0 can be used to force headless even when + * WAYLAND_DISPLAY is present (e.g. for automated testing). */ + { + const char *nested_env = getenv("WMAKER_WAYLAND_NESTED"); + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (nested_env && nested_env[0] == '0') + wl_state.is_nested = 0; /* explicit override: force headless */ + else if (nested_env && nested_env[0]) + wl_state.is_nested = 1; /* explicit override: force nested */ + else + wl_state.is_nested = (wayland_display && wayland_display[0]) ? 1 : 0; + } + + /* Always create a headless backend for the output in headless mode. */ + wl_state.headless_inner = wlr_headless_backend_create( + wl_display_get_event_loop(wl_state.display)); + if (!wl_state.headless_inner) { + wwarning("wl_backend: failed to create headless backend"); + wl_display_destroy(wl_state.display); + wl_state.display = NULL; + return NULL; + } + + if (wl_state.is_nested) { + /* Nested: multi-backend wrapping nested Wayland + headless. */ + wl_state.wl_backend_inner = wlr_wl_backend_create( + wl_display_get_event_loop(wl_state.display), NULL); + if (!wl_state.wl_backend_inner) { + wwarning("wl_backend: failed to create nested Wayland backend; falling back to standalone"); + wl_state.is_nested = 0; + } + } + + if (wl_state.is_nested && wl_state.wl_backend_inner) { + struct wlr_backend *multi = wlr_multi_backend_create( + wl_display_get_event_loop(wl_state.display)); + wlr_multi_backend_add(multi, wl_state.wl_backend_inner); + wlr_multi_backend_add(multi, wl_state.headless_inner); + wl_state.backend = multi; + wwarning("wl_backend: nested Wayland compositor mode"); + } else { + /* Standalone: use autocreate to get DRM/KMS + libinput on real hardware. + * Clear DISPLAY and WAYLAND_DISPLAY so wlroots goes straight to DRM. */ + unsetenv("DISPLAY"); + unsetenv("WAYLAND_DISPLAY"); + struct wlr_session *session = NULL; + wl_state.backend = wlr_backend_autocreate( + wl_display_get_event_loop(wl_state.display), &session); + wl_state.session = session; + if (!wl_state.backend) { + wwarning("wl_backend: wlr_backend_autocreate failed; falling back to headless"); + wl_state.backend = wl_state.headless_inner; + } else { + wwarning("wl_backend: standalone DRM/KMS compositor mode"); + } + } + + /* Renderer: use the appropriate backend as source. */ + { + struct wlr_backend *render_src; + if (wl_state.wl_backend_inner) + render_src = wl_state.wl_backend_inner; + else + render_src = wl_state.backend; + wl_state.renderer = wlr_renderer_autocreate(render_src); + } + if (!wl_state.renderer) { + wwarning("wl_backend: failed to create renderer"); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + + /* Allocator — needed for wlr_output_init_render. */ + { + struct wlr_backend *alloc_src = wl_state.wl_backend_inner + ? wl_state.wl_backend_inner : wl_state.backend; + wl_state.allocator = wlr_allocator_autocreate(alloc_src, wl_state.renderer); + } + if (!wl_state.allocator) + wwarning("wl_backend: allocator unavailable; wlr_output_init_render will be skipped"); + + /* Phase 34: output layout for multi-head geometry tracking. + * Must be created before backend start so handle_backend_new_output + * can add the output to the layout when it fires. */ + wl_state.output_layout = wlr_output_layout_create(wl_state.display); + + /* Phase 23: create hardware cursor tracker and load xcursor theme. + * wlr_cursor tracks the logical cursor position across the output + * layout and communicates cursor image changes to each output. + * In nested Wayland mode this calls wl_pointer.set_cursor on the host + * compositor so the cursor is visible in the window. + * Theme and image are applied in handle_backend_new_output after each + * output is added to the layout, ensuring the scale factor is known. */ + wl_state.cursor = wlr_cursor_create(); + if (wl_state.cursor) { + wlr_cursor_attach_output_layout(wl_state.cursor, wl_state.output_layout); + { + const char *theme = getenv("XCURSOR_THEME"); + const char *size_str = getenv("XCURSOR_SIZE"); + unsigned int size = size_str ? (unsigned int)atoi(size_str) : 24; + if (size == 0) size = 24; + wl_state.xcursor_mgr = wlr_xcursor_manager_create(theme, size); + } + if (wl_state.xcursor_mgr) + wlr_xcursor_manager_load(wl_state.xcursor_mgr, 1.0f); + wwarning("wl_backend: cursor created, xcursor theme loaded"); + } else { + wwarning("wl_backend: wlr_cursor_create failed; cursor will be invisible"); + } + + /* Phase 18: scene graph — MUST be created before wlr_backend_start so + * that handle_backend_new_output can create the scene_output immediately + * when the output fires (for nested Wayland, new_output fires during + * wlr_backend_start via the host compositor roundtrip). */ + wl_state.scene = wlr_scene_create(); + /* Register commit callback for WINGs views (defined later in file) */ + { + extern void (*W_ViewCommitBackingFn)(unsigned long id); + extern void wl_view_commit_backing_impl(unsigned long id); + extern void wl_view_destroy_backing_impl(unsigned long id); + W_ViewCommitBackingFn = wl_view_commit_backing_impl; + extern void (*W_ViewDestroyBackingFn)(unsigned long id); + W_ViewDestroyBackingFn = wl_view_destroy_backing_impl; + + extern void (*W_ViewSceneNodeSetEnabledFn)(unsigned long id, int enabled); + extern void wl_view_scene_node_set_enabled(unsigned long id, int enabled); + W_ViewSceneNodeSetEnabledFn = wl_view_scene_node_set_enabled; + + extern void (*W_ClipboardStoreFn)(const char *text, int len); + extern char *(*W_ClipboardFetchFn)(int *len_out); + W_ClipboardStoreFn = wl_clipboard_store_to_seat; + W_ClipboardFetchFn = wl_clipboard_fetch_from_seat; + + extern void (*W_DragIconShowFn)(struct W_View *, int, int); + extern void (*W_DragIconMoveFn)(int, int); + extern void (*W_DragIconHideFn)(void); + W_DragIconShowFn = wl_drag_icon_show; + W_DragIconMoveFn = wl_drag_icon_move; + W_DragIconHideFn = wl_drag_icon_hide; + + extern void (*W_DragCursorFeedbackFn)(Bool); + W_DragCursorFeedbackFn = wl_drag_cursor_feedback; + } + if (wl_state.scene) { + wwarning("wl_backend: scene graph created"); + /* 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 + * input devices from the nested Wayland backend (fired during + * wlr_backend_start) are captured before the headless devices are + * added explicitly below. */ + wl_state.backend_new_input_listener.notify = handle_backend_new_input; + wl_signal_add(&wl_state.backend->events.new_input, + &wl_state.backend_new_input_listener); + + /* Wire new_output BEFORE starting the backend. */ + wl_state.backend_new_output_listener.notify = handle_backend_new_output; + wl_signal_add(&wl_state.backend->events.new_output, + &wl_state.backend_new_output_listener); + + /* For nested mode, queue an output window before start; wlr_wl_output_create + * returns NULL before the backend is initialised — the output is created + * during wlr_backend_start and delivered via new_output. */ + if (wl_state.is_nested && wl_state.wl_backend_inner) + wlr_wl_output_create(wl_state.wl_backend_inner); + + if (!wlr_backend_start(wl_state.backend)) { + wwarning("wl_backend: failed to start backend"); + wlr_renderer_destroy(wl_state.renderer); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + + /* For headless mode, add the output explicitly after start; this fires + * handle_backend_new_output synchronously (scene already exists). */ + if (!wl_state.is_nested) + wlr_headless_add_output(wl_state.headless_inner, 1920, 1080); + + /* Phase 34: create the UNIX socket so Wayland clients can connect. */ + sock = wl_display_add_socket_auto(wl_state.display); + if (!sock) { + wwarning("wl_backend: failed to create Wayland compositor socket"); + wlr_renderer_destroy(wl_state.renderer); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + snprintf(wl_state.socket_name, sizeof(wl_state.socket_name), "%s", sock); + setenv("WAYLAND_DISPLAY", wl_state.socket_name, 1); + wwarning("wl_backend: compositor socket WAYLAND_DISPLAY=%s DISPLAY=%s", + wl_state.socket_name, + getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)"); + + /* Phase 35: advertise core Wayland protocol globals. */ + wl_state.compositor = + wlr_compositor_create(wl_state.display, 6, wl_state.renderer); + wlr_subcompositor_create(wl_state.display); + wl_state.seat = wlr_seat_create(wl_state.display, "seat0"); + wl_state.data_device_mgr = + wlr_data_device_manager_create(wl_state.display); + + /* Phase 24: handle client cursor-surface requests on the seat. */ + if (wl_state.seat) { + wl_state.seat_request_cursor_listener.notify = handle_seat_request_cursor; + wl_signal_add(&wl_state.seat->events.request_set_cursor, + &wl_state.seat_request_cursor_listener); + } + /* Phase 25: handle clipboard set-selection requests on the seat so + * Ctrl-C / copy operations from clients propagate to the seat selection + * and become available for paste in other clients. */ + if (wl_state.seat) { + wl_state.seat_request_set_selection_listener.notify = + handle_seat_request_set_selection; + wl_signal_add(&wl_state.seat->events.request_set_selection, + &wl_state.seat_request_set_selection_listener); + wwarning("wl_backend: clipboard selection wired"); + + wl_state.seat_request_start_drag_listener.notify = + handle_seat_request_start_drag; + wl_signal_add(&wl_state.seat->events.request_start_drag, + &wl_state.seat_request_start_drag_listener); + wwarning("wl_backend: DND start_drag wired"); + } + + /* Phase 26: primary selection (middle-click paste) via + * zwp_primary_selection_device_manager_v1. */ + wl_state.primary_selection_mgr = + wlr_primary_selection_v1_device_manager_create(wl_state.display); + if (wl_state.primary_selection_mgr && wl_state.seat) { + wl_state.seat_request_primary_selection_listener.notify = + handle_seat_request_primary_selection; + wl_signal_add(&wl_state.seat->events.request_set_primary_selection, + &wl_state.seat_request_primary_selection_listener); + wwarning("wl_backend: primary selection (middle-click) wired"); + } + + /* Phase 27: xdg-output — panels and HiDPI apps query logical output + * geometry via zxdg_output_manager_v1. Requires output_layout. */ + if (wl_state.output_layout) { + wl_state.xdg_output_mgr = + wlr_xdg_output_manager_v1_create(wl_state.display, + wl_state.output_layout); + if (wl_state.xdg_output_mgr) + wwarning("wl_backend: xdg-output-manager created"); + } + + /* Phase 28: idle-inhibit — video players and games create inhibitor + * objects to suppress screen blanking while active. */ + wl_state.idle_inhibit_mgr = + wlr_idle_inhibit_v1_create(wl_state.display); + if (wl_state.idle_inhibit_mgr) { + wl_state.idle_new_inhibitor_listener.notify = handle_idle_new_inhibitor; + wl_signal_add(&wl_state.idle_inhibit_mgr->events.new_inhibitor, + &wl_state.idle_new_inhibitor_listener); + wwarning("wl_backend: idle-inhibit manager created"); + } + + /* Phase 29: wp_viewporter — hardware video scaling and subsurface damage + * subset used by mpv, Chromium, Firefox, SDL2. */ + wl_state.viewporter = wlr_viewporter_create(wl_state.display); + if (wl_state.viewporter) + wwarning("wl_backend: wp_viewporter created"); + else + wwarning("wl_backend: wp_viewporter unavailable"); + + /* Phase 30: relative pointer + pointer constraints for games / pointer lock. */ + wl_state.relative_pointer_mgr = + wlr_relative_pointer_manager_v1_create(wl_state.display); + if (wl_state.relative_pointer_mgr) + wwarning("wl_backend: relative-pointer-manager created"); + + wl_state.pointer_constraints = + wlr_pointer_constraints_v1_create(wl_state.display); + if (wl_state.pointer_constraints) { + wl_state.new_constraint_listener.notify = handle_new_pointer_constraint; + wl_signal_add(&wl_state.pointer_constraints->events.new_constraint, + &wl_state.new_constraint_listener); + wwarning("wl_backend: pointer-constraints created"); + } + + /* Phase 31: keyboard shortcuts inhibitor — fullscreen apps / remote desktop + * request to suppress WM keyboard shortcuts. */ + wl_state.shortcuts_inhibit_mgr = + wlr_keyboard_shortcuts_inhibit_v1_create(wl_state.display); + if (wl_state.shortcuts_inhibit_mgr) { + wl_state.new_shortcuts_inhibitor_listener.notify = + handle_new_shortcuts_inhibitor; + wl_signal_add(&wl_state.shortcuts_inhibit_mgr->events.new_inhibitor, + &wl_state.new_shortcuts_inhibitor_listener); + wwarning("wl_backend: keyboard-shortcuts-inhibit created"); + } + /* Phase 32: GPU zero-copy (linux-dmabuf), screencopy, export-dmabuf. + * All three are purely passive globals: wlroots handles the protocol + * implementation internally; no event listeners are required. */ + wl_state.linux_dmabuf = + wlr_linux_dmabuf_v1_create_with_renderer(wl_state.display, 4, wl_state.renderer); + if (wl_state.linux_dmabuf) + wwarning("wl_backend: linux-dmabuf created (zwp_linux_dmabuf_v1)"); + else + wwarning("wl_backend: linux-dmabuf unavailable (renderer has no DRM FD)"); + wl_state.screencopy_mgr = + wlr_screencopy_manager_v1_create(wl_state.display); + if (wl_state.screencopy_mgr) + wwarning("wl_backend: screencopy manager created (zwlr_screencopy_manager_v1)"); + wl_state.export_dmabuf_mgr = + wlr_export_dmabuf_manager_v1_create(wl_state.display); + if (wl_state.export_dmabuf_mgr) + wwarning("wl_backend: export-dmabuf manager created (zwlr_export_dmabuf_manager_v1)"); + + /* Phase 33: output management (wlr-output-management), output power (DPMS), + * gamma control (Night Light / redshift), and presentation time feedback. */ + wl_state.output_mgr = wlr_output_manager_v1_create(wl_state.display); + if (wl_state.output_mgr) { + wl_state.output_mgr_apply_listener.notify = handle_output_mgr_apply; + wl_signal_add(&wl_state.output_mgr->events.apply, + &wl_state.output_mgr_apply_listener); + wl_state.output_mgr_test_listener.notify = handle_output_mgr_test; + wl_signal_add(&wl_state.output_mgr->events.test, + &wl_state.output_mgr_test_listener); + wwarning("wl_backend: output-management created (zwlr_output_management_v1)"); + } + wl_state.output_power_mgr = wlr_output_power_manager_v1_create(wl_state.display); + if (wl_state.output_power_mgr) { + wl_state.output_power_set_mode_listener.notify = handle_output_power_set_mode; + wl_signal_add(&wl_state.output_power_mgr->events.set_mode, + &wl_state.output_power_set_mode_listener); + wwarning("wl_backend: output-power-management created (zwlr_output_power_management_v1)"); + } + wl_state.gamma_control_mgr = wlr_gamma_control_manager_v1_create(wl_state.display); + if (wl_state.gamma_control_mgr) + wwarning("wl_backend: gamma-control created (zwlr_gamma_control_manager_v1)"); + wl_state.presentation = wlr_presentation_create(wl_state.display, wl_state.backend, 1); + if (wl_state.presentation) + wwarning("wl_backend: presentation-time created (wp_presentation)"); + + /* Phase 34: text input v3, input method v2, virtual keyboard v1. + * All three are passive globals for IME / on-screen keyboard support. */ + wl_state.text_input_mgr = + wlr_text_input_manager_v3_create(wl_state.display); + if (wl_state.text_input_mgr) { + wl_state.text_input_new_listener.notify = handle_text_input_new; + wl_signal_add(&wl_state.text_input_mgr->events.text_input, + &wl_state.text_input_new_listener); + wwarning("wl_backend: text-input-v3 created (zwp_text_input_manager_v3)"); + } + wl_state.input_method_mgr = + wlr_input_method_manager_v2_create(wl_state.display); + if (wl_state.input_method_mgr) { + wl_state.input_method_new_listener.notify = handle_input_method_new; + wl_signal_add(&wl_state.input_method_mgr->events.input_method, + &wl_state.input_method_new_listener); + wwarning("wl_backend: input-method-v2 created (zwp_input_method_manager_v2)"); + } + wl_state.virtual_keyboard_mgr = + wlr_virtual_keyboard_manager_v1_create(wl_state.display); + if (wl_state.virtual_keyboard_mgr) { + wl_state.virtual_keyboard_new_listener.notify = handle_virtual_keyboard_new; + wl_signal_add(&wl_state.virtual_keyboard_mgr->events.new_virtual_keyboard, + &wl_state.virtual_keyboard_new_listener); + wwarning("wl_backend: virtual-keyboard-v1 created (zwp_virtual_keyboard_manager_v1)"); + } + + /* Phase 35: foreign toplevel management — lets taskbars enumerate windows. + * xdg_activation — focus-token protocol replacing _NET_ACTIVE_WINDOW. */ + wl_state.foreign_toplevel_mgr = + wlr_foreign_toplevel_manager_v1_create(wl_state.display); + if (wl_state.foreign_toplevel_mgr) + wwarning("wl_backend: foreign-toplevel-management created (zwlr_foreign_toplevel_management_v1)"); + wl_state.xdg_activation = wlr_xdg_activation_v1_create(wl_state.display); + if (wl_state.xdg_activation) { + wl_state.xdg_activation_request_listener.notify = handle_xdg_activation_request; + wl_signal_add(&wl_state.xdg_activation->events.request_activate, + &wl_state.xdg_activation_request_listener); + wwarning("wl_backend: xdg-activation created (xdg_activation_v1)"); + } + + /* Phase 36: layer shell — panels, docks, wallpapers, lock screens. */ + wl_state.layer_shell = wlr_layer_shell_v1_create(wl_state.display, 4); + if (wl_state.layer_shell) { + wl_state.layer_shell_new_surface_listener.notify = + handle_layer_shell_new_surface; + wl_signal_add(&wl_state.layer_shell->events.new_surface, + &wl_state.layer_shell_new_surface_listener); + wwarning("wl_backend: layer-shell created (zwlr_layer_shell_v1)"); + } + + /* Phase 37: clipboard manager (wl-clipboard/cliphist), input inhibitor + * (lock screen input gating), virtual pointer (remote desktop / VNC), + * KDE idle protocol, xdg-foreign v2 (cross-client window embedding). */ + wl_state.data_control_mgr = + wlr_data_control_manager_v1_create(wl_state.display); + if (wl_state.data_control_mgr) + wwarning("wl_backend: data-control created (zwlr_data_control_manager_v1)"); + + /* zwlr_input_inhibitor_v1 removed from wlroots 0.18 — use ext-session-lock-v1 */ + + wl_state.virtual_pointer_mgr = + wlr_virtual_pointer_manager_v1_create(wl_state.display); + if (wl_state.virtual_pointer_mgr) { + wl_state.virtual_pointer_new_listener.notify = + handle_virtual_pointer_new; + wl_signal_add(&wl_state.virtual_pointer_mgr->events.new_virtual_pointer, + &wl_state.virtual_pointer_new_listener); + wwarning("wl_backend: virtual-pointer created (zwlr_virtual_pointer_manager_v1)"); + } + + /* org_kde_kwin_idle (wlr_idle) removed from wlroots 0.18 — use ext-idle-notify-v1 */ + + wl_state.xdg_foreign_registry = + wlr_xdg_foreign_registry_create(wl_state.display); + if (wl_state.xdg_foreign_registry) { + wl_state.xdg_foreign_v2 = + wlr_xdg_foreign_v2_create(wl_state.display, + wl_state.xdg_foreign_registry); + if (wl_state.xdg_foreign_v2) + wwarning("wl_backend: xdg-foreign-v2 created (zxdg_exporter/importer_v2)"); + } + + wl_state.xdg_shell = wlr_xdg_shell_create(wl_state.display, 3); + + /* Advertise wmaker-popup-v1 — clients use it to drive popup positioning. */ + wl_state.wmaker_popup_manager = wl_global_create(wl_state.display, + &wmaker_popup_manager_v1_interface, 1, NULL, + wmaker_popup_manager_bind); + if (wl_state.wmaker_popup_manager) + wwarning("wl_backend: wmaker-popup-v1 manager created"); + + /* Phase 64: advertise zxdg_decoration_manager_v1 so clients that + * support server-side decorations can negotiate; we respond with + * CLIENT_SIDE since the Wayland path has no X11 frame rendering. */ + wl_state.decoration_manager = + wlr_xdg_decoration_manager_v1_create(wl_state.display); + if (wl_state.decoration_manager) { + wl_state.decoration_new_listener.notify = handle_new_toplevel_decoration; + wl_signal_add(&wl_state.decoration_manager->events.new_toplevel_decoration, + &wl_state.decoration_new_listener); + wwarning("wl_backend: xdg-decoration-manager created"); + } + + /* Phase 36: advertise wl_shm (needed by SHM buffer clients). */ + wlr_renderer_init_wl_shm(wl_state.renderer, wl_state.display); + /* wl_output global is created in handle_backend_new_output. */ + + /* Phase 43/53: In wlroots 0.19, the headless backend has no input devices. + * Real keyboard/pointer devices arrive via handle_backend_new_input when + * the nested Wayland backend starts. We pre-create the default XKB keymap + * here so wl_keysym_to_keycode() can resolve key bindings during StartUp() + * before any real keyboard device has been received. */ + { + struct xkb_context *xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (xkb_ctx) { + struct xkb_rule_names rules = { + .rules = getenv("XKB_DEFAULT_RULES"), + .model = getenv("XKB_DEFAULT_MODEL"), + .layout = getenv("XKB_DEFAULT_LAYOUT"), + .variant = getenv("XKB_DEFAULT_VARIANT"), + .options = getenv("XKB_DEFAULT_OPTIONS"), + }; + wl_state.xkb_keymap = xkb_keymap_new_from_names( + xkb_ctx, &rules, XKB_KEYMAP_COMPILE_NO_FLAGS); + xkb_context_unref(xkb_ctx); + if (wl_state.xkb_keymap) { + wwarning("wl_backend: default XKB keymap created for key binding resolution"); + /* Apply to any keyboard that was wired before keymap was ready, + * but only if it doesn't already have one from the host. */ + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + if (!kb->xkb_state) + wlr_keyboard_set_keymap(kb, wl_state.xkb_keymap); + } + } + } + } + /* Advertise keyboard+pointer capability unconditionally so clients do not + * need to wait for a real device to appear before binding wl_keyboard / wl_pointer. */ + if (wl_state.seat) + wlr_seat_set_capabilities(wl_state.seat, + WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_POINTER); + + /* Phase 38: initialise per-toplevel tracking and context store. */ + wl_list_init(&wl_state.toplevel_list); + wl_list_init(&wl_state.or_list); + wl_list_init(&wl_state.frame_list); + wl_list_init(&wl_state.layer_list); + { + int ki; + for (ki = 0; ki < 3; ki++) + wl_list_init(&wl_state.ctx_lists[ki]); + } + wl_state.next_synthetic_id = 0x10000000UL; + wl_state.managed_surface_count = 0; + + /* Phase 36: listen for new XDG toplevels (wlroots 0.19+). */ + wl_state.xdg_new_surface_listener.notify = handle_xdg_new_toplevel; + wl_signal_add(&wl_state.xdg_shell->events.new_toplevel, + &wl_state.xdg_new_surface_listener); + + /* Path 1: XWayland bridge — start Xwayland eagerly (lazy=false) so that + * the compositor can open a Display* from its socket and pass it back to + * main.c. WindowMaker's own X11 UI (title bars, root menus, dock) then + * renders through XWayland, making the WM visible on-screen. + * The ready signal fires once Xwayland's socket accepts connections. */ + wl_state.xwayland = wlr_xwayland_create(wl_state.display, + wl_state.compositor, false); + if (wl_state.xwayland) { + /* Wire server destroy signal to detect early failure (e.g. stale + * display lock → Xwayland exits before ready fires). */ + if (wl_state.xwayland->server) { + wl_state.xwayland_server_destroy_listener.notify = + handle_xwayland_server_destroy; + wl_signal_add(&wl_state.xwayland->server->events.destroy, + &wl_state.xwayland_server_destroy_listener); + } + /* Wire ready signal BEFORE new_surface so Display* is set before + * any X11 window events arrive. */ + wl_state.xwayland_ready_listener.notify = handle_xwayland_ready; + wl_signal_add(&wl_state.xwayland->events.ready, + &wl_state.xwayland_ready_listener); + wl_state.xwayland_new_surface_listener.notify = handle_xwayland_new_surface; + wl_signal_add(&wl_state.xwayland->events.new_surface, + &wl_state.xwayland_new_surface_listener); + wlr_xwayland_set_seat(wl_state.xwayland, wl_state.seat); + /* display_name may already be set (display number reserved by wlroots + * synchronously); set DISPLAY early as a best-effort so that any + * code between here and the ready signal can still find the socket. */ + if (wl_state.xwayland->display_name) { + setenv("DISPLAY", wl_state.xwayland->display_name, 1); + wwarning("wl_backend: XWayland starting DISPLAY=%s (eager mode)", + wl_state.xwayland->display_name); + } else { + wwarning("wl_backend: XWayland created (display_name pending)"); + } + } else { + wwarning("wl_backend: XWayland unavailable (X11 clients will not work)"); + } + + /* Phase 37: periodic timer to drive client frame callbacks at ~60 fps. + * Using a timer avoids the swapchain/allocator complexity of the full + * render pipeline while still keeping Wayland clients unblocked. */ + { + struct wl_event_loop *loop = + wl_display_get_event_loop(wl_state.display); + wl_state.frame_timer = + wl_event_loop_add_timer(loop, frame_timer_cb, NULL); + if (wl_state.frame_timer) + wl_event_source_timer_update(wl_state.frame_timer, 16); + } + + /* Phase 54: initialise virtual pointer at screen centre. */ + wl_state.pointer_x = 960; + wl_state.pointer_y = 540; + wwarning("wl_backend: virtual pointer initialised at %d,%d", + wl_state.pointer_x, wl_state.pointer_y); + + /* Self-pipe shutdown: signal handler writes a byte; the event loop reads + * it here and calls wl_display_terminate() from within loop context, + * avoiding the async-signal-safety issues of calling wl_display_destroy() + * directly from a signal handler. */ + wl_state.shutdown_pipe[0] = wl_state.shutdown_pipe[1] = -1; + wl_state.shutdown_source = NULL; + wl_state.sigfd = -1; + wl_state.sigfd_source = NULL; + if (pipe(wl_state.shutdown_pipe) == 0) { + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + /* Make read end non-blocking so the drain loop in the handler returns + * instead of blocking after all bytes are consumed. */ + fcntl(wl_state.shutdown_pipe[0], F_SETFL, O_NONBLOCK); + wl_state.shutdown_source = wl_event_loop_add_fd(loop, + wl_state.shutdown_pipe[0], + WL_EVENT_READABLE, + wl_shutdown_pipe_handler, + NULL); + } else { + wwarning("wl_backend: shutdown pipe creation failed; SIGTERM may crash"); + } + + /* Phase 20: if a real keyboard was found from the nested backend + * (captured in handle_backend_new_input during wlr_backend_start), + * switch the seat to it now so keyboard focus notifications use the + * correct keymap and modifier state from the host compositor. This + * must happen after the headless keyboard setup above (which may have + * called wlr_seat_set_keyboard with the headless device). */ + if (wl_state.real_keyboard_dev) + wlr_seat_set_keyboard(wl_state.seat, + wlr_keyboard_from_input_device(wl_state.real_keyboard_dev)); + + wl_state.started = 1; + + /* Block until XWayland fires the "ready" signal — pump the Wayland server + * event loop so the XWayland process can complete its handshake. + * handle_xwayland_ready() calls XOpenDisplay and stores the result in + * wl_state.x_display, then sets xwayland_ready = true to break the loop. + * If XWayland is not available, return NULL (pure Wayland path). */ + /* XWayland is lazy — it starts when the first X11 client connects. + * No need to wait for it at startup. Mark as ready so the rest of + * init proceeds without blocking. */ + if (wl_state.xwayland) { + wwarning("wl_backend: XWayland configured (eager mode) — ready at startup"); + wl_state.xwayland_ready = true; + if (wl_state.x_display) { + wwarning("wl_backend: XWayland bridge ready — Display*=%p, full WM UI enabled", + (void *)wl_state.x_display); + /* Start a pump thread to keep the Wayland event loop running + * while the main thread calls WMCreateScreenWithRContext() and + * other blocking X11 init in StartUp(). XWayland needs the + * Wayland compositor to be responsive to handle round-trips like + * XGetModifierMapping (which queries keyboard state from wlroots). + * The x11_event_source fd watcher is registered later in + * wl_event_loop_run() after this thread exits, so the pump + * thread never touches Xlib and there is no Xlib contention. */ + wl_state.startup_pump_running = 1; + if (pthread_create(&wl_state.startup_pump_thread, NULL, + wl_startup_pump_thread, NULL) != 0) { + wwarning("wl_backend: pthread_create for pump thread failed — " + "WINGs init may deadlock"); + wl_state.startup_pump_running = 0; + } + } else { + wwarning("wl_backend: XWayland ready but XOpenDisplay failed — pure Wayland mode"); + } + } + + return NULL; /* dpy=NULL so WM core skips Xlib calls */ +} + +static void +wl_display_close(void) +{ + if (!wl_state.started) + return; + + /* Remove the X11 event source and close the Xlib bridge connection + * before killing XWayland so we don't get stale readable events. */ + if (wl_state.x11_event_source) { + wl_event_source_remove(wl_state.x11_event_source); + wl_state.x11_event_source = NULL; + } + if (wl_state.x_display) { + XCloseDisplay(wl_state.x_display); + wl_state.x_display = NULL; + } + + /* Phase 17: shut down XWayland. + * + * wlroots 0.15 bug: wlr_xwayland_server_destroy() calls wl_client_destroy() + * on the XWayland Wayland client. After any Wayland client has connected + * and disconnected during the session, the XWayland server's wl_client ends + * up in an inconsistent state: the client struct is freed but server->client + * is NOT nulled. wlr_xwayland_server_destroy() then calls wl_client_destroy() + * on the freed struct and crashes in wl_list_remove(). + * + * Workaround: skip wlr_xwayland_destroy() entirely. The Xwayland process is + * a child of wmaker and will receive SIGHUP/SIGKILL when the parent exits. + * The X11 socket is in a tmpdir removed on exit. wl_display_destroy() (called + * below) handles all remaining Wayland client/resource cleanup. + */ + if (wl_state.xwayland) { + /* wlroots 0.15 bug: wlr_xwayland_server_destroy() (called from the + * display destroy signal below) sends SIGTERM to Xwayland and calls + * waitpid(). If Xwayland is blocked waiting for compositor events, + * waitpid() hangs indefinitely. + * + * Fix: pre-kill Xwayland with SIGKILL. SIGKILL cannot be caught or + * deferred, so the process dies immediately (kernel-synchronous). + * The zombie remains until wlroots' waitpid() reaps it — which now + * returns instantly. The Wayland client struct (server->client) is + * still valid because the event loop is stopped and the socket closure + * has not been dispatched, so wlroots' wl_client_destroy() succeeds. */ + if (wl_state.xwayland->server) { + int xdisp = wl_state.xwayland->server->display; + if (wl_state.xwayland->server->pid > 0) + kill(wl_state.xwayland->server->pid, SIGKILL); + /* wlroots 0.15 bug: after any Wayland client has connected and + * disconnected during the session, server->client holds a freed + * (stale) wl_client pointer. wlr_xwayland_server_destroy() (fired + * from the display destroy signal below) calls wl_client_destroy() + * on this freed pointer, which corrupts the list and triggers + * SIGABRT. Null the pointer so wlroots skips the destroy call. + * If the client is still valid, wl_display_destroy_clients() will + * clean it up; if it is stale, it has already been removed from + * the display's client list and nothing more is needed. */ + wl_state.xwayland->server->client = NULL; + /* Clean up X lock file and Unix socket left by SIGKILL'd Xwayland. + * Without this, the next run may find a stale lock file whose PID + * has been reused by an unrelated process, causing wlroots to + * refuse to reclaim the display number → "xcb connect failed". */ + { + char path[64]; + snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp); + unlink(path); + snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp); + unlink(path); + } + } else if (wl_state.xwayland->display_name && + wl_state.xwayland->display_name[0] == ':') { + /* Path 1: server exited early (e.g. stale lock) before wl_display_close + * ran — wlroots already freed server and set it to NULL. Clean up + * any lock file that Xwayland created before dying. */ + int xdisp = atoi(wl_state.xwayland->display_name + 1); + char path[64]; + snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp); + unlink(path); + snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp); + unlink(path); + } + wl_state.xwayland = NULL; + unsetenv("DISPLAY"); + } + /* Clean up the signalfd source (registered in wl_event_loop_run). */ + if (wl_state.sigfd_source) { + wl_event_source_remove(wl_state.sigfd_source); + wl_state.sigfd_source = NULL; + } + if (wl_state.sigfd >= 0) { + close(wl_state.sigfd); + wl_state.sigfd = -1; + } + /* Clean up the shutdown self-pipe (remove event source before display + * destruction so the fd is not left in the epoll set). */ + if (wl_state.shutdown_source) { + wl_event_source_remove(wl_state.shutdown_source); + wl_state.shutdown_source = NULL; + } + if (wl_state.shutdown_pipe[0] >= 0) { + close(wl_state.shutdown_pipe[0]); + wl_state.shutdown_pipe[0] = -1; + } + if (wl_state.shutdown_pipe[1] >= 0) { + close(wl_state.shutdown_pipe[1]); + wl_state.shutdown_pipe[1] = -1; + } + /* Remove all globally-registered wl_listener nodes before calling + * wl_display_destroy(). wl_display_destroy() frees wlroots objects + * (xdg_shell, backend, keyboard, pointer, output, etc.), which frees + * their signal structs. Any wl_listener.link that is still in one of + * those signal lists after the struct is freed becomes a dangling pointer. + * Subsequent wl_list operations (in wayland-server cleanup or by glibc's + * heap allocator detecting corruption) then trigger SIGABRT. + * Guard each removal with a link.next check: wayland-server zeroes the + * link after wl_list_remove(), so a non-NULL link means the listener is + * currently inserted in a signal list. */ +#define WL_LISTENER_REMOVE(l) \ + do { if ((l).link.next) wl_list_remove(&(l).link); } while (0) + + /* Output frame and new-output listeners */ + WL_LISTENER_REMOVE(wl_state.output_frame_listener); + WL_LISTENER_REMOVE(wl_state.backend_new_output_listener); + /* XDG shell and decoration listeners */ + WL_LISTENER_REMOVE(wl_state.xdg_new_surface_listener); + WL_LISTENER_REMOVE(wl_state.decoration_new_listener); + /* XWayland listeners (xwayland ptr already nulled in step 1, + * but listener links are still inserted in the (now-orphaned) xwayland + * object's signal lists — remove them to prevent heap corruption). */ + WL_LISTENER_REMOVE(wl_state.xwayland_ready_listener); + WL_LISTENER_REMOVE(wl_state.xwayland_new_surface_listener); + WL_LISTENER_REMOVE(wl_state.xwayland_server_destroy_listener); + /* Keyboard listeners */ + WL_LISTENER_REMOVE(wl_state.keyboard_key_listener); + WL_LISTENER_REMOVE(wl_state.keyboard_modifiers_listener); + /* Pointer listeners */ + WL_LISTENER_REMOVE(wl_state.pointer_motion_listener); + WL_LISTENER_REMOVE(wl_state.pointer_motion_abs_listener); + WL_LISTENER_REMOVE(wl_state.pointer_button_listener); + WL_LISTENER_REMOVE(wl_state.pointer_frame_listener); + /* Phase 20: backend new_input listener and real device listeners */ + WL_LISTENER_REMOVE(wl_state.backend_new_input_listener); + WL_LISTENER_REMOVE(wl_state.real_keyboard_key_listener); + WL_LISTENER_REMOVE(wl_state.real_keyboard_modifiers_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_motion_abs_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_button_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_frame_listener); + /* Phase 24/25: seat cursor + selection listeners */ + WL_LISTENER_REMOVE(wl_state.seat_request_cursor_listener); + WL_LISTENER_REMOVE(wl_state.seat_request_set_selection_listener); + /* Phase 26/28: primary selection + idle inhibitor listeners */ + WL_LISTENER_REMOVE(wl_state.seat_request_primary_selection_listener); + WL_LISTENER_REMOVE(wl_state.idle_new_inhibitor_listener); + /* Phase 29: axis listeners */ + WL_LISTENER_REMOVE(wl_state.pointer_axis_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_axis_listener); + /* Phase 30/31: pointer constraints + shortcuts inhibitor listeners */ + WL_LISTENER_REMOVE(wl_state.new_constraint_listener); + WL_LISTENER_REMOVE(wl_state.new_shortcuts_inhibitor_listener); + /* Phase 33: output management + output power listeners */ + WL_LISTENER_REMOVE(wl_state.output_mgr_apply_listener); + WL_LISTENER_REMOVE(wl_state.output_mgr_test_listener); + WL_LISTENER_REMOVE(wl_state.output_power_set_mode_listener); + /* Phase 34: text input, input method, virtual keyboard listeners */ + WL_LISTENER_REMOVE(wl_state.text_input_new_listener); + WL_LISTENER_REMOVE(wl_state.input_method_new_listener); + WL_LISTENER_REMOVE(wl_state.virtual_keyboard_new_listener); + /* Phase 35: xdg_activation listener */ + WL_LISTENER_REMOVE(wl_state.xdg_activation_request_listener); + /* Phase 36: layer shell new_surface listener */ + WL_LISTENER_REMOVE(wl_state.layer_shell_new_surface_listener); + /* Phase 37: virtual pointer listener */ + WL_LISTENER_REMOVE(wl_state.virtual_pointer_new_listener); +#undef WL_LISTENER_REMOVE + + /* Phase 23: destroy cursor and xcursor manager before display teardown. */ + if (wl_state.xcursor_mgr) { + wlr_xcursor_manager_destroy(wl_state.xcursor_mgr); + wl_state.xcursor_mgr = NULL; + } + if (wl_state.cursor) { + wlr_cursor_destroy(wl_state.cursor); + wl_state.cursor = NULL; + } + + /* Skip wl_display_destroy() and wl_display_destroy_clients(). + * + * wl_display_destroy() fires the display's destroy signal which triggers + * wlroots 0.15 internal object teardown. That teardown has a double- + * remove bug in the XWayland path: wlroots registers a listener on the + * Xwayland wl_client's destroy signal; that listener self-removes during + * client destruction, and wlr_xwayland_server_destroy() then tries to + * wl_list_remove() the already-nulled link, corrupting the heap + * (SIGABRT). + * + * wl_display_destroy_clients() also blocks: wlroots' XWayland client + * destroy handler performs blocking cleanup that can hang if Xwayland + * is in the middle of protocol dispatch. + * + * Both calls are unnecessary here because Exit() calls exit(0) immediately + * after wl_display_close() returns. The kernel reclaims all resources: + * - file descriptors closed → all Wayland clients get POLLHUP + * - memory freed by OS + * - Xwayland already SIGKILL'd in step 1 + * - event sources removed in step 2 + * - all our signal listeners removed in step 3 */ + unsetenv("WAYLAND_DISPLAY"); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.headless_inner = NULL; + wl_state.wl_backend_inner = NULL; + wl_state.display = NULL; + wl_state.output = NULL; + wl_state.frame_timer = NULL; + wl_state.output_layout = NULL; + wl_state.scene_output = NULL; + wl_state.scene = NULL; + wl_state.allocator = NULL; + wl_state.started = 0; +} + +static void +wl_display_post_open(const char *display_name, int *multi_head_out) +{ + (void)display_name; + (void)multi_head_out; +} + +static int +wl_display_screen_count(void) +{ + return wl_state.started ? 1 : 0; +} + +static int +wl_display_default_screen(void) +{ + return 0; +} + +static unsigned long +wl_atom_intern(const char *name) +{ + /* The WM's own UI runs on XWayland — it needs real X11 atoms for + * property operations (WM_STATE, WM_PROTOCOLS, etc.). */ + if (wl_state.x_display) + return (unsigned long)XInternAtom(wl_state.x_display, name, False); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Phase 29+: signalfd-based signal handling */ +/* ------------------------------------------------------------------ */ + +/* + * wl_signalfd_handler -- called from within wl_event_loop_dispatch() when + * SIGTERM, SIGINT, or SIGHUP is pending on the signalfd. + * + * Running inside the event loop means we can safely call + * wl_display_terminate() without risking a re-entrant call into the Wayland + * event loop machinery, which is the failure mode of the traditional + * signal-handler → DispatchEvent → wl_display_terminate path. + */ +static int +wl_signalfd_handler(int fd, uint32_t mask, void *data) +{ + struct signalfd_siginfo info; + ssize_t n; + (void)mask; (void)data; + + n = read(fd, &info, sizeof(info)); + if (n != (ssize_t)sizeof(info)) + return 0; + + wwarning("wl_backend: signalfd received signal %u - terminating event loop", + (unsigned)info.ssi_signo); + + /* Set the exit state flag so post-loop cleanup paths see it. */ + SIG_WCHANGE_STATE(WSTATE_NEED_EXIT); + + /* Terminate the event loop. */ + wl_state.terminate = 1; + + return 0; +} + +#ifdef HAVE_INOTIFY +static void wl_handle_inotify_events(void) +{ + ssize_t eventQLength; + size_t i = 0; + char buff[(sizeof(struct inotify_event) + NAME_MAX + 1) * 5]; + int oneShotFlag = 0; + + eventQLength = read(w_global.inotify.fd_event_queue, buff, sizeof(buff)); + if (eventQLength < 0) { + wwarning(_("read problem when trying to get INotify event: %s"), strerror(errno)); + return; + } + + while (i < (size_t)eventQLength) { + struct inotify_event *pevent = (struct inotify_event *)&buff[i]; + + if (pevent->mask & IN_DELETE_SELF) { + wwarning(_("the defaults database has been deleted!")); + if (w_global.inotify.fd_event_queue >= 0) { + close(w_global.inotify.fd_event_queue); + w_global.inotify.fd_event_queue = -1; + } + } + if (pevent->mask & IN_UNMOUNT) { + wwarning(_("the unit containing the defaults database has been unmounted.")); + if (w_global.inotify.fd_event_queue >= 0) { + close(w_global.inotify.fd_event_queue); + w_global.inotify.fd_event_queue = -1; + } + wPreferences.flags.noupdates = 1; + } + /* React on IN_MOVED_TO (atomic rename — WMWritePropListToFile pattern) + * or IN_CLOSE_WRITE for direct writes. Skip temp files (they have + * a dot in their name like 'WindowMaker.XXXXXX'). */ + if (oneShotFlag == 0 && pevent->len > 0) { + const char *name = pevent->name; + int is_tempfile = (strchr(name, '.') != NULL); + if (((pevent->mask & IN_MOVED_TO) || + ((pevent->mask & IN_CLOSE_WRITE) && !is_tempfile))) { + wDefaultsCheckDomains(NULL); + oneShotFlag = 1; + } + } + i += sizeof(struct inotify_event) + pevent->len; + } +} +#endif + +static void +wl_event_loop_run(void) +{ + if (!wl_state.display) + return; + + /* + * Install signalfd as the primary SIGTERM/SIGINT/SIGHUP handler. + * + * The traditional path (startup.c handleExitSig → DispatchEvent → + * event_loop_terminate → pipe write) works in headless/smoke-test mode + * because epoll_wait with SA_RESTART sees the pipe immediately after the + * signal handler returns. In nested-Wayland mode the wlroots nested + * backend processes parent-compositor events alongside our own clients, + * and the combination of SA_RESTART + epoll ordering can cause the pipe + * write to be missed until the next event, leaving SIGTERM apparently + * ignored. + * + * signalfd is the compositor-standard solution: block async delivery, + * route signals through a regular fd in the epoll set, and handle them + * synchronously from within the dispatch loop. The pipe-based path + * (wl_event_loop_terminate) is retained as a fallback for code paths + * that call it before wl_event_loop_run() returns. + */ + { + sigset_t ss; + sigemptyset(&ss); + sigaddset(&ss, SIGTERM); + sigaddset(&ss, SIGINT); + sigaddset(&ss, SIGHUP); + + wl_state.sigfd = signalfd(-1, &ss, SFD_NONBLOCK | SFD_CLOEXEC); + if (wl_state.sigfd >= 0) { + /* Block async delivery so signals only arrive via the fd. */ + sigprocmask(SIG_BLOCK, &ss, NULL); + + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + wl_state.sigfd_source = wl_event_loop_add_fd(loop, + wl_state.sigfd, + WL_EVENT_READABLE, + wl_signalfd_handler, + NULL); + if (wl_state.sigfd_source) { + wwarning("wl_backend: signalfd installed for SIGTERM/SIGINT/SIGHUP"); + /* Disable the old signal handlers — signalfd owns these now */ + signal(SIGTERM, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGHUP, SIG_DFL); + } else { + wwarning("wl_backend: wl_event_loop_add_fd failed; falling back to pipe"); + close(wl_state.sigfd); + wl_state.sigfd = -1; + /* Restore async delivery so the old handler still works. */ + sigprocmask(SIG_UNBLOCK, &ss, NULL); + } + } else { + wwarning("wl_backend: signalfd(2) failed (%s); SIGTERM via pipe fallback", + strerror(errno)); + } + } + + /* Stop the startup pump thread — wl_display_run() takes over Wayland + * event dispatching from here. Join before registering x11_event_source + * so there is no concurrent access to the event loop. */ + if (wl_state.startup_pump_running) { + wl_state.startup_pump_running = 0; + pthread_join(wl_state.startup_pump_thread, NULL); + wwarning("wl_backend: startup pump thread stopped"); + } + + /* Deferred dock creation — safe now that the pump thread has stopped. + * wm_dock_create calls wlr_scene_buffer_create which must not race with + * the pump thread's frame-timer wlr_scene_output_commit. */ + if (wl_state.dock_pending_scr) { + wm_dock_create(wl_state.dock_pending_scr); + wl_state.dock_pending_scr = NULL; + } + + /* Create scene nodes for any OR views whose associate signal fired + * during the pump thread (when scene ops were unsafe). */ + { + 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) { + 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); + } + } + } + + /* Register the X11 fd watcher now that StartUp() has completed and + * WINGs is initialised. Doing this here (not in wl_display_open) ensures + * the pump thread never touches Xlib. */ + if (wl_state.x_display && !wl_state.x11_event_source) { + wl_state.x11_event_source = wl_event_loop_add_fd( + wl_display_get_event_loop(wl_state.display), + ConnectionNumber(wl_state.x_display), + WL_EVENT_READABLE, + wl_handle_x11_events, NULL); + if (wl_state.x11_event_source) + wwarning("wl_backend: X11 event source registered for WINGs dispatch"); + } + + wwarning("wl_backend: DISPLAY=%s WAYLAND_DISPLAY=%s", + getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)", + getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(unset)"); + + /* Phase A: register compositor dispatch callback so WINGs modal loops + * (WMRunModalLoop, color panels, etc.) can tick the event loop. */ + { + WMScreen *wmscr = wl_get_wmscreen(); + if (wmscr) { + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + int loop_fd = wl_event_loop_get_fd(loop); + W_WL_SetLoopDispatch(wmscr, loop_fd, wl_wings_dispatch, NULL); + wwarning("wl_backend: WINGs loop dispatch registered (fd=%d)", loop_fd); + } + } + + wwarning("wl_backend: event loop running"); + + /* Custom event loop: pump wlroots, then drain WINGs events. + * We can't use wl_display_run() because WM event handlers (OpenRootMenu, + * etc.) call WMMaskEvent which re-enters the wlroots loop. By draining + * WINGs events BETWEEN dispatches (not inside a timer callback), modal + * loops in WMMaskEvent can safely call wl_event_loop_dispatch. */ + { + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + while (!wl_state.terminate) { + wl_display_flush_clients(wl_state.display); + extern void W_WLClientDispatchPending(void); + W_WLClientDispatchPending(); + wl_event_loop_dispatch(loop, 16); + /* Schedule frame AFTER dispatch so the frame event on the next + * iteration renders content from commits processed above. */ + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); + + /* Check inotify for defaults database changes */ +#ifdef HAVE_INOTIFY + if (w_global.inotify.fd_event_queue >= 0) { + fd_set rfds; + struct timeval tv = { 0, 0 }; + FD_ZERO(&rfds); + FD_SET(w_global.inotify.fd_event_queue, &rfds); + if (select(w_global.inotify.fd_event_queue + 1, &rfds, NULL, NULL, &tv) > 0 + && FD_ISSET(w_global.inotify.fd_event_queue, &rfds)) { + wl_handle_inotify_events(); + } + } +#endif + + /* Fire any expired WINGs timers (workspace name fade, etc.) */ + W_CheckTimerHandlers(); + + /* Drain WINGs event queue */ + WMScreen *wmscr = wl_get_wmscreen(); + if (wmscr) { + const W_EventBackend *be = wmscr->eventBackend; + void *ectx = wmscr->eventBackendCtx; + WMEvent wev; + int limit = 64; + int handled = 0; + while (be->pending(ectx) && limit-- > 0) { + be->fetch(ectx, &wev); + WMHandleEvent(&wev); + handled++; + } + /* If WINGs events were processed (which may trigger client + * surface commits via wl_redisplay), do another short dispatch + * to process those commits so the next frame renders them. */ + if (handled) + wl_event_loop_dispatch(loop, 0); + } + } + } + + wwarning("wl_backend: event loop exited"); + + /* Remove pointer listeners before wlroots destroys the pointer device */ + if (wl_state.pointer_dev) { + wl_list_remove(&wl_state.real_pointer_motion_abs_listener.link); + wl_list_remove(&wl_state.real_pointer_button_listener.link); + wl_list_remove(&wl_state.real_pointer_frame_listener.link); + wl_list_remove(&wl_state.real_pointer_axis_listener.link); + } +} + +static void +wl_event_loop_terminate(void) +{ + /* Write a byte to the self-pipe. write(2) is async-signal-safe, so this + * can safely be called from a signal handler. The event loop will read + * the pipe and call wl_display_terminate() from within loop context, + * avoiding the double-free that results from calling wl_display_destroy() + * while wl_event_loop_dispatch() is active. */ + if (wl_state.shutdown_pipe[1] >= 0) { + char c = 'T'; + (void)write(wl_state.shutdown_pipe[1], &c, 1); + } else if (wl_state.display) { + /* fallback if pipe creation failed at startup */ + wl_display_terminate(wl_state.display); + } +} + +/* ================================================================== */ +/* Phase 29: WM-owned SHM surfaces (pixman rendering → wlroots scene) */ +/* ================================================================== */ + +/* + * wm_shm_surface — a compositor-owned pixel buffer that plugs into the + * wlroots scene graph. + * + * Memory layout: + * wm_shm_surface embeds a wlr_buffer so wlroots can reference-count and + * upload its pixels as a GPU texture. The pixel array (ARGB8888) is + * malloc'd and wrapped by a pixman_image_t for drawing. + * + * Lifecycle: + * wm_shm_surface_create — alloc + pixman init + * wm_shm_surface_fill — draw into the pixman image (no upload yet) + * wm_shm_surface_commit — create/reposition the wlr_scene_buffer node + * wm_shm_surface_destroy — tear down scene node + drop wlr_buffer ref + * (shm_buf_destroy frees memory when ref = 0) + * + * Update model (Phase 29): surfaces are static after first commit. + * A re-draw path (drop + recreate scene node) will be added in Phase 30 + * once the dock and title-bar code drives it. + */ + +struct wm_shm_surface { + /* wlr_buffer interface — MUST be first so wl_container_of works */ + struct wlr_buffer wlr_buf; + /* Pixel store */ + uint32_t *data; /* ARGB8888, row-major */ + int width; + int height; + size_t stride; /* bytes per row = width * 4 */ + /* pixman drawing surface wrapping data[] */ + pixman_image_t *image; + /* scene graph presence */ + struct wlr_scene_buffer *scene_buf; /* NULL until first commit */ +}; + +/* ---------- wlr_buffer implementation -------------------------------- */ + +static void +shm_buf_destroy(struct wlr_buffer *buf) +{ + struct wm_shm_surface *s = wl_container_of(buf, s, wlr_buf); + if (s->image) + pixman_image_unref(s->image); + free(s->data); + free(s); +} + +static bool +shm_buf_begin_data_ptr(struct wlr_buffer *buf, uint32_t flags, + void **out_data, uint32_t *out_fmt, size_t *out_stride) +{ + struct wm_shm_surface *s = wl_container_of(buf, s, wlr_buf); + (void)flags; + *out_data = s->data; + *out_fmt = DRM_FORMAT_ARGB8888; + *out_stride = s->stride; + return true; +} + +static void +shm_buf_end_data_ptr(struct wlr_buffer *buf) +{ + (void)buf; +} + +static const struct wlr_buffer_impl wm_shm_buf_impl = { + .destroy = shm_buf_destroy, + .begin_data_ptr_access = shm_buf_begin_data_ptr, + .end_data_ptr_access = shm_buf_end_data_ptr, +}; + +/* ---------- surface API ---------------------------------------------- */ + +static struct wm_shm_surface * +wm_shm_surface_create(int width, int height) +{ + if (width <= 0 || height <= 0) + return NULL; + + struct wm_shm_surface *s = calloc(1, sizeof(*s)); + if (!s) + return NULL; + + s->width = width; + s->height = height; + s->stride = (size_t)width * 4; + s->data = calloc(1, (size_t)height * s->stride); + if (!s->data) { + free(s); + return NULL; + } + + /* wlr_buffer init — producer holds one reference */ + wlr_buffer_init(&s->wlr_buf, &wm_shm_buf_impl, width, height); + + /* pixman image wrapping the same memory */ + s->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, + width, height, + (uint32_t *)s->data, + (int)s->stride); + if (!s->image) { + /* wlr_buffer_drop triggers shm_buf_destroy → frees data + s */ + wlr_buffer_drop(&s->wlr_buf); + return NULL; + } + + return s; +} + +/* + * Fill a rectangle with a solid ARGB colour. + * argb: 0xAARRGGBB — e.g. 0xFF4080C0 for opaque steel-blue. + */ +static void +wm_shm_surface_fill(struct wm_shm_surface *s, + int x, int y, int w, int h, + uint32_t argb) +{ + if (!s || !s->image) + return; + /* clamp to surface bounds */ + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x + w > s->width) w = s->width - x; + if (y + h > s->height) h = s->height - y; + if (w <= 0 || h <= 0) + return; + + pixman_color_t c = { + .alpha = (uint16_t)(((argb >> 24) & 0xff) * 0x0101), + .red = (uint16_t)(((argb >> 16) & 0xff) * 0x0101), + .green = (uint16_t)(((argb >> 8) & 0xff) * 0x0101), + .blue = (uint16_t)(((argb >> 0) & 0xff) * 0x0101), + }; + pixman_image_t *fill = pixman_image_create_solid_fill(&c); + if (!fill) + return; + pixman_image_composite32(PIXMAN_OP_SRC, fill, NULL, s->image, + 0, 0, 0, 0, x, y, w, h); + pixman_image_unref(fill); +} + +/* + * Commit the surface to the scene graph. + * + * parent — scene-graph parent node (e.g. &wl_state.scene->tree.node) + * x, y — position in the compositor coordinate space + * + * First call creates the scene_buffer node; subsequent calls just + * reposition it (pixel updates are uploaded lazily by wlroots on the + * next wlr_scene_output_commit). + */ +static void +wm_shm_surface_commit(struct wm_shm_surface *s, + struct wlr_scene_tree *parent, + int x, int y) +{ + if (!s) + return; + if (!s->scene_buf) { + s->scene_buf = wlr_scene_buffer_create(parent, &s->wlr_buf); + if (!s->scene_buf) { + wwarning("wm_shm_surface_commit: wlr_scene_buffer_create failed"); + return; + } + wwarning("wl_backend: SHM surface %dx%d committed to scene at %d,%d", + s->width, s->height, x, y); + } + wlr_scene_node_set_position(&s->scene_buf->node, x, y); +} + +/* + * Destroy a surface and remove it from the scene graph. + * After this call s is invalid — the memory is freed asynchronously + * by shm_buf_destroy once the scene node releases its lock. + */ +static void +wm_shm_surface_destroy(struct wm_shm_surface *s) +{ + if (!s) + return; + if (s->scene_buf) { + wlr_scene_node_destroy(&s->scene_buf->node); + s->scene_buf = NULL; + } + wlr_buffer_drop(&s->wlr_buf); /* producer done; shm_buf_destroy frees s */ +} + +/* ================================================================== */ +/* Phase 30: WM dock panel (layer-anchored, pixman-rendered) */ +/* ================================================================== */ + +/* + * The dock is a vertical strip anchored to the right edge of the primary + * output. It is rendered into a wm_shm_surface whose pixels flow through + * the wlroots scene graph, exactly like any other WM UI element. + * + * Layout (right edge, full screen height): + * + * ┌──────────────────────────────────────────────────┬────────┐ + * │ │ ██████ │ + * │ desktop / client windows │ ██████ │ + * │ (constrained to usable area) │ ██████ │ + * │ │ dock │ + * └──────────────────────────────────────────────────┴────────┘ + * ← dw → + * + * The usable area (where windows are placed) is shrunk by dock_width so + * clients never overlap the panel. + */ + +/* Default width when wPreferences.icon_size is not yet set. */ +#define WM_DOCK_DEFAULT_WIDTH 68 /* 64 px icon + 4 px border */ + +/* Colours (ARGB8888, fully opaque) */ +#define WM_DOCK_BG 0xFF1C1C1C /* dark charcoal background */ +#define WM_DOCK_HIGHLIGHT 0xFF404040 /* 1-px highlight on left edge */ +#define WM_DOCK_TOPSEP 0xFF303030 /* 1-px separator at top */ +#define WM_DOCK_TILE_BG 0xFF252525 /* icon-slot background */ +#define WM_DOCK_TILE_HI 0xFF353535 /* icon-slot top/left highlight */ +#define WM_DOCK_TILE_SH 0xFF151515 /* icon-slot bottom/right shadow */ + +/* wm_dock_render_tile — paint one icon slot starting at (x, y). + * Slots are icon_size × icon_size with a 1-px inset bevel. */ +static void +wm_dock_render_tile(struct wm_shm_surface *s, + int x, int y, int tile_sz) +{ + /* Background */ + wm_shm_surface_fill(s, x, y, tile_sz, tile_sz, WM_DOCK_TILE_BG); + /* Top highlight */ + wm_shm_surface_fill(s, x, y, tile_sz, 1, WM_DOCK_TILE_HI); + /* Left highlight */ + wm_shm_surface_fill(s, x, y, 1, tile_sz, WM_DOCK_TILE_HI); + /* Bottom shadow */ + wm_shm_surface_fill(s, x, y + tile_sz - 1, tile_sz, 1, WM_DOCK_TILE_SH); + /* Right shadow */ + wm_shm_surface_fill(s, x + tile_sz - 1, y, 1, tile_sz, WM_DOCK_TILE_SH); +} + +/* wm_dock_render — paint the full dock panel. + * Draws the background, left-edge highlight, top separator, and a column + * of empty icon slots to give visual structure before real icons arrive. */ +static void +wm_dock_render(struct wm_shm_surface *s, int icon_sz, int n_slots) +{ + int w = s->width; + int h = s->height; + int tile_sz = icon_sz; + int pad = (w - tile_sz) / 2; /* horizontal centering margin */ + + /* Background */ + wm_shm_surface_fill(s, 0, 0, w, h, WM_DOCK_BG); + + /* Left-edge highlight (the dock's inner border with the desktop) */ + wm_shm_surface_fill(s, 0, 0, 1, h, WM_DOCK_HIGHLIGHT); + + /* Top separator */ + wm_shm_surface_fill(s, 0, 0, w, 1, WM_DOCK_TOPSEP); + + /* Icon-slot placeholders */ + for (int i = 0; i < n_slots; i++) { + int ty = 2 + i * (tile_sz + 2); + if (ty + tile_sz > h) + break; + wm_dock_render_tile(s, pad, ty, tile_sz); + } +} + +/* wm_dock_create — allocate the dock surface, render it, attach to scene, + * and shrink the head-0 usable area by dock_width pixels on the right. */ +static void +wm_dock_create(WScreen *scr) +{ + if (!wl_state.scene || !wl_state.output) + return; + + int sw = (int)wl_state.output->width; + int sh = (int)wl_state.output->height; + int icon_sz = (wPreferences.icon_size > 0) + ? wPreferences.icon_size : 64; + int dw = icon_sz + 4; /* dock width = icon + 2px pad each side */ + int n_slots = (sh - 4) / (icon_sz + 2); /* how many slots fit */ + + struct wm_shm_surface *s = wm_shm_surface_create(dw, sh); + if (!s) { + wwarning("wl_backend: wm_dock_create: failed to allocate surface"); + return; + } + + wm_dock_render(s, icon_sz, n_slots); + /* 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; + wl_state.dock_icon_sz = icon_sz; + wl_state.dock_n_slots = n_slots; + wl_state.dock_app_count = 0; + + /* Shrink head-0 usable area so window placement avoids the dock. */ + if (scr->usableArea) { + int right = scr->usableArea[0].x2 - dw; + if (right < scr->usableArea[0].x1) + right = scr->usableArea[0].x1; + scr->usableArea[0].x2 = right; + scr->totalUsableArea[0].x2 = right; + } + + wwarning("wl_backend: dock panel created at %d,0 (%dx%d, %d slots)", + sw - dw, dw, sh, n_slots); +} + +/* ------------------------------------------------------------------ */ +/* Phase 30+: dock icon management */ +/* ------------------------------------------------------------------ */ + +/* + * wm_shm_surface_repaint — after updating the pixman pixel data, + * force wlroots to re-upload the buffer to the GPU. + * + * wlroots 0.15 caches a texture per wlr_buffer pointer. Since our + * buffer is the same C object the cache entry stays valid even when + * pixel data changes. The reliable solution is to destroy the + * scene_buffer node and recreate it; wlroots then creates a fresh + * texture from the updated pixel data on the next frame. + */ +static void +wm_shm_surface_repaint(struct wm_shm_surface *s) +{ + if (!s || !s->scene_buf) return; + int x = s->scene_buf->node.x; + int y = s->scene_buf->node.y; + struct wlr_scene_tree *parent = s->scene_buf->node.parent; + wlr_scene_node_destroy(&s->scene_buf->node); + s->scene_buf = NULL; + if (parent) { + s->scene_buf = wlr_scene_buffer_create(parent, &s->wlr_buf); + if (s->scene_buf) + wlr_scene_node_set_position(&s->scene_buf->node, x, y); + } +} + +/* + * wm_dock_load_icon — read _NET_WM_ICON from an XWayland window via + * wrlib and return a heap-allocated ARGB8888 pixel array. + * Returns NULL for XDG native clients (no X11 icon available) or on + * failure; caller must free() the returned pointer when done. + */ +static uint32_t * +wm_dock_load_icon(struct wl_toplevel_view *v, int *out_w, int *out_h) +{ + *out_w = *out_h = 0; + if (!v->xw_surface || !wl_state.x_display) + return NULL; /* XDG native: no X11 icon path */ + + RImage *ri = get_window_image_from_x11(wl_state.x_display, (WNativeWindow)v->id); + if (!ri) + return NULL; + + int w = ri->width, h = ri->height; + int bpp = (ri->format == RRGBAFormat) ? 4 : 3; + uint32_t *argb = malloc((size_t)(w * h) * 4); + if (!argb) { RReleaseImage(ri); return NULL; } + + unsigned char *p = ri->data; + for (int i = 0; i < w * h; i++, p += bpp) { + uint8_t r = p[0], g = p[1], b = p[2]; + uint8_t a = (bpp == 4) ? p[3] : 255; + argb[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + RReleaseImage(ri); + *out_w = w; + *out_h = h; + return argb; +} + +/* + * wm_dock_refresh — repaint the dock surface with empty tiles, then + * overlay each managed app's icon into its slot, and re-upload to GPU. + */ +static void +wm_dock_refresh(void) +{ + struct wm_shm_surface *s = wl_state.dock_surface; + if (!s) return; + + int icon_sz = wl_state.dock_icon_sz; + int n_slots = wl_state.dock_n_slots; + int w = s->width; + int tile_sz = icon_sz; + int pad = (w - tile_sz) / 2; + + /* Repaint background and all placeholder tiles */ + wm_dock_render(s, icon_sz, n_slots); + + /* Composite each app icon into its slot */ + for (int i = 0; i < wl_state.dock_app_count && i < n_slots; i++) { + int iw = wl_state.dock_apps[i].icon_w; + int ih = wl_state.dock_apps[i].icon_h; + uint32_t *px = wl_state.dock_apps[i].icon_argb; + + if (!px || iw <= 0 || ih <= 0) + continue; + + pixman_image_t *src = pixman_image_create_bits( + PIXMAN_a8r8g8b8, iw, ih, px, iw * 4); + if (!src) continue; + + /* Scale into the inset area (1px bevel border) */ + int dst_sz = tile_sz - 2; + if (dst_sz < 1) dst_sz = 1; + pixman_transform_t xf; + pixman_transform_init_scale(&xf, + pixman_double_to_fixed((double)iw / dst_sz), + pixman_double_to_fixed((double)ih / dst_sz)); + pixman_image_set_transform(src, &xf); + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + + int ty = 2 + i * (tile_sz + 2); + if (ty + tile_sz > s->height) { pixman_image_unref(src); break; } + + pixman_image_composite32(PIXMAN_OP_OVER, + src, NULL, s->image, + 0, 0, 0, 0, + pad + 1, ty + 1, + dst_sz, dst_sz); + pixman_image_unref(src); + } + + /* Re-upload to GPU */ + wm_shm_surface_repaint(s); +} + +/* + * wm_dock_app_add — called when a window is managed; adds it to the + * dock list, attempts to load its icon, and refreshes the dock panel. + */ +static void +wm_dock_app_add(struct wl_toplevel_view *v) +{ + if (!wl_state.dock_surface) return; + if (wl_state.dock_app_count >= WM_DOCK_MAX_APPS) return; + + int slot = wl_state.dock_app_count; + wl_state.dock_apps[slot].id = v->id; + wl_state.dock_apps[slot].icon_argb = + wm_dock_load_icon(v, &wl_state.dock_apps[slot].icon_w, + &wl_state.dock_apps[slot].icon_h); + wl_state.dock_app_count++; + + wm_dock_refresh(); + + wwarning("wl_backend: dock: add id=0x%lx icon=%s slot=%d", + v->id, + wl_state.dock_apps[slot].icon_argb ? "loaded" : "none", + slot); +} + +/* + * wm_dock_app_remove — called when a window is unmanaged; removes it + * from the dock list and refreshes the panel. + */ +static void +wm_dock_app_remove(WNativeWindow id) +{ + if (!wl_state.dock_surface) return; + + for (int i = 0; i < wl_state.dock_app_count; i++) { + if (wl_state.dock_apps[i].id != id) + continue; + free(wl_state.dock_apps[i].icon_argb); + int tail = wl_state.dock_app_count - i - 1; + if (tail > 0) + memmove(&wl_state.dock_apps[i], + &wl_state.dock_apps[i + 1], + (size_t)tail * sizeof(wl_state.dock_apps[0])); + wl_state.dock_app_count--; + wm_dock_refresh(); + wwarning("wl_backend: dock: remove id=0x%lx total=%d", + id, wl_state.dock_app_count); + return; + } +} + +/* ------------------------------------------------------------------ */ +/* Screen open/close */ +/* ------------------------------------------------------------------ */ + +static void +wl_screen_open(WScreen *scr) +{ + /* Set geometry from the headless output created in display_open(). */ + scr->scr_width = wl_state.output ? + (int)wl_state.output->width : 1920; + scr->scr_height = wl_state.output ? + (int)wl_state.output->height : 1080; + scr->depth = 24; + scr->root_win = (WNativeWindow)0x0FFFFFFF; /* synthetic root window ID */ + scr->colormap = 0; + + /* Wayland has no X11-style window border; the frame_buf renders the + * full decoration. Set border width to 0 so policy code does not + * offset the client position by a non-existent border. */ + scr->frame_border_width = 0; + + /* XWayland bridge: fill in real X11 screen values so RCreateContext + * and WINGs can use the display connection for drawing and fonts. */ + if (wl_state.x_display) { + int snum = DefaultScreen(wl_state.x_display); + scr->root_win = DefaultRootWindow(wl_state.x_display); + scr->depth = DefaultDepth(wl_state.x_display, snum); + scr->colormap = DefaultColormap(wl_state.x_display, snum); + } +} + +static void wl_screen_close(WScreen *scr) { (void)scr; } + +static int +wl_screen_init_display(WScreen *scr, int screen_number) +{ + (void)screen_number; + scr->w_depth = 32; + scr->rcontext = RCreateContextWayland(scr->scr_width, scr->scr_height, NULL); + + /* WINGs widget backends are selected via W_SetNextScreenKind — the + * compositor uses the Wayland-mode set even though wmaker itself is + * the compositor (rendering goes through pixman → wlroots scenes). */ + W_SetNextScreenKind(WM_SCREEN_KIND_WAYLAND); + scr->wmscreen = WMCreateScreenWithRContext(NULL, 0, scr->rcontext); + if (!scr->wmscreen) { + wfatal(_("could not initialize WINGs widget set")); + return 0; + } + + scr->black = WMBlackColor(scr->wmscreen); + scr->white = WMWhiteColor(scr->wmscreen); + scr->gray = WMGrayColor(scr->wmscreen); + scr->darkGray = WMDarkGrayColor(scr->wmscreen); + + scr->black_pixel = scr->black ? WMColorPixel(scr->black) : 0xFF000000; + scr->white_pixel = scr->white ? WMColorPixel(scr->white) : 0xFFFFFFFF; + scr->light_pixel = scr->gray ? WMColorPixel(scr->gray) : 0xFFAAAAAA; + scr->dark_pixel = scr->darkGray ? WMColorPixel(scr->darkGray) : 0xFF555555; + + return 1; +} + +static int +wl_show_crash_dialog(int sig) +{ + /* Wayland: showing a UI from a crashed compositor's child process is + * not possible (the child would need to connect to the dead parent + * for its compositor). Default to auto-restart, which is the most + * useful behavior for a daily-driver compositor — the user's session + * recovers on its own. The crash signal is logged separately by + * the monitor parent. */ + (void)sig; + return WMRestart; +} + +/* ------------------------------------------------------------------ */ +/* Frame buffer (pixman + wlr_scene_buffer) */ +/* ------------------------------------------------------------------ */ + +static void frame_buf_destroy(struct wlr_buffer *wlr_buf) +{ + struct wl_frame_buf *fb = wl_container_of(wlr_buf, fb, base); + if (fb->image) + pixman_image_unref(fb->image); + wfree(fb); +} + +static bool frame_buf_begin_data_ptr_access(struct wlr_buffer *wlr_buf, + uint32_t flags, void **data, uint32_t *format, size_t *stride) +{ + struct wl_frame_buf *fb = wl_container_of(wlr_buf, fb, base); + (void)flags; + *data = pixman_image_get_data(fb->image); + *format = DRM_FORMAT_ARGB8888; + *stride = (size_t)pixman_image_get_stride(fb->image); + return true; +} + +static void frame_buf_end_data_ptr_access(struct wlr_buffer *wlr_buf) +{ + (void)wlr_buf; +} + +static const struct wlr_buffer_impl frame_buf_impl = { + .destroy = frame_buf_destroy, + .begin_data_ptr_access = frame_buf_begin_data_ptr_access, + .end_data_ptr_access = frame_buf_end_data_ptr_access, +}; + +static struct wl_frame_buf * +frame_buf_find(WNativeWindow id) +{ + struct wl_frame_buf *fb; + wl_list_for_each(fb, &wl_state.frame_list, link) { + if (fb->id == id) return fb; + } + return NULL; +} + +/* + * fb_scene_buf_attach / fb_tree_attach + * + * Register destroy listeners on a scene_buf or tree so that fb->scene_buf + * (or fb->tree) is NULL'd when wlroots auto-destroys the node — typically + * via cascade when an ancestor scene_tree is destroyed. Without this, + * follow-up cleanup paths (wl_view_destroy_backing_impl, wl_frame_destroy) + * call wlr_scene_node_destroy on a freed node and segfault inside + * wl_list_remove against a zeroed list link. + */ +static void fb_scene_buf_destroy_handler(struct wl_listener *l, void *d) +{ + (void)d; + struct wl_frame_buf *fb = wl_container_of(l, fb, scene_buf_destroy); + fb->scene_buf = NULL; + wl_list_remove(&fb->scene_buf_destroy.link); +} +static void fb_tree_destroy_handler(struct wl_listener *l, void *d) +{ + (void)d; + struct wl_frame_buf *fb = wl_container_of(l, fb, tree_destroy); + fb->tree = NULL; + /* The tree's destroy cascades into scene_buf — its destroy handler + * will fire too and NULL fb->scene_buf separately. */ + wl_list_remove(&fb->tree_destroy.link); +} +static void fb_attach_scene_buf_listener(struct wl_frame_buf *fb) +{ + if (!fb || !fb->scene_buf) return; + fb->scene_buf_destroy.notify = fb_scene_buf_destroy_handler; + wl_signal_add(&fb->scene_buf->node.events.destroy, + &fb->scene_buf_destroy); +} +static void fb_attach_tree_listener(struct wl_frame_buf *fb) +{ + if (!fb || !fb->tree) return; + fb->tree_destroy.notify = fb_tree_destroy_handler; + wl_signal_add(&fb->tree->node.events.destroy, &fb->tree_destroy); +} + +/* Recursively update fb->x, fb->y for all descendants of `parent`, used + * to keep absolute-position tracking in sync after the parent moves. + * Hit-testing (frame_buf_at, frame_buf_button_at) and click coordinate + * translation depend on these fields being current. */ +static void +frame_buf_update_children_abs(WNativeWindow parent_id) +{ + struct wl_frame_buf *parent = frame_buf_find(parent_id); + if (!parent) return; + struct wl_frame_buf *child; + wl_list_for_each(child, &wl_state.frame_list, link) { + if (child->parent_id == parent_id) { + child->x = parent->x + child->rel_x; + child->y = parent->y + child->rel_y; + /* Recurse into grandchildren */ + frame_buf_update_children_abs(child->id); + } + } + /* Also update toplevel views parented to this frame */ + struct wl_toplevel_view *cv; + wl_list_for_each(cv, &wl_state.toplevel_list, link) { + if (cv->frame_id == parent_id) { + cv->x = parent->x + cv->frame_rel_x; + cv->y = parent->y + cv->frame_rel_y; + } + } +} + +/* 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) +{ + if (!wl_state.scene) 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; + } + + /* 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: match a tree-less frame_buf (WINGs dialog) whose scene_buf + * is a direct child of a level_tree bucket. */ + wl_list_for_each(fb, &wl_state.frame_list, link) { + if (!fb->tree && fb->scene_buf && &fb->scene_buf->node == n) + return fb; + } + + /* Step 6: topmost node was a client surface — not a WM frame. */ + 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->node.enabled) continue; /* hidden */ + /* Walk up the scene tree — if any ancestor is disabled, this + * frame_buf is not actually visible (just its node enabled flag + * is set, but a parent tree was disabled e.g. by window_unmap). */ + struct wlr_scene_node *n = fb->scene_buf->node.parent + ? &fb->scene_buf->node.parent->node : NULL; + int ancestor_disabled = 0; + while (n) { + if (!n->enabled) { ancestor_disabled = 1; break; } + n = n->parent ? &n->parent->node : NULL; + } + if (ancestor_disabled) continue; + /* Buttons are small children of the titlebar with enabled scene_bufs. + * Skip large nodes (titlebars, resizebars) by size threshold. */ + 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 */ +/* ------------------------------------------------------------------ */ + +static WNativeWindow +wl_frame_create_toplevel(WScreen *scr, + int x, int y, int width, int height, + int border_width, + int depth, void *visual, + unsigned long colormap, + WMPixel border_pixel) +{ + (void)border_width; (void)depth; (void)visual; + (void)colormap; (void)border_pixel; (void)scr; + + if (width <= 0) width = 1; + if (height <= 0) height = 1; + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, + width, height, NULL, 0); + if (!fb->image) { + wfree(fb); + return WNATIVE_NONE; + } + + wlr_buffer_init(&fb->base, &frame_buf_impl, width, height); + + fb->id = wl_state.next_synthetic_id++; + fb->x = x; + fb->y = y; + fb->width = width; + fb->height = height; + + /* Create scene buffer node under a per-window tree. + * The tree is positioned at (x,y) so all children use local offsets. + * Default to WL_LAYER_NORMAL; wl_stacking_set_level() will reparent + * to the correct bucket when AddToStackList() fires immediately after. */ + if (wl_state.scene) { + 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) { + fb_attach_tree_listener(fb); + wlr_scene_node_set_position(&fb->tree->node, x, y); + fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + /* Tree starts disabled — wl_window_map enables it when the + * frame is logically mapped. Without this, decoration + * children (titlebar, buttons) of unmapped menus/dialogs + * appear as orphans because window_map_raised on those + * children enables their scene_bufs while the frame itself + * has not been mapped. */ + wlr_scene_node_set_enabled(&fb->tree->node, false); + } + } + + 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; +} + +static WNativeWindow +wl_frame_create_child(WNativeWindow parent_win, WScreen *scr, + int x, int y, int width, int height) +{ + (void)scr; + + if (width <= 0) width = 1; + if (height <= 0) height = 1; + + /* Compute absolute position from parent */ + int abs_x = x, abs_y = y; + struct wl_frame_buf *parent = frame_buf_find(parent_win); + if (parent) { + abs_x = parent->x + x; + abs_y = parent->y + y; + } + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, + width, height, NULL, 0); + if (!fb->image) { + wfree(fb); + return WNATIVE_NONE; + } + + wlr_buffer_init(&fb->base, &frame_buf_impl, width, height); + + fb->id = wl_state.next_synthetic_id++; + fb->parent_id = parent_win; + fb->rel_x = x; + fb->rel_y = y; + fb->x = abs_x; + fb->y = abs_y; + fb->width = width; + fb->height = height; + + if (wl_state.scene) { + struct wlr_scene_tree *ptree = (parent && parent->tree) + ? parent->tree : &wl_state.scene->tree; + fb->scene_buf = wlr_scene_buffer_create(ptree, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + W_RegisterBacking(fb->id, fb->image); + return fb->id; +} + +static void +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); + /* Destroy the scene_buf and (if any) the tree. Without destroying + * the tree, leaked tree nodes accumulate in the scene graph and + * can cause stale rendering or hit-testing issues (e.g. ghost + * titlebars after a menu is closed). */ + if (fb->scene_buf) + wlr_scene_node_destroy(&fb->scene_buf->node); + if (fb->tree) + wlr_scene_node_destroy(&fb->tree->node); + wl_list_remove(&fb->link); + wlr_buffer_drop(&fb->base); +} + +static void +wl_frame_configure(WNativeWindow win, int x, int y, int width, int height) +{ + struct wl_frame_buf *fb = frame_buf_find(win); + if (!fb) return; + + /* Update position tracking */ + fb->rel_x = x; + fb->rel_y = y; + if (fb->parent_id) { + struct wl_frame_buf *parent = frame_buf_find(fb->parent_id); + if (parent) { + fb->x = parent->x + x; + fb->y = parent->y + y; + } else { + fb->x = x; + fb->y = y; + } + /* Child frame_buf: reposition scene_buf at relative offset within parent tree */ + if (fb->scene_buf) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } else { + fb->x = x; + fb->y = y; + /* Top-level frame: move the tree (children follow automatically) */ + if (fb->tree) + wlr_scene_node_set_position(&fb->tree->node, x, y); + } + + /* Resize: reallocate the pixman image if dimensions changed. */ + if (width > 0 && height > 0 && (width != fb->width || height != fb->height)) { + if (fb->image) + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, + width, height, NULL, 0); + fb->width = width; + fb->height = height; + fb->base.width = width; + fb->base.height = height; + /* Notify scene graph of new size — required for hit-testing. + * wlroots caches buffer dimensions; without this, clicks beyond + * the old size would miss this scene_buf. */ + if (fb->scene_buf) + wlr_scene_buffer_set_dest_size(fb->scene_buf, width, height); + } + + /* Update absolute position tracking — recursively for all descendants. */ + frame_buf_update_children_abs(win); +} + +static void wl_frame_paint_button(WCoreWindow *button, WTexture *texture, + unsigned long color, WPixmap *image, int pushed); + +/* + * wl_frame_paint — render the entire frame decoration into the pixman buffer. + * + * Uses wTextureRenderImage() (pure CPU) for titlebar/resizebar backgrounds, + * pixman for bevel lines, and the WINGs font backend for title text. + * Returns 1 (handled) so wFrameWindowPaint skips the Xlib code path. + */ +static int +wl_frame_paint(struct WFrameWindow *fwin) +{ + struct wl_frame_buf *fb = frame_buf_find(fwin->core->window); + if (!fb || !fb->image) + return 0; + + int state = fwin->flags.state; + int total_w = fb->width; + int total_h = fb->height; + /* Clear the buffer to transparent black. */ + pixman_color_t clear = {0, 0, 0, 0}; /* transparent */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, fb->image, &clear, + 1, &(pixman_rectangle16_t){0, 0, total_w, total_h}); + + /* --- Titlebar --- */ + if (fwin->titlebar && fwin->top_width > 0) { + struct wl_frame_buf *tb_fb = frame_buf_find(fwin->titlebar->window); + pixman_image_t *tb_img = tb_fb ? tb_fb->image : fb->image; + 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, 0, 0); + } + + /* Ensure titlebar buffer is full width */ + if (tb_fb && tb_fb->width < tb_w) { + pixman_image_unref(tb_fb->image); + tb_fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, + tb_w, tb_h, NULL, 0); + tb_fb->width = tb_w; + tb_fb->height = tb_h; + tb_fb->base.width = tb_w; + tb_fb->base.height = tb_h; + tb_img = tb_fb->image; + /* Notify scene graph so hit-testing covers the new size. */ + if (tb_fb->scene_buf) + wlr_scene_buffer_set_dest_size(tb_fb->scene_buf, tb_w, tb_h); + } + WTexture *tex = fwin->title_texture[state]; + + /* Clear titlebar buffer */ + if (tb_fb) { + pixman_color_t clear = {0, 0, 0, 0}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, tb_img, &clear, + 1, &(pixman_rectangle16_t){0, 0, tb_w, tb_h}); + } + + /* Render texture to RImage. */ + RImage *img = wTextureRenderImage(tex, tb_w, tb_h, WREL_RAISED); + if (img) { + uint32_t *dst = pixman_image_get_data(tb_img); + int dst_stride = pixman_image_get_stride(tb_img) / 4; + int x, y; + if (img->format == RRGBAFormat) { + for (y = 0; y < tb_h && y < img->height; y++) { + unsigned char *src_row = img->data + y * img->width * 4; + uint32_t *dst_row = dst + y * dst_stride; + for (x = 0; x < tb_w && x < img->width; x++) { + unsigned char r = src_row[x*4+0]; + unsigned char g = src_row[x*4+1]; + unsigned char b = src_row[x*4+2]; + unsigned char a = src_row[x*4+3]; + dst_row[x] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + } else { + /* RGB format */ + for (y = 0; y < tb_h && y < img->height; y++) { + unsigned char *src_row = img->data + y * img->width * 3; + uint32_t *dst_row = dst + y * dst_stride; + for (x = 0; x < tb_w && x < img->width; x++) { + unsigned char r = src_row[x*3+0]; + unsigned char g = src_row[x*3+1]; + unsigned char b = src_row[x*3+2]; + dst_row[x] = 0xFF000000U | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + } + RReleaseImage(img); + } + + /* Draw title text. */ + if (fwin->title && *fwin->font && fwin->title_color[state]) { + int lofs = 6, rofs = 6; + int titlelen, tw, tx, ty, th; + char *title; + + if (fwin->left_button && !fwin->flags.hide_left_button && + !fwin->flags.lbutton_dont_fit) + lofs += fwin->left_button->width + 3; + if (fwin->right_button && !fwin->flags.hide_right_button && + !fwin->flags.rbutton_dont_fit) + rofs += fwin->right_button->width + 3; + + title = ShrinkString(*fwin->font, fwin->title, tb_w - lofs - rofs); + titlelen = strlen(title); + tw = WMWidthOfString(*fwin->font, title, titlelen); + th = WMFontHeight(*fwin->font); + + switch (fwin->flags.justification) { + case WTJ_LEFT: tx = lofs; break; + case WTJ_RIGHT: tx = tb_w - tw - rofs; break; + default: tx = (tb_w - tw) / 2; break; + } + ty = (tb_h - th) / 2; + + WMDrawString(fwin->screen_ptr->wmscreen, (WMDrawable)tb_img, + fwin->title_color[state], *fwin->font, + tx, ty, title, titlelen); + wfree(title); + } + + /* Commit titlebar buffer */ + if (tb_fb && tb_fb->scene_buf) { + wlr_scene_buffer_set_buffer_with_damage(tb_fb->scene_buf, &tb_fb->base, NULL); + } + } + + /* --- Titlebar buttons --- */ + if (fwin->titlebar && fwin->top_width > 0) { + if (fwin->left_button && !fwin->flags.hide_left_button && + !fwin->flags.lbutton_dont_fit) + wl_frame_paint_button(fwin->left_button, + fwin->title_texture[state], + WMColorPixel(fwin->title_color[state]), + fwin->lbutton_image, False); + if (fwin->right_button && !fwin->flags.hide_right_button && + !fwin->flags.rbutton_dont_fit) + wl_frame_paint_button(fwin->right_button, + fwin->title_texture[state], + WMColorPixel(fwin->title_color[state]), + fwin->rbutton_image, False); + } + + /* --- Resizebar --- */ + /* Skip resizebar when frame is shaded (height ≤ titlebar). */ + bool show_resizebar = fwin->resizebar && fwin->bottom_width > 0 + && total_h > (int)fwin->top_width; + if (show_resizebar) { + int rb_y = total_h - fwin->bottom_width; + int rb_w = total_w; + int rb_h = fwin->bottom_width; + WTexture *tex = fwin->resizebar_texture[0]; + + RImage *img = wTextureRenderImage(tex, rb_w, rb_h, WREL_FLAT); + if (img) { + uint32_t *dst = pixman_image_get_data(fb->image); + int dst_stride = pixman_image_get_stride(fb->image) / 4; + int x, y; + if (img->format == RRGBAFormat) { + for (y = 0; y < rb_h && y < img->height; y++) { + unsigned char *src_row = img->data + y * img->width * 4; + uint32_t *dst_row = dst + (rb_y + y) * dst_stride; + for (x = 0; x < rb_w && x < img->width; x++) { + unsigned char r = src_row[x*4+0]; + unsigned char g = src_row[x*4+1]; + unsigned char b = src_row[x*4+2]; + unsigned char a = src_row[x*4+3]; + dst_row[x] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + } else { + for (y = 0; y < rb_h && y < img->height; y++) { + unsigned char *src_row = img->data + y * img->width * 3; + uint32_t *dst_row = dst + (rb_y + y) * dst_stride; + for (x = 0; x < rb_w && x < img->width; x++) { + unsigned char r = src_row[x*3+0]; + unsigned char g = src_row[x*3+1]; + unsigned char b = src_row[x*3+2]; + dst_row[x] = 0xFF000000U | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + } + RReleaseImage(img); + } + + /* Draw resizebar corner grab indicators */ + int cw = fwin->resizebar_corner_width; + uint32_t *dst = pixman_image_get_data(fb->image); + int dst_stride = pixman_image_get_stride(fb->image) / 4; + int midx = (rb_w < 1) ? 0 : rb_w / 2; + int midy = (rb_h < 1) ? 0 : rb_h / 2; + uint32_t base_px = dst[(rb_y + midy) * dst_stride + midx]; + unsigned char br = (base_px >> 16) & 0xFF; + unsigned char bg = (base_px >> 8) & 0xFF; + unsigned char bb = (base_px) & 0xFF; + uint32_t dim_px = 0xFF000000U | + (((unsigned)((br > 40) ? (br - 40) : 0) << 16)) | + (((unsigned)((bg > 40) ? (bg - 40) : 0) << 8)) | + ((unsigned)((bb > 40) ? (bb - 40) : 0)); + uint32_t light_px = 0xFF000000U | + (((unsigned)((br < 175) ? (br + 80) : 255) << 16)) | + (((unsigned)((bg < 175) ? (bg + 80) : 255) << 8)) | + ((unsigned)((bb < 175) ? (bb + 80) : 255)); + + /* Top bevel: dim at y=0, light at y=1 */ + for (int x = 0; x < rb_w; x++) { + dst[(rb_y + 0) * dst_stride + x] = dim_px; + dst[(rb_y + 1) * dst_stride + x] = light_px; + } + + /* Left corner grab handle: vertical lines from y=2 to rb_h-1 */ + int xl = (cw < rb_w) ? cw : rb_w - 1; + if (xl > 0) { + for (int y = 2; y < rb_h; y++) { + dst[(rb_y + y) * dst_stride + xl - 1] = dim_px; + dst[(rb_y + y) * dst_stride + xl] = light_px; + } + } + + /* Right corner grab handle */ + int xr = (rb_w - cw - 2 >= 0) ? (rb_w - cw - 2) : 0; + if (xr + 1 < rb_w) { + for (int y = 2; y < rb_h; y++) { + dst[(rb_y + y) * dst_stride + xr] = dim_px; + dst[(rb_y + y) * dst_stride + xr + 1] = light_px; + } + } + } + + /* Hide the resizebar's own scene node when shaded. */ + if (fwin->resizebar) { + struct wl_frame_buf *rb_fb = frame_buf_find(fwin->resizebar->window); + if (rb_fb && rb_fb->scene_buf) + wlr_scene_node_set_enabled(&rb_fb->scene_buf->node, show_resizebar); + } + + /* Commit the buffer to the scene graph only if it has content (resizebar). */ + if (fb->scene_buf) { + if (show_resizebar) { + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + } else { + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); + + return 1; +} + +/* ------------------------------------------------------------------ */ +/* Focus */ +/* ------------------------------------------------------------------ */ + +static void +wl_focus_set(WNativeWindow win, int revert_to, unsigned long timestamp) +{ + /* Phase 43/62: deliver keyboard focus to the XDG toplevel, or clear it. */ + (void)revert_to; (void)timestamp; + if (!wl_state.seat || !wl_state.keyboard_dev) + return; + /* WNATIVE_NONE or the focus-sink window → clear Wayland keyboard focus. + * no_focus_xwin is the off-screen InputOnly window created in + * wl_screen_create_internals; focusing it on the Xlib side is harmless + * (it's a real window), but on the Wayland side we clear seat focus. */ + if (win == WNATIVE_NONE || + (wl_state.no_focus_xwin && win == (WNativeWindow)wl_state.no_focus_xwin)) { + wlr_seat_keyboard_clear_focus(wl_state.seat); + return; + } + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return; + /* Phase 17: XWayland windows need both X11 focus and Wayland seat focus. */ + if (v->xw_surface) { + wlr_xwayland_surface_activate(v->xw_surface, true); + if (v->xw_surface->surface) { + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + wlr_seat_set_keyboard(wl_state.seat, kb); + } + wlr_seat_keyboard_notify_enter(wl_state.seat, + v->xw_surface->surface, + NULL, 0, NULL); + } + return; + } + if (!v->xdg_surface || !v->xdg_surface->surface) + return; + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + wlr_seat_set_keyboard(wl_state.seat, kb); + } + /* Flush pending events to client so it has processed wl_seat.capabilities + * and bound wl_keyboard before we send keyboard.enter. */ + wl_display_flush_clients(wl_state.display); + wlr_seat_keyboard_notify_enter(wl_state.seat, + v->xdg_surface->surface, + NULL, 0, NULL); +} + +/* ------------------------------------------------------------------ */ +/* Key / button grabs */ +/* ------------------------------------------------------------------ */ + +static void +wl_key_grab(WNativeWindow win, unsigned int keycode, + unsigned int modifier, int owner_events) +{ (void)win; (void)keycode; (void)modifier; (void)owner_events; } + +static void wl_key_ungrab_all(WNativeWindow win) { (void)win; } + +static void +wl_button_grab(WNativeWindow win, unsigned int button, + unsigned int modifier, unsigned int event_mask, + int owner_events) +{ (void)win; (void)button; (void)modifier; (void)event_mask; (void)owner_events; } + +static void wl_button_ungrab_all(WNativeWindow win) { (void)win; } + +/* ------------------------------------------------------------------ */ +/* Phase 14: pointer input event handlers */ +/* ------------------------------------------------------------------ */ + +static void +handle_pointer_motion(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_pointer_motion_event *ev = data; + int ow = wl_state.output ? (int)wl_state.output->width : 1920; + int oh = wl_state.output ? (int)wl_state.output->height : 1080; + wl_state.pointer_x += (int)ev->delta_x; + wl_state.pointer_y += (int)ev->delta_y; + if (wl_state.pointer_x < 0) wl_state.pointer_x = 0; + if (wl_state.pointer_y < 0) wl_state.pointer_y = 0; + 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 23: update hardware cursor position. */ + 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. + * 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_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; + wev.window = root; + 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; + /* Resolve WINGs child view (slider, etc.) at pointer for + * motion routing during drag operations. */ + struct W_View *mtarget = NULL; + if (fb && fb->id >= 0x20000000) { + extern W_View *W_GetViewForWindow(unsigned long id); + extern struct W_View *W_FindViewAtPoint(struct W_View *parent, int x, int y); + W_View *toplevel = W_GetViewForWindow((unsigned long)fb->id); + if (toplevel) { + mtarget = W_FindViewAtPoint(toplevel, + wev.u.motion.x, wev.u.motion.y); + /* Adjust event coordinates to target view's local space */ + if (mtarget && mtarget != toplevel) { + W_View *p = mtarget; + int ox = 0, oy = 0; + while (p && p != toplevel) { + ox += p->pos.x; + oy += p->pos.y; + p = p->parent; + } + wev.u.motion.x -= ox; + wev.u.motion.y -= oy; + } + } + } + W_WL_EnqueueEvent(wmscr, &wev, mtarget); + /* 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 = WM_NOTIFY_NORMAL; + xev.u.crossing.detail = WM_NOTIFY_ANCESTOR; + 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 = WM_NOTIFY_NORMAL; + xev.u.crossing.detail = WM_NOTIFY_ANCESTOR; + 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; } + wl_update_pointer_focus(ev->time_msec); +} + +static void +handle_pointer_motion_absolute(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_pointer_motion_absolute_event *ev = data; + /* Phase 23: use wlr_cursor_warp_absolute so wlroots maps the 0..1 + * device-normalized coordinates to layout pixels and updates the + * hardware cursor in the nested window automatically. */ + if (wl_state.cursor) { + wlr_cursor_warp_absolute(wl_state.cursor, &ev->pointer->base, ev->x, ev->y); + wl_state.pointer_x = (int)wl_state.cursor->x; + wl_state.pointer_y = (int)wl_state.cursor->y; + } else { + int ow = wl_state.output ? (int)wl_state.output->width : 1920; + int oh = wl_state.output ? (int)wl_state.output->height : 1080; + wl_state.pointer_x = (int)(ev->x * (double)ow); + wl_state.pointer_y = (int)(ev->y * (double)oh); + if (wl_state.pointer_x < 0) wl_state.pointer_x = 0; + if (wl_state.pointer_y < 0) wl_state.pointer_y = 0; + 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. + * 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_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; + wev.window = root; + 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 = WM_NOTIFY_NORMAL; + xev.u.crossing.detail = WM_NOTIFY_ANCESTOR; + 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 = WM_NOTIFY_NORMAL; + xev.u.crossing.detail = WM_NOTIFY_ANCESTOR; + 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; } + wl_update_pointer_focus(ev->time_msec); +} + +/* + * Phase 24: seat request_set_cursor — client wants to set a custom cursor + * image (e.g. I-beam in a text field, hand over a link). Validate that + * the request comes from the currently focused pointer client, then hand + * the surface to wlr_cursor so the hardware cursor is updated on all + * outputs (which in nested mode sends wl_pointer.set_cursor to the host). + */ +static void +handle_seat_request_cursor(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_seat_pointer_request_set_cursor_event *ev = data; + if (!wl_state.seat || !wl_state.cursor) + return; + /* Only honour cursor requests from the client that currently has + * pointer focus; reject requests from unfocused clients. */ + struct wlr_seat_client *focused = + wl_state.seat->pointer_state.focused_client; + if (ev->seat_client != focused) + return; + wlr_cursor_set_surface(wl_state.cursor, ev->surface, + ev->hotspot_x, ev->hotspot_y); +} + +/* ------------------------------------------------------------------ */ +/* Compositor-internal clipboard (WINGs ↔ Wayland clients) */ +/* ------------------------------------------------------------------ */ + +static char *wl_seat_clipboard_text = NULL; +static int wl_seat_clipboard_len = 0; +static struct wlr_data_source wl_seat_clipboard_source; + +static void wl_clipboard_source_send(struct wlr_data_source *source, + const char *mime_type, int32_t fd) +{ + (void)source; (void)mime_type; + if (wl_seat_clipboard_text && wl_seat_clipboard_len > 0) + write(fd, wl_seat_clipboard_text, wl_seat_clipboard_len); + close(fd); +} + +static void wl_clipboard_source_destroy(struct wlr_data_source *source) +{ + (void)source; + /* Don't free — static lifetime, text freed separately */ +} + +static const struct wlr_data_source_impl wl_clipboard_source_impl = { + .send = wl_clipboard_source_send, + .destroy = wl_clipboard_source_destroy, +}; + +static void wl_clipboard_store_to_seat(const char *text, int len) +{ + if (!wl_state.seat || !text || len <= 0) + return; + + /* If we already own the seat selection, just update the buffer + * instead of re-initializing the source (which can corrupt + * wlroots' internal state for subsequent paste requests). */ + if (wl_state.seat->selection_source == &wl_seat_clipboard_source) { + if (wl_seat_clipboard_text) + free(wl_seat_clipboard_text); + wl_seat_clipboard_text = malloc(len + 1); + memcpy(wl_seat_clipboard_text, text, len); + wl_seat_clipboard_text[len] = '\0'; + wl_seat_clipboard_len = len; + return; + } + + if (wl_seat_clipboard_text) + free(wl_seat_clipboard_text); + wl_seat_clipboard_text = malloc(len + 1); + memcpy(wl_seat_clipboard_text, text, len); + wl_seat_clipboard_text[len] = '\0'; + wl_seat_clipboard_len = len; + + wlr_data_source_init(&wl_seat_clipboard_source, &wl_clipboard_source_impl); + wl_array_release(&wl_seat_clipboard_source.mime_types); + wl_array_init(&wl_seat_clipboard_source.mime_types); + char **mime = wl_array_add(&wl_seat_clipboard_source.mime_types, sizeof(char *)); + *mime = strdup("text/plain;charset=utf-8"); + mime = wl_array_add(&wl_seat_clipboard_source.mime_types, sizeof(char *)); + *mime = strdup("text/plain"); + + wlr_seat_set_selection(wl_state.seat, &wl_seat_clipboard_source, + wl_display_next_serial(wl_state.display)); +} + +static char *wl_clipboard_fetch_from_seat(int *len_out) +{ + if (!wl_state.seat || !wl_state.seat->selection_source) + return NULL; + + /* If we own the selection, return our buffer directly */ + if (wl_state.seat->selection_source == &wl_seat_clipboard_source) { + if (wl_seat_clipboard_text && wl_seat_clipboard_len > 0) { + char *copy = malloc(wl_seat_clipboard_len + 1); + memcpy(copy, wl_seat_clipboard_text, wl_seat_clipboard_len); + copy[wl_seat_clipboard_len] = '\0'; + if (len_out) + *len_out = wl_seat_clipboard_len; + return copy; + } + return NULL; + } + + /* Foreign source — read via pipe */ + int fds[2]; + if (pipe(fds) < 0) + return NULL; + + wlr_data_source_send(wl_state.seat->selection_source, + "text/plain;charset=utf-8", fds[1]); + close(fds[1]); + + char buf[4096]; + int total = 0; + ssize_t n; + while ((n = read(fds[0], buf + total, sizeof(buf) - total - 1)) > 0) + total += n; + close(fds[0]); + + if (total <= 0) + return NULL; + + buf[total] = '\0'; + char *result = malloc(total + 1); + memcpy(result, buf, total + 1); + if (len_out) + *len_out = total; + return result; +} + +/* ------------------------------------------------------------------ */ +/* Drag icon overlay for WINGs compositor-internal DND */ +/* ------------------------------------------------------------------ */ + +/* State for the currently active drag icon (only one drag at a time) */ +static struct { + struct wlr_scene_buffer *scene_buf; + struct wlr_scene_tree *tree; + pixman_image_t *image; + struct wlr_buffer base; + int width, height; + int offset_x, offset_y; /* cursor hotspot relative to top-left */ +} wl_drag_icon_state = {0}; + +static void +wl_drag_icon_destroy(void) +{ + if (wl_drag_icon_state.tree) { + wlr_scene_node_destroy(&wl_drag_icon_state.tree->node); + wl_drag_icon_state.tree = NULL; + wl_drag_icon_state.scene_buf = NULL; + } + if (wl_drag_icon_state.image) { + pixman_image_unref(wl_drag_icon_state.image); + wl_drag_icon_state.image = NULL; + } + memset(&wl_drag_icon_state, 0, sizeof(wl_drag_icon_state)); +} + +static void +wl_drag_icon_show(struct W_View *view, int x, int y) +{ + /* Clean up any previous drag icon */ + wl_drag_icon_destroy(); + + if (!view || !view->dragImage || !wl_state.scene) + return; + + struct W_Pixmap *p = view->dragImage; + pixman_image_t *src = (pixman_image_t *)(uintptr_t)p->pixmap; + if (!src) + return; + + int w = p->width; + int h = p->height; + + /* Copy the pixman image so we own it independently */ + pixman_image_t *copy = pixman_image_create_bits( + PIXMAN_a8r8g8b8, w, h, NULL, w * 4); + if (!copy) + return; + pixman_image_composite32(PIXMAN_OP_SRC, src, NULL, copy, + 0, 0, 0, 0, 0, 0, w, h); + + wl_drag_icon_state.image = copy; + wl_drag_icon_state.width = w; + wl_drag_icon_state.height = h; + + /* Center the icon on the cursor */ + wl_drag_icon_state.offset_x = -w / 2; + wl_drag_icon_state.offset_y = -h / 2; + + /* Wrap in wlr_buffer using the existing frame_buf_impl */ + wlr_buffer_init(&wl_drag_icon_state.base, &frame_buf_impl, w, h); + + /* Create scene tree under overlay layer */ + struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + wl_drag_icon_state.tree = wlr_scene_tree_create(parent); + if (!wl_drag_icon_state.tree) { + wl_drag_icon_destroy(); + return; + } + + wl_drag_icon_state.scene_buf = wlr_scene_buffer_create( + wl_drag_icon_state.tree, &wl_drag_icon_state.base); + if (!wl_drag_icon_state.scene_buf) { + wl_drag_icon_destroy(); + return; + } + + /* Position at cursor (centered) */ + wlr_scene_node_set_position(&wl_drag_icon_state.tree->node, + x + wl_drag_icon_state.offset_x, + y + wl_drag_icon_state.offset_y); +} + +static void +wl_drag_icon_move(int x, int y) +{ + if (!wl_drag_icon_state.tree) + return; + wlr_scene_node_set_position(&wl_drag_icon_state.tree->node, + x + wl_drag_icon_state.offset_x, + y + wl_drag_icon_state.offset_y); +} + +static void +wl_drag_icon_hide(void) +{ + wl_drag_icon_destroy(); +} + +static void +wl_drag_cursor_feedback(Bool accepted) +{ + if (accepted) + wl_set_cursor_image("plus"); + else + wl_set_cursor_image("left_ptr"); +} + +/* + * Phase 25: seat request_set_selection — client calls wl_data_device.set_selection + * (i.e. Ctrl-C or other clipboard write). Forward to wlr_seat so the + * selection becomes available to the next client that reads it. + */ +static void +handle_seat_request_set_selection(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_seat_request_set_selection_event *ev = data; + if (!wl_state.seat) + return; + wlr_seat_set_selection(wl_state.seat, ev->source, ev->serial); +} + +/* ------------------------------------------------------------------ */ +/* Phase 25b: DND request_start_drag — external client starts a drag */ +/* ------------------------------------------------------------------ */ + +/* Currently active external wlr_drag (NULL if none). */ +static struct wlr_drag *wl_current_drag = NULL; + +static void handle_drag_motion(struct wl_listener *listener, void *data); +static void handle_drag_drop(struct wl_listener *listener, void *data); +static void handle_drag_destroy(struct wl_listener *listener, void *data); + +static struct wl_listener wl_drag_motion_listener = {.notify = handle_drag_motion}; +static struct wl_listener wl_drag_drop_listener = {.notify = handle_drag_drop}; +static struct wl_listener wl_drag_destroy_listener = {.notify = handle_drag_destroy}; + +static void +handle_seat_request_start_drag(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_seat_request_start_drag_event *ev = data; + if (!wl_state.seat) + return; + wwarning("wl_backend: external client started DND"); + wlr_seat_start_pointer_drag(wl_state.seat, ev->drag, ev->serial); + wl_current_drag = ev->drag; + wl_signal_add(&ev->drag->events.motion, &wl_drag_motion_listener); + wl_signal_add(&ev->drag->events.drop, &wl_drag_drop_listener); + wl_signal_add(&ev->drag->events.destroy, &wl_drag_destroy_listener); +} + +static void +handle_drag_motion(struct wl_listener *listener, void *data) +{ + struct wlr_drag_motion_event *ev = data; + (void)listener; + (void)ev; + + if (!wl_current_drag) + return; + + /* Hit-test at current pointer position */ + struct wl_frame_buf *fb = frame_buf_hit(wl_state.pointer_x, + wl_state.pointer_y); + if (fb && fb->id >= 0x20000000) { + /* Over a WINGs view — look up view and its DND procs */ + extern struct W_View *W_GetViewForWindow(unsigned long id); + struct W_View *top = W_GetViewForWindow((unsigned long)fb->id); + if (top && top->dragDestinationProcs) { + /* Convert MIME types from wlr_data_source to WMArray */ + /* (For now, just acknowledge the view exists) */ + } + } +} + +static void +handle_drag_drop(struct wl_listener *listener, void *data) +{ + struct wlr_drag_drop_event *ev = data; + (void)listener; + wwarning("wl_backend: external DND drop"); + + if (!wl_current_drag || !wl_current_drag->source) + return; + + struct wlr_data_source *src = wl_current_drag->source; + + /* Hit-test to find the drop target */ + struct wl_frame_buf *fb = frame_buf_hit(wl_state.pointer_x, + wl_state.pointer_y); + if (fb && fb->id >= 0x20000000) { + /* Over a WINGs view — read data and deliver */ + extern struct W_View *W_GetViewForWindow(unsigned long id); + struct W_View *top = W_GetViewForWindow((unsigned long)fb->id); + if (top && top->dragDestinationProcs && + top->dragDestinationProcs->performDragOperation) { + + /* Build array of MIME type strings from data source */ + WMArray *mimeTypes = WMCreateArray(WMGetArrayItemCount( + (WMArray*)&src->mime_types)); + char **mime_ptr; + wl_array_for_each(mime_ptr, &src->mime_types) { + if (*mime_ptr) + WMAddToArray(mimeTypes, wstrdup(*mime_ptr)); + } + + /* Read first available data via pipe */ + WMArray *dropDatas = WMCreateArray(1); + char **mt; + wl_array_for_each(mt, &src->mime_types) { + char *mime = *mt; + if (!mime) continue; + + int fds[2]; + if (pipe(fds) < 0) continue; + + wlr_data_source_send(src, mime, fds[1]); + close(fds[1]); + + char buf[65536]; + int total = 0; + ssize_t n; + while ((n = read(fds[0], buf + total, + sizeof(buf) - total - 1)) > 0) + total += n; + close(fds[0]); + + if (total > 0) { + WMData *data = WMCreateDataWithBytes(buf, total); + if (data) { + WMAddToArray(dropDatas, data); + break; /* use first available type */ + } + } + } + + WMPoint pt = wmkpoint(wl_state.pointer_x, wl_state.pointer_y); + top->dragDestinationProcs->performDragOperation( + top, dropDatas, NULL, &pt); + WMFreeArray(dropDatas); + WMFreeArray(mimeTypes); + } + } + + wlr_data_source_dnd_drop(src); + (void)ev; +} + +static void +handle_drag_destroy(struct wl_listener *listener, void *data) +{ + (void)listener; + (void)data; + wwarning("wl_backend: external DND finished"); + wl_current_drag = NULL; +} + +/* + * Phase 26: seat request_set_primary_selection — client writes to the + * primary selection buffer (e.g. by selecting text). Forward to + * wlr_seat so middle-click paste works in other clients. + */ +static void +handle_seat_request_primary_selection(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_seat_request_set_primary_selection_event *ev = data; + if (!wl_state.seat) + return; + wlr_seat_set_primary_selection(wl_state.seat, ev->source, ev->serial); +} + +/* + * Phase 28: idle_inhibit new_inhibitor — a client (e.g. mpv, a game) created + * an idle-inhibit object. wlroots tracks the inhibitor list internally; + * the compositor can query wl_state.idle_inhibit_mgr->inhibitors to decide + * whether to suppress screen blanking. For now we just log the event so + * the capability is advertised and the inhibitor object doesn't error out. + */ +static void +handle_idle_new_inhibitor(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_idle_inhibitor_v1 *inhibitor = data; + (void)inhibitor; + wwarning("wl_backend: idle inhibitor created (client suppressing screen blank)"); +} + +/* + * Phase 29 (fix): scroll-wheel axis event handler. + * wlr_event_pointer_axis carries orientation (vertical/horizontal), delta, + * delta_discrete (notches), and source (wheel vs finger). Forward directly + * to the seat so the focused Wayland client receives wl_pointer.axis events. + * Without this, scrolling is silently dropped in every Wayland client. + */ +static void +handle_pointer_axis(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_pointer_axis_event *ev = data; + if (!wl_state.seat) + return; + /* Drop axis events during a compositor grab so the grabbed window + * does not receive unexpected scroll events mid-move/resize. */ + if (wl_state.grab_type) + return; + wlr_seat_pointer_notify_axis(wl_state.seat, + ev->time_msec, + ev->orientation, + ev->delta, + ev->delta_discrete, + ev->source, + ev->relative_direction); +} + +/* + * Phase 30: new pointer constraint — a client requested pointer lock or + * pointer confinement (e.g. a game locking the cursor to its window). + * We activate the constraint immediately so the client gets confirmed. + * In headless mode the pointer is virtual so there is nothing physically + * to lock, but activating keeps the client happy. + */ +static void +handle_new_pointer_constraint(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_pointer_constraint_v1 *constraint = data; + (void)constraint; + /* Activate immediately — in nested mode the real pointer is bounded + * by the host compositor's window anyway. */ + wlr_pointer_constraint_v1_send_activated(constraint); + wwarning("wl_backend: pointer constraint activated"); +} + +/* + * Phase 31: new keyboard shortcuts inhibitor — a fullscreen app or remote + * desktop client wants to suppress WM keyboard shortcuts. + * Activate the inhibitor immediately so the client knows it is granted. + * The existing wWaylandKeyPress() dispatcher in handle_keyboard_key already + * forwards all non-shortcut keys; with the inhibitor active we should skip + * the shortcut check for that focused client. For now we simply activate + * and log; per-client inhibitor tracking can be refined later. + */ +static void +handle_new_shortcuts_inhibitor(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor = data; + wlr_keyboard_shortcuts_inhibitor_v1_activate(inhibitor); + wwarning("wl_backend: keyboard shortcuts inhibitor activated"); +} + +/* ------------------------------------------------------------------ */ +/* Phase 33: output management, output power, gamma control */ +/* ------------------------------------------------------------------ */ + +/* + * handle_output_mgr_apply — client requests a randr-like output reconfiguration. + * For a single-output headless compositor there is nothing to reconfigure; just + * acknowledge success so the client does not hang waiting for feedback. + * The configuration object is compositor-owned and must be destroyed here. + */ +static void +handle_output_mgr_apply(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_output_configuration_v1 *config = data; + wlr_output_configuration_v1_send_succeeded(config); + wlr_output_configuration_v1_destroy(config); + wwarning("wl_backend: output manager apply request — acknowledged (no-op)"); +} + +/* + * handle_output_mgr_test — client tests a configuration without applying it. + * Always report success (any single-output config is trivially valid). + */ +static void +handle_output_mgr_test(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_output_configuration_v1 *config = data; + wlr_output_configuration_v1_send_succeeded(config); + wlr_output_configuration_v1_destroy(config); +} + +/* + * handle_output_power_set_mode — client requests DPMS on/off. + * Apply the requested power state via wlr_output_enable + wlr_output_commit. + */ +static void +handle_output_power_set_mode(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_output_power_v1_set_mode_event *ev = data; + bool enable = (ev->mode == ZWLR_OUTPUT_POWER_V1_MODE_ON); + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_enabled(&state, enable); + wlr_output_commit_state(ev->output, &state); + wlr_output_state_finish(&state); + wwarning("wl_backend: output power set_mode %s", enable ? "ON" : "OFF"); +} + +/* ------------------------------------------------------------------ */ +/* Phase 34: text input v3, input method v2, virtual keyboard v1 */ +/* ------------------------------------------------------------------ */ + +/* + * handle_text_input_new — a client created a zwp_text_input_v3 object. + * Text input objects are managed entirely by wlroots; the compositor just + * needs to call wlr_text_input_v3_send_enter/leave to activate the IME + * for a focused surface. For now we log and do nothing — the IME framework + * will still register; full integration (enter on focus) can be added later. + */ +static void +handle_text_input_new(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wwarning("wl_backend: text input v3 object created"); +} + +/* + * handle_input_method_new — an IME server (ibus, fcitx5) connected. + * The input method protocol is symmetric with text_input; wlroots handles + * the protocol exchange. We log its arrival. + */ +static void +handle_input_method_new(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wwarning("wl_backend: input method v2 connected"); +} + +/* + * handle_virtual_keyboard_new — a client created a virtual keyboard device. + * wlroots has already created the wlr_virtual_keyboard_v1 and wired its + * input_device into the backend. We just log it; the seat will advertise + * the keyboard capability automatically via handle_backend_new_input. + */ +static void +handle_virtual_keyboard_new(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wwarning("wl_backend: virtual keyboard v1 created"); +} + +/* ------------------------------------------------------------------ */ +/* Phase 35: foreign toplevel management + xdg_activation */ +/* ------------------------------------------------------------------ */ + +/* + * handle_xdg_activation_request — a client requests focus for a surface + * using an activation token (xdg_activation_v1 protocol). + * Find the view owning that surface, deliver WM keyboard focus to it, and + * raise it to the top of the stacking order. + */ +static void +handle_xdg_activation_request(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_xdg_activation_v1_request_activate_event *ev = data; + if (!ev->surface) + return; + + /* Walk toplevel list to find a view whose wlr_surface matches. */ + struct wl_toplevel_view *view; + wl_list_for_each(view, &wl_state.toplevel_list, link) { + struct wlr_surface *surf = NULL; + if (view->xdg_surface) + surf = view->xdg_surface->surface; + else if (view->xw_surface) + surf = view->xw_surface->surface; + if (surf != ev->surface) + continue; + + WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + wSetFocusTo(wScreenWithNumber(0), wwin); + /* Raise the entire window assembly (frame tree), not just the + * client scene_node — raising the client alone would push it + * above the titlebar and hide decorations. */ + if (view->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(view->frame_id); + if (fb && fb->tree) + wlr_scene_node_raise_to_top(&fb->tree->node); + } else if (view->scene_node) { + wlr_scene_node_raise_to_top(&view->scene_node->node); + } + /* Move to front of toplevel list (Z-order). */ + wl_list_remove(&view->link); + wl_list_insert(wl_state.toplevel_list.prev, &view->link); + /* Update foreign toplevel activated state. */ + if (view->ft_handle) + wlr_foreign_toplevel_handle_v1_set_activated(view->ft_handle, true); + wwarning("wl_backend: xdg_activation focus granted id=0x%lx", view->id); + } + return; + } + wwarning("wl_backend: xdg_activation request for unknown surface (ignored)"); +} + +/* ------------------------------------------------------------------ */ +/* Phase 36: layer shell — panels, docks, wallpapers, overlays */ +/* ------------------------------------------------------------------ */ + +#define LAYER_VIEW_FROM(l, field) \ + ((struct wl_layer_view *)((char *)(l) - offsetof(struct wl_layer_view, field))) + +/* + * Compute the geometry for a layer surface given its anchor, margins, + * desired size and the output dimensions. + */ +static void +layer_surface_compute_geometry(struct wlr_layer_surface_v1 *ls, + uint32_t ow, uint32_t oh, + uint32_t *out_w, uint32_t *out_h) +{ + const struct wlr_layer_surface_v1_state *s = &ls->current; + uint32_t w = s->desired_width; + uint32_t h = s->desired_height; + + bool anchor_lr = (s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) && + (s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + bool anchor_tb = (s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) && + (s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); + + if (w == 0 && anchor_lr) + w = ow - s->margin.left - s->margin.right; + if (h == 0 && anchor_tb) + h = oh - s->margin.top - s->margin.bottom; + + /* Ensure minimum 1×1. */ + *out_w = w > 0 ? w : 1; + *out_h = h > 0 ? h : 1; +} + +/* + * Compute the position of a layer surface on the output given its + * anchor, margins and final dimensions. + */ +static void +layer_surface_compute_position(struct wlr_layer_surface_v1 *ls, + uint32_t ow, uint32_t oh, + uint32_t fw, uint32_t fh, + int *out_x, int *out_y) +{ + const struct wlr_layer_surface_v1_state *s = &ls->current; + int x = 0, y = 0; + + bool anchor_l = s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; + bool anchor_r = s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + bool anchor_t = s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + bool anchor_b = s->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + + if (anchor_l && anchor_r) + x = s->margin.left; + else if (anchor_l) + x = s->margin.left; + else if (anchor_r) + x = (int)ow - (int)fw - s->margin.right; + else + x = ((int)ow - (int)fw) / 2; + + if (anchor_t && anchor_b) + y = s->margin.top; + else if (anchor_t) + y = s->margin.top; + else if (anchor_b) + y = (int)oh - (int)fh - s->margin.bottom; + else + y = ((int)oh - (int)fh) / 2; + + *out_x = x; + *out_y = y; +} + +static void +handle_layer_surface_map(struct wl_listener *listener, void *data) +{ + (void)data; + struct wl_layer_view *lv = LAYER_VIEW_FROM(listener, map_listener); + wwarning("wl_backend: layer surface mapped (layer=%d namespace=%s)", + lv->layer_surface->current.layer, + lv->layer_surface->namespace ? lv->layer_surface->namespace : ""); +} + +static void +handle_layer_surface_unmap(struct wl_listener *listener, void *data) +{ + (void)data; + struct wl_layer_view *lv = LAYER_VIEW_FROM(listener, unmap_listener); + wwarning("wl_backend: layer surface unmapped"); + if (lv->scene_node) { + wlr_scene_node_destroy(&lv->scene_node->node); + lv->scene_node = NULL; + } +} + +static void +handle_layer_surface_destroy(struct wl_listener *listener, void *data) +{ + (void)data; + struct wl_layer_view *lv = LAYER_VIEW_FROM(listener, destroy_listener); + wl_list_remove(&lv->map_listener.link); + wl_list_remove(&lv->unmap_listener.link); + wl_list_remove(&lv->destroy_listener.link); + if (lv->scene_node) { + wlr_scene_node_destroy(&lv->scene_node->node); + lv->scene_node = NULL; + } + wl_list_remove(&lv->link); + free(lv); +} + +/* + * handle_layer_shell_new_surface — a client created a layer surface. + * Assign the output if unset, compute geometry, send configure, and + * wire map/unmap/destroy lifecycle. + */ +static void +handle_layer_shell_new_surface(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_layer_surface_v1 *ls = data; + + /* Assign output if client did not specify one. */ + if (!ls->output && wl_state.output) + ls->output = wl_state.output; + + struct wl_layer_view *lv = calloc(1, sizeof(*lv)); + if (!lv) { + wlr_layer_surface_v1_destroy(ls); + return; + } + lv->layer_surface = ls; + + /* 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) { + 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. */ + uint32_t ow = 1920, oh = 1080; + if (ls->output) { + ow = ls->output->width > 0 ? (uint32_t)ls->output->width : 1920; + oh = ls->output->height > 0 ? (uint32_t)ls->output->height : 1080; + } + uint32_t fw, fh; + layer_surface_compute_geometry(ls, ow, oh, &fw, &fh); + wlr_layer_surface_v1_configure(ls, fw, fh); + + /* Position the scene node. */ + if (lv->scene_node) { + int sx, sy; + layer_surface_compute_position(ls, ow, oh, fw, fh, &sx, &sy); + wlr_scene_node_set_position(&lv->scene_node->node, sx, sy); + } + + /* Wire lifecycle listeners. */ + lv->map_listener.notify = handle_layer_surface_map; + lv->unmap_listener.notify = handle_layer_surface_unmap; + lv->destroy_listener.notify = handle_layer_surface_destroy; + wl_signal_add(&ls->surface->events.map, &lv->map_listener); + wl_signal_add(&ls->surface->events.unmap, &lv->unmap_listener); + wl_signal_add(&ls->events.destroy, &lv->destroy_listener); + + wl_list_insert(&wl_state.layer_list, &lv->link); + + wwarning("wl_backend: layer surface new layer=%d %ux%u namespace=%s", + ls->current.layer, fw, fh, + ls->namespace ? ls->namespace : ""); +} + +/* ------------------------------------------------------------------ */ +/* Phase 37: data control, input inhibitor, virtual pointer, idle, */ +/* xdg-foreign */ +/* ------------------------------------------------------------------ */ + +/* + * handle_virtual_pointer_new — a client created a virtual pointer device + * (e.g. a VNC / remote desktop server injecting cursor events). + */ +static void +handle_virtual_pointer_new(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + wwarning("wl_backend: virtual pointer created"); +} + +static void +handle_pointer_button(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_pointer_button_event *ev = data; + if (!wl_state.seat) + return; + /* Phase 22: any button release ends an active compositor grab (move/resize). + * Type 3 (WM policy grab) is NOT ended here — the policy code calls + * pointer_ungrab itself after processing the button release event. */ + if (wl_state.grab_type && wl_state.grab_type != 3 && + ev->state == WLR_BUTTON_RELEASED) { + wwarning("wl_backend: grab ended (button release) grab_type=%d", wl_state.grab_type); + wl_grab_end(); + wl_update_pointer_focus(ev->time_msec); + return; + } + /* Phase 24: click-to-focus — on button press, find the view under the + * pointer and focus + raise it via the WM policy layer. + * This mirrors what the X11 backend does via ButtonPress on frame windows. */ + if (ev->state == WLR_BUTTON_PRESSED) { + double sx, sy; + struct wl_toplevel_view *view = NULL; + view_surface_at(wl_state.pointer_x, wl_state.pointer_y, + &sx, &sy, &view); + if (view) { + WObjDescriptor *desc = + wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + WScreen *scr = wScreenWithNumber(0); + wSetFocusTo(scr, wwin); + /* Raise entire window assembly via tree */ + if (view->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(view->frame_id); + if (fb && fb->tree) + wlr_scene_node_raise_to_top(&fb->tree->node); + } else if (view->scene_node) { + wlr_scene_node_raise_to_top(&view->scene_node->node); + } + /* Also raise it in our toplevel list so view_surface_at() + * returns it first on subsequent hits. */ + wl_list_remove(&view->link); + wl_list_insert(&wl_state.toplevel_list, &view->link); + } + } else { + /* No toplevel surface — check if a WINGs dialog frame_buf was clicked */ + struct wl_frame_buf *hit = frame_buf_hit(wl_state.pointer_x, + wl_state.pointer_y); + if (hit) { + /* Walk up to the parent frame to find the WWindow */ + WNativeWindow lookup = hit->parent_id ? hit->parent_id : hit->id; + WObjDescriptor *desc = + wm_backend->context_find(lookup, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + WScreen *scr = wScreenWithNumber(0); + wSetFocusTo(scr, wwin); + } + } + } + } + + /* Phase A: enqueue native WMEvent for WINGs */ + WMScreen *wmscr = wl_get_wmscreen(); + if (wmscr) { + WMEvent wev; + memset(&wev, 0, sizeof(wev)); + wev.type = (ev->state == WLR_BUTTON_PRESSED) + ? WME_BUTTON_PRESS : WME_BUTTON_RELEASE; + + /* 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; + wev.u.button.x = wl_state.pointer_x - fb->x; + wev.u.button.y = wl_state.pointer_y - fb->y; + } else { + WScreen *scr0 = wScreenWithNumber(0); + WNativeWindow root = scr0 ? scr0->root_win : (WNativeWindow)0x0FFFFFFF; + wev.window = root; + wev.u.button.window = root; + wev.u.button.x = wl_state.pointer_x; + wev.u.button.y = wl_state.pointer_y; + } + wev.u.button.time = ev->time_msec; + wev.u.button.x_root = wl_state.pointer_x; + wev.u.button.y_root = wl_state.pointer_y; + /* Linux evdev BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112 + * Button1=1(left), Button2=2(middle), Button3=3(right) */ + switch (ev->button) { + case 0x110: wev.u.button.button = 1; break; + case 0x111: wev.u.button.button = 3; break; + case 0x112: wev.u.button.button = 2; break; + default: wev.u.button.button = (ev->button - 0x110) + 1; break; + } + /* For WINGs views, resolve the child widget at click coordinates */ + struct W_View *target = NULL; + if (fb && fb->id >= 0x20000000) { + extern W_View *W_GetViewForWindow(unsigned long id); + extern struct W_View *W_FindViewAtPoint(struct W_View *parent, int x, int y); + W_View *toplevel = W_GetViewForWindow((unsigned long)fb->id); + if (toplevel) { + target = W_FindViewAtPoint(toplevel, + wev.u.button.x, wev.u.button.y); + if (!target) + target = toplevel; + /* Adjust event coordinates to target view's local space */ + if (target != toplevel) { + W_View *p = target; + int ox = 0, oy = 0; + while (p && p != toplevel) { + ox += p->pos.x; + oy += p->pos.y; + p = p->parent; + } + wev.u.button.x -= ox; + wev.u.button.y -= oy; + } + } + } + W_WL_EnqueueEvent(wmscr, &wev, target); + } + + /* Refresh pointer focus before delivering button event. + * The last wl_update_pointer_focus call was during the most recent + * motion event. If no motion happened since then, the XWayland + * surface might not have pointer focus, causing + * wlr_seat_pointer_notify_button to be a no-op. */ + wl_update_pointer_focus(ev->time_msec); + + wlr_seat_pointer_notify_button(wl_state.seat, ev->time_msec, + ev->button, ev->state); + wlr_seat_pointer_notify_frame(wl_state.seat); +} + +static void +handle_pointer_frame(struct wl_listener *listener, void *data) +{ + (void)listener; (void)data; + if (wl_state.seat) + wlr_seat_pointer_notify_frame(wl_state.seat); +} + +/* ------------------------------------------------------------------ */ +/* Pointer grabs */ +/* ------------------------------------------------------------------ */ + +static int +wl_pointer_grab(WNativeWindow grab_win, WNativeWindow confine_to, + unsigned int event_mask, int cursor_id, + int owner_events, unsigned long timestamp) +{ + (void)confine_to; (void)event_mask; + (void)timestamp; (void)grab_win; + wwarning("wl_pointer_grab: win=0x%lx owner_events=%d cursor_id=%d grab_type_before=%d", + grab_win, owner_events, cursor_id, wl_state.grab_type); + /* Only set up a WM policy grab (type 3) for actual drags (owner_events=True). + * Brief protective grabs (e.g., during icon creation) don't need to suppress + * pointer focus — they just need to return success. */ + if (owner_events) { + wl_state.grab_type = 3; + wl_state.grab_view = NULL; + if (wl_state.seat) + wlr_seat_pointer_clear_focus(wl_state.seat); + } + if (cursor_id > 0) { + /* Map WCUR_* cursor IDs to xcursor names (same as pointer_change_grab) */ + switch (cursor_id) { + case WCUR_MOVE: wl_set_cursor_image("fleur"); break; + case WCUR_WAIT: wl_set_cursor_image("watch"); break; + case WCUR_TOPLEFTRESIZE: wl_set_cursor_image("nw-resize"); break; + case WCUR_TOPRIGHTRESIZE: wl_set_cursor_image("ne-resize"); break; + case WCUR_BOTTOMLEFTRESIZE: wl_set_cursor_image("sw-resize"); break; + case WCUR_BOTTOMRIGHTRESIZE: wl_set_cursor_image("se-resize"); break; + case WCUR_VERTICALRESIZE: wl_set_cursor_image("n-resize"); break; + case WCUR_HORIZONRESIZE: wl_set_cursor_image("e-resize"); break; + default: wl_set_cursor_image("left_ptr"); break; + } + } + return 1; /* success */ +} + +static void wl_pointer_ungrab(unsigned long timestamp) +{ + (void)timestamp; + wwarning("wl_pointer_ungrab: grab_type=%d", wl_state.grab_type); + if (wl_state.grab_type == 3) { + wl_state.grab_type = 0; + wl_state.grab_view = NULL; + /* Reset cursor to default arrow when WM policy grab ends. */ + wl_set_cursor_image("left_ptr"); + } +} + +/* ------------------------------------------------------------------ */ +/* Phase 13: pointer focus delivery to Wayland surfaces */ +/* ------------------------------------------------------------------ */ + +/* + * Find the topmost mapped view whose surface contains screen point (px, py). + * Returns the wlr_surface and sets sx,sy to surface-local coordinates. + * Iterates toplevel_list in reverse-insertion order (last = topmost). + */ +static struct wlr_surface * +view_surface_at(int px, int py, double *sx_out, double *sy_out, + struct wl_toplevel_view **view_out) +{ + /* Use wlr_scene_node_at as the authority on what is visually + * topmost at (px,py). The old geometry-scan loop over + * toplevel_list caused toggling focus on overlapping windows + * because toplevel_list order diverged from scene Z-order. */ + if (!wl_state.scene) 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; + + /* Walk up until parent is a level_tree bucket: that node is + * fb->tree (framed) or v->scene_node (unframed). */ + struct wlr_scene_node *n = node; + while (n->parent && &n->parent->node != &wl_state.scene->tree.node) { + int _bucket = 0, _li; + for (_li = 0; _li < WL_NUM_LEVEL_BUCKETS; _li++) { + if (wl_state.level_trees[_li] && n->parent == wl_state.level_trees[_li]) + { _bucket = 1; break; } + } + if (_bucket) break; + n = &n->parent->node; + } + + struct wl_toplevel_view *v; + wl_list_for_each(v, &wl_state.toplevel_list, link) { + /* Framed window: resolve via the fb->tree node. */ + int match = 0; + if (v->frame_id) { + struct wl_frame_buf *_fb = frame_buf_find(v->frame_id); + match = (_fb && _fb->tree && &_fb->tree->node == n); + } else { + match = (v->scene_node && &v->scene_node->node == n); + } + if (!match) continue; + + if (v->xw_surface && v->xw_surface->surface) { + double sx = px - (double)v->x; + double sy = py - (double)v->y; + if (sx_out) *sx_out = sx; + if (sy_out) *sy_out = sy; + if (view_out) *view_out = v; + return v->xw_surface->surface; + } + if (v->xdg_surface) { + double sx = px - (double)v->x; + double sy = py - (double)v->y; + double sub_sx, sub_sy; + struct wlr_surface *sub = wlr_xdg_surface_surface_at( + v->xdg_surface, sx, sy, &sub_sx, &sub_sy); + if (sub) { + if (sx_out) *sx_out = sub_sx; + if (sy_out) *sy_out = sub_sy; + if (view_out) *view_out = v; + return sub; + } + } + } + return NULL; +} + +/* + * Update the seat's pointer focus to the surface at the current virtual + * pointer position. Called after every pointer position change so clients + * receive wl_pointer.enter / wl_pointer.leave events correctly. + */ +static void +wl_update_pointer_focus(uint32_t time_msec) +{ + if (!wl_state.seat) + return; + + double sx, sy; + struct wl_toplevel_view *view = NULL; + struct wlr_surface *surface = NULL; + + /* If pointer is over an internal frame_buf (menu, titlebar, etc.), + * don't send pointer events to any client surface — UNLESS the + * pointer is actually over the client content area within the frame. */ + struct wl_frame_buf *fb = frame_buf_at(wl_state.pointer_x, wl_state.pointer_y); + if (fb) { + /* Check if this frame_buf has a client surface (toplevel view) + * and the pointer is over the client area, not the decorations. */ + struct wl_toplevel_view *v = NULL; + struct wlr_surface *cs = view_surface_at( + wl_state.pointer_x, wl_state.pointer_y, &sx, &sy, &v); + if (cs) { + surface = cs; + view = v; + fb = NULL; /* not over decorations */ + } + } + if (!fb && !surface) { + surface = view_surface_at( + wl_state.pointer_x, wl_state.pointer_y, &sx, &sy, &view); + } + + static struct wlr_surface *last_focused_surface = NULL; + if (surface) { + if (surface != last_focused_surface) { + last_focused_surface = surface; + } + wlr_seat_pointer_notify_enter(wl_state.seat, surface, sx, sy); + wlr_seat_pointer_notify_motion(wl_state.seat, time_msec, sx, sy); + wlr_seat_pointer_notify_frame(wl_state.seat); + } else { + if (last_focused_surface) { + last_focused_surface = NULL; + } + wlr_seat_pointer_clear_focus(wl_state.seat); + /* When pointer leaves all surfaces, reset to default arrow. + * Don't reset during grabs — the grab system manages the cursor. */ + if (!wl_state.grab_type) + wl_set_cursor_image("left_ptr"); + } + /* Phase 29 (FFP): focus-follows-pointer — when the pointer enters a new + * managed view, deliver WM keyboard focus without raising. + * Only fires when wPreferences.focus_mode == WKF_SLOPPY. */ + { + static struct wl_toplevel_view *last_ffp_view = NULL; + if (view != last_ffp_view) { + last_ffp_view = view; + if (view) { + WObjDescriptor *desc = + wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN); + if (desc && desc->parent_type == WCLASS_WINDOW) { + WWindow *wwin = (WWindow *)desc->parent; + if (wPreferences.focus_mode == WKF_SLOPPY) { + wSetFocusTo(wScreenWithNumber(0), wwin); + } + } + } + } + } +} + +static void +wl_pointer_warp(int dx, int dy) +{ + /* Phase 54: relative warp — clamp to output bounds. */ + int ow = wl_state.output ? (int)wl_state.output->width : 1920; + int oh = wl_state.output ? (int)wl_state.output->height : 1080; + wl_state.pointer_x += dx; + wl_state.pointer_y += dy; + if (wl_state.pointer_x < 0) wl_state.pointer_x = 0; + if (wl_state.pointer_y < 0) wl_state.pointer_y = 0; + 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 23: update hardware cursor position. */ + if (wl_state.cursor) + wlr_cursor_warp(wl_state.cursor, NULL, + (double)wl_state.pointer_x, (double)wl_state.pointer_y); + /* Phase 13: update pointer focus after relative warp */ + wl_update_pointer_focus(0); +} + +static void +wl_pointer_warp_to(int x, int y) +{ + /* Phase 54: absolute warp — clamp to output bounds. */ + int ow = wl_state.output ? (int)wl_state.output->width : 1920; + int oh = wl_state.output ? (int)wl_state.output->height : 1080; + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= ow) x = ow - 1; + if (y >= oh) y = oh - 1; + wl_state.pointer_x = x; + wl_state.pointer_y = y; + /* Phase 23: update hardware cursor position. */ + if (wl_state.cursor) + wlr_cursor_warp(wl_state.cursor, NULL, (double)x, (double)y); + /* Phase 13: update pointer focus after absolute warp */ + wl_update_pointer_focus(0); +} + +static int +wl_pointer_query(WNativeWindow root, int *root_x, int *root_y) +{ + /* Phase 54: return tracked virtual pointer position. */ + (void)root; + *root_x = wl_state.pointer_x; + *root_y = wl_state.pointer_y; + return wl_state.started ? 1 : 0; +} + +static int +wl_pointer_query_full(WNativeWindow root, + WNativeWindow *child_out, + int *root_x, int *root_y, + unsigned int *buttons_out) +{ + /* Phase 54: return tracked virtual pointer position. */ + (void)root; + if (child_out) *child_out = WNATIVE_NONE; + if (root_x) *root_x = wl_state.pointer_x; + if (root_y) *root_y = wl_state.pointer_y; + if (buttons_out) *buttons_out = 0; + return wl_state.started ? 1 : 0; +} + +static void +wl_pointer_change_grab(unsigned int event_mask, int cursor_id, + unsigned long timestamp) +{ + (void)event_mask; (void)timestamp; + /* If no grab is active, start a WM policy grab (type 3) so that + * wl_update_pointer_focus doesn't reset the cursor on every motion + * event when the pointer is over frame decorations. + * This handles the opaque-move path where moveres.c calls + * pointer_change_grab without first calling pointer_grab. */ + if (!wl_state.grab_type) { + wl_state.grab_type = 3; + wl_state.grab_view = NULL; + if (wl_state.seat) + wlr_seat_pointer_clear_focus(wl_state.seat); + wwarning("wl_backend: grab established via pointer_change_grab"); + } + /* Map WCUR_* cursor IDs to xcursor names */ + const char *name; + switch (cursor_id) { + case WCUR_MOVE: name = "fleur"; break; + case WCUR_WAIT: name = "watch"; break; + case WCUR_TOPLEFTRESIZE: name = "nw-resize"; break; + case WCUR_TOPRIGHTRESIZE: name = "ne-resize"; break; + case WCUR_BOTTOMLEFTRESIZE: name = "sw-resize"; break; + case WCUR_BOTTOMRIGHTRESIZE: name = "se-resize"; break; + case WCUR_VERTICALRESIZE: name = "n-resize"; break; + case WCUR_HORIZONRESIZE: name = "e-resize"; break; + default: name = "left_ptr"; break; + } + wl_set_cursor_image(name); +} + +/* ------------------------------------------------------------------ */ +/* Keyboard grabs */ +/* ------------------------------------------------------------------ */ + +static int +wl_keyboard_grab(WNativeWindow win) +{ + (void)win; + wl_state.keyboard_grabbed = true; + return 0; +} + +static void +wl_keyboard_ungrab(void) +{ + wl_state.keyboard_grabbed = false; +} + +/* ------------------------------------------------------------------ */ +/* Phase 81: Overlay animation surface — XOR pixel engine + bitmap */ +/* font for geometry display (miniaturize zoom, transparent */ +/* move/resize frames, position/size readouts). */ +/* ------------------------------------------------------------------ */ + +/* + * XOR mask: 0xFFFFFFFF XOR 0x00000000 (transparent) = 0xFFFFFFFF (opaque + * white); XOR 0xFFFFFFFF again = 0x00000000 (transparent, i.e. erased). + * The overlay scene buffer is composited OVER all client windows via + * standard alpha blending, so transparent pixels show through to the + * desktop and opaque-white pixels appear on top. + */ +#define OVL_MASK 0xFFFFFFFFu + +/* + * 5-wide × 9-tall bitmap font for printable ASCII (0x20–0x7E). + * Encoding: bit 7 = column 0 (left), bit 3 = column 4 (right), bits 2–0 + * unused. Cell width = 6 px (glyph 5 + 1 gap); cell height = 10 px. + */ +#define OVL_GW 5 /* glyph pixel width */ +#define OVL_GH 9 /* glyph pixel height */ +#define OVL_CW 6 /* cell width (px) */ +#define OVL_CH 10 /* cell height (px) */ + +static const uint8_t ovl_font[96][OVL_GH] = { + /*20 ' '*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /*21 '!'*/{0x20,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x00}, + /*22 '"'*/{0x50,0x50,0x50,0x00,0x00,0x00,0x00,0x00,0x00}, + /*23 '#'*/{0x50,0x50,0xF8,0x50,0xF8,0x50,0x50,0x00,0x00}, + /*24 '$'*/{0x20,0x70,0xA0,0x70,0x28,0x70,0x20,0x00,0x00}, + /*25 '%'*/{0xC0,0xC8,0x10,0x20,0x40,0x98,0x18,0x00,0x00}, + /*26 '&'*/{0x60,0x90,0x60,0xA0,0x98,0x90,0x68,0x00,0x00}, + /*27'\''*/ {0x60,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00}, + /*28 '('*/{0x10,0x20,0x40,0x40,0x40,0x20,0x10,0x00,0x00}, + /*29 ')'*/{0x40,0x20,0x10,0x10,0x10,0x20,0x40,0x00,0x00}, + /*2A '*'*/{0x00,0x88,0x50,0xF8,0x50,0x88,0x00,0x00,0x00}, + /*2B '+'*/{0x00,0x20,0x20,0xF8,0x20,0x20,0x00,0x00,0x00}, + /*2C ','*/{0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00}, + /*2D '-'*/{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00}, + /*2E '.'*/{0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00}, + /*2F '/'*/{0x08,0x08,0x10,0x20,0x40,0x80,0x80,0x00,0x00}, + /*30 '0'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*31 '1'*/{0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*32 '2'*/{0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00,0x00}, + /*33 '3'*/{0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00,0x00}, + /*34 '4'*/{0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00,0x00}, + /*35 '5'*/{0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00,0x00}, + /*36 '6'*/{0x38,0x40,0x80,0xF0,0x88,0x88,0x70,0x00,0x00}, + /*37 '7'*/{0xF8,0x08,0x10,0x20,0x20,0x20,0x20,0x00,0x00}, + /*38 '8'*/{0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00,0x00}, + /*39 '9'*/{0x70,0x88,0x88,0x78,0x08,0x08,0x70,0x00,0x00}, + /*3A ':'*/{0x00,0x60,0x60,0x00,0x60,0x60,0x00,0x00,0x00}, + /*3B ';'*/{0x00,0x60,0x60,0x00,0x60,0x20,0x40,0x00,0x00}, + /*3C '<'*/{0x10,0x20,0x40,0x80,0x40,0x20,0x10,0x00,0x00}, + /*3D '='*/{0x00,0x00,0xF8,0x00,0xF8,0x00,0x00,0x00,0x00}, + /*3E '>'*/{0x40,0x20,0x10,0x08,0x10,0x20,0x40,0x00,0x00}, + /*3F '?'*/{0x70,0x88,0x08,0x10,0x20,0x00,0x20,0x00,0x00}, + /*40 '@'*/{0x70,0x88,0x98,0xA8,0x98,0x80,0x70,0x00,0x00}, + /*41 'A'*/{0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00,0x00}, + /*42 'B'*/{0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00,0x00}, + /*43 'C'*/{0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00,0x00}, + /*44 'D'*/{0xE0,0x90,0x88,0x88,0x88,0x90,0xE0,0x00,0x00}, + /*45 'E'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00,0x00}, + /*46 'F'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00,0x00}, + /*47 'G'*/{0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00,0x00}, + /*48 'H'*/{0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00,0x00}, + /*49 'I'*/{0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*4A 'J'*/{0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x00}, + /*4B 'K'*/{0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00,0x00}, + /*4C 'L'*/{0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00}, + /*4D 'M'*/{0x88,0xD8,0xA8,0x88,0x88,0x88,0x88,0x00,0x00}, + /*4E 'N'*/{0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00,0x00}, + /*4F 'O'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*50 'P'*/{0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00,0x00}, + /*51 'Q'*/{0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00,0x00}, + /*52 'R'*/{0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00,0x00}, + /*53 'S'*/{0x70,0x88,0x80,0x70,0x08,0x88,0x70,0x00,0x00}, + /*54 'T'*/{0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x00}, + /*55 'U'*/{0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*56 'V'*/{0x88,0x88,0x88,0x88,0x50,0x50,0x20,0x00,0x00}, + /*57 'W'*/{0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00,0x00}, + /*58 'X'*/{0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00,0x00}, + /*59 'Y'*/{0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00,0x00}, + /*5A 'Z'*/{0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00,0x00}, + /*5B '['*/{0x70,0x40,0x40,0x40,0x40,0x40,0x70,0x00,0x00}, + /*5C'\\'*/ {0x80,0x80,0x40,0x20,0x10,0x08,0x08,0x00,0x00}, + /*5D ']'*/{0x70,0x10,0x10,0x10,0x10,0x10,0x70,0x00,0x00}, + /*5E '^'*/{0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00}, + /*5F '_'*/{0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x00}, + /*60 '`'*/{0x40,0x20,0x10,0x00,0x00,0x00,0x00,0x00,0x00}, + /*61 'a'*/{0x00,0x00,0x70,0x08,0x78,0x88,0x78,0x00,0x00}, + /*62 'b'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0xF0,0x00,0x00}, + /*63 'c'*/{0x00,0x00,0x70,0x88,0x80,0x88,0x70,0x00,0x00}, + /*64 'd'*/{0x08,0x08,0x78,0x88,0x88,0x88,0x78,0x00,0x00}, + /*65 'e'*/{0x00,0x00,0x70,0x88,0xF8,0x80,0x70,0x00,0x00}, + /*66 'f'*/{0x30,0x48,0x40,0xE0,0x40,0x40,0x40,0x00,0x00}, + /*67 'g'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x70,0x00}, + /*68 'h'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, + /*69 'i'*/{0x20,0x00,0x60,0x20,0x20,0x20,0x70,0x00,0x00}, + /*6A 'j'*/{0x10,0x00,0x30,0x10,0x10,0x10,0x90,0x60,0x00}, + /*6B 'k'*/{0x80,0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00,0x00}, + /*6C 'l'*/{0x60,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*6D 'm'*/{0x00,0x00,0xD8,0xA8,0xA8,0xA8,0x88,0x00,0x00}, + /*6E 'n'*/{0x00,0x00,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, + /*6F 'o'*/{0x00,0x00,0x70,0x88,0x88,0x88,0x70,0x00,0x00}, + /*70 'p'*/{0x00,0x00,0xF0,0x88,0x88,0xF0,0x80,0x80,0x00}, + /*71 'q'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x08,0x00}, + /*72 'r'*/{0x00,0x00,0xB0,0xC8,0x80,0x80,0x80,0x00,0x00}, + /*73 's'*/{0x00,0x00,0x70,0x80,0x70,0x08,0xF0,0x00,0x00}, + /*74 't'*/{0x40,0x40,0xF0,0x40,0x40,0x48,0x30,0x00,0x00}, + /*75 'u'*/{0x00,0x00,0x88,0x88,0x88,0x98,0x68,0x00,0x00}, + /*76 'v'*/{0x00,0x00,0x88,0x88,0x88,0x50,0x20,0x00,0x00}, + /*77 'w'*/{0x00,0x00,0x88,0x88,0xA8,0xD8,0x88,0x00,0x00}, + /*78 'x'*/{0x00,0x00,0x88,0x50,0x20,0x50,0x88,0x00,0x00}, + /*79 'y'*/{0x00,0x00,0x88,0x88,0x78,0x08,0x70,0x00,0x00}, + /*7A 'z'*/{0x00,0x00,0xF8,0x10,0x20,0x40,0xF8,0x00,0x00}, + /*7B '{'*/{0x18,0x20,0x20,0x40,0x20,0x20,0x18,0x00,0x00}, + /*7C '|'*/{0x20,0x20,0x20,0x00,0x20,0x20,0x20,0x00,0x00}, + /*7D '}'*/{0x60,0x10,0x10,0x08,0x10,0x10,0x60,0x00,0x00}, + /*7E '~'*/{0x40,0xA8,0x18,0x00,0x00,0x00,0x00,0x00,0x00}, +}; + +/* Lazily allocate (or resize) the full-screen overlay buffer. */ +static void +overlay_ensure(WScreen *scr) +{ + int w, h; + if (scr && scr->scr_width > 0 && scr->scr_height > 0) { + w = scr->scr_width; + h = scr->scr_height; + } else if (wl_state.output) { + w = (int)wl_state.output->width; + h = (int)wl_state.output->height; + } else { + w = 1920; h = 1080; + } + if (wl_state.overlay_surf && + wl_state.overlay_surf->width == w && + wl_state.overlay_surf->height == h) + return; + if (wl_state.overlay_surf) { + wm_shm_surface_destroy(wl_state.overlay_surf); + wl_state.overlay_surf = NULL; + } + wl_state.overlay_surf = wm_shm_surface_create(w, h); +} + +/* XOR a single pixel on the overlay buffer (clipped). */ +static inline void +ovl_plot(struct wm_shm_surface *s, int x, int y) +{ + if ((unsigned)x < (unsigned)s->width && (unsigned)y < (unsigned)s->height) + s->data[y * (int)(s->stride >> 2) + x] ^= OVL_MASK; +} + +/* XOR a horizontal run of `len` pixels starting at (x, y). */ +static void +ovl_hline(struct wm_shm_surface *s, int x, int y, int len) +{ + if ((unsigned)y >= (unsigned)s->height || len <= 0) return; + int x0 = x < 0 ? 0 : x; + int x1 = x + len; if (x1 > s->width) x1 = s->width; + if (x0 >= x1) return; + uint32_t *p = s->data + y * (int)(s->stride >> 2) + x0; + for (int i = x0; i < x1; i++) *p++ ^= OVL_MASK; +} + +/* XOR a vertical run of `len` pixels starting at (x, y). */ +static void +ovl_vline(struct wm_shm_surface *s, int x, int y, int len) +{ + if ((unsigned)x >= (unsigned)s->width || len <= 0) return; + int y0 = y < 0 ? 0 : y; + int y1 = y + len; if (y1 > s->height) y1 = s->height; + int stride = (int)(s->stride >> 2); + for (int j = y0; j < y1; j++) + s->data[j * stride + x] ^= OVL_MASK; +} + +/* Bresenham line (with fast h/v paths). */ +static void +ovl_line(struct wm_shm_surface *s, int x0, int y0, int x1, int y1) +{ + if (x0 == x1) { + int y = y0 < y1 ? y0 : y1; + ovl_vline(s, x0, y, abs(y1 - y0) + 1); + return; + } + if (y0 == y1) { + int x = x0 < x1 ? x0 : x1; + ovl_hline(s, x, y0, abs(x1 - x0) + 1); + return; + } + int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + for (;;) { + ovl_plot(s, x0, y0); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } +} + +/* Render one glyph at cell origin (cx, cy) using XOR. */ +static void +ovl_draw_glyph(struct wm_shm_surface *s, int cx, int cy, unsigned char ch) +{ + if (ch < 0x20 || ch > 0x7E) ch = '?'; + const uint8_t *g = ovl_font[ch - 0x20]; + for (int row = 0; row < OVL_GH; row++) { + uint8_t bits = g[row]; + if (!bits) continue; + int py = cy + row; + if (py < 0 || py >= s->height) continue; + for (int col = 0; col < OVL_GW; col++) { + if (bits & (0x80u >> col)) + ovl_plot(s, cx + col, py); + } + } +} + +/* Push the modified overlay buffer to the scene and render a frame. */ +static void +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. */ + /* 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) { + /* Ensure the node is visible for the animation frame. */ + wlr_scene_node_set_enabled(&s->scene_buf->node, true); + /* Notify wlroots that the pixel data has changed. */ + wlr_scene_buffer_set_buffer_with_damage(s->scene_buf, &s->wlr_buf, NULL); + } + /* Commit the scene immediately so the animation step is visible + * before the caller's wusleep() delay. */ + if (wl_state.scene_output && wl_state.allocator) + wlr_scene_output_commit(wl_state.scene_output, NULL); +} + +/* ------------------------------------------------------------------ */ +/* Server grab */ +/* ------------------------------------------------------------------ */ + +static void wl_server_grab(void) {} /* no-op: Wayland double-buffers for us */ +static void wl_server_ungrab(void) +{ + overlay_flush(); + /* Disable the overlay scene node so it doesn't intercept hit-testing. + * The XOR animation has already erased its pixels; leaving the node + * enabled blocks frame_buf_at (wlr_scene_node_at hits the overlay + * before any frame_buf). */ + if (wl_state.overlay_surf && wl_state.overlay_surf->scene_buf) + wlr_scene_node_set_enabled(&wl_state.overlay_surf->scene_buf->node, false); +} + +/* ------------------------------------------------------------------ */ +/* 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) +{ + 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, &v->link); + if (v->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(v->frame_id); + if (fb && fb->tree) { + wlr_scene_node_raise_to_top(&fb->tree->node); + return; + } + } + if (v->scene_node) + wlr_scene_node_raise_to_top(&v->scene_node->node); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + 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); + } + return; + } +} + +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); + } +} + +static void +wl_stacking_restack(WNativeWindow *windows, int count) +{ + /* 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) { + /* 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); + wl_list_insert(&wl_state.toplevel_list, &v->link); + if (v->scene_node) + wlr_scene_node_raise_to_top(&v->scene_node->node); + } + } +} + +static int +wl_stacking_query(WNativeWindow root, + WNativeWindow **out, unsigned int *count) +{ + /* Phase 58: return synthetic IDs in Z-order (top to bottom). + * RemakeStackList will find no WM_CTX_STACK entries for them + * and set window_count=0, but returning 1 avoids the "could not + * get window list" warning. */ + struct wl_toplevel_view *v; + unsigned int n = 0, i = 0; + WNativeWindow *wins; + (void)root; + + wl_list_for_each(v, &wl_state.toplevel_list, link) n++; + if (n == 0) { *out = NULL; *count = 0; return 1; } + + wins = wmalloc(n * sizeof(WNativeWindow)); + wl_list_for_each(v, &wl_state.toplevel_list, link) wins[i++] = v->id; + *out = wins; *count = n; + return 1; +} + +/* ------------------------------------------------------------------ */ +/* Events */ +/* ------------------------------------------------------------------ */ + +static void wl_event_flush(void) +{ + /* Flush pending Xlib requests to XWayland so drawing becomes visible. */ + if (wl_state.x_display) + XFlush(wl_state.x_display); +} +static void wl_event_flush_expose(void) {} +static void wl_event_sync(int discard) +{ + if (wl_state.x_display) + XSync(wl_state.x_display, discard ? True : False); +} +static int wl_event_pending(void) { return 0; } +static void wl_event_drain_motion(void) {} +static void wl_event_discard_enter_leave_until(unsigned long timestamp) { (void)timestamp; } +static void wl_event_handle_extensions(WMEvent *event) { (void)event; } +static void wl_event_randr_update(WMEvent *event) { (void)event; } + + +/* ------------------------------------------------------------------ */ +/* WNativeWindow map / unmap / geometry */ +/* ------------------------------------------------------------------ */ + +static void +wl_window_map(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, true); + return; + } + if (v) + return; + /* Frame buffer: enable its scene node and children. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && fb->scene_buf) { + /* If this is a child, only enable if parent is enabled */ + if (fb->parent_id) { + struct wl_frame_buf *parent = frame_buf_find(fb->parent_id); + if (parent && parent->scene_buf && + !parent->scene_buf->node.enabled) + return; /* parent not visible yet */ + } + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + /* Raise the tree (raises entire window assembly together) */ + 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) + wlr_scene_node_raise_to_top(&fb->scene_buf->node); + /* Also enable children (and grandchildren) */ + 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, true); + struct wl_frame_buf *gc; + wl_list_for_each(gc, &wl_state.frame_list, link) { + if (gc->parent_id == child->id && gc->scene_buf) + wlr_scene_node_set_enabled(&gc->scene_buf->node, true); + } + } + } + return; + } +} + +static void +wl_window_map_raised(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, true); + if (v->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(v->frame_id); + if (fb && fb->tree) { + wlr_scene_node_raise_to_top(&fb->tree->node); + return; + } + } + wlr_scene_node_raise_to_top(&v->scene_node->node); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && fb->scene_buf) { + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + if (fb->tree) { + /* Top-level frame: enable tree (decoration children become visible). */ + wlr_scene_node_set_enabled(&fb->tree->node, true); + wlr_scene_node_raise_to_top(&fb->tree->node); + } else { + wlr_scene_node_raise_to_top(&fb->scene_buf->node); + } + return; + } +} +static void wl_window_map_subwindows(WNativeWindow parent) +{ + (void)parent; + /* Child windows are sub-regions of the frame buffer — no-op. */ +} + +static void +wl_window_unmap(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, false); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + 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) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v) { + 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) { + /* Scene node is a child of the frame tree — use relative coords */ + if (v->frame_id) + wlr_scene_node_set_position(&v->scene_node->node, x, y); + else + 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)v->x, (int16_t)v->y, + v->xw_surface->width, v->xw_surface->height); + /* Re-apply any popups whose parent is this toplevel so they + * track the parent's position. */ + struct wl_toplevel_view *p; + wl_list_for_each(p, &wl_state.toplevel_list, link) { + if (p->popup && p->popup_parent + && wl_find_view_by_xdg_surface(p->popup_parent->base) == v) { + wmaker_popup_apply(p); + } + } + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + fb->rel_x = x; fb->rel_y = y; + if (fb->parent_id) { + struct wl_frame_buf *parent = frame_buf_find(fb->parent_id); + if (parent) { + fb->x = parent->x + x; + fb->y = parent->y + y; + } else { + fb->x = x; fb->y = y; + } + /* Child frame_buf: reposition scene_buf at relative offset */ + if (fb->scene_buf) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } else { + fb->x = x; fb->y = y; + /* Top-level frame: move the tree (children follow automatically) */ + if (fb->tree) + wlr_scene_node_set_position(&fb->tree->node, x, y); + } + /* Update absolute position tracking for hit-testing — recursively + * for all descendants so grandchildren (e.g. WINGs panel inside + * a fake_leader inside a WMaker frame) stay in sync. */ + frame_buf_update_children_abs(win); + return; + } +} + +static void wl_window_resize(WNativeWindow win, int w, int h) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v) { + if (w <= 0 || h <= 0) return; + if (v->xw_surface) { + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)v->x, (int16_t)v->y, (uint16_t)w, (uint16_t)h); + return; + } + if (!v->xdg_surface || !v->xdg_surface->toplevel) return; + wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)w, (uint32_t)h); + wlr_xdg_surface_schedule_configure(v->xdg_surface); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && w > 0 && h > 0 && (w != fb->width || h != fb->height)) { + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + fb->width = w; + fb->height = h; + fb->base.width = w; + fb->base.height = h; + /* Update scene graph's cached buffer size for hit-testing. */ + if (fb->scene_buf) + wlr_scene_buffer_set_dest_size(fb->scene_buf, w, h); + } +} +static void wl_window_set_border(WNativeWindow win, unsigned long pixel) +{ + (void)win; (void)pixel; /* borders are drawn into the frame buffer */ +} +static void wl_window_set_border_width(WNativeWindow win, int width) +{ + (void)win; (void)width; /* no-op for native frames */ +} +static void wl_window_set_save_under(WNativeWindow win, int save_under) { (void)win; (void)save_under; } +static void wl_noop_window_set_background(WNativeWindow win, unsigned long pixel) { (void)win; (void)pixel; } +extern void *W_GetViewBacking(unsigned long id); + +static void wl_window_set_background_pixmap(WNativeWindow win, unsigned long pixmap) +{ + if (pixmap == 0) return; + struct wl_frame_buf *fb = frame_buf_find(win); + pixman_image_t *dst_img = NULL; + + if (fb && fb->image) { + dst_img = fb->image; + fb->bg_pixmap = (pixman_image_t *)(uintptr_t)pixmap; + } else { + dst_img = (pixman_image_t *)W_GetViewBacking(win); + } + if (!dst_img) return; + + pixman_image_t *src = (pixman_image_t *)(uintptr_t)pixmap; + int src_w = pixman_image_get_width(src); + int src_h = pixman_image_get_height(src); + + /* Blit source pixmap into frame_buf's image */ + pixman_image_composite32(PIXMAN_OP_SRC, + src, /* src */ + NULL, /* mask */ + dst_img, /* dest */ + 0, 0, /* src x,y */ + 0, 0, /* mask x,y */ + 0, 0, /* dest x,y */ + src_w < pixman_image_get_width(dst_img) ? src_w : pixman_image_get_width(dst_img), + src_h < pixman_image_get_height(dst_img) ? src_h : pixman_image_get_height(dst_img)); + if (fb) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +static void wl_pixmap_free(unsigned long pixmap) +{ + if (pixmap == 0) return; + pixman_image_t *img = (pixman_image_t *)(uintptr_t)pixmap; + pixman_image_unref(img); +} +static void wl_window_clear(WNativeWindow win) +{ + struct wl_frame_buf *fb = frame_buf_find(win); + pixman_image_t *img = NULL; + + if (fb && fb->image) { + img = fb->image; + } else { + img = (pixman_image_t *)W_GetViewBacking(win); + } + if (!img) return; + + int width = pixman_image_get_width(img); + int height = pixman_image_get_height(img); + + /* If a background pixmap is set, repaint from it (like XClearWindow) */ + if (fb && fb->bg_pixmap) { + pixman_image_composite32(PIXMAN_OP_SRC, + fb->bg_pixmap, NULL, img, + 0, 0, 0, 0, 0, 0, + width, height); + } else { + /* No background pixmap — fill with transparent black */ + int stride = pixman_image_get_stride(img); + uint32_t *data = (uint32_t *)pixman_image_get_data(img); + memset(data, 0, stride * height); + } + + /* Notify the scene buffer of the update */ + if (fb) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +static int +wl_window_get_rect(WNativeWindow win, + int *win_x, int *win_y, int *win_w, int *win_h) +{ + /* Phase 55: return real geometry for synthetic toplevel IDs. */ + if (win == WNATIVE_NONE) { + if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; } + if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; } + return 0; /* invalid/none */ + } + struct wl_toplevel_view *v = wl_find_view_by_id(win); + /* Phase 17: XWayland geometry. */ + if (v && v->xw_surface) { + if (win_x) { *win_x = v->x; } + if (win_y) { *win_y = v->y; } + if (win_w) { *win_w = v->xw_surface->width > 0 ? v->xw_surface->width : 1; } + if (win_h) { *win_h = v->xw_surface->height > 0 ? v->xw_surface->height : 1; } + return 1; + } + if (v && v->xdg_surface && v->xdg_surface->surface) { + int w = v->xdg_surface->surface->current.width; + int h = v->xdg_surface->surface->current.height; + if (win_x) { *win_x = v->x; } + if (win_y) { *win_y = v->y; } + if (win_w) { *win_w = w > 0 ? w : 1; } + if (win_h) { *win_h = h > 0 ? h : 1; } + return 1; + } + /* Frame buffer geometry. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + if (win_x) { *win_x = fb->x; } + if (win_y) { *win_y = fb->y; } + if (win_w) { *win_w = fb->width; } + if (win_h) { *win_h = fb->height; } + return 1; + } + /* Synthetic child IDs (titlebar, buttons) — return minimal geometry. */ + if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; } + if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; } + return 0; +} + +static void wl_window_add_events(WNativeWindow win, unsigned long extra_mask) { (void)win; (void)extra_mask; } +static void wl_window_select_events(WNativeWindow win, unsigned long mask) { (void)win; (void)mask; } + +static void wl_window_shape_clear(WWindow *wwin) { (void)wwin; } +static void wl_window_shape_set(WWindow *wwin) { (void)wwin; } + +/* ------------------------------------------------------------------ */ +/* Overlays */ +/* ------------------------------------------------------------------ */ + +static void +wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2) +{ + overlay_ensure(scr); + if (wl_state.overlay_surf) { + ovl_line(wl_state.overlay_surf, x1, y1, x2, y2); + overlay_flush(); + } +} + +static void +wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || n < 2) return; + for (int i = 0; i < n - 1; i++) + ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y); +} + +static void +wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || w <= 0 || h <= 0) return; + /* Matching XDrawRectangle semantics: corners are shared between + * adjacent edges and therefore XOR'd twice (become transparent). */ + ovl_hline(s, x, y, w + 1); /* top */ + ovl_hline(s, x, y + h, w + 1); /* bottom */ + ovl_vline(s, x, y, h + 1); /* left */ + ovl_vline(s, x + w, y, h + 1); /* right */ + overlay_flush(); +} + +static void +wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s) return; + for (int i = 0; i < n; i++) + ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2); +} + +static void +wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || !str) return; + /* y is the text baseline; render from the top of the cell. */ + int top = y - (OVL_GH - 2); + for (int i = 0; i < len; i++) + ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]); +} + +static int +wl_overlay_text_width(WScreen *scr, const char *str, int n) +{ + (void)scr; (void)str; + return n * OVL_CW; +} + +/* ------------------------------------------------------------------ */ +/* Client */ +/* ------------------------------------------------------------------ */ + +static int +wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out) +{ + /* Phase 42: return real map_state and geometry from XDG surface. + * Phase 17: XWayland path uses xw_surface->mapped and width/height. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + memset(out, 0, sizeof(*out)); + if (v && v->xw_surface) { + if (v->xw_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640; + out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + } else if (v && v->xdg_surface && v->xdg_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xdg_surface->surface->current.width; + out->height = v->xdg_surface->surface->current.height; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + return 1; +} + +static void +wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out) +{ + (void)win; + out->depth = 24; out->visual = NULL; out->colormap = 0; +} + +static void +wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask) +{ (void)win; (void)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) { + /* Move client's scene tree under the frame's tree */ + if (parent_fb->tree) + wlr_scene_node_reparent(&v->scene_node->node, parent_fb->tree); + /* Position at relative offset within the tree */ + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + } + return; + } + + + + /* For WINGs internal windows: create a child frame_buf so the dialog + * content is visible in the scene graph. */ + if (client >= 0x20000000 && new_parent >= 0x10000000) { + /* client is a WINGs view ID, new_parent is a frame_buf */ + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + if (!parent_fb) return; + + /* Check if we already have a frame_buf for this client */ + struct wl_frame_buf *existing = frame_buf_find(client); + if (existing) { + /* Just update parent and position */ + existing->parent_id = new_parent; + existing->rel_x = x; + existing->rel_y = y; + existing->x = parent_fb->x + x; + existing->y = parent_fb->y + y; + if (existing->scene_buf) { + /* Ensure it's under the parent frame's tree, then position + * at relative offset within the tree. */ + if (parent_fb->tree && + existing->scene_buf->node.parent != parent_fb->tree) { + wlr_scene_node_reparent(&existing->scene_buf->node, + parent_fb->tree); + } + if (parent_fb->tree) + wlr_scene_node_set_position(&existing->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&existing->scene_buf->node, + existing->x, existing->y); + } + return; + } + + /* Create a new frame_buf for this WINGs view. + * Get size from the view itself. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)client); + int w = view ? view->size.width : parent_fb->width; + int h = view ? view->size.height : (parent_fb->height - y); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + if (!fb->image) { wfree(fb); return; } + + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + + fb->id = client; /* use the WINGs view ID directly */ + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + fb->width = w; + fb->height = h; + + if (wl_state.scene) { + /* Place the WINGs content under the parent frame's tree so it + * moves with the WM-managed dialog decorations. */ + struct wlr_scene_tree *_wp = parent_fb->tree + ? parent_fb->tree + : (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) { + fb_attach_scene_buf_listener(fb); + /* If under the frame tree, use relative coords; otherwise absolute. */ + if (parent_fb->tree) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + + /* Connect the WINGs view's backing store to this frame_buf's image */ + extern void W_SetViewBacking(unsigned long id, void *img); + W_SetViewBacking((unsigned long)client, fb->image); + } + /* Reparent an existing frame_buf into a new parent */ + else if (client >= 0x10000000 && new_parent >= 0x10000000) { + struct wl_frame_buf *fb = frame_buf_find(client); + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + fprintf(stderr, "reparent_framebuf: client=0x%lx fb=%p new_parent=0x%lx parent_fb=%p\n", + (unsigned long)client, (void*)fb, + (unsigned long)new_parent, (void*)parent_fb); + if (fb) fprintf(stderr, " fb: tree=%p scene_buf=%p pos=(%d,%d)\n", + (void*)fb->tree, (void*)fb->scene_buf, + fb->tree ? fb->tree->node.x : -1, + fb->tree ? fb->tree->node.y : -1); + if (parent_fb) fprintf(stderr, " parent_fb: tree=%p pos=(%d,%d)\n", + (void*)parent_fb->tree, + parent_fb->tree ? parent_fb->tree->node.x : -1, + parent_fb->tree ? parent_fb->tree->node.y : -1); + if (fb && parent_fb) { + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + if (fb->tree) { + /* This frame_buf has a tree (e.g. fake_leader): reparent + * the whole tree so all children move with it. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->tree->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->tree->node, x, y); + } else if (fb->scene_buf) { + /* Plain frame_buf (no tree): reparent scene_buf directly. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->scene_buf->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } + /* Update absolute tracking recursively for all descendants */ + frame_buf_update_children_abs(client); + } + } +} + +/* Called by WINGs after a view repaints — commit its frame_buf to scene graph */ +void wl_view_commit_backing_impl(unsigned long id) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + WNativeWindow saved_parent_id = 0; + int saved_rel_x = 0, saved_rel_y = 0; + /* If the view was resized, the old frame_buf's image is stale. */ + if (fb) { + extern void *W_GetViewBacking(unsigned long id); + void *current = W_GetViewBacking((unsigned long)id); + if (current != (void *)fb->image) { + /* Save parent relation before destroying — the new frame_buf + * must be positioned within the same parent tree so dialog + * content reparented via wl_client_reparent stays visible. */ + saved_parent_id = fb->parent_id; + saved_rel_x = fb->rel_x; + saved_rel_y = fb->rel_y; + wlr_scene_node_destroy(&fb->scene_buf->node); + wl_list_remove(&fb->link); + wlr_buffer_drop(&fb->base); + wfree(fb); + fb = NULL; + } + } + if (!fb) { + extern W_View *W_GetViewForWindow(unsigned long id); + extern void *W_GetViewBacking(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)id); + if (view && view->flags.topLevel) { + fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + fb->id = id; + fb->parent_id = saved_parent_id; + fb->rel_x = saved_rel_x; + fb->rel_y = saved_rel_y; + fb->x = view->pos.x; + fb->y = view->pos.y; + fb->width = view->size.width; + fb->height = view->size.height; + fb->image = (pixman_image_t *)W_GetViewBacking(id); + if (fb->image) { + pixman_image_ref(fb->image); + wlr_buffer_init(&fb->base, &frame_buf_impl, + pixman_image_get_width(fb->image), + pixman_image_get_height(fb->image)); + if (wl_state.scene) { + struct wlr_scene_tree *parent = NULL; + /* Re-parent under the saved parent's tree so + * dialogs reparented via wl_client_reparent stay + * correctly positioned within their frame. */ + if (saved_parent_id) { + struct wl_frame_buf *pfb = + frame_buf_find(saved_parent_id); + if (pfb && pfb->tree) + parent = pfb->tree; + } + if (!parent) + parent = 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(parent, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + /* Position relative to parent's tree or absolute */ + if (parent != &wl_state.scene->tree && saved_parent_id) + wlr_scene_node_set_position(&fb->scene_buf->node, + saved_rel_x, saved_rel_y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, + fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + wl_list_insert(&wl_state.frame_list, &fb->link); + } else { + wfree(fb); + fb = NULL; + } + } + } + if (fb && fb->scene_buf) { + /* Sync position from the WINGs view — it may have moved since + * the frame_buf was created (e.g. switch panel is positioned + * after widget creation via WMMoveWidget). + * When reparented (parent_id set), use the relative offset + * instead of the view's absolute position. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *v = W_GetViewForWindow((unsigned long)id); + if (v && !fb->parent_id) { + if (fb->x != v->pos.x || fb->y != v->pos.y) { + fb->x = v->pos.x; + fb->y = v->pos.y; + wlr_scene_node_set_position(&fb->scene_buf->node, + fb->x, fb->y); + } + } + /* Only enable the scene node if the view is still mapped. + * If the view was unmapped (e.g. move ended), wl_unmap disables + * it via W_ViewSceneNodeSetEnabledFn — don't re-enable here. */ + if (!v || v->flags.mapped) { + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + } + } +} +void +wl_view_scene_node_set_enabled(unsigned long id, int enabled) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + if (fb && fb->scene_buf) + wlr_scene_node_set_enabled(&fb->scene_buf->node, !!enabled); +} + + +void wl_view_destroy_backing_impl(unsigned long id) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + if (!fb) return; + wl_list_remove(&fb->link); + /* fb->scene_buf may have been auto-destroyed by wlroots already if a + * parent tree was destroyed earlier (e.g. wl_frame_destroy on the + * surrounding WMaker frame). The destroy listener NULLs scene_buf + * in that case. */ + if (fb->scene_buf) + wlr_scene_node_destroy(&fb->scene_buf->node); + wlr_buffer_drop(&fb->base); +} + +static void +wl_client_synth_configure(WNativeWindow client_win, + int x, int y, int width, int height, + int border_width, WNativeWindow above) +{ + /* Phase 44: send an XDG configure with the new size to the client. + * Phase 13: also record the WM-assigned position in the view so + * pointer hit-testing (view_surface_at) uses current coordinates. + * Phase 17: XWayland uses wlr_xwayland_surface_configure instead. */ + (void)border_width; (void)above; + struct wl_toplevel_view *v = wl_find_view_by_id(client_win); + if (!v) return; + v->x = x; + v->y = y; + /* Update relative offset so frame_configure keeps us in sync. */ + if (v->frame_id) { + struct wl_frame_buf *pfb = frame_buf_find(v->frame_id); + if (pfb) { + v->frame_rel_x = x - pfb->x; + v->frame_rel_y = y - pfb->y; + } + /* Scene node is child of frame tree — use relative coords */ + if (v->scene_node) + wlr_scene_node_set_position(&v->scene_node->node, + v->frame_rel_x, v->frame_rel_y); + } else { + /* No frame — use absolute coords */ + if (v->scene_node) + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + if (v->xw_surface) { + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)x, (int16_t)y, + width > 0 ? (uint16_t)width : v->xw_surface->width, + height > 0 ? (uint16_t)height : v->xw_surface->height); + return; + } + if (!v->xdg_surface || !v->xdg_surface->toplevel) return; + if (width > 0 && height > 0) + wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)width, (uint32_t)height); + wlr_xdg_surface_schedule_configure(v->xdg_surface); +} + +static void wl_client_add_to_save_set(WNativeWindow win) { (void)win; } +static void wl_client_remove_from_save_set(WNativeWindow win) { (void)win; } + +static void +wl_client_close(WNativeWindow win) +{ + /* Phase 46: ask the surface to close. + * Phase 17: XWayland uses wlr_xwayland_surface_close. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return; + if (v->xw_surface) { + wlr_xwayland_surface_close(v->xw_surface); + } else if (v->xdg_surface && v->xdg_surface->toplevel) { + wlr_xdg_toplevel_send_close(v->xdg_surface->toplevel); + /* Flush so the in-process client receives the close event, + * then dispatch so it processes it on this iteration. */ + wl_display_flush_clients(wl_state.display); + } +} + +static void wl_client_query_shaped(WNativeWindow win, int *shaped_out) +{ (void)win; *shaped_out = 0; } + +static int +wl_client_get_gnustep_attrs(WNativeWindow win, GNUstepWMAttributes **attr_out) +{ (void)win; *attr_out = NULL; return 0; } + +static int wl_client_get_pid(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return 0; + /* Phase 17: XWayland surfaces have a direct pid field. */ + if (v->xw_surface) + return (int)v->xw_surface->pid; + if (!v->xdg_surface || !v->xdg_surface->resource) + return 0; + struct wl_client *client = wl_resource_get_client(v->xdg_surface->resource); + if (!client) return 0; + pid_t pid = 0; uid_t uid = 0; gid_t gid = 0; + wl_client_get_credentials(client, &pid, &uid, &gid); + return (int)pid; +} + +static RImage * +wl_client_snapshot(WScreen *scr, WNativeWindow client_win, WNativeWindow frame_win) +{ + (void)frame_win; + struct wl_toplevel_view *v = wl_find_view_by_id(client_win); + if (!v) return NULL; + + /* XWayland: capture via XGetImage on the WM's X display */ + if (v->xw_surface && wl_state.x_display) { + Window xid = (Window)v->xw_surface->window_id; + XWindowAttributes attr; + if (!XGetWindowAttributes(wl_state.x_display, xid, &attr)) + return NULL; + XImage *img = XGetImage(wl_state.x_display, xid, + 0, 0, attr.width, attr.height, + AllPlanes, ZPixmap); + if (!img) return NULL; + RImage *res = RCreateImageFromXImage(scr->rcontext, img, NULL); + XDestroyImage(img); + return res; + } + + /* Native Wayland: read pixels from last committed surface buffer */ + if (!v->xdg_surface || !v->xdg_surface->surface) return NULL; + struct wlr_surface *s = v->xdg_surface->surface; + if (!s->buffer) return NULL; + struct wlr_buffer *buf = &s->buffer->base; + + uint32_t fmt; + void *data; + size_t stride; + if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &fmt, &stride)) + return NULL; + + int w = s->current.width; + int h = s->current.height; + if (w <= 0 || h <= 0) { + wlr_buffer_end_data_ptr_access(buf); + return NULL; + } + + /* Create RImage and copy pixels (ARGB32 → RGBA byte order swap) */ + RImage *res = RCreateImage(w, h, True); + if (res) { + unsigned char *src = (unsigned char *)data; + unsigned char *dst = res->data; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int si = y * stride + x * 4; + int di = (y * w + x) * 4; + dst[di + 0] = src[si + 2]; /* B → R */ + dst[di + 1] = src[si + 1]; /* G → G */ + dst[di + 2] = src[si + 0]; /* R → B */ + dst[di + 3] = src[si + 3]; /* A → A */ + } + } + } + + wlr_buffer_end_data_ptr_access(buf); + return res; +} + +static int +wl_coords_translate(WNativeWindow src, WNativeWindow dst, + int src_x, int src_y, int *dst_x, int *dst_y) +{ + (void)src; (void)dst; + *dst_x = src_x; *dst_y = src_y; + return 0; +} +/* ------------------------------------------------------------------ */ +/* Screen internals */ +/* ------------------------------------------------------------------ */ + +static int wl_screen_replace_wm(WScreen *scr) { (void)scr; return 1; } +static int wl_screen_select_input(WScreen *scr, long mask) { (void)scr; (void)mask; return 1; } +static void wl_screen_set_root_cursor(WScreen *scr, int cursor_id) { (void)scr; (void)cursor_id; } +static void wl_screen_set_icon_sizes(WScreen *scr, int min_size, int max_size) { (void)scr; (void)min_size; (void)max_size; } +static void wl_screen_setup_noticeboard(WScreen *scr) { (void)scr; } +static void +wl_screen_alloc_gcs(WScreen *scr) +{ + XGCValues gcv; + int gcm; + static const char stipple_data[] = { 0x02, 0x01 }; + + /* Pre-initialise menu text colors to black so that setMenuTextColor / + * setMenuDisabledColor in wReadDefaults can safely compare them before + * both have been set from the defaults DB. */ + scr->menu_title_color[0] = WMRetainColor(scr->white); + scr->mtext_color = WMCreateRGBColor(scr->wmscreen, 0, 0, 0, True); + scr->dtext_color = WMCreateRGBColor(scr->wmscreen, 0, 0, 0, True); + + /* Create X11 GCs on the XWayland connection. wmaker's rendering code + * (framewin, icon, menu) uses these regardless of the display backend. + * Mirrors x11_screen_alloc_gcs exactly; without these, CopyArea and + * XRender calls use GC/drawable 0 → BadDrawable → XWayland broken pipe. */ + if (!wl_state.x_display) + return; + + /* Mark the wrlib drawable as override_redirect so XWayland's XWM does not + * manage it as a client window. XWM reads override_redirect from the + * CreateNotify event (already gone), but any subsequent ConfigureNotify + * updates the flag on the wlr_xwayland_surface it created. Combined with + * the explicit window-ID filter in handle_xwayland_new_surface, this + * prevents BadDrawable/RenderBadPicture cascade and the resulting broken + * pipe from the nested Wayland backend. */ + if (scr->w_win) { + wl_state.rcontext_xwin = (WNativeWindow)scr->w_win; + XSetWindowAttributes or_attr; + or_attr.override_redirect = True; + XChangeWindowAttributes(wl_state.x_display, (WNativeWindow)scr->w_win, CWOverrideRedirect, &or_attr); + XFlush(wl_state.x_display); + } + + scr->stipple_bitmap = XCreateBitmapFromData(wl_state.x_display, (WNativeWindow)scr->w_win, + stipple_data, 2, 2); + + gcv.stipple = scr->stipple_bitmap; + gcv.foreground = scr->white_pixel; + gcv.fill_style = FillStippled; + gcv.graphics_exposures = False; + gcm = GCForeground | GCStipple | GCFillStyle | GCGraphicsExposures; + scr->stipple_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, gcm, &gcv); + + gcv.function = GXcopy; + gcv.foreground = scr->white_pixel; + gcv.background = scr->black_pixel; + gcv.line_width = 1; + gcv.line_style = LineDoubleDash; + gcv.fill_style = FillSolid; + gcv.dash_offset = 0; + gcv.dashes = 4; + gcv.graphics_exposures = False; + gcm = GCFunction | GCGraphicsExposures | GCForeground | GCBackground + | GCLineWidth | GCLineStyle | GCFillStyle | GCDashOffset | GCDashList; + scr->icon_select_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, gcm, &gcv); + + gcv.function = GXxor; + gcv.plane_mask = AllPlanes; + gcv.foreground = scr->white_pixel; + if (gcv.foreground == 0) + gcv.foreground = 1; + gcv.line_width = DEF_FRAME_THICKNESS; + gcv.subwindow_mode = IncludeInferiors; + gcv.graphics_exposures = False; + scr->frame_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->root_win, + GCForeground | GCGraphicsExposures | GCFunction + | GCSubwindowMode | GCLineWidth | GCPlaneMask, &gcv); + + gcv.foreground = scr->white_pixel; + if (gcv.foreground == 0) + gcv.foreground = 17; + gcv.function = GXxor; + gcv.subwindow_mode = IncludeInferiors; + gcv.line_width = 1; + gcv.cap_style = CapRound; + gcv.graphics_exposures = False; + gcm = GCForeground | GCFunction | GCSubwindowMode | GCLineWidth + | GCCapStyle | GCGraphicsExposures; + scr->line_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->root_win, gcm, &gcv); + scr->line_pixel = gcv.foreground; + + gcv.foreground = scr->white_pixel; + gcv.background = scr->black_pixel; + gcv.graphics_exposures = False; + scr->copy_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, + GCForeground | GCBackground | GCGraphicsExposures, &gcv); + + gcv.graphics_exposures = False; + scr->draw_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, GCGraphicsExposures, &gcv); + + scr->mono_gc = XCreateGC(wl_state.x_display, scr->stipple_bitmap, GCGraphicsExposures, &gcv); +} +static void +wl_screen_create_internals(WScreen *scr) +{ + /* Create a real off-screen InputOnly X window that absorbs keyboard focus + * when no application window holds it. This mirrors x11_screen_create_internals + * exactly: without a mapped, viewable focus-sink window, any XSetInputFocus + * call targeting it (serial ~1626 during manageAllWindows) returns BadMatch. */ + if (wl_state.x_display) { + XSetWindowAttributes attribs; + int vmask = CWEventMask | CWOverrideRedirect; + attribs.event_mask = WM_EVMASK_KEY_PRESS | WM_EVMASK_FOCUS_CHANGE; + attribs.override_redirect = True; + WNativeWindow xwin = XCreateWindow(wl_state.x_display, DefaultRootWindow(wl_state.x_display), + -10, -10, 4, 4, 0, + 0, InputOnly, CopyFromParent, vmask, &attribs); + if (xwin) { + XSelectInput(wl_state.x_display, xwin, WM_EVMASK_KEY_PRESS | WM_EVMASK_KEY_RELEASE); + XMapWindow(wl_state.x_display, xwin); + /* Do NOT call XSetInputFocus here: the window may not be viewable + * yet in XWayland (map is async), triggering BadMatch that perturbs + * XWayland state and causes a Wayland broken-pipe cascade. + * wl_focus_set() will clear Wayland seat focus via + * wlr_seat_keyboard_clear_focus when this window is the target. */ + wl_state.no_focus_xwin = xwin; + scr->no_focus_win = (WNativeWindow)xwin; + } + } + + wPreferences.auto_focus = 1; + if (wl_state.seat) + wlr_seat_keyboard_clear_focus(wl_state.seat); + + wSetModifierMasks(WM_MOD_MOD1, WM_MOD_MOD1, WM_MOD_MOD4, 0); + + /* XWayland bridge: create a real X11 helper window for EWMH + * (_NET_SUPPORTING_WM_CHECK) and clipboard selection ownership. + * override_redirect=True prevents XWayland's XWM from managing it as a + * client window (otherwise it appears as class=(none) and generates + * BadDrawable errors from wlroots when wManageWindow tries to reparent it). */ + if (wl_state.x_display) { + XSetWindowAttributes iattr; + iattr.override_redirect = True; + WNativeWindow xwin = XCreateWindow(wl_state.x_display, DefaultRootWindow(wl_state.x_display), + -2, -2, 1, 1, 0, + 0, InputOutput, CopyFromParent, + CWOverrideRedirect, &iattr); + if (xwin) + scr->info_window = (WNativeWindow)xwin; + } + + /* Create dock_shadow: scene_buffer used as ghost overlay during icon drag */ + if (wl_state.scene) { + int sz = wPreferences.icon_size; + struct wl_frame_buf *dsfb = wmalloc(sizeof(*dsfb)); + memset(dsfb, 0, sizeof(*dsfb)); + dsfb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, sz, sz, NULL, sz * 4); + if (dsfb->image) { + wlr_buffer_init(&dsfb->base, &frame_buf_impl, sz, sz); + dsfb->id = wl_state.next_synthetic_id++; + dsfb->width = sz; + dsfb->height = sz; + struct wlr_scene_tree *ds_parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + dsfb->tree = wlr_scene_tree_create(ds_parent); + if (dsfb->tree) { + dsfb->scene_buf = wlr_scene_buffer_create(dsfb->tree, &dsfb->base); + if (dsfb->scene_buf) + wlr_scene_node_set_enabled(&dsfb->scene_buf->node, false); + wlr_scene_node_set_enabled(&dsfb->tree->node, false); + } + wl_list_insert(&wl_state.frame_list, &dsfb->link); + scr->dock_shadow = dsfb->id; + } else { + wfree(dsfb); + } + } + + /* Phase 30: dock/clip now uses XWayland windows for rendering. + * The WM's dock logic (dock.c) creates X11 windows on the XWayland + * Display* and draws on them with Xlib — the compositor composites + * them like any other XWayland surface. */ + /* wPreferences.flags.noclip = 1; */ + /* wPreferences.flags.nodock = 1; */ + + /* Phase 30: dock creation is deferred to wl_event_loop_run() (after the + * startup pump thread stops). wm_dock_create() calls wlr_scene_buffer_create + * which is not thread-safe — calling it here races with the pump thread's + * frame-timer callback that calls wlr_scene_output_commit on the same scene. + * The dock_scr pointer tells wl_event_loop_run() to create the dock once the + * compositor's single-threaded event loop is running. + * NOTE: native dock panel disabled — WM dock uses XWayland windows now. */ + /* wl_state.dock_pending_scr = scr; */ + + /* Wayland has no X11-style window border; force to 0 so policy code + * does not offset client positions by a non-existent border. */ + scr->frame_border_width = 0; + + /* Create workspace name badge overlay (initially 10x10, resized on use) */ + if (wl_state.scene) { + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, 10, 10, NULL, 0); + if (fb->image) { + wlr_buffer_init(&fb->base, &frame_buf_impl, 10, 10); + fb->id = wl_state.next_synthetic_id++; + fb->width = 10; + fb->height = 10; + struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + fb->tree = wlr_scene_tree_create(parent); + if (fb->tree) { + fb_attach_tree_listener(fb); + fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + wlr_scene_node_set_enabled(&fb->tree->node, false); + } + wl_list_insert(&wl_state.frame_list, &fb->link); + W_RegisterBacking(fb->id, fb->image); + scr->workspace_name = fb->id; + } else { + wfree(fb); + } + } +} +static void wl_screen_create_pixmaps(WScreen *scr) { (void)scr; } +static void +wl_screen_load_tech_font(WScreen *scr) +{ + if (!scr) return; + /* Wayland mode has no native font handle (overlay text is drawn via + * the embedded 5×9 bitmap font in wl_overlay_draw_string). We just + * publish the metric fields so moveres.c gets correct geometry. */ + scr->tech_draw_font = NULL; + scr->tech_draw_font_ascent = OVL_GH - 2; /* 7 */ + scr->tech_draw_font_descent = 2; +} +static void wl_screen_show_mini_screenshot(WScreen *scr, RImage *img) { (void)scr; (void)img; } +static RImage *wl_screen_capture_area(WScreen *scr) { (void)scr; return NULL; } +static RImage *wl_screen_capture_window(WScreen *scr, WWindow *wwin) { (void)scr; (void)wwin; return NULL; } +static void wl_refresh_desktop(WScreen *scr) { (void)scr; } + +/* --- Desktop background rendering --- */ + +/* Simple wlr_buffer implementation backed by a pixman_image_t */ +struct wl_bg_buffer { + struct wlr_buffer base; + pixman_image_t *image; +}; + +static void bg_buffer_destroy(struct wlr_buffer *wlr_buf) +{ + struct wl_bg_buffer *buf = wl_container_of(wlr_buf, buf, base); + if (buf->image) + pixman_image_unref(buf->image); + free(buf); +} + +static bool bg_buffer_begin_data_ptr_access(struct wlr_buffer *wlr_buf, + uint32_t flags, void **data, uint32_t *format, size_t *stride) +{ + (void)flags; + struct wl_bg_buffer *buf = wl_container_of(wlr_buf, buf, base); + *data = pixman_image_get_data(buf->image); + *format = DRM_FORMAT_ARGB8888; + *stride = pixman_image_get_stride(buf->image); + return true; +} + +static void bg_buffer_end_data_ptr_access(struct wlr_buffer *wlr_buf) +{ + (void)wlr_buf; +} + +static const struct wlr_buffer_impl bg_buffer_impl = { + .destroy = bg_buffer_destroy, + .begin_data_ptr_access = bg_buffer_begin_data_ptr_access, + .end_data_ptr_access = bg_buffer_end_data_ptr_access, +}; + +static int parse_color_to_rcolor(WScreen *scr, const char *name, RColor *out) +{ + WMColorRGB wmc; + if (!name || !wGetColor(scr, name, &wmc)) + return 0; + out->red = wmc.red >> 8; + out->green = wmc.green >> 8; + out->blue = wmc.blue >> 8; + out->alpha = 255; + return 1; +} + +static void wl_set_desktop_background(WScreen *scr, const char *texture_desc) +{ + if (!scr || !texture_desc || !wl_state.output) + return; + + + int width = wl_state.output->width; + int height = wl_state.output->height; + + /* Parse the texture proplist */ + WMPropList *texpl = WMCreatePropListFromDescription(texture_desc); + if (!texpl) + return; + + /* Render the texture to an RImage using wrlib */ + RContext *rc = scr->rcontext; + RImage *bg = NULL; + + if (WMIsPLArray(texpl) && WMGetPropListItemCount(texpl) > 0) { + WMPropList *type_pl = WMGetFromPLArray(texpl, 0); + const char *type = type_pl ? WMGetFromPLString(type_pl) : ""; + + if (strcasecmp(type, "solid") == 0 && WMGetPropListItemCount(texpl) >= 2) { + const char *colorstr = WMGetFromPLString(WMGetFromPLArray(texpl, 1)); + RColor color = {0x50, 0x50, 0x75, 0xFF}; + if (colorstr) + parse_color_to_rcolor(scr, colorstr, &color); + /* For solid colors, just update the scene rect directly */ + if (wl_state.bg_rect) { + float c[4] = { color.red/255.0f, color.green/255.0f, color.blue/255.0f, 1.0f }; + wlr_scene_rect_set_color(wl_state.bg_rect, c); + wlr_scene_node_set_enabled(&wl_state.bg_rect->node, true); + if (wl_state.bg_image) + wlr_scene_node_set_enabled(&wl_state.bg_image->node, false); + } + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); + WMReleasePropList(texpl); + return; + } else if ((strcasecmp(type, "vgradient") == 0 || + strcasecmp(type, "hgradient") == 0 || + strcasecmp(type, "dgradient") == 0) && + WMGetPropListItemCount(texpl) >= 3) { + const char *c1str = WMGetFromPLString(WMGetFromPLArray(texpl, 1)); + const char *c2str = WMGetFromPLString(WMGetFromPLArray(texpl, 2)); + RColor color1 = {0, 0, 0, 255}, color2 = {0, 0, 0, 255}; + if (c1str) parse_color_to_rcolor(scr, c1str, &color1); + if (c2str) parse_color_to_rcolor(scr, c2str, &color2); + int style = (type[0] == 'v' || type[0] == 'V') ? RVerticalGradient : + (type[0] == 'h' || type[0] == 'H') ? RHorizontalGradient : + RDiagonalGradient; + bg = RRenderGradient(width, height, &color1, &color2, style); + } else if ((strcasecmp(type, "mvgradient") == 0 || + strcasecmp(type, "mhgradient") == 0 || + strcasecmp(type, "mdgradient") == 0) && + WMGetPropListItemCount(texpl) >= 3) { + int ncolors = WMGetPropListItemCount(texpl) - 1; + RColor **colors = wmalloc((ncolors + 1) * sizeof(RColor *)); + RColor *cbuf = wmalloc(ncolors * sizeof(RColor)); + int ci; + for (ci = 0; ci < ncolors; ci++) { + const char *cs = WMGetFromPLString(WMGetFromPLArray(texpl, ci + 1)); + cbuf[ci] = (RColor){0, 0, 0, 255}; + if (cs) parse_color_to_rcolor(scr, cs, &cbuf[ci]); + colors[ci] = &cbuf[ci]; + } + colors[ncolors] = NULL; + int style = (type[1] == 'v' || type[1] == 'V') ? RVerticalGradient : + (type[1] == 'h' || type[1] == 'H') ? RHorizontalGradient : + RDiagonalGradient; + bg = RRenderMultiGradient(width, height, colors, style); + wfree(cbuf); + wfree(colors); + } else if ((strcasecmp(type, "pixmap") == 0 || + strcasecmp(type, "spixmap") == 0 || + strcasecmp(type, "mpixmap") == 0 || + strcasecmp(type, "cpixmap") == 0 || + strcasecmp(type, "fpixmap") == 0 || + strcasecmp(type, "tpixmap") == 0) && + WMGetPropListItemCount(texpl) >= 2) { + const char *path = WMGetFromPLString(WMGetFromPLArray(texpl, 1)); + /* Background color (3rd argument, optional) */ + RColor bgcolor = {0, 0, 0, 255}; + if (WMGetPropListItemCount(texpl) >= 3) { + const char *cstr = WMGetFromPLString(WMGetFromPLArray(texpl, 2)); + if (cstr) parse_color_to_rcolor(scr, cstr, &bgcolor); + } + if (path) { + RImage *img = RLoadImage(rc, path, 0); + if (img) { + /* Combine with background color if image has alpha */ + if (img->format == RRGBAFormat) + RCombineImageWithColor(img, &bgcolor); + + char mode = (type[0] >= 'a' && type[0] <= 'z') ? type[0] - 32 : type[0]; + if (mode == 'S') { + /* Scaled: stretch to exact screen size */ + bg = RSmoothScaleImage(img, width, height); + } else if (mode == 'F') { + /* Filled: scale preserving aspect, fill screen (crop) */ + int w, h; + if (img->width * height > img->height * width) { + w = (height * img->width) / img->height; + h = height; + } else { + w = width; + h = (width * img->height) / img->width; + } + RImage *scaled = RSmoothScaleImage(img, w, h); + if (scaled) { + bg = RCreateImage(width, height, 0); + if (bg) { + RClearImage(bg, &bgcolor); + int sx = (scaled->width - width) / 2; + int sy = (scaled->height - height) / 2; + RCopyArea(bg, scaled, sx, sy, width, height, 0, 0); + } + RReleaseImage(scaled); + } + } else if (mode == 'M') { + /* Maximized: scale preserving aspect, fit inside (letterbox) */ + int w, h; + if (img->width * height > img->height * width) { + w = width; + h = (width * img->height) / img->width; + } else { + w = (height * img->width) / img->height; + h = height; + } + RImage *scaled = RSmoothScaleImage(img, w, h); + if (scaled) { + bg = RCreateImage(width, height, 0); + if (bg) { + RClearImage(bg, &bgcolor); + int dx = (width - scaled->width) / 2; + int dy = (height - scaled->height) / 2; + RCombineArea(bg, scaled, 0, 0, scaled->width, scaled->height, dx, dy); + } + RReleaseImage(scaled); + } + } else if (mode == 'C') { + /* Centered: place at center, no scaling */ + bg = RCreateImage(width, height, 0); + if (bg) { + RClearImage(bg, &bgcolor); + int dx = (width - img->width) / 2; + int dy = (height - img->height) / 2; + /* Handle images larger than screen */ + int sx = 0, sy = 0, cw = img->width, ch = img->height; + if (dx < 0) { sx = -dx; dx = 0; cw = width; } + if (dy < 0) { sy = -dy; dy = 0; ch = height; } + RCombineArea(bg, img, sx, sy, cw, ch, dx, dy); + } + } else if (mode == 'T') { + /* Tiled: repeat the image */ + bg = RCreateImage(width, height, 0); + if (bg) { + for (int ty = 0; ty < height; ty += img->height) { + for (int tx = 0; tx < width; tx += img->width) { + int cw = img->width; + int ch = img->height; + if (tx + cw > width) cw = width - tx; + if (ty + ch > height) ch = height - ty; + RCombineArea(bg, img, 0, 0, cw, ch, tx, ty); + } + } + } + } else { + /* Default (plain "pixmap"): scale to fill */ + bg = RSmoothScaleImage(img, width, height); + } + RReleaseImage(img); + } + } + } + } + + WMReleasePropList(texpl); + + if (!bg) { + return; + } + + + /* Convert RImage to pixman_image and set as scene buffer */ + struct wl_bg_buffer *buf = calloc(1, sizeof(*buf)); + if (!buf) { RReleaseImage(bg); return; } + + buf->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL, width * 4); + if (!buf->image) { free(buf); RReleaseImage(bg); return; } + + /* Copy RImage (RGBA or RGB) into ARGB32 pixman buffer */ + uint32_t *dst = (uint32_t *)pixman_image_get_data(buf->image); + unsigned char *src = bg->data; + int npixels = width * height; + if (bg->format == RRGBAFormat) { + for (int i = 0; i < npixels; i++) { + dst[i] = ((uint32_t)src[3] << 24) | ((uint32_t)src[0] << 16) | + ((uint32_t)src[1] << 8) | (uint32_t)src[2]; + src += 4; + } + } else { + for (int i = 0; i < npixels; i++) { + dst[i] = 0xFF000000 | ((uint32_t)src[0] << 16) | + ((uint32_t)src[1] << 8) | (uint32_t)src[2]; + src += 3; + } + } + RReleaseImage(bg); + + wlr_buffer_init(&buf->base, &bg_buffer_impl, width, height); + + /* Hide the solid-color rect */ + if (wl_state.bg_rect) + wlr_scene_node_set_enabled(&wl_state.bg_rect->node, false); + + /* Replace or create the background scene buffer */ + if (wl_state.bg_image) { + wlr_scene_buffer_set_buffer(wl_state.bg_image, &buf->base); + wlr_scene_node_set_enabled(&wl_state.bg_image->node, true); + } else { + wl_state.bg_image = wlr_scene_buffer_create( + wl_state.level_trees[WL_LAYER_BACKGROUND], &buf->base); + } + + /* Release old buffer, keep new one */ + if (wl_state.bg_wlr_buf) + wlr_buffer_drop(wl_state.bg_wlr_buf); + wl_state.bg_wlr_buf = &buf->base; + + /* Schedule a frame to display the new background immediately */ + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); +} +/* ------------------------------------------------------------------ */ + +static void +wl_monitors_query(WScreen *scr, WHeadGeometry **heads_out, int *count_out) +{ + (void)scr; + + if (!wl_state.output) { + *heads_out = NULL; + *count_out = 0; + return; + } + + /* Phase 34: return the single headless output as head 0. */ + WHeadGeometry *heads = wmalloc(sizeof(WHeadGeometry)); + + if (wl_state.output_layout) { + struct wlr_box box = {0}; + wlr_output_layout_get_box(wl_state.output_layout, wl_state.output, &box); + if (box.width > 0 || box.height > 0) { + heads[0].x = box.x; + heads[0].y = box.y; + heads[0].width = box.width; + heads[0].height = box.height; + } else { + heads[0].x = heads[0].y = 0; + heads[0].width = (int)wl_state.output->width; + heads[0].height = (int)wl_state.output->height; + } + } else { + heads[0].x = heads[0].y = 0; + heads[0].width = (int)wl_state.output->width; + heads[0].height = (int)wl_state.output->height; + } + + *heads_out = heads; + *count_out = 1; +} + +static void wl_monitors_select_events(WScreen *scr) { (void)scr; } + +/* ------------------------------------------------------------------ */ +/* Workspace badge */ +/* ------------------------------------------------------------------ */ + +static RImage * +wl_workspace_badge_render(WScreen *scr, WNativeWindow badge_win, + const char *name, int name_len, + int width, int height) +{ + struct wl_frame_buf *fb = frame_buf_find(badge_win); + if (!fb) return NULL; + + /* Resize pixman buffer if needed */ + if (width > 0 && height > 0 && (width != fb->width || height != fb->height)) { + W_UnregisterBacking(fb->id); + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL, 0); + if (!fb->image) return NULL; + fb->width = width; + fb->height = height; + fb->base.width = width; + fb->base.height = height; + W_RegisterBacking(fb->id, fb->image); + } + + /* Fill transparent */ + uint32_t *px = pixman_image_get_data(fb->image); + for (int i = 0; i < width * height; i++) + px[i] = 0x00000000; + + /* Draw white text */ + WMDrawString(scr->wmscreen, (WMDrawable)badge_win, scr->white, + scr->workspace_name_font, 2, 2, name, name_len); + + /* Update scene buffer */ + if (fb->scene_buf) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + + /* Return RGBA RImage copy for fade animation */ + RImage *result = RCreateImage(width, height, True); + if (result) { + uint32_t *src = pixman_image_get_data(fb->image); + for (int i = 0; i < width * height; i++) { + uint32_t p = src[i]; + result->data[i * 4 + 0] = (p >> 16) & 0xFF; /* R */ + result->data[i * 4 + 1] = (p >> 8) & 0xFF; /* G */ + result->data[i * 4 + 2] = p & 0xFF; /* B */ + result->data[i * 4 + 3] = (p >> 24) & 0xFF; /* A */ + } + } + return result; +} + +static RImage * +wl_workspace_badge_get_background(WScreen *scr, int x, int y, int width, int height) +{ + (void)scr; (void)x; (void)y; + /* Return a fully transparent background so the badge floats over the desktop */ + RImage *img = RCreateImage(width, height, True); + if (img) + memset(img->data, 0, width * height * 4); + return img; +} + +static void +wl_workspace_badge_update(WScreen *scr, WNativeWindow badge_win, RImage *img) +{ + (void)scr; + if (!img) return; + struct wl_frame_buf *fb = frame_buf_find(badge_win); + if (!fb || !fb->image) return; + + int w = img->width; + int h = img->height; + if (w > fb->width) w = fb->width; + if (h > fb->height) h = fb->height; + + /* Blit RImage (RGB or RGBA) into pixman ARGB32 buffer */ + uint32_t *dst = pixman_image_get_data(fb->image); + int bpp = img->format == RRGBAFormat ? 4 : 3; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int si = (y * img->width + x) * bpp; + uint8_t r = img->data[si]; + uint8_t g = img->data[si + 1]; + uint8_t b = img->data[si + 2]; + uint8_t a = (bpp == 4) ? img->data[si + 3] : 0xFF; + dst[y * fb->width + x] = ((uint32_t)a << 24) | (r << 16) | (g << 8) | b; + } + } + + if (fb->scene_buf) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + if (wl_state.output) + wlr_output_schedule_frame(wl_state.output); +} + +/* ------------------------------------------------------------------ */ +/* XKB */ +/* ------------------------------------------------------------------ */ + +static WKeyCode +wl_keysym_to_keycode(unsigned long keysym) +{ + /* Phase 77: scan the XKB keymap to find the first keycode that produces + * the given keysym at any level/layout. The XKB keycode == + * evdev_scancode + 8, which equals the X11 keycode — so the values stored + * in wKeyBindings are directly compatible. + * Use the real keyboard's keymap if available; fall back to the default + * keymap created at startup (before any real keyboard arrives). */ + struct xkb_keymap *km = wl_state.xkb_keymap; + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + if (kb && kb->keymap) km = kb->keymap; + } + if (!km) + return 0; + + xkb_keycode_t min_kc = xkb_keymap_min_keycode(km); + xkb_keycode_t max_kc = xkb_keymap_max_keycode(km); + + for (xkb_keycode_t kc = min_kc; kc <= max_kc; kc++) { + xkb_layout_index_t nlayouts = xkb_keymap_num_layouts_for_key(km, kc); + xkb_layout_index_t layout; + for (layout = 0; layout < nlayouts; layout++) { + xkb_level_index_t nlevels = xkb_keymap_num_levels_for_key(km, kc, layout); + xkb_level_index_t level; + for (level = 0; level < nlevels; level++) { + const xkb_keysym_t *syms = NULL; + int n = xkb_keymap_key_get_syms_by_level(km, kc, layout, level, &syms); + int si; + for (si = 0; si < n; si++) { + if (syms[si] == (xkb_keysym_t)keysym) + return (WKeyCode)kc; + } + } + } + } + return 0; +} + +static unsigned long +wl_keysym_from_keycode(unsigned int keycode, unsigned int index) +{ + /* Phase 77: reverse lookup — return the keysym at the given keycode/level. */ + struct xkb_keymap *km = wl_state.xkb_keymap; + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + if (kb && kb->keymap) km = kb->keymap; + } + if (!km) + return 0; + const xkb_keysym_t *syms = NULL; + int n = xkb_keymap_key_get_syms_by_level(km, (xkb_keycode_t)keycode, + 0, (xkb_level_index_t)index, &syms); + if (n > 0 && syms) + return (unsigned long)syms[0]; + return 0; +} + +static void wl_xkb_lock_group(int group) { (void)group; } +static int wl_xkb_get_group(void) { return 0; } + +/* ------------------------------------------------------------------ */ +/* Extensions / cursors / error handlers */ +/* ------------------------------------------------------------------ */ + +static void wl_extensions_init(void) {} +static void wl_cursors_init(void) {} +static WNativeWindow wl_cursor_create_named(const char *name) { (void)name; return WNATIVE_NONE; } +static void +wl_shape_combine_mask(WNativeWindow win, WNativeWindow mask) +{ + struct wl_frame_buf *fb = frame_buf_find(win); + if (!fb) return; + + pixman_image_t *mask_img = (pixman_image_t *)mask; + if (!mask_img) { + /* No mask: clear opaque region (accept all input) */ + wlr_scene_buffer_set_opaque_region(fb->scene_buf, NULL); + return; + } + + /* Build a pixman region from the mask image (non-transparent pixels) */ + int w = pixman_image_get_width(mask_img); + int h = pixman_image_get_height(mask_img); + uint32_t *data = (uint32_t *)pixman_image_get_data(mask_img); + int stride = pixman_image_get_stride(mask_img) / 4; + + pixman_region32_t region; + pixman_region32_init(®ion); + + for (int y = 0; y < h; y++) { + int run_start = -1; + for (int x = 0; x < w; x++) { + uint32_t pixel = data[y * stride + x]; + uint8_t alpha = (pixel >> 24) & 0xFF; + if (alpha > 0) { + if (run_start < 0) run_start = x; + } else { + if (run_start >= 0) { + pixman_region32_union_rect(®ion, ®ion, run_start, y, x - run_start, 1); + run_start = -1; + } + } + } + if (run_start >= 0) { + pixman_region32_union_rect(®ion, ®ion, run_start, y, w - run_start, 1); + } + } + + /* Set opaque region for the scene buffer */ + wlr_scene_buffer_set_opaque_region(fb->scene_buf, ®ion); + + pixman_region32_fini(®ion); +} +static int +wl_catch_xwayland_error(Display *edpy, XErrorEvent *error) +{ + (void)edpy; + /* BadMatch on X_SetInputFocus is benign: the target window may not be + * viewable yet (e.g. during XWayland startup or between map/unmap). + * Silence it so it does not reach Xlib's default handler. */ + if (error->error_code == BadMatch && + error->request_code == X_SetInputFocus) + return 0; + /* BadWindow is also common during window teardown; ignore silently. */ + if (error->error_code == BadWindow) + return 0; + /* Everything else: print via wwarning and continue. */ + char buf[256]; + XGetErrorText(edpy, error->error_code, buf, sizeof(buf)); + wwarning("wl_backend: X error from XWayland: %s (request %d, resource 0x%lx)", + buf, (int)error->request_code, (unsigned long)error->resourceid); + return 0; +} + +/* _XDefaultError interposition ----------------------------------------------- + * libX11's _XDefaultError (called when no user error handler is installed, or + * when the user handler IS _XDefaultError itself) calls exit(1) after printing. + * In the XWayland bridge the error handler can be temporarily displaced by + * WINGs/wrlib save-restore code (selection.c, dragcommon.c, texture.c) between + * the time a request is sent and the time its BadMatch reply is received. + * + * Because libX11 calls _XDefaultError via PLT (confirmed with objdump — it is + * a GLOBAL DEFAULT symbol with no version tag), our definition in the wmaker + * binary takes precedence over the library version via ELF symbol interposition. + * This is the last-resort safety net: even if _XErrorFunction is reset to + * _XDefaultError, we never exit on a recoverable X protocol error while the + * XWayland connection is open. + */ +int _XDefaultError(Display *dpy, XErrorEvent *event) +{ + /* Only intercept errors on our XWayland Xlib connection. */ + if (wl_state.x_display != NULL) { + char buf[256] = "unknown"; + XGetErrorText(dpy, event->error_code, buf, sizeof(buf)); + wwarning("wl_backend: X error reached _XDefaultError: " + "%s (request %d, resource 0x%lx) — suppressing exit", + buf, (int)event->request_code, + (unsigned long)event->resourceid); + return 0; + } + /* Pure X11 mode (no XWayland connection): behave as libX11 normally would. + * x11_backend installs x11_catch_error so _XDefaultError is only reached + * for truly unexpected errors — exiting here is the right thing. */ + fprintf(stderr, "X Error of failed request: error_code=%d request_code=%d\n", + (int)event->error_code, (int)event->request_code); + exit(1); +} + +static int +wl_catch_xwayland_io_error(Display *edpy) +{ + (void)edpy; + /* XIO error = the X server closed the connection (e.g. XWayland crashed or + * its XWM connection died and it shut down). The default handler calls + * exit(1), which then makes XWayland print "(EE) failed to read Wayland + * events: Broken pipe" — our exit closes the Wayland socket that XWayland's + * client side reads. Intercept it so we can shut down gracefully. */ + wwarning("wl_backend: X IO error — XWayland connection lost, terminating"); + /* Trigger graceful shutdown via the event loop. */ + wl_event_loop_terminate(); + /* XIO error handlers must not return; call exit directly. */ + exit(0); +} + +static void +wl_register_error_handlers(void) +{ + /* Install a permissive X error handler for the XWayland connection. + * The _XDefaultError interposition above provides a last-resort safety + * net if any code temporarily displaces this handler. */ + XSetErrorHandler(wl_catch_xwayland_error); + /* Install an XIO error handler so we get a proper log message instead of + * the silent exit(1) from Xlib's default handler when XWayland disconnects. */ + XSetIOErrorHandler(wl_catch_xwayland_io_error); +} + +static void +wl_texture_alloc_color(WScreen *scr, WMColorRGB *color) +{ + (void) scr; (void) color; +} + +static void +wl_texture_free_colors(WScreen *scr, unsigned long *pixels, int count) +{ + (void) scr; (void) pixels; (void) count; +} + +static GC +wl_texture_create_gc(WScreen *scr, unsigned long pixel) +{ + (void) scr; (void) pixel; + return NULL; +} + +static void +wl_texture_free_gc(GC gc) +{ + (void) gc; +} + +/* ------------------------------------------------------------------ */ +/* Phase 80: no-op stubs for X11-only operations */ +/* ------------------------------------------------------------------ */ + +static void wl_noop_frame_alloc_border_pixel(unsigned long colormap, + const char *color_name, unsigned long **pixel_out) +{ (void)colormap; (void)color_name; *pixel_out = NULL; } + +static void wl_noop_frame_update_texture(struct WFrameWindow *fwin) { (void)fwin; } + +static void wl_frame_paint_button(WCoreWindow *button, WTexture *texture, + unsigned long color, WPixmap *image, int pushed) +{ + (void)texture; (void)image; + WFrameWindow *fwin = (WFrameWindow *)button->descriptor.parent; + if (!fwin || !fwin->core) + return; + + struct wl_frame_buf *btn_fb = frame_buf_find(button->window); + if (!btn_fb || !btn_fb->image) + return; + + int bw = btn_fb->width; + int bh = btn_fb->height; + + /* Determine which icon to use */ + RImage *icon = NULL; + if (button == fwin->right_button) { + if (!wl_state.btn_close_img) + wl_state.btn_close_img = RGetImageFromXPMData( + button->screen_ptr->rcontext, PRED_CLOSE_XPM); + icon = wl_state.btn_close_img; + } else if (button == fwin->left_button) { + if (!wl_state.btn_iconify_img) + wl_state.btn_iconify_img = RGetImageFromXPMData( + button->screen_ptr->rcontext, PRED_ICONIFY_XPM); + icon = wl_state.btn_iconify_img; + } + if (!icon) + return; + + /* Get titlebar background color */ + struct wl_frame_buf *tb_fb = fwin->titlebar ? frame_buf_find(fwin->titlebar->window) : NULL; + uint32_t bg_pixel = 0xFF333333; + if (tb_fb && tb_fb->image) { + uint32_t *tb_data = pixman_image_get_data(tb_fb->image); + int tb_stride = pixman_image_get_stride(tb_fb->image) / 4; + int sample_y = btn_fb->rel_y + bh / 2; + int sample_x = btn_fb->rel_x + bw / 2; + if (sample_y >= 0 && sample_y < tb_fb->height && + sample_x >= 0 && sample_x < tb_fb->width) + bg_pixel = tb_data[sample_y * tb_stride + sample_x]; + } + + /* Foreground = title text color; when pushed, swap fg/bg */ + uint32_t fg_pixel = 0xFF000000 | ((color >> 16 & 0xFF) << 16) | + ((color >> 8 & 0xFF) << 8) | (color & 0xFF); + uint32_t fill_bg = pushed ? fg_pixel : bg_pixel; + uint32_t fill_fg = pushed ? bg_pixel : fg_pixel; + + uint32_t *dst = pixman_image_get_data(btn_fb->image); + int dst_stride = pixman_image_get_stride(btn_fb->image) / 4; + + /* Fill background */ + for (int y = 0; y < bh; y++) + for (int x = 0; x < bw; x++) + dst[y * dst_stride + x] = fill_bg; + + /* Draw border bevel derived from bg color (raised normal, sunken pushed) */ + unsigned char bg_r = (bg_pixel >> 16) & 0xFF; + unsigned char bg_g = (bg_pixel >> 8) & 0xFF; + unsigned char bg_b = bg_pixel & 0xFF; + uint32_t hi_c = 0xFF000000 | ((uint32_t)(bg_r + (255-bg_r)/3) << 16) | + ((uint32_t)(bg_g + (255-bg_g)/3) << 8) | (bg_b + (255-bg_b)/3); + uint32_t lo_c = 0xFF000000 | ((uint32_t)(bg_r * 2/3) << 16) | + ((uint32_t)(bg_g * 2/3) << 8) | (bg_b * 2/3); + uint32_t hi = pushed ? lo_c : hi_c; + uint32_t lo = pushed ? hi_c : lo_c; + for (int x = 0; x < bw; x++) { dst[x] = hi; dst[(bh-1) * dst_stride + x] = lo; } + for (int y = 0; y < bh; y++) { dst[y * dst_stride] = hi; dst[y * dst_stride + bw - 1] = lo; } + + /* Blit icon centered, using fill_fg color for opaque pixels */ + int ix = (bw - icon->width) / 2 + (pushed ? 1 : 0); + int iy = (bh - icon->height) / 2 + (pushed ? 1 : 0); + + for (int y = 0; y < icon->height; y++) { + int dy = iy + y; + if (dy < 0 || dy >= bh) continue; + uint32_t *dst_row = dst + dy * dst_stride; + for (int x = 0; x < icon->width; x++) { + int dx = ix + x; + if (dx < 0 || dx >= bw) continue; + unsigned char a; + if (icon->format == RRGBAFormat) + a = icon->data[(y * icon->width + x) * 4 + 3]; + else + a = 255; + if (a == 0) continue; + dst_row[dx] = fill_fg; + } + } + + /* Enable and commit the button's own scene_buf */ + wlr_scene_buffer_set_buffer_with_damage(btn_fb->scene_buf, &btn_fb->base, NULL); + wlr_scene_node_set_enabled(&btn_fb->scene_buf->node, true); + wlr_scene_node_raise_to_top(&btn_fb->scene_buf->node); +} + +static void wl_noop_netwm_init(WScreen *scr) { (void)scr; } +static void wl_noop_netwm_cleanup(WScreen *scr) { (void)scr; } +static void wl_noop_netwm_update_desktop(WScreen *scr) { (void)scr; } +static int wl_noop_netwm_get_current_desktop(WScreen *scr) { (void)scr; return -1; } +static void wl_netwm_update_opacity(WWindow *wwin) +{ + if (!wwin->frame) + return; + + struct wl_frame_buf *fb = frame_buf_find(wwin->frame->core->window); + if (!fb || !fb->scene_buf) + return; + + float alpha = 1.0f; + if (wl_state.x_display && wwin->client_win) { + WMNativeAtom type; + int format; + unsigned long items, rest; + unsigned char *prop = NULL; + WMNativeAtom opacity_atom = XInternAtom(wl_state.x_display, + "_NET_WM_WINDOW_OPACITY", False); + if (XGetWindowProperty(wl_state.x_display, (WNativeWindow)wwin->client_win, + opacity_atom, 0L, 1L, False, AnyPropertyType, + &type, &format, &items, &rest, &prop) == Success + && format == 32 && items == 1 && prop) { + unsigned long val = *(unsigned long *)prop; + alpha = (float)val / (float)0xFFFFFFFF; + } + if (prop) + XFree(prop); + } + + wlr_scene_buffer_set_opacity(fb->scene_buf, alpha); + + /* Apply to all child frame_bufs (titlebar buttons, resizebar) */ + struct wl_frame_buf *child; + wl_list_for_each(child, &wl_state.frame_list, link) { + if (child->parent_id == fb->id && child->scene_buf) + wlr_scene_buffer_set_opacity(child->scene_buf, alpha); + } + + /* Apply to the client surface */ + struct wl_toplevel_view *v; + wl_list_for_each(v, &wl_state.toplevel_list, link) { + if (v->frame_id == fb->id && v->scene_node) { + struct wlr_scene_node *node; + wl_list_for_each(node, &v->scene_node->children, link) { + if (node->type == WLR_SCENE_NODE_BUFFER) + wlr_scene_buffer_set_opacity( + wlr_scene_buffer_from_node(node), alpha); + } + } + } +} +static void wl_noop_netwm_update_actions(WWindow *wwin, int del) { (void)wwin; (void)del; } +static void wl_noop_netwm_check_client_hints(WWindow *wwin, int *layer, int *workspace) +{ (void)wwin; (void)layer; (void)workspace; } +static void wl_noop_netwm_check_initial_state(WWindow *wwin) { (void)wwin; } +static int wl_noop_netwm_get_pid(WNativeWindow window) { (void)window; return 0; } +static void wl_noop_netwm_cleanup_frame_extents(WWindow *wwin) { (void)wwin; } +static void wl_noop_netwm_set_frame_extents(WWindow *wwin) { (void)wwin; } + +static void wl_noop_client_restore(WWindow *wwin) { (void)wwin; } +static void wl_client_set_state(WWindow *wwin, int state, WNativeWindow icon_win) +{ + (void)icon_win; + struct wl_toplevel_view *v = wl_find_view_by_id(wwin->client_win); + if (!v) + return; + switch (state) { + case 3: /* WM_STATE_ICONIC */ + if (v->scene_node) + wlr_scene_node_set_enabled(&v->scene_node->node, false); + break; + case 1: /* WM_STATE_NORMAL */ + if (v->scene_node) + wlr_scene_node_set_enabled(&v->scene_node->node, true); + break; + } +} + +static void wl_client_set_maximized(WNativeWindow win, int maximized) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return; + if (v->xdg_surface && v->xdg_surface->toplevel) + wlr_xdg_toplevel_set_maximized(v->xdg_surface->toplevel, maximized); +} + +static void wl_client_send_protocol(WWindow *wwin, unsigned long protocol, + unsigned long timestamp) +{ + (void)protocol; (void)timestamp; + wm_backend->client_close(wwin->client_win); +} + +static void wl_client_kill_impl(WWindow *wwin) +{ + wm_backend->client_close(wwin->client_win); +} + +static void wl_noop_client_get_colormap_windows(WWindow *wwin) { (void)wwin; } + +static void wl_noop_eat_expose(void) { } + +static void wl_noop_move_window(WNativeWindow win, int from_x, int from_y, + int to_x, int to_y) +{ (void)win; (void)from_x; (void)from_y; (void)to_x; (void)to_y; } + +static void wl_noop_slide_windows(WNativeWindow *wins, int n, + int from_x, int from_y, int to_x, int to_y) +{ (void)wins; (void)n; (void)from_x; (void)from_y; (void)to_x; (void)to_y; } + +static char *wl_noop_get_text_selection(WScreen *scr, unsigned long selection) +{ (void)scr; (void)selection; return NULL; } + +static char *wl_noop_get_shortcut_string(WShortKey key) +{ (void)key; return NULL; } + +static char *wl_get_command_for_window(WNativeWindow win, int elements) +{ + char **argv, *command = NULL; + int argc; + if (!wm_backend->icccm_get_command(win, &argv, &argc)) + return NULL; + if (argc > 0 && argv != NULL) { + if (elements == 0) + elements = argc; + command = wtokenjoin(argv, WMIN(argc, elements)); + if (command[0] == 0) { + wfree(command); + command = NULL; + } + } + wm_backend->icccm_free_command(argv); + return command; +} + +static void wl_noop_icon_update_pixmap(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_with_damage(fb->scene_buf, &fb->base, NULL); + 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_appicon_show(WApplication *wapp) { paint_app_icon(wapp); } +static void wl_appicon_paint(WAppIcon *aicon) +{ + WIcon *icon = aicon->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; + int size = wPreferences.icon_size; + + /* Compose the tile image */ + RImage *tile = NULL; + switch (icon->tile_type) { + case TILE_CLIP: tile = scr->clip_tile ? RCloneImage(scr->clip_tile) : NULL; break; + case TILE_DRAWER: tile = scr->drawer_tile ? RCloneImage(scr->drawer_tile) : NULL; break; + default: tile = scr->icon_tile ? RCloneImage(scr->icon_tile) : NULL; break; + } + if (!tile) { + /* Fallback: solid grey */ + tile = RCreateImage(size, size, 1); + if (!tile) return; + RColor grey = {0xAE, 0xAA, 0xAE, 0xFF}; + RClearImage(tile, &grey); + } + + /* Composite the app image on top */ + if (icon->file_image) { + RImage *img = icon->file_image; + int theight = icon->show_title ? WMFontHeight(scr->icon_title_font) : 0; + unsigned w = (img->width > size) ? size : img->width; + unsigned h = (img->height + theight > size) ? size - theight : 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) → pixman ARGB32 buffer */ + uint32_t *dst = (uint32_t *)pixman_image_get_data(fb->image); + int stride = pixman_image_get_stride(fb->image) / 4; + unsigned char *src = 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 = src[si], g = src[si+1], b = src[si+2]; + unsigned char a = has_alpha ? src[si+3] : 0xFF; + dst[row * stride + col] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + + /* Draw title text */ + if (icon->show_title && icon->icon_name && scr->icon_title_font) { + WMDrawable d = (WMDrawable)(uintptr_t)fb->image; + int fh = WMFontHeight(scr->icon_title_font); + int tw = WMWidthOfString(scr->icon_title_font, icon->icon_name, + strlen(icon->icon_name)); + int tx = (size - tw) / 2; + if (tx < 2) tx = 2; + int ty = 0; /* top of text area */ + WMColor *color = scr->icon_title_color; + if (color) + WMDrawString(scr->wmscreen, d, color, scr->icon_title_font, + tx, ty, icon->icon_name, strlen(icon->icon_name)); + } + + RReleaseImage(tile); + + /* Commit to scene graph */ + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); +} +static void wl_noop_restore_desktop(WScreen *scr) { (void)scr; } +static void wl_noop_wipe_desktop(WScreen *scr) { (void)scr; } + +static int wl_get_color_for_colormap(unsigned long colormap, + const char *color_name, WMColorRGB *color) +{ + unsigned int r, g, b; + (void)colormap; + + if (!color_name || !color) + return 0; + + /* Parse #RGB, #RRGGBB, #RRRRGGGGBBBB */ + if (color_name[0] == '#') { + int len = strlen(color_name + 1); + if (len == 6) { + if (sscanf(color_name, "#%2x%2x%2x", &r, &g, &b) == 3) { + color->red = r << 8 | r; + color->green = g << 8 | g; + color->blue = b << 8 | b; + color->pixel = (0xFF << 24) | (r << 16) | (g << 8) | b; + return 1; + } + } else if (len == 12) { + if (sscanf(color_name, "#%4hx%4hx%4hx", + &color->red, &color->green, &color->blue) == 3) { + color->pixel = (0xFF << 24) | + ((color->red >> 8) << 16) | + ((color->green >> 8) << 8) | + (color->blue >> 8); + return 1; + } + } else if (len == 3) { + if (sscanf(color_name, "#%1x%1x%1x", &r, &g, &b) == 3) { + color->red = r << 12 | r << 8 | r << 4 | r; + color->green = g << 12 | g << 8 | g << 4 | g; + color->blue = b << 12 | b << 8 | b << 4 | b; + color->pixel = (0xFF << 24) | (r*17 << 16) | (g*17 << 8) | b*17; + return 1; + } + } + } + + /* X11 rgb:RR/GG/BB or rgb:RRRR/GGGG/BBBB format */ + if (strncasecmp(color_name, "rgb:", 4) == 0) { + unsigned int rv, gv, bv; + const char *p = color_name + 4; + int n = sscanf(p, "%x/%x/%x", &rv, &gv, &bv); + if (n == 3) { + /* Determine component width from first component */ + const char *slash = strchr(p, '/'); + int width = slash ? (int)(slash - p) : 2; + if (width == 1) { + color->red = rv << 12 | rv << 8 | rv << 4 | rv; + color->green = gv << 12 | gv << 8 | gv << 4 | gv; + color->blue = bv << 12 | bv << 8 | bv << 4 | bv; + } else if (width == 2) { + color->red = rv << 8 | rv; + color->green = gv << 8 | gv; + color->blue = bv << 8 | bv; + } else { + color->red = rv; + color->green = gv; + color->blue = bv; + } + color->pixel = (0xFF << 24) | + ((color->red >> 8) << 16) | + ((color->green >> 8) << 8) | + (color->blue >> 8); + return 1; + } + } + + /* Named colors — common ones */ + struct { const char *name; unsigned short r, g, b; } named[] = { + {"black", 0x0000, 0x0000, 0x0000}, + {"white", 0xFFFF, 0xFFFF, 0xFFFF}, + {"red", 0xFFFF, 0x0000, 0x0000}, + {"green", 0x0000, 0xFFFF, 0x0000}, + {"blue", 0x0000, 0x0000, 0xFFFF}, + {"gray", 0xBEBE, 0xBEBE, 0xBEBE}, + {"grey", 0xBEBE, 0xBEBE, 0xBEBE}, + {"darkgray",0xA9A9, 0xA9A9, 0xA9A9}, + {"darkgrey",0xA9A9, 0xA9A9, 0xA9A9}, + {"lightgray",0xD3D3, 0xD3D3, 0xD3D3}, + {"lightgrey",0xD3D3, 0xD3D3, 0xD3D3}, + {NULL, 0, 0, 0} + }; + for (int i = 0; named[i].name; i++) { + if (strcasecmp(color_name, named[i].name) == 0) { + color->red = named[i].r; + color->green = named[i].g; + color->blue = named[i].b; + color->pixel = (0xFF << 24) | + ((color->red >> 8) << 16) | + ((color->green >> 8) << 8) | + (color->blue >> 8); + return 1; + } + } + + /* grayNN / greyNN (0-100) */ + if ((strncasecmp(color_name, "gray", 4) == 0 || strncasecmp(color_name, "grey", 4) == 0) + && color_name[4] >= '0' && color_name[4] <= '9') { + int pct = atoi(color_name + 4); + if (pct >= 0 && pct <= 100) { + unsigned int v = (pct * 255 + 50) / 100; + color->red = color->green = color->blue = v << 8 | v; + color->pixel = (0xFF << 24) | (v << 16) | (v << 8) | v; + return 1; + } + } + + /* Look up in X11 rgb.txt database */ + { + static const char *rgb_paths[] = { + "/usr/share/X11/rgb.txt", + "/usr/lib/X11/rgb.txt", + "/etc/X11/rgb.txt", + NULL + }; + FILE *f = NULL; + for (int i = 0; rgb_paths[i] && !f; i++) + f = fopen(rgb_paths[i], "r"); + if (f) { + char line[256]; + while (fgets(line, sizeof(line), f)) { + unsigned int rv, gv, bv; + char name[128]; + if (line[0] == '!' || line[0] == '#') + continue; + if (sscanf(line, "%u %u %u %127[^\n]", &rv, &gv, &bv, name) == 4) { + /* Trim leading whitespace/tabs from name */ + char *n = name; + while (*n == ' ' || *n == '\t') n++; + if (strcasecmp(n, color_name) == 0) { + color->red = rv << 8 | rv; + color->green = gv << 8 | gv; + color->blue = bv << 8 | bv; + color->pixel = (0xFF << 24) | (rv << 16) | (gv << 8) | bv; + fclose(f); + return 1; + } + } + } + fclose(f); + } + } + + wwarning(_("could not parse color \"%s\" (Wayland)"), color_name); + return 0; +} + +static void wl_noop_free_color(WScreen *scr, unsigned long pixel) +{ (void)scr; (void)pixel; } + +static void wl_noop_colormap_install(WScreen *scr, WWindow *wwin) +{ (void)scr; (void)wwin; } + +static void wl_noop_xdnd_init(void) { } + +static int wl_noop_motif_get_hints(WNativeWindow window, MWMHints *hints_out) +{ (void)window; (void)hints_out; return 0; } + +static void wl_noop_appicon_bounce(WAppIcon *aicon, void *data) +{ (void)aicon; (void)data; } + +static void wl_noop_manage_all_windows(WScreen *scr, int crash_recovery) +{ (void)scr; (void)crash_recovery; } + +static void wl_event_handle_signal_exit(void) +{ + wm_backend->event_loop_terminate(); +} + +static void wl_noop_icon_select(WIcon *icon) { (void)icon; } +static void wl_noop_icon_change_title(WIcon *icon, WWindow *wwin) { (void)icon; (void)wwin; } +static void wl_clip_icon_paint(WAppIcon *aicon) +{ + if (!aicon || !aicon->icon || !aicon->icon->core) + return; + + WScreen *scr = aicon->icon->core->screen_ptr; + if (!scr->workspaces || !scr->workspaces[scr->current_workspace]) + return; + + /* Paint base tile + icon image */ + wl_appicon_paint(aicon); + + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)aicon->icon->core->window); + if (!fb || !fb->image) + return; + + WWorkspace *workspace = scr->workspaces[scr->current_workspace]; + /* Use aicon->dock to read collapsed state — workspace->clip may be a + * stale freed pointer during wWorkspaceRestoreState (the old clip is + * destroyed before scr->workspaces[i]->clip is reassigned, and we + * are called from wDockCreate inside that gap). */ + int collapsed = aicon->dock ? aicon->dock->collapsed : 0; + WMColor *color; + if (wPreferences.flags.noclip || !collapsed) + color = scr->clip_title_color[CLIP_NORMAL]; + else + color = scr->clip_title_color[CLIP_COLLAPSED]; + if (!color || !scr->clip_title_font) + return; + + int size = wPreferences.icon_size; + WMDrawable d = (WMDrawable)(uintptr_t)fb->image; + + /* Draw workspace number at top center */ + char ws_number[20]; + snprintf(ws_number, sizeof ws_number, "%u", scr->current_workspace + 1); + int nlength = strlen(ws_number); + int tx = (size / 2 - WMWidthOfString(scr->clip_title_font, ws_number, nlength)) / 2; + WMDrawString(scr->wmscreen, d, color, scr->clip_title_font, tx, 2, ws_number, nlength); + + /* Draw workspace name at bottom */ + if (wPreferences.show_clip_title) { + int length = strlen(workspace->name); + int ty = size - WMFontHeight(scr->clip_title_font) - 3; + tx = CLIP_BUTTON_SIZE * size / 64; + WMDrawString(scr->wmscreen, d, color, scr->clip_title_font, tx, ty, workspace->name, length); + } + + /* Commit updated buffer */ + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} +static void wl_drawer_icon_paint(WAppIcon *aicon) +{ + if (!aicon || !aicon->icon || !aicon->icon->core) + return; + + wl_appicon_paint(aicon); + + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)aicon->icon->core->window); + if (!fb || !fb->image) + return; + + WScreen *scr = aicon->icon->core->screen_ptr; + WMColor *color; + if (!aicon->dock->collapsed) + color = scr->clip_title_color[CLIP_NORMAL]; + else + color = scr->clip_title_color[CLIP_COLLAPSED]; + if (!color) + return; + + int size = wPreferences.icon_size; + uint32_t pixel = ((uint32_t)0xFF << 24) | + ((uint32_t)WMRedComponentOfColor(color) >> 8 << 16) | + ((uint32_t)WMGreenComponentOfColor(color) >> 8 << 8) | + ((uint32_t)WMBlueComponentOfColor(color) >> 8); + uint32_t *data = (uint32_t *)pixman_image_get_data(fb->image); + int stride = pixman_image_get_stride(fb->image) / 4; + + /* Draw directional arrow (triangle) */ + int cx, cy = size / 2; + if (aicon->dock->on_right_side) { + /* Arrow pointing left: tip at x=5, base at x=10 */ + for (int dy = -5; dy <= 5; dy++) { + int row = cy + dy; + if (row < 0 || row >= size) continue; + int width = 5 - abs(dy); + for (int dx = 0; dx <= width; dx++) { + cx = 10 - dx; + if (cx >= 0 && cx < size) + data[row * stride + cx] = pixel; + } + } + } else { + /* Arrow pointing right: tip at x=size-6, base at x=size-11 */ + for (int dy = -5; dy <= 5; dy++) { + int row = cy + dy; + if (row < 0 || row >= size) continue; + int width = 5 - abs(dy); + for (int dx = 0; dx <= width; dx++) { + cx = size - 11 + dx; + if (cx >= 0 && cx < size) + data[row * stride + cx] = pixel; + } + } + } + + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} +static void wl_balloon_init(WScreen *scr) +{ + scr->balloon = wmalloc(sizeof(WBalloon)); + memset(scr->balloon, 0, sizeof(WBalloon)); + + /* Create a frame_buf in the overlay layer for balloon tooltips */ + if (wl_state.scene) { + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + int w = 1, h = 1; + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + fb->id = wl_state.next_synthetic_id++; + fb->width = w; + fb->height = h; + + struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + fb->tree = wlr_scene_tree_create(parent); + if (fb->tree) { + fb_attach_tree_listener(fb); + wlr_scene_node_set_position(&fb->tree->node, 0, 0); + fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0); + } + wlr_scene_node_set_enabled(&fb->tree->node, false); + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + W_RegisterBacking(fb->id, fb->image); + scr->balloon->window = (WNativeWindow)fb->id; + } +} +static void wl_noop_do_kaboom(WScreen *scr, WNativeWindow win, int x, int y) +{ (void)scr; (void)win; (void)x; (void)y; } +static void wl_menu_paint_entry(WMenu *menu, int index, int selected) +{ + if (!menu || !menu->menu || !menu->menu->window) + return; + + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)menu->menu->window); + if (!fb || !fb->image) + return; + + /* Guard against painting before wMenuRealize: entry_height is 0, + * fb->image is the initial 1x10 placeholder, and the bevel-line + * computations below would write outside the pixman buffer. + * wMenuSetEnabled() in particular triggers paints on the brother + * during updateOptionsMenu before the menu is realized. */ + if (menu->entry_height <= 0 || fb->width <= 1 || fb->height <= 10) + return; + + WScreen *scr = menu->frame->screen_ptr; + WMenuEntry *entry = menu->entries[index]; + int y = menu->entry_height * index; + int h = menu->entry_height; + int w = fb->width; + uint32_t *dst = (uint32_t *)pixman_image_get_data(fb->image); + int stride = pixman_image_get_stride(fb->image) / 4; + + /* Background color from appearance preferences */ + uint32_t bg; + if (selected) { + WMColor *sc = scr->select_color; + bg = 0xFF000000 | ((uint32_t)WMRedComponentOfColor(sc) >> 8 << 16) + | ((uint32_t)WMGreenComponentOfColor(sc) >> 8 << 8) + | ((uint32_t)WMBlueComponentOfColor(sc) >> 8); + } else { + WTexSolid *ts = (WTexSolid *)scr->menu_item_texture; + if (ts) + bg = 0xFF000000 | ((uint32_t)ts->normal.red >> 8 << 16) + | ((uint32_t)ts->normal.green >> 8 << 8) + | ((uint32_t)ts->normal.blue >> 8); + else + bg = 0xFFAEAAAE; + } + + for (int row = y; row < y + h && row < fb->height; row++) + for (int col = 0; col < w; col++) + dst[row * stride + col] = bg; + + /* Draw text using WINGs font backend (renders via PangoFT2 → pixman) */ + WMColor *color = selected ? scr->select_text_color : + (entry->flags.enabled ? scr->mtext_color : scr->dtext_color); + WMFont *font = scr->menu_entry_font; + WMDrawable d = (WMDrawable)(uintptr_t)fb->image; + + if (entry->text && font && color) { + int text_y = y + 3; + WMDrawString(scr->wmscreen, d, color, font, + 8, text_y, entry->text, strlen(entry->text)); + } + + if (entry->rtext && entry->cascade < 0 && font && color) { + int tw = WMWidthOfString(font, entry->rtext, strlen(entry->rtext)); + int text_y = y + 3; + WMDrawString(scr->wmscreen, d, color, font, + w - 8 - tw, text_y, entry->rtext, strlen(entry->rtext)); + } + + /* Draw cascade arrow for submenus (3D right-pointing triangle matching X11) */ + if (entry->cascade >= 0) { + WTexSolid *aux = (WTexSolid *)scr->menu_item_texture; + uint32_t dim_c = 0xFF606060; + uint32_t light_c = 0xFFE0E0E0; + uint32_t dark_c = 0xFF303030; + if (aux) { + dim_c = 0xFF000000 | ((uint32_t)aux->dim.red >> 8 << 16) + | ((uint32_t)aux->dim.green >> 8 << 8) + | ((uint32_t)aux->dim.blue >> 8); + light_c = 0xFF000000 | ((uint32_t)aux->light.red >> 8 << 16) + | ((uint32_t)aux->light.green >> 8 << 8) + | ((uint32_t)aux->light.blue >> 8); + dark_c = 0xFF000000 | ((uint32_t)aux->dark.red >> 8 << 16) + | ((uint32_t)aux->dark.green >> 8 << 8) + | ((uint32_t)aux->dark.blue >> 8); + } + /* Top edge: dim line from (w-11, y+6) to (w-6, y+h/2-1) */ + int x0 = w - 11, y0 = y + 6, x1 = w - 6, y1 = y + h / 2 - 1; + for (int i = 0; i <= 5; i++) { + int lx = x0 + i * (x1 - x0) / 5; + int ly = y0 + i * (y1 - y0) / 5; + if (lx >= 0 && lx < (int)fb->width && ly >= 0 && ly < fb->height) + dst[ly * stride + lx] = dim_c; + } + /* Bottom edge: light line from (w-11, y+h-8) to (w-6, y+h/2-1) */ + y0 = y + h - 8; + for (int i = 0; i <= 5; i++) { + int lx = x0 + i * (x1 - x0) / 5; + int ly = y0 + i * (y1 - y0) / 5; + if (lx >= 0 && lx < (int)fb->width && ly >= 0 && ly < fb->height) + dst[ly * stride + lx] = light_c; + } + /* Left edge: dark vertical line at w-12 from y+6 to y+h-8 */ + int lx = w - 12; + if (lx >= 0 && lx < (int)fb->width) + for (int ly = y + 6; ly <= y + h - 8 && ly < fb->height; ly++) + if (ly >= 0) + dst[ly * stride + lx] = dark_c; + } + + /* Bevel lines (separators between entries) */ + uint32_t light = 0xFFDDDDDD; + uint32_t dark = 0xFF555555; + + /* Left edge - light */ + for (int row = y; row < y + h && row < fb->height; row++) + dst[row * stride] = light; + /* Right edge - dark */ + for (int row = y; row < y + h && row < fb->height; row++) + if (w - 1 < fb->width) + dst[row * stride + (w - 1)] = dark; + /* Top line - light (skip first entry) */ + if (index > 0 && y < fb->height) + for (int col = 0; col < w && col < fb->width; col++) + dst[y * stride + col] = light; + /* Bottom line - dark (skip last entry) */ + if (index < menu->entry_no - 1 && y + h - 1 < fb->height) + for (int col = 0; col < w && col < fb->width; col++) + dst[(y + h - 1) * stride + col] = dark; + + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +static void wl_menu_draw_frame(WScreen *scr, WNativeWindow win, int y, int w, int h, int type) +{ + struct wl_frame_buf *fb = frame_buf_find(win); + if (!fb || !fb->image) + return; + + uint32_t *dst = (uint32_t *)pixman_image_get_data(fb->image); + int stride = pixman_image_get_stride(fb->image) / 4; + uint32_t dark = 0xFF555555; + uint32_t light = 0xFFDDDDDD; + + /* Right edge */ + for (int row = y; row < y + h && row < fb->height; row++) + if (w - 1 < fb->width) + dst[row * stride + (w - 1)] = dark; + /* Bottom edge */ + if (y + h - 1 < fb->height) + for (int col = 0; col < w && col < fb->width; col++) + dst[(y + h - 1) * stride + col] = dark; + /* Left edge */ + for (int row = y; row < y + h && row < fb->height; row++) + dst[row * stride] = light; + /* Top edge */ + if (y < fb->height) + for (int col = 0; col < w && col < fb->width; col++) + dst[y * stride + col] = light; + + (void)scr; (void)type; + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +static void wl_noop_draw_bevel(WNativeWindow d, unsigned int width, unsigned int height, + WTexSolid *texture, int relief) +{ (void)d; (void)width; (void)height; (void)texture; (void)relief; } + +static void wl_noop_appmenu_send(WNativeWindow window, int what, int tag) +{ (void)window; (void)what; (void)tag; } + +static const char *wl_display_get_name(const char *name) +{ + /* For X11 clients launched under XWayland, return the XWayland display. + * For native Wayland clients, DON'T set DISPLAY (they use WAYLAND_DISPLAY). + * Fallback: return the Wayland display name for backward compatibility. */ + (void)name; + if (wl_state.xwayland && wl_state.xwayland->display_name) + return wl_state.xwayland->display_name; + return "wayland-0"; +} + +static void wl_noop_switchpanel_send_enter(WNativeWindow info_win, WNativeWindow window) +{ (void)info_win; (void)window; } + +static void wl_noop_format_x_error(void *error, char *buf, int size) +{ (void)error; if (size > 0) buf[0] = '\0'; } + +static WPixmap *wl_noop_pixmap_create_from_xbm(WScreen *scr, char *data, char *mask, + int width, int height, unsigned long fg, unsigned long bg) +{ (void)scr; (void)data; (void)mask; (void)width; (void)height; (void)fg; (void)bg; return NULL; } + +static WPixmap *wl_noop_pixmap_create(unsigned long image, unsigned long mask) +{ (void)image; (void)mask; return NULL; } + +static unsigned long wl_keysym_from_name(const char *name) +{ + return (unsigned long)xkb_keysym_from_name(name, XKB_KEYSYM_NO_FLAGS); +} + +static void wl_frame_paint_resizebar(WFrameWindow *fwin) +{ + /* Resizebar is rendered into the parent frame buffer by wl_frame_paint. + * Don't commit a separate buffer here — it would overlay the texture. */ + (void)fwin; +} + +static void wl_frame_paint_titlebar(WFrameWindow *fwin, int state) +{ + if (!fwin->titlebar || !fwin->titlebar->window) + return; + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)fwin->titlebar->window); + if (!fb || !fb->image) + return; + + WScreen *scr = fwin->screen_ptr; + uint32_t *dst = (uint32_t *)pixman_image_get_data(fb->image); + int stride = pixman_image_get_stride(fb->image) / 4; + + /* Background: use texture color or default */ + uint32_t bg; + if (fwin->title_texture[state] && + fwin->title_texture[state]->any.type == WTEX_SOLID) { + unsigned long px = fwin->title_texture[state]->solid.normal.pixel; + bg = 0xFF000000 | (px & 0xFFFFFF); + } else { + bg = (state == 0) ? 0xFF4A6984 : 0xFF808080; /* focused blue / unfocused grey */ + } + + for (int row = 0; row < fb->height; row++) + for (int col = 0; col < fb->width; col++) + dst[row * stride + col] = bg; + + /* Draw title text */ + if (fwin->title && fwin->font && fwin->title_color[state]) { + char *title = ShrinkString(*fwin->font, fwin->title, fb->width - 12); + int titlelen = strlen(title); + int tw = WMWidthOfString(*fwin->font, title, titlelen); + int x; + switch (fwin->flags.justification) { + case WTJ_LEFT: x = 6; break; + case WTJ_RIGHT: x = fb->width - tw - 6; break; + default: x = (fb->width - tw) / 2; break; + } + int fh = WMFontHeight(*fwin->font); + int y = (fb->height - fh) / 2; + + WMDrawable d = (WMDrawable)(uintptr_t)fb->image; + WMDrawString(scr->wmscreen, d, fwin->title_color[state], + *fwin->font, x, y, title, titlelen); + wfree(title); + } + + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + + +static void wl_noop_setup_modifier_lock_masks(void) { } +static void wl_noop_key_grab_hacked(WNativeWindow window, int keycode, unsigned int modifiers) +{ (void)window; (void)keycode; (void)modifiers; } +static void wl_noop_button_grab_hacked(unsigned int button, unsigned int modifiers, + WNativeWindow window, int owner_events, unsigned int event_mask) +{ (void)button; (void)modifiers; (void)window; (void)owner_events; (void)event_mask; } +static void wl_context_init(void) +{ + /* Wayland uses a hash table instead of X11 contexts */ + w_global.context.client_win = 1; + w_global.context.app_win = 2; + w_global.context.stack = 3; +} +static void wl_noop_query_tree(WNativeWindow root, WNativeWindow **children_out, + unsigned int *nchildren_out) +{ (void)root; *children_out = NULL; *nchildren_out = 0; } + +static WScreen *wl_noop_screen_for_window(WNativeWindow window) +{ (void)window; return wScreenWithNumber(0); } + +static int wl_parse_color(WScreen *scr, const char *name, WMColorRGB *color_out) +{ (void)scr; return wl_get_color_for_colormap(0, name, color_out); } +static int wl_noop_cursor_create_from_files(WScreen *scr, const char *bitmap_path, + const char *mask_path, WMNativeCursor *cursor_out) +{ (void)scr; (void)bitmap_path; (void)mask_path; (void)cursor_out; return 0; } +static void wl_noop_cursor_free(WMNativeCursor cursor) { (void)cursor; } +static void wl_noop_cursor_set(WNativeWindow window, WMNativeCursor cursor) { (void)window; (void)cursor; } +static void wl_pointer_change_cursor(int cursor_id) +{ + const char *name; + switch (cursor_id) { + case 1: /* WCUR_MOVE */ name = "fleur"; break; + case 2: /* WCUR_RESIZE */ name = "bottom_right_corner"; break; + case 7: /* WCUR_WAIT */ name = "watch"; break; + case 8: /* WCUR_ARROW */ name = "left_ptr"; break; + case 9: /* WCUR_QUESTION */name = "question_arrow"; break; + case 10:/* WCUR_TEXT */ name = "xterm"; break; + default: name = "left_ptr"; break; + } + wl_set_cursor_image(name); +} +static void wl_noop_appicon_paint_dock_dots(WAppIcon *aicon, void *wapp) +{ (void)aicon; (void)wapp; } + +static int wl_noop_pixmap_get_size(unsigned long drawable, unsigned int *w, unsigned int *h, unsigned int *depth) +{ (void)drawable; *w = 0; *h = 0; *depth = 0; return 0; } +static void wl_noop_icon_paint_title_bg(WScreen *scr, unsigned long pixmap, int height) +{ (void)scr; (void)pixmap; (void)height; } +static void wl_noop_icon_select_paint(WIcon *icon, int step) +{ (void)icon; (void)step; } +static unsigned long wl_noop_window_get_event_mask(WNativeWindow window) +{ (void)window; return 0; } +static void wl_balloon_show(WScreen *scr, void *bal_ptr) +{ + WBalloon *bal = (WBalloon *)bal_ptr; + if (!bal->text || !bal->text[0]) + return; + + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)bal->window); + if (!fb) + return; + + WMFont *font = scr->info_text_font; + int textlen = strlen(bal->text); + int tw = WMWidthOfString(font, bal->text, textlen); + int th = WMFontHeight(font); + int pad = 4; + int bw = tw + pad * 2; + int bh = th + pad * 2; + + /* Resize pixman buffer if needed */ + if (bw != fb->width || bh != fb->height) { + W_UnregisterBacking(fb->id); + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, bw, bh, NULL, 0); + fb->width = bw; + fb->height = bh; + fb->base.width = bw; + fb->base.height = bh; + W_RegisterBacking(fb->id, fb->image); + } + + /* Fill pale yellow background */ + uint32_t *data = pixman_image_get_data(fb->image); + for (int i = 0; i < bw * bh; i++) + data[i] = 0xFFFFFFE0; + + /* 1px black border */ + for (int x = 0; x < bw; x++) { + data[x] = 0xFF000000; + data[(bh - 1) * bw + x] = 0xFF000000; + } + for (int y = 0; y < bh; y++) { + data[y * bw] = 0xFF000000; + data[y * bw + bw - 1] = 0xFF000000; + } + + /* Draw text */ + WMDrawString(scr->wmscreen, (WMDrawable)fb->id, scr->black, font, + pad, pad, bal->text, textlen); + + /* Position near objectWindow */ + int bx = 0, by = 0; + struct wl_frame_buf *obj = frame_buf_find((WNativeWindow)bal->objectWindow); + if (obj) { + bx = obj->x; + by = obj->y + bal->h + 2; + } + + /* Clamp to screen */ + if (bx + bw > scr->scr_width) + bx = scr->scr_width - bw; + if (by + bh > scr->scr_height) + by = by - bal->h - bh - 2; + + if (fb->scene_buf) { + wlr_scene_node_set_position(&fb->scene_buf->node, bx, by); + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + wlr_scene_node_set_enabled(&fb->tree->node, true); + } + + bal->mapped = 1; +} +static void wl_noop_window_fill_stipple(WNativeWindow win, int x, int y, int w, int h) +{ (void)win; (void)x; (void)y; (void)w; (void)h; } +static int wl_noop_event_check_enter_leave(void *dock) { (void)dock; return 1; } + +static void wl_noop_event_allow_async(void) { } +static void wl_noop_event_replay_pointer(void) { } +static void wl_noop_keyboard_mapping_refresh(WMEvent *event) { (void)event; } +static void wl_noop_event_flush_window(WNativeWindow win, int t) { (void)win; (void)t; } +static void wl_noop_event_flush_type(int t) { (void)t; } +static int wl_noop_event_check_window(WNativeWindow win, int t) { (void)win; (void)t; return 0; } +static int wl_noop_event_check_type(int t, WMEvent *e) { (void)t; (void)e; return 0; } +static void wl_noop_client_send_event(WNativeWindow win, void *d) { (void)win; (void)d; } +static void wl_client_message_forward(WMEvent *event, WNativeWindow target_win) +{ + /* Forward OffiX DND ClientMessage to the real client_win for + * XWayland clients. Native Wayland clients have no analog. */ + if (!wl_state.x_display || !event) return; + XEvent *xev = (XEvent *)event->_x11_raw; + if (!xev) return; + xev->xclient.window = (WNativeWindow)target_win; + XSendEvent(wl_state.x_display, (WNativeWindow)target_win, False, 0L, xev); +} +static void wl_client_send_key(WNativeWindow win, unsigned int keycode, + unsigned int modifier_mask, int press) +{ + /* For XWayland clients, deliver via the X server (wl_state.x_display) + * the same way the X11 backend does. Native Wayland clients are not + * yet supported (would require zwp_virtual_keyboard_v1). */ + if (!wl_state.x_display) return; + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.xkey.type = press ? KeyPress : KeyRelease; + ev.xkey.display = wl_state.x_display; + ev.xkey.window = (WNativeWindow)win; + ev.xkey.root = DefaultRootWindow(wl_state.x_display); + ev.xkey.subwindow = WM_NATIVE_NONE; + ev.xkey.same_screen = True; + ev.xkey.time = WM_CURRENT_TIME; + ev.xkey.keycode = keycode; + ev.xkey.state = modifier_mask; + XSendEvent(wl_state.x_display, (WNativeWindow)win, press ? False : True, + press ? WM_EVMASK_KEY_PRESS : WM_EVMASK_KEY_RELEASE, &ev); +} +static int wl_noop_window_exists(WNativeWindow win) { (void)win; return 1; } +static void wl_noop_window_get_colormap(WNativeWindow win, unsigned long *c) { (void)win; (void)c; } +static WNativeWindow wl_noop_pointer_query_window(WNativeWindow root) { (void)root; return WNATIVE_NONE; } +static void wl_noop_dialog_paint_icon(unsigned long d, void *b, void *p, WScreen *s, + int x, int y, int w, int h) { (void)d;(void)b;(void)p;(void)s;(void)x;(void)y;(void)w;(void)h; } +static void wl_pointer_get_position(int *x, int *y) { + *x = (int)wl_state.pointer_x; + *y = (int)wl_state.pointer_y; +} +static const char *wl_noop_keysym_to_string(unsigned long ks) { (void)ks; return ""; } +static int wl_noop_netwm_process_client_message(WMEvent *e) { (void)e; return 0; } +static int wl_noop_xdnd_process_client_message(WMEvent *e) { (void)e; return 0; } +static void wl_noop_xdnd_process_selection(WMEvent *e) { (void)e; } +static void wl_noop_xdnd_make_awareness(WNativeWindow w) { (void)w; } +static void wl_noop_handle_selection_request(WMEvent *e) { (void)e; } +static void wl_noop_handle_selection_clear(WMEvent *e) { (void)e; } +static void wl_noop_netwm_check_client_hint_change(WWindow *w, void *e) { (void)w; (void)e; } +static void wl_noop_netwm_position_splash(WWindow *w, int *x, int *y, int ww, int h) { (void)w;(void)x;(void)y;(void)ww;(void)h; } +static void wl_noop_netwm_check_initial_frame_state(WWindow *w) { (void)w; } + +static RContext *wl_create_rcontext(int screen_number, RContextAttributes *attribs) +{ + (void)screen_number; + return RCreateContextWayland(1920, 1080, attribs); +} + +/* ------------------------------------------------------------------ */ +/* Fake leader */ +/* ------------------------------------------------------------------ */ + +WNativeWindow wl_fake_leader_create(WScreen *scr) +{ + (void)scr; + /* Create a frame_buf with a tree to act as the dialog's content + * container. The tree is essential — content reparented into the + * fake_leader (e.g. WINGs panel views) becomes a child of the tree + * and follows it when the fake_leader itself is reparented into + * a WMaker frame. */ + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->id = wl_state.next_synthetic_id++; + fb->parent_id = 0; + fb->width = 1; + fb->height = 1; + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, 1, 1, NULL, 0); + wlr_buffer_init(&fb->base, &frame_buf_impl, 1, 1); + + if (wl_state.scene) { + struct wlr_scene_tree *_pp = wl_state.level_trees[WL_LAYER_NORMAL] + ? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree; + fb->tree = wlr_scene_tree_create(_pp); + if (fb->tree) { + fb_attach_tree_listener(fb); + wlr_scene_node_set_position(&fb->tree->node, 0, 0); + fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + wlr_scene_node_set_enabled(&fb->tree->node, true); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + return fb->id; +} +void wl_fake_leader_destroy(WNativeWindow win) { (void)win; } + + +/* ------------------------------------------------------------------ */ +/* vtable */ +/* ------------------------------------------------------------------ */ + +const WMBackend wl_backend = { + .is_wayland = 1, /* Path 1: distinguishes Wayland from X11 */ + .display_open = wl_display_open, + .display_close = wl_display_close, + .display_post_open = wl_display_post_open, + .display_screen_count = wl_display_screen_count, + .display_default_screen = wl_display_default_screen, + .atom_intern = wl_atom_intern, + .event_loop_run = wl_event_loop_run, + .event_loop_terminate = wl_event_loop_terminate, + + .screen_open = wl_screen_open, + .screen_init_display = wl_screen_init_display, + .screen_close = wl_screen_close, + .show_crash_dialog = wl_show_crash_dialog, + + .frame_create_toplevel = wl_frame_create_toplevel, + .frame_create_child = wl_frame_create_child, + .frame_destroy = wl_frame_destroy, + .frame_configure = wl_frame_configure, + .frame_paint = wl_frame_paint, + + .context_save = wl_context_save, + .context_find = wl_context_find, + .context_delete = wl_context_delete, + + .focus_set = wl_focus_set, + + .key_grab = wl_key_grab, + .key_ungrab_all = wl_key_ungrab_all, + .button_grab = wl_button_grab, + .button_ungrab_all = wl_button_ungrab_all, + + .pointer_grab = wl_pointer_grab, + .pointer_ungrab = wl_pointer_ungrab, + .pointer_warp = wl_pointer_warp, + .pointer_warp_to = wl_pointer_warp_to, + .pointer_query = wl_pointer_query, + .pointer_query_full = wl_pointer_query_full, + .pointer_change_grab = wl_pointer_change_grab, + + .keyboard_grab = wl_keyboard_grab, + .keyboard_ungrab = wl_keyboard_ungrab, + + .server_grab = wl_server_grab, + .server_ungrab = wl_server_ungrab, + + .stacking_raise = wl_stacking_raise, + .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, + .event_sync = wl_event_sync, + .event_pending = wl_event_pending, + .event_drain_motion = wl_event_drain_motion, + .event_discard_enter_leave_until = wl_event_discard_enter_leave_until, + .event_handle_extensions = wl_event_handle_extensions, + .event_randr_update = wl_event_randr_update, + + .prop_get_normal_hints = wl_prop_get_normal_hints, + .prop_get_wm_hints = wl_prop_get_wm_hints, + .prop_get_wm_class = wl_prop_get_wm_class, + .window_fetch_name = wl_window_fetch_name, + .window_get_icon_name = wl_window_get_icon_name, + .screen_get_head_usable_area = wl_noop_screen_get_head_usable_area, + .window_get_icon_image = wl_window_get_icon_image, + .prop_get_wmaker_menu = wl_prop_get_wmaker_menu, + .prop_get_transient_for = wl_prop_get_transient_for, + .prop_has_wmaker_menu = wl_prop_has_wmaker_menu, + .prop_has_gtk_object_path = wl_prop_has_gtk_object_path, + .prop_set_wm_state = wl_prop_set_wm_state, + .prop_get_wm_state = wl_prop_get_wm_state, + + .window_map = wl_window_map, + .window_map_raised = wl_window_map_raised, + .window_map_subwindows = wl_window_map_subwindows, + .pixmap_free = wl_pixmap_free, + .window_unmap = wl_window_unmap, + .window_move = wl_window_move, + .window_resize = wl_window_resize, + .window_set_border = wl_window_set_border, + .window_set_border_width = wl_window_set_border_width, + .window_set_save_under = wl_window_set_save_under, + .window_set_background = wl_noop_window_set_background, + .window_set_background_pixmap = wl_window_set_background_pixmap, + .window_clear = wl_window_clear, + .window_get_rect = wl_window_get_rect, + .window_add_events = wl_window_add_events, + .window_select_events = wl_window_select_events, + .window_shape_clear = wl_window_shape_clear, + .window_shape_set = wl_window_shape_set, + + .overlay_draw_line = wl_overlay_draw_line, + .overlay_draw_lines = wl_overlay_draw_lines, + .overlay_draw_rect = wl_overlay_draw_rect, + .overlay_draw_segments = wl_overlay_draw_segments, + .overlay_draw_string = wl_overlay_draw_string, + .overlay_text_width = wl_overlay_text_width, + + .window_get_client_attrs = wl_window_get_client_attrs, + .client_get_visual_attrs = wl_client_get_visual_attrs, + .client_adopt_attributes = wl_client_adopt_attributes, + .client_reparent = wl_client_reparent, + .client_synth_configure = wl_client_synth_configure, + .client_add_to_save_set = wl_client_add_to_save_set, + .client_remove_from_save_set = wl_client_remove_from_save_set, + .client_close = wl_client_close, + .client_query_shaped = wl_client_query_shaped, + .client_get_gnustep_attrs = wl_client_get_gnustep_attrs, + .client_get_pid = wl_client_get_pid, + .client_snapshot = wl_client_snapshot, + .coords_translate = wl_coords_translate, + + .icccm_set_wm_hints = wl_icccm_set_wm_hints, + .icccm_set_class_hint = wl_icccm_set_class_hint, + .icccm_set_class_hint_if_absent = wl_icccm_set_class_hint_if_absent, + .icccm_set_command = wl_icccm_set_command, + .icccm_get_command = wl_icccm_get_command, + .icccm_free_command = wl_icccm_free_command, + + .fake_leader_create = wl_fake_leader_create, + .fake_leader_destroy = wl_fake_leader_destroy, + + .screen_replace_wm = wl_screen_replace_wm, + .screen_select_input = wl_screen_select_input, + .screen_set_root_cursor = wl_screen_set_root_cursor, + .screen_set_icon_sizes = wl_screen_set_icon_sizes, + .screen_setup_noticeboard = wl_screen_setup_noticeboard, + .screen_alloc_gcs = wl_screen_alloc_gcs, + .screen_create_internals = wl_screen_create_internals, + .screen_create_pixmaps = wl_screen_create_pixmaps, + .screen_load_tech_font = wl_screen_load_tech_font, + .screen_show_mini_screenshot = wl_screen_show_mini_screenshot, + .screen_capture_area = wl_screen_capture_area, + .screen_capture_window = wl_screen_capture_window, + .refresh_desktop = wl_refresh_desktop, + .set_desktop_background = wl_set_desktop_background, + + .monitors_query = wl_monitors_query, + .monitors_select_events = wl_monitors_select_events, + + .workspace_badge_render = wl_workspace_badge_render, + .workspace_badge_update = wl_workspace_badge_update, + .workspace_badge_get_background = wl_workspace_badge_get_background, + + .keysym_to_keycode = wl_keysym_to_keycode, + .keysym_from_keycode = wl_keysym_from_keycode, + .xkb_lock_group = wl_xkb_lock_group, + .xkb_get_group = wl_xkb_get_group, + + .extensions_init = wl_extensions_init, + .cursors_init = wl_cursors_init, + .cursor_create_named = wl_cursor_create_named, + .shape_combine_mask = wl_shape_combine_mask, + .register_error_handlers = wl_register_error_handlers, + .texture_alloc_color = wl_texture_alloc_color, + .texture_free_colors = wl_texture_free_colors, + .texture_create_gc = wl_texture_create_gc, + .texture_free_gc = wl_texture_free_gc, + .frame_alloc_border_pixel = wl_noop_frame_alloc_border_pixel, + .frame_update_texture = wl_noop_frame_update_texture, + .frame_paint_button = wl_frame_paint_button, + .netwm_init = wl_noop_netwm_init, + .netwm_cleanup = wl_noop_netwm_cleanup, + .netwm_update_desktop = wl_noop_netwm_update_desktop, + .netwm_get_current_desktop = wl_noop_netwm_get_current_desktop, + .netwm_update_opacity = wl_netwm_update_opacity, + .netwm_update_actions = wl_noop_netwm_update_actions, + .netwm_check_client_hints = wl_noop_netwm_check_client_hints, + .netwm_check_initial_state = wl_noop_netwm_check_initial_state, + .netwm_get_pid = wl_noop_netwm_get_pid, + .netwm_cleanup_frame_extents = wl_noop_netwm_cleanup_frame_extents, + .netwm_set_frame_extents = wl_noop_netwm_set_frame_extents, + .prop_get_protocols = wl_prop_get_protocols, + .prop_get_check_property = wl_noop_prop_get_check_property, + .prop_set_wmaker_protocols = wl_noop_prop_set_wmaker_protocols, + .prop_set_icon_tile = wl_noop_prop_set_icon_tile, + .prop_cleanup = wl_noop_prop_cleanup, + .client_restore = wl_noop_client_restore, + .client_set_state = wl_client_set_state, + .client_set_maximized = wl_client_set_maximized, + .client_send_protocol = wl_client_send_protocol, + .client_kill = wl_client_kill_impl, + .client_get_colormap_windows = wl_noop_client_get_colormap_windows, + .eat_expose = wl_noop_eat_expose, + .move_window = wl_noop_move_window, + .slide_windows = wl_noop_slide_windows, + .get_text_selection = wl_noop_get_text_selection, + .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_icon_paint, + .appicon_show = wl_appicon_show, + .appicon_paint = wl_appicon_paint, + .restore_desktop = wl_noop_restore_desktop, + .wipe_desktop = wl_noop_wipe_desktop, + .get_color_for_colormap = wl_get_color_for_colormap, + .free_color = wl_noop_free_color, + .colormap_install = wl_noop_colormap_install, + .xdnd_init = wl_noop_xdnd_init, + .motif_get_hints = wl_noop_motif_get_hints, + .appicon_bounce = wl_noop_appicon_bounce, + .manage_all_windows = wl_noop_manage_all_windows, + .event_handle_signal_exit = wl_event_handle_signal_exit, + .icon_select = wl_noop_icon_select, + .icon_change_title = wl_noop_icon_change_title, + .clip_icon_paint = wl_clip_icon_paint, + .drawer_icon_paint = wl_drawer_icon_paint, + .balloon_init = wl_balloon_init, + .do_kaboom = wl_noop_do_kaboom, + .menu_paint_entry = wl_menu_paint_entry, + .menu_draw_frame = wl_menu_draw_frame, + .draw_bevel = wl_noop_draw_bevel, + .appmenu_send = wl_noop_appmenu_send, + .display_get_name = wl_display_get_name, + .switchpanel_send_enter = wl_noop_switchpanel_send_enter, + .format_x_error = wl_noop_format_x_error, + .pixmap_create_from_xbm = wl_noop_pixmap_create_from_xbm, + .pixmap_create = wl_noop_pixmap_create, + .keysym_from_name = wl_keysym_from_name, + .frame_paint_resizebar = wl_frame_paint_resizebar, + .frame_paint_titlebar = wl_frame_paint_titlebar, + .prop_get_client_leader = wl_noop_prop_get_client_leader, + .prop_get_window_state = wl_noop_prop_get_window_state, + .setup_modifier_lock_masks = wl_noop_setup_modifier_lock_masks, + .key_grab_hacked = wl_noop_key_grab_hacked, + .button_grab_hacked = wl_noop_button_grab_hacked, + .context_init = wl_context_init, + .query_tree = wl_noop_query_tree, + .screen_for_window = wl_noop_screen_for_window, + .parse_color = wl_parse_color, + .cursor_create_from_files = wl_noop_cursor_create_from_files, + .cursor_free = wl_noop_cursor_free, + .cursor_set = wl_noop_cursor_set, + .pointer_change_cursor = wl_pointer_change_cursor, + .appicon_paint_dock_dots = wl_noop_appicon_paint_dock_dots, + .pixmap_get_size = wl_noop_pixmap_get_size, + .icon_paint_title_bg = wl_noop_icon_paint_title_bg, + .icon_select_paint = wl_noop_icon_select_paint, + .window_get_event_mask = wl_noop_window_get_event_mask, + .balloon_show = wl_balloon_show, + .window_fill_stipple = wl_noop_window_fill_stipple, + .event_check_enter_leave = wl_noop_event_check_enter_leave, + .event_allow_async = wl_noop_event_allow_async, + .event_replay_pointer = wl_noop_event_replay_pointer, + .keyboard_mapping_refresh = wl_noop_keyboard_mapping_refresh, + .event_flush_window = wl_noop_event_flush_window, + .event_flush_type = wl_noop_event_flush_type, + .event_check_window = wl_noop_event_check_window, + .event_check_type = wl_noop_event_check_type, + .client_send_event = wl_noop_client_send_event, + .client_message_forward = wl_client_message_forward, + .client_send_key = wl_client_send_key, + .window_exists = wl_noop_window_exists, + .window_get_colormap = wl_noop_window_get_colormap, + .pointer_query_window = wl_noop_pointer_query_window, + .dialog_paint_icon = wl_noop_dialog_paint_icon, + .pointer_get_position = wl_pointer_get_position, + .keysym_to_string = wl_noop_keysym_to_string, + .window_get_command = wl_window_get_command, + .netwm_process_client_message = wl_noop_netwm_process_client_message, + .xdnd_process_client_message = wl_noop_xdnd_process_client_message, + .xdnd_process_selection = wl_noop_xdnd_process_selection, + .xdnd_make_awareness = wl_noop_xdnd_make_awareness, .netwm_check_client_hint_change = wl_noop_netwm_check_client_hint_change, + .netwm_position_splash = wl_noop_netwm_position_splash, + .netwm_check_initial_frame_state = wl_noop_netwm_check_initial_frame_state, + .handle_selection_request = wl_noop_handle_selection_request, + .handle_selection_clear = wl_noop_handle_selection_clear, + .create_rcontext = wl_create_rcontext, +}; diff --git a/src/backend/wayland/wl_client.c b/src/backend/wayland/wl_client.c new file mode 100644 index 0000000..4e542bd --- /dev/null +++ b/src/backend/wayland/wl_client.c @@ -0,0 +1,460 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../window.h" +#include "wl_types.h" + + (void)scr; (void)str; + return n * OVL_CW; +} + +/* ------------------------------------------------------------------ */ +/* Client */ +/* ------------------------------------------------------------------ */ + +int +wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out) +{ + /* Phase 42: return real map_state and geometry from XDG surface. + * Phase 17: XWayland path uses xw_surface->mapped and width/height. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + memset(out, 0, sizeof(*out)); + if (v && v->xw_surface) { + if (v->xw_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640; + out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + } else if (v && v->xdg_surface && v->xdg_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xdg_surface->surface->current.width; + out->height = v->xdg_surface->surface->current.height; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + return 1; +} + +void +wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out) +{ + (void)win; + out->depth = 24; out->visual = NULL; out->colormap = 0; +} + +void +wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask) +{ (void)win; (void)event_mask; } + +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) { + /* Move client's scene tree under the frame's tree */ + if (parent_fb->tree) + wlr_scene_node_reparent(&v->scene_node->node, parent_fb->tree); + /* Position at relative offset within the tree */ + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + } + return; + } + + + + /* For WINGs internal windows: create a child frame_buf so the dialog + * content is visible in the scene graph. */ + if (client >= 0x20000000 && new_parent >= 0x10000000) { + /* client is a WINGs view ID, new_parent is a frame_buf */ + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + if (!parent_fb) return; + + /* Check if we already have a frame_buf for this client */ + struct wl_frame_buf *existing = frame_buf_find(client); + if (existing) { + /* Just update parent and position */ + existing->parent_id = new_parent; + existing->rel_x = x; + existing->rel_y = y; + existing->x = parent_fb->x + x; + existing->y = parent_fb->y + y; + if (existing->scene_buf) { + /* Ensure it's under the parent frame's tree, then position + * at relative offset within the tree. */ + if (parent_fb->tree && + existing->scene_buf->node.parent != parent_fb->tree) { + wlr_scene_node_reparent(&existing->scene_buf->node, + parent_fb->tree); + } + if (parent_fb->tree) + wlr_scene_node_set_position(&existing->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&existing->scene_buf->node, + existing->x, existing->y); + } + return; + } + + /* Create a new frame_buf for this WINGs view. + * Get size from the view itself. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)client); + int w = view ? view->size.width : parent_fb->width; + int h = view ? view->size.height : (parent_fb->height - y); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + if (!fb->image) { wfree(fb); return; } + + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + + fb->id = client; /* use the WINGs view ID directly */ + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + fb->width = w; + fb->height = h; + + if (wl_state.scene) { + /* Place the WINGs content under the parent frame's tree so it + * moves with the WM-managed dialog decorations. */ + struct wlr_scene_tree *_wp = parent_fb->tree + ? parent_fb->tree + : (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) { + fb_attach_scene_buf_listener(fb); + /* If under the frame tree, use relative coords; otherwise absolute. */ + if (parent_fb->tree) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + + /* Connect the WINGs view's backing store to this frame_buf's image */ + extern void W_SetViewBacking(unsigned long id, void *img); + W_SetViewBacking((unsigned long)client, fb->image); + } + /* Reparent an existing frame_buf into a new parent */ + else if (client >= 0x10000000 && new_parent >= 0x10000000) { + struct wl_frame_buf *fb = frame_buf_find(client); + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + fprintf(stderr, "reparent_framebuf: client=0x%lx fb=%p new_parent=0x%lx parent_fb=%p\n", + (unsigned long)client, (void*)fb, + (unsigned long)new_parent, (void*)parent_fb); + if (fb) fprintf(stderr, " fb: tree=%p scene_buf=%p pos=(%d,%d)\n", + (void*)fb->tree, (void*)fb->scene_buf, + fb->tree ? fb->tree->node.x : -1, + fb->tree ? fb->tree->node.y : -1); + if (parent_fb) fprintf(stderr, " parent_fb: tree=%p pos=(%d,%d)\n", + (void*)parent_fb->tree, + parent_fb->tree ? parent_fb->tree->node.x : -1, + parent_fb->tree ? parent_fb->tree->node.y : -1); + if (fb && parent_fb) { + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + if (fb->tree) { + /* This frame_buf has a tree (e.g. fake_leader): reparent + * the whole tree so all children move with it. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->tree->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->tree->node, x, y); + } else if (fb->scene_buf) { + /* Plain frame_buf (no tree): reparent scene_buf directly. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->scene_buf->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } + /* Update absolute tracking recursively for all descendants */ + frame_buf_update_children_abs(client); + } + } +} + +/* Called by WINGs after a view repaints — commit its frame_buf to scene graph */ +void wl_view_commit_backing_impl(unsigned long id) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + WNativeWindow saved_parent_id = 0; + int saved_rel_x = 0, saved_rel_y = 0; + /* If the view was resized, the old frame_buf's image is stale. */ + if (fb) { + extern void *W_GetViewBacking(unsigned long id); + void *current = W_GetViewBacking((unsigned long)id); + if (current != (void *)fb->image) { + /* Save parent relation before destroying — the new frame_buf + * must be positioned within the same parent tree so dialog + * content reparented via wl_client_reparent stays visible. */ + saved_parent_id = fb->parent_id; + saved_rel_x = fb->rel_x; + saved_rel_y = fb->rel_y; + wlr_scene_node_destroy(&fb->scene_buf->node); + wl_list_remove(&fb->link); + wlr_buffer_drop(&fb->base); + wfree(fb); + fb = NULL; + } + } + if (!fb) { + extern W_View *W_GetViewForWindow(unsigned long id); + extern void *W_GetViewBacking(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)id); + if (view && view->flags.topLevel) { + fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + fb->id = id; + fb->parent_id = saved_parent_id; + fb->rel_x = saved_rel_x; + fb->rel_y = saved_rel_y; + fb->x = view->pos.x; + fb->y = view->pos.y; + fb->width = view->size.width; + fb->height = view->size.height; + fb->image = (pixman_image_t *)W_GetViewBacking(id); + if (fb->image) { + pixman_image_ref(fb->image); + wlr_buffer_init(&fb->base, &frame_buf_impl, + pixman_image_get_width(fb->image), + pixman_image_get_height(fb->image)); + if (wl_state.scene) { + struct wlr_scene_tree *parent = NULL; + /* Re-parent under the saved parent's tree so + * dialogs reparented via wl_client_reparent stay + * correctly positioned within their frame. */ + if (saved_parent_id) { + struct wl_frame_buf *pfb = + frame_buf_find(saved_parent_id); + if (pfb && pfb->tree) + parent = pfb->tree; + } + if (!parent) + parent = 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(parent, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + /* Position relative to parent's tree or absolute */ + if (parent != &wl_state.scene->tree && saved_parent_id) + wlr_scene_node_set_position(&fb->scene_buf->node, + saved_rel_x, saved_rel_y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, + fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + wl_list_insert(&wl_state.frame_list, &fb->link); + } else { + wfree(fb); + fb = NULL; + } + } + } + if (fb && fb->scene_buf) { + /* Sync position from the WINGs view — it may have moved since + * the frame_buf was created (e.g. switch panel is positioned + * after widget creation via WMMoveWidget). + * When reparented (parent_id set), use the relative offset + * instead of the view's absolute position. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *v = W_GetViewForWindow((unsigned long)id); + if (v && !fb->parent_id) { + if (fb->x != v->pos.x || fb->y != v->pos.y) { + fb->x = v->pos.x; + fb->y = v->pos.y; + wlr_scene_node_set_position(&fb->scene_buf->node, + fb->x, fb->y); + } + } + /* Only enable the scene node if the view is still mapped. + * If the view was unmapped (e.g. move ended), wl_unmap disables + * it via W_ViewSceneNodeSetEnabledFn — don't re-enable here. */ + if (!v || v->flags.mapped) { + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + } + } +} +void +wl_view_scene_node_set_enabled(unsigned long id, int enabled) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + if (fb && fb->scene_buf) + wlr_scene_node_set_enabled(&fb->scene_buf->node, !!enabled); +} + + +void wl_view_destroy_backing_impl(unsigned long id) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + if (!fb) return; + wl_list_remove(&fb->link); + /* fb->scene_buf may have been auto-destroyed by wlroots already if a + * parent tree was destroyed earlier (e.g. wl_frame_destroy on the + * surrounding WMaker frame). The destroy listener NULLs scene_buf + * in that case. */ + if (fb->scene_buf) + wlr_scene_node_destroy(&fb->scene_buf->node); + wlr_buffer_drop(&fb->base); +} + +void +wl_client_synth_configure(WNativeWindow client_win, + int x, int y, int width, int height, + int border_width, WNativeWindow above) +{ + /* Phase 44: send an XDG configure with the new size to the client. + * Phase 13: also record the WM-assigned position in the view so + * pointer hit-testing (view_surface_at) uses current coordinates. + * Phase 17: XWayland uses wlr_xwayland_surface_configure instead. */ + (void)border_width; (void)above; + struct wl_toplevel_view *v = wl_find_view_by_id(client_win); + if (!v) return; + v->x = x; + v->y = y; + /* Update relative offset so frame_configure keeps us in sync. */ + if (v->frame_id) { + struct wl_frame_buf *pfb = frame_buf_find(v->frame_id); + if (pfb) { + v->frame_rel_x = x - pfb->x; + v->frame_rel_y = y - pfb->y; + } + /* Scene node is child of frame tree — use relative coords */ + if (v->scene_node) + wlr_scene_node_set_position(&v->scene_node->node, + v->frame_rel_x, v->frame_rel_y); + } else { + /* No frame — use absolute coords */ + if (v->scene_node) + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + if (v->xw_surface) { + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)x, (int16_t)y, + width > 0 ? (uint16_t)width : v->xw_surface->width, + height > 0 ? (uint16_t)height : v->xw_surface->height); + return; + } + if (!v->xdg_surface || !v->xdg_surface->toplevel) return; + if (width > 0 && height > 0) + wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)width, (uint32_t)height); + wlr_xdg_surface_schedule_configure(v->xdg_surface); +} + +void wl_client_add_to_save_set(WNativeWindow win) { (void)win; } +void wl_client_remove_from_save_set(WNativeWindow win) { (void)win; } + +void +wl_client_close(WNativeWindow win) +{ + /* Phase 46: ask the surface to close. + * Phase 17: XWayland uses wlr_xwayland_surface_close. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return; + if (v->xw_surface) { + wlr_xwayland_surface_close(v->xw_surface); + } else if (v->xdg_surface && v->xdg_surface->toplevel) { + wlr_xdg_toplevel_send_close(v->xdg_surface->toplevel); + /* Flush so the in-process client receives the close event, + * then dispatch so it processes it on this iteration. */ + wl_display_flush_clients(wl_state.display); + } +} + +void wl_client_query_shaped(WNativeWindow win, int *shaped_out) +{ (void)win; *shaped_out = 0; } + +int +wl_client_get_gnustep_attrs(WNativeWindow win, GNUstepWMAttributes **attr_out) +{ (void)win; *attr_out = NULL; return 0; } + +int wl_client_get_pid(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (!v) return 0; + /* Phase 17: XWayland surfaces have a direct pid field. */ + if (v->xw_surface) + return (int)v->xw_surface->pid; + if (!v->xdg_surface || !v->xdg_surface->resource) + return 0; + struct wl_client *client = wl_resource_get_client(v->xdg_surface->resource); + if (!client) return 0; + pid_t pid = 0; uid_t uid = 0; gid_t gid = 0; + wl_client_get_credentials(client, &pid, &uid, &gid); + return (int)pid; +} + +RImage * +wl_client_snapshot(WScreen *scr, WNativeWindow client_win, WNativeWindow frame_win) +{ + (void)frame_win; + struct wl_toplevel_view *v = wl_find_view_by_id(client_win); + if (!v) return NULL; + + /* XWayland: capture via XGetImage on the WM's X display */ + if (v->xw_surface && wl_state.x_display) { + Window xid = (Window)v->xw_surface->window_id; + XWindowAttributes attr; + if (!XGetWindowAttributes(wl_state.x_display, xid, &attr)) + return NULL; + XImage *img = XGetImage(wl_state.x_display, xid, + 0, 0, attr.width, attr.height, + AllPlanes, ZPixmap); + if (!img) return NULL; + RImage *res = RCreateImageFromXImage(scr->rcontext, img, NULL); + XDestroyImage(img); + return res; + } + + /* Native Wayland: read pixels from last committed surface buffer */ + if (!v->xdg_surface || !v->xdg_surface->surface) return NULL; + struct wlr_surface *s = v->xdg_surface->surface; + if (!s->buffer) return NULL; + struct wlr_buffer *buf = &s->buffer->base; + + uint32_t fmt; + void *data; + size_t stride; + if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &fmt, &stride)) + return NULL; diff --git a/src/backend/wayland/wl_display.c b/src/backend/wayland/wl_display.c new file mode 100644 index 0000000..07e78da --- /dev/null +++ b/src/backend/wayland/wl_display.c @@ -0,0 +1,1070 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../WindowMaker.h" +#include "../../screen.h" +#include "../../window.h" +#include "../../event.h" +#include "../../xmodifier.h" +#include "../../startup.h" +#include "../backend.h" +#include "wl_types.h" + +/* ------------------------------------------------------------------ */ +/* Display lifecycle (Phase 32 / Phase 34) */ +/* ------------------------------------------------------------------ */ + +/* Startup pump thread: keeps the Wayland event loop running while the main + * thread initialises WINGs/RContext during StartUp(). XWayland needs the + * Wayland compositor event loop to be responsive so it can answer X11 + * round-trips like XGetModifierMapping (which queries keyboard state from + * wlroots via Wayland protocol). The thread does NOT touch Xlib — the + * x11_event_source fd watcher is registered AFTER this thread exits in + * wl_event_loop_run(), preventing any concurrent Xlib access. */ +static void * +wl_startup_pump_thread(void *arg) +{ + (void)arg; + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + while (wl_state.startup_pump_running) { + wl_display_flush_clients(wl_state.display); + wl_event_loop_dispatch(loop, 1); + } + return NULL; +} + +/* Called by the Wayland event loop whenever the X11 connection fd is readable. + * Drains all pending X11 events and dispatches them through WINGs so that + * menus, dialogs, and other WINGs widgets work during wl_display_run(). */ +static int +wl_handle_x11_events(int fd, uint32_t mask, void *data) +{ + (void)fd; (void)mask; (void)data; + if (!wl_state.x_display) + return 0; + XEvent event; + while (XPending(wl_state.x_display)) { + XNextEvent(wl_state.x_display, &event); + /* Guard: WINGs screen is not initialised until StartUp() completes. */ + if (w_global.screen_count > 0) { + /* Translate XEvent → WMEvent at the XWayland boundary. + * _x11_raw carries the original XEvent so WMHandleEvent can + * do X11-specific operations (view lookup, XFilterEvent, etc.) */ + WMEvent wev; + memset(&wev, 0, sizeof(wev)); + wev._x11_raw = &event; + wev.window = (WNativeWindow)event.xany.window; + switch (event.type) { + case KeyPress: wev.type = WME_KEY_PRESS; break; + case KeyRelease: wev.type = WME_KEY_RELEASE; break; + case ButtonPress: wev.type = WME_BUTTON_PRESS; break; + case ButtonRelease: wev.type = WME_BUTTON_RELEASE; break; + case MotionNotify: wev.type = WME_MOTION; break; + case EnterNotify: wev.type = WME_ENTER; break; + case LeaveNotify: wev.type = WME_LEAVE; break; + case Expose: wev.type = WME_EXPOSE; break; + case ConfigureNotify: wev.type = WME_CONFIGURE; break; + case MapNotify: wev.type = WME_MAP; break; + case UnmapNotify: wev.type = WME_UNMAP; break; + case DestroyNotify: wev.type = WME_DESTROY; break; + case FocusIn: wev.type = WME_FOCUS_IN; break; + case FocusOut: wev.type = WME_FOCUS_OUT; break; + case PropertyNotify: wev.type = WME_PROPERTY; break; + case ClientMessage: wev.type = WME_CLIENT_MESSAGE; break; + case SelectionNotify: wev.type = WME_SELECTION; break; + case SelectionRequest: wev.type = WME_SELECTION_REQUEST; break; + case SelectionClear: wev.type = WME_SELECTION_CLEAR; break; + case MappingNotify: wev.type = WME_MAPPING; break; + case ColormapNotify: wev.type = WME_COLORMAP; break; + case VisibilityNotify: wev.type = WME_VISIBILITY; break; + case ReparentNotify: wev.type = WME_REPARENT; break; + case ConfigureRequest: wev.type = WME_CONFIGURE_REQUEST; break; + case MapRequest: wev.type = WME_MAP_REQUEST; break; + default: wev.type = WME_NONE; break; + } + WMHandleEvent(&wev); + } + } + return 0; +} + +/* Public accessor for the XWayland Display* — used by screen.c to create + * an RContext for texture rendering without exposing wl_state globally. */ +Display *wl_get_x_display(void) +{ + return wl_state.x_display; +} + +static void * +wl_display_open(const char *name) +{ + const char *sock; + + (void)name; /* Wayland compositor picks its own socket name */ + + /* Initialize listener links so wl_list_remove is safe before first add */ + wl_list_init(&wl_state.real_keyboard_key_listener.link); + wl_list_init(&wl_state.real_keyboard_modifiers_listener.link); + wl_list_init(&wl_state.real_keyboard_destroy_listener.link); + wl_list_init(&wl_state.pointer_motion_listener.link); + wl_list_init(&wl_state.real_pointer_motion_abs_listener.link); + wl_list_init(&wl_state.real_pointer_button_listener.link); + wl_list_init(&wl_state.real_pointer_frame_listener.link); + wl_list_init(&wl_state.real_pointer_axis_listener.link); + wl_list_init(&wl_state.real_pointer_destroy_listener.link); + + XInitThreads(); + + /* Install X error handler immediately: Mesa/EGL may open a connection to + * DISPLAY (e.g. the host compositor's XWayland) during wlr_backend_start, + * which happens before StartUp() calls register_error_handlers(). + * Installing here prevents Xlib's default handler from aborting the + * process on the benign BadMatch that XWayland generates on SetInputFocus + * during WM startup. */ + XSetErrorHandler(wl_catch_xwayland_error); + + wlr_log_init(WLR_ERROR, NULL); + + wl_state.display = wl_display_create(); + if (!wl_state.display) { + wwarning("wl_backend: failed to create wl_display"); + return NULL; + } + + /* Phase 18: choose backend — nested Wayland if WMAKER_WAYLAND_NESTED=1 + * is set (run inside an existing Wayland compositor), otherwise headless. + * Both modes always include a headless backend (headless_inner) so that + * wlr_headless_add_input_device works regardless of nesting. */ + /* Auto-detect nested mode: if WAYLAND_DISPLAY is set we are running inside + * an existing Wayland session and should use wlr_wl_backend so the + * compositor appears as a window on the host desktop. + * WMAKER_WAYLAND_NESTED=0 can be used to force headless even when + * WAYLAND_DISPLAY is present (e.g. for automated testing). */ + { + const char *nested_env = getenv("WMAKER_WAYLAND_NESTED"); + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (nested_env && nested_env[0] == '0') + wl_state.is_nested = 0; /* explicit override: force headless */ + else if (nested_env && nested_env[0]) + wl_state.is_nested = 1; /* explicit override: force nested */ + else + wl_state.is_nested = (wayland_display && wayland_display[0]) ? 1 : 0; + } + + /* Always create a headless backend for the output in headless mode. */ + wl_state.headless_inner = wlr_headless_backend_create( + wl_display_get_event_loop(wl_state.display)); + if (!wl_state.headless_inner) { + wwarning("wl_backend: failed to create headless backend"); + wl_display_destroy(wl_state.display); + wl_state.display = NULL; + return NULL; + } + + if (wl_state.is_nested) { + /* Nested: multi-backend wrapping nested Wayland + headless. */ + wl_state.wl_backend_inner = wlr_wl_backend_create( + wl_display_get_event_loop(wl_state.display), NULL); + if (!wl_state.wl_backend_inner) { + wwarning("wl_backend: failed to create nested Wayland backend; falling back to standalone"); + wl_state.is_nested = 0; + } + } + + if (wl_state.is_nested && wl_state.wl_backend_inner) { + struct wlr_backend *multi = wlr_multi_backend_create( + wl_display_get_event_loop(wl_state.display)); + wlr_multi_backend_add(multi, wl_state.wl_backend_inner); + wlr_multi_backend_add(multi, wl_state.headless_inner); + wl_state.backend = multi; + wwarning("wl_backend: nested Wayland compositor mode"); + } else { + /* Standalone: use autocreate to get DRM/KMS + libinput on real hardware. + * Clear DISPLAY and WAYLAND_DISPLAY so wlroots goes straight to DRM. */ + unsetenv("DISPLAY"); + unsetenv("WAYLAND_DISPLAY"); + struct wlr_session *session = NULL; + wl_state.backend = wlr_backend_autocreate( + wl_display_get_event_loop(wl_state.display), &session); + wl_state.session = session; + if (!wl_state.backend) { + wwarning("wl_backend: wlr_backend_autocreate failed; falling back to headless"); + wl_state.backend = wl_state.headless_inner; + } else { + wwarning("wl_backend: standalone DRM/KMS compositor mode"); + } + } + + /* Renderer: use the appropriate backend as source. */ + { + struct wlr_backend *render_src; + if (wl_state.wl_backend_inner) + render_src = wl_state.wl_backend_inner; + else + render_src = wl_state.backend; + wl_state.renderer = wlr_renderer_autocreate(render_src); + } + if (!wl_state.renderer) { + wwarning("wl_backend: failed to create renderer"); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + + /* Allocator — needed for wlr_output_init_render. */ + { + struct wlr_backend *alloc_src = wl_state.wl_backend_inner + ? wl_state.wl_backend_inner : wl_state.backend; + wl_state.allocator = wlr_allocator_autocreate(alloc_src, wl_state.renderer); + } + if (!wl_state.allocator) + wwarning("wl_backend: allocator unavailable; wlr_output_init_render will be skipped"); + + /* Phase 34: output layout for multi-head geometry tracking. + * Must be created before backend start so handle_backend_new_output + * can add the output to the layout when it fires. */ + wl_state.output_layout = wlr_output_layout_create(wl_state.display); + + /* Phase 23: create hardware cursor tracker and load xcursor theme. + * wlr_cursor tracks the logical cursor position across the output + * layout and communicates cursor image changes to each output. + * In nested Wayland mode this calls wl_pointer.set_cursor on the host + * compositor so the cursor is visible in the window. + * Theme and image are applied in handle_backend_new_output after each + * output is added to the layout, ensuring the scale factor is known. */ + wl_state.cursor = wlr_cursor_create(); + if (wl_state.cursor) { + wlr_cursor_attach_output_layout(wl_state.cursor, wl_state.output_layout); + { + const char *theme = getenv("XCURSOR_THEME"); + const char *size_str = getenv("XCURSOR_SIZE"); + unsigned int size = size_str ? (unsigned int)atoi(size_str) : 24; + if (size == 0) size = 24; + wl_state.xcursor_mgr = wlr_xcursor_manager_create(theme, size); + } + if (wl_state.xcursor_mgr) + wlr_xcursor_manager_load(wl_state.xcursor_mgr, 1.0f); + wwarning("wl_backend: cursor created, xcursor theme loaded"); + } else { + wwarning("wl_backend: wlr_cursor_create failed; cursor will be invisible"); + } + + /* Phase 18: scene graph — MUST be created before wlr_backend_start so + * that handle_backend_new_output can create the scene_output immediately + * when the output fires (for nested Wayland, new_output fires during + * wlr_backend_start via the host compositor roundtrip). */ + wl_state.scene = wlr_scene_create(); + /* Register commit callback for WINGs views (defined later in file) */ + { + extern void (*W_ViewCommitBackingFn)(unsigned long id); + extern void wl_view_commit_backing_impl(unsigned long id); + extern void wl_view_destroy_backing_impl(unsigned long id); + W_ViewCommitBackingFn = wl_view_commit_backing_impl; + extern void (*W_ViewDestroyBackingFn)(unsigned long id); + W_ViewDestroyBackingFn = wl_view_destroy_backing_impl; + + extern void (*W_ViewSceneNodeSetEnabledFn)(unsigned long id, int enabled); + extern void wl_view_scene_node_set_enabled(unsigned long id, int enabled); + W_ViewSceneNodeSetEnabledFn = wl_view_scene_node_set_enabled; + + extern void (*W_ClipboardStoreFn)(const char *text, int len); + extern char *(*W_ClipboardFetchFn)(int *len_out); + W_ClipboardStoreFn = wl_clipboard_store_to_seat; + W_ClipboardFetchFn = wl_clipboard_fetch_from_seat; + + extern void (*W_DragIconShowFn)(struct W_View *, int, int); + extern void (*W_DragIconMoveFn)(int, int); + extern void (*W_DragIconHideFn)(void); + W_DragIconShowFn = wl_drag_icon_show; + W_DragIconMoveFn = wl_drag_icon_move; + W_DragIconHideFn = wl_drag_icon_hide; + + extern void (*W_DragCursorFeedbackFn)(Bool); + W_DragCursorFeedbackFn = wl_drag_cursor_feedback; + } + if (wl_state.scene) { + wwarning("wl_backend: scene graph created"); + /* 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 + * input devices from the nested Wayland backend (fired during + * wlr_backend_start) are captured before the headless devices are + * added explicitly below. */ + wl_state.backend_new_input_listener.notify = handle_backend_new_input; + wl_signal_add(&wl_state.backend->events.new_input, + &wl_state.backend_new_input_listener); + + /* Wire new_output BEFORE starting the backend. */ + wl_state.backend_new_output_listener.notify = handle_backend_new_output; + wl_signal_add(&wl_state.backend->events.new_output, + &wl_state.backend_new_output_listener); + + /* For nested mode, queue an output window before start; wlr_wl_output_create + * returns NULL before the backend is initialised — the output is created + * during wlr_backend_start and delivered via new_output. */ + if (wl_state.is_nested && wl_state.wl_backend_inner) + wlr_wl_output_create(wl_state.wl_backend_inner); + + if (!wlr_backend_start(wl_state.backend)) { + wwarning("wl_backend: failed to start backend"); + wlr_renderer_destroy(wl_state.renderer); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + + /* For headless mode, add the output explicitly after start; this fires + * handle_backend_new_output synchronously (scene already exists). */ + if (!wl_state.is_nested) + wlr_headless_add_output(wl_state.headless_inner, 1920, 1080); + + /* Phase 34: create the UNIX socket so Wayland clients can connect. */ + sock = wl_display_add_socket_auto(wl_state.display); + if (!sock) { + wwarning("wl_backend: failed to create Wayland compositor socket"); + wlr_renderer_destroy(wl_state.renderer); + wlr_backend_destroy(wl_state.backend); + wl_display_destroy(wl_state.display); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.display = NULL; + return NULL; + } + snprintf(wl_state.socket_name, sizeof(wl_state.socket_name), "%s", sock); + setenv("WAYLAND_DISPLAY", wl_state.socket_name, 1); + wwarning("wl_backend: compositor socket WAYLAND_DISPLAY=%s DISPLAY=%s", + wl_state.socket_name, + getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)"); + + /* Phase 35: advertise core Wayland protocol globals. */ + wl_state.compositor = + wlr_compositor_create(wl_state.display, 6, wl_state.renderer); + wlr_subcompositor_create(wl_state.display); + wl_state.seat = wlr_seat_create(wl_state.display, "seat0"); + wl_state.data_device_mgr = + wlr_data_device_manager_create(wl_state.display); + + /* Phase 24: handle client cursor-surface requests on the seat. */ + if (wl_state.seat) { + wl_state.seat_request_cursor_listener.notify = handle_seat_request_cursor; + wl_signal_add(&wl_state.seat->events.request_set_cursor, + &wl_state.seat_request_cursor_listener); + } + /* Phase 25: handle clipboard set-selection requests on the seat so + * Ctrl-C / copy operations from clients propagate to the seat selection + * and become available for paste in other clients. */ + if (wl_state.seat) { + wl_state.seat_request_set_selection_listener.notify = + handle_seat_request_set_selection; + wl_signal_add(&wl_state.seat->events.request_set_selection, + &wl_state.seat_request_set_selection_listener); + wwarning("wl_backend: clipboard selection wired"); + + wl_state.seat_request_start_drag_listener.notify = + handle_seat_request_start_drag; + wl_signal_add(&wl_state.seat->events.request_start_drag, + &wl_state.seat_request_start_drag_listener); + wwarning("wl_backend: DND start_drag wired"); + } + + /* Phase 26: primary selection (middle-click paste) via + * zwp_primary_selection_device_manager_v1. */ + wl_state.primary_selection_mgr = + wlr_primary_selection_v1_device_manager_create(wl_state.display); + if (wl_state.primary_selection_mgr && wl_state.seat) { + wl_state.seat_request_primary_selection_listener.notify = + handle_seat_request_primary_selection; + wl_signal_add(&wl_state.seat->events.request_set_primary_selection, + &wl_state.seat_request_primary_selection_listener); + wwarning("wl_backend: primary selection (middle-click) wired"); + } + + /* Phase 27: xdg-output — panels and HiDPI apps query logical output + * geometry via zxdg_output_manager_v1. Requires output_layout. */ + if (wl_state.output_layout) { + wl_state.xdg_output_mgr = + wlr_xdg_output_manager_v1_create(wl_state.display, + wl_state.output_layout); + if (wl_state.xdg_output_mgr) + wwarning("wl_backend: xdg-output-manager created"); + } + + /* Phase 28: idle-inhibit — video players and games create inhibitor + * objects to suppress screen blanking while active. */ + wl_state.idle_inhibit_mgr = + wlr_idle_inhibit_v1_create(wl_state.display); + if (wl_state.idle_inhibit_mgr) { + wl_state.idle_new_inhibitor_listener.notify = handle_idle_new_inhibitor; + wl_signal_add(&wl_state.idle_inhibit_mgr->events.new_inhibitor, + &wl_state.idle_new_inhibitor_listener); + wwarning("wl_backend: idle-inhibit manager created"); + } + + /* Phase 29: wp_viewporter — hardware video scaling and subsurface damage + * subset used by mpv, Chromium, Firefox, SDL2. */ + wl_state.viewporter = wlr_viewporter_create(wl_state.display); + if (wl_state.viewporter) + wwarning("wl_backend: wp_viewporter created"); + else + wwarning("wl_backend: wp_viewporter unavailable"); + + /* Phase 30: relative pointer + pointer constraints for games / pointer lock. */ + wl_state.relative_pointer_mgr = + wlr_relative_pointer_manager_v1_create(wl_state.display); + if (wl_state.relative_pointer_mgr) + wwarning("wl_backend: relative-pointer-manager created"); + + wl_state.pointer_constraints = + wlr_pointer_constraints_v1_create(wl_state.display); + if (wl_state.pointer_constraints) { + wl_state.new_constraint_listener.notify = handle_new_pointer_constraint; + wl_signal_add(&wl_state.pointer_constraints->events.new_constraint, + &wl_state.new_constraint_listener); + wwarning("wl_backend: pointer-constraints created"); + } + + /* Phase 31: keyboard shortcuts inhibitor — fullscreen apps / remote desktop + * request to suppress WM keyboard shortcuts. */ + wl_state.shortcuts_inhibit_mgr = + wlr_keyboard_shortcuts_inhibit_v1_create(wl_state.display); + if (wl_state.shortcuts_inhibit_mgr) { + wl_state.new_shortcuts_inhibitor_listener.notify = + handle_new_shortcuts_inhibitor; + wl_signal_add(&wl_state.shortcuts_inhibit_mgr->events.new_inhibitor, + &wl_state.new_shortcuts_inhibitor_listener); + wwarning("wl_backend: keyboard-shortcuts-inhibit created"); + } + /* Phase 32: GPU zero-copy (linux-dmabuf), screencopy, export-dmabuf. + * All three are purely passive globals: wlroots handles the protocol + * implementation internally; no event listeners are required. */ + wl_state.linux_dmabuf = + wlr_linux_dmabuf_v1_create_with_renderer(wl_state.display, 4, wl_state.renderer); + if (wl_state.linux_dmabuf) + wwarning("wl_backend: linux-dmabuf created (zwp_linux_dmabuf_v1)"); + else + wwarning("wl_backend: linux-dmabuf unavailable (renderer has no DRM FD)"); + wl_state.screencopy_mgr = + wlr_screencopy_manager_v1_create(wl_state.display); + if (wl_state.screencopy_mgr) + wwarning("wl_backend: screencopy manager created (zwlr_screencopy_manager_v1)"); + wl_state.export_dmabuf_mgr = + wlr_export_dmabuf_manager_v1_create(wl_state.display); + if (wl_state.export_dmabuf_mgr) + wwarning("wl_backend: export-dmabuf manager created (zwlr_export_dmabuf_manager_v1)"); + + /* Phase 33: output management (wlr-output-management), output power (DPMS), + * gamma control (Night Light / redshift), and presentation time feedback. */ + wl_state.output_mgr = wlr_output_manager_v1_create(wl_state.display); + if (wl_state.output_mgr) { + wl_state.output_mgr_apply_listener.notify = handle_output_mgr_apply; + wl_signal_add(&wl_state.output_mgr->events.apply, + &wl_state.output_mgr_apply_listener); + wl_state.output_mgr_test_listener.notify = handle_output_mgr_test; + wl_signal_add(&wl_state.output_mgr->events.test, + &wl_state.output_mgr_test_listener); + wwarning("wl_backend: output-management created (zwlr_output_management_v1)"); + } + wl_state.output_power_mgr = wlr_output_power_manager_v1_create(wl_state.display); + if (wl_state.output_power_mgr) { + wl_state.output_power_set_mode_listener.notify = handle_output_power_set_mode; + wl_signal_add(&wl_state.output_power_mgr->events.set_mode, + &wl_state.output_power_set_mode_listener); + wwarning("wl_backend: output-power-management created (zwlr_output_power_management_v1)"); + } + wl_state.gamma_control_mgr = wlr_gamma_control_manager_v1_create(wl_state.display); + if (wl_state.gamma_control_mgr) + wwarning("wl_backend: gamma-control created (zwlr_gamma_control_manager_v1)"); + wl_state.presentation = wlr_presentation_create(wl_state.display, wl_state.backend, 1); + if (wl_state.presentation) + wwarning("wl_backend: presentation-time created (wp_presentation)"); + + /* Phase 34: text input v3, input method v2, virtual keyboard v1. + * All three are passive globals for IME / on-screen keyboard support. */ + wl_state.text_input_mgr = + wlr_text_input_manager_v3_create(wl_state.display); + if (wl_state.text_input_mgr) { + wl_state.text_input_new_listener.notify = handle_text_input_new; + wl_signal_add(&wl_state.text_input_mgr->events.text_input, + &wl_state.text_input_new_listener); + wwarning("wl_backend: text-input-v3 created (zwp_text_input_manager_v3)"); + } + wl_state.input_method_mgr = + wlr_input_method_manager_v2_create(wl_state.display); + if (wl_state.input_method_mgr) { + wl_state.input_method_new_listener.notify = handle_input_method_new; + wl_signal_add(&wl_state.input_method_mgr->events.input_method, + &wl_state.input_method_new_listener); + wwarning("wl_backend: input-method-v2 created (zwp_input_method_manager_v2)"); + } + wl_state.virtual_keyboard_mgr = + wlr_virtual_keyboard_manager_v1_create(wl_state.display); + if (wl_state.virtual_keyboard_mgr) { + wl_state.virtual_keyboard_new_listener.notify = handle_virtual_keyboard_new; + wl_signal_add(&wl_state.virtual_keyboard_mgr->events.new_virtual_keyboard, + &wl_state.virtual_keyboard_new_listener); + wwarning("wl_backend: virtual-keyboard-v1 created (zwp_virtual_keyboard_manager_v1)"); + } + + /* Phase 35: foreign toplevel management — lets taskbars enumerate windows. + * xdg_activation — focus-token protocol replacing _NET_ACTIVE_WINDOW. */ + wl_state.foreign_toplevel_mgr = + wlr_foreign_toplevel_manager_v1_create(wl_state.display); + if (wl_state.foreign_toplevel_mgr) + wwarning("wl_backend: foreign-toplevel-management created (zwlr_foreign_toplevel_management_v1)"); + wl_state.xdg_activation = wlr_xdg_activation_v1_create(wl_state.display); + if (wl_state.xdg_activation) { + wl_state.xdg_activation_request_listener.notify = handle_xdg_activation_request; + wl_signal_add(&wl_state.xdg_activation->events.request_activate, + &wl_state.xdg_activation_request_listener); + wwarning("wl_backend: xdg-activation created (xdg_activation_v1)"); + } + + /* Phase 36: layer shell — panels, docks, wallpapers, lock screens. */ + wl_state.layer_shell = wlr_layer_shell_v1_create(wl_state.display, 4); + if (wl_state.layer_shell) { + wl_state.layer_shell_new_surface_listener.notify = + handle_layer_shell_new_surface; + wl_signal_add(&wl_state.layer_shell->events.new_surface, + &wl_state.layer_shell_new_surface_listener); + wwarning("wl_backend: layer-shell created (zwlr_layer_shell_v1)"); + } + + /* Phase 37: clipboard manager (wl-clipboard/cliphist), input inhibitor + * (lock screen input gating), virtual pointer (remote desktop / VNC), + * KDE idle protocol, xdg-foreign v2 (cross-client window embedding). */ + wl_state.data_control_mgr = + wlr_data_control_manager_v1_create(wl_state.display); + if (wl_state.data_control_mgr) + wwarning("wl_backend: data-control created (zwlr_data_control_manager_v1)"); + + /* zwlr_input_inhibitor_v1 removed from wlroots 0.18 — use ext-session-lock-v1 */ + + wl_state.virtual_pointer_mgr = + wlr_virtual_pointer_manager_v1_create(wl_state.display); + if (wl_state.virtual_pointer_mgr) { + wl_state.virtual_pointer_new_listener.notify = + handle_virtual_pointer_new; + wl_signal_add(&wl_state.virtual_pointer_mgr->events.new_virtual_pointer, + &wl_state.virtual_pointer_new_listener); + wwarning("wl_backend: virtual-pointer created (zwlr_virtual_pointer_manager_v1)"); + } + + /* org_kde_kwin_idle (wlr_idle) removed from wlroots 0.18 — use ext-idle-notify-v1 */ + + wl_state.xdg_foreign_registry = + wlr_xdg_foreign_registry_create(wl_state.display); + if (wl_state.xdg_foreign_registry) { + wl_state.xdg_foreign_v2 = + wlr_xdg_foreign_v2_create(wl_state.display, + wl_state.xdg_foreign_registry); + if (wl_state.xdg_foreign_v2) + wwarning("wl_backend: xdg-foreign-v2 created (zxdg_exporter/importer_v2)"); + } + + wl_state.xdg_shell = wlr_xdg_shell_create(wl_state.display, 3); + + /* Advertise wmaker-popup-v1 — clients use it to drive popup positioning. */ + wl_state.wmaker_popup_manager = wl_global_create(wl_state.display, + &wmaker_popup_manager_v1_interface, 1, NULL, + wmaker_popup_manager_bind); + if (wl_state.wmaker_popup_manager) + wwarning("wl_backend: wmaker-popup-v1 manager created"); + + /* Phase 64: advertise zxdg_decoration_manager_v1 so clients that + * support server-side decorations can negotiate; we respond with + * CLIENT_SIDE since the Wayland path has no X11 frame rendering. */ + wl_state.decoration_manager = + wlr_xdg_decoration_manager_v1_create(wl_state.display); + if (wl_state.decoration_manager) { + wl_state.decoration_new_listener.notify = handle_new_toplevel_decoration; + wl_signal_add(&wl_state.decoration_manager->events.new_toplevel_decoration, + &wl_state.decoration_new_listener); + wwarning("wl_backend: xdg-decoration-manager created"); + } + + /* Phase 36: advertise wl_shm (needed by SHM buffer clients). */ + wlr_renderer_init_wl_shm(wl_state.renderer, wl_state.display); + /* wl_output global is created in handle_backend_new_output. */ + + /* Phase 43/53: In wlroots 0.19, the headless backend has no input devices. + * Real keyboard/pointer devices arrive via handle_backend_new_input when + * the nested Wayland backend starts. We pre-create the default XKB keymap + * here so wl_keysym_to_keycode() can resolve key bindings during StartUp() + * before any real keyboard device has been received. */ + { + struct xkb_context *xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (xkb_ctx) { + struct xkb_rule_names rules = { + .rules = getenv("XKB_DEFAULT_RULES"), + .model = getenv("XKB_DEFAULT_MODEL"), + .layout = getenv("XKB_DEFAULT_LAYOUT"), + .variant = getenv("XKB_DEFAULT_VARIANT"), + .options = getenv("XKB_DEFAULT_OPTIONS"), + }; + wl_state.xkb_keymap = xkb_keymap_new_from_names( + xkb_ctx, &rules, XKB_KEYMAP_COMPILE_NO_FLAGS); + xkb_context_unref(xkb_ctx); + if (wl_state.xkb_keymap) { + wwarning("wl_backend: default XKB keymap created for key binding resolution"); + /* Apply to any keyboard that was wired before keymap was ready, + * but only if it doesn't already have one from the host. */ + if (wl_state.keyboard_dev) { + struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev); + if (!kb->xkb_state) + wlr_keyboard_set_keymap(kb, wl_state.xkb_keymap); + } + } + } + } + /* Advertise keyboard+pointer capability unconditionally so clients do not + * need to wait for a real device to appear before binding wl_keyboard / wl_pointer. */ + if (wl_state.seat) + wlr_seat_set_capabilities(wl_state.seat, + WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_POINTER); + + /* Phase 38: initialise per-toplevel tracking and context store. */ + wl_list_init(&wl_state.toplevel_list); + wl_list_init(&wl_state.or_list); + wl_list_init(&wl_state.frame_list); + wl_list_init(&wl_state.layer_list); + { + int ki; + for (ki = 0; ki < 3; ki++) + wl_list_init(&wl_state.ctx_lists[ki]); + } + wl_state.next_synthetic_id = 0x10000000UL; + wl_state.managed_surface_count = 0; + + /* Phase 36: listen for new XDG toplevels (wlroots 0.19+). */ + wl_state.xdg_new_surface_listener.notify = handle_xdg_new_toplevel; + wl_signal_add(&wl_state.xdg_shell->events.new_toplevel, + &wl_state.xdg_new_surface_listener); + + /* Path 1: XWayland bridge — start Xwayland eagerly (lazy=false) so that + * the compositor can open a Display* from its socket and pass it back to + * main.c. WindowMaker's own X11 UI (title bars, root menus, dock) then + * renders through XWayland, making the WM visible on-screen. + * The ready signal fires once Xwayland's socket accepts connections. */ + wl_state.xwayland = wlr_xwayland_create(wl_state.display, + wl_state.compositor, false); + if (wl_state.xwayland) { + /* Wire server destroy signal to detect early failure (e.g. stale + * display lock → Xwayland exits before ready fires). */ + if (wl_state.xwayland->server) { + wl_state.xwayland_server_destroy_listener.notify = + handle_xwayland_server_destroy; + wl_signal_add(&wl_state.xwayland->server->events.destroy, + &wl_state.xwayland_server_destroy_listener); + } + /* Wire ready signal BEFORE new_surface so Display* is set before + * any X11 window events arrive. */ + wl_state.xwayland_ready_listener.notify = handle_xwayland_ready; + wl_signal_add(&wl_state.xwayland->events.ready, + &wl_state.xwayland_ready_listener); + wl_state.xwayland_new_surface_listener.notify = handle_xwayland_new_surface; + wl_signal_add(&wl_state.xwayland->events.new_surface, + &wl_state.xwayland_new_surface_listener); + wlr_xwayland_set_seat(wl_state.xwayland, wl_state.seat); + /* display_name may already be set (display number reserved by wlroots + * synchronously); set DISPLAY early as a best-effort so that any + * code between here and the ready signal can still find the socket. */ + if (wl_state.xwayland->display_name) { + setenv("DISPLAY", wl_state.xwayland->display_name, 1); + wwarning("wl_backend: XWayland starting DISPLAY=%s (eager mode)", + wl_state.xwayland->display_name); + } else { + wwarning("wl_backend: XWayland created (display_name pending)"); + } + } else { + wwarning("wl_backend: XWayland unavailable (X11 clients will not work)"); + } + + /* Phase 37: periodic timer to drive client frame callbacks at ~60 fps. + * Using a timer avoids the swapchain/allocator complexity of the full + * render pipeline while still keeping Wayland clients unblocked. */ + { + struct wl_event_loop *loop = + wl_display_get_event_loop(wl_state.display); + wl_state.frame_timer = + wl_event_loop_add_timer(loop, frame_timer_cb, NULL); + if (wl_state.frame_timer) + wl_event_source_timer_update(wl_state.frame_timer, 16); + } + + /* Phase 54: initialise virtual pointer at screen centre. */ + wl_state.pointer_x = 960; + wl_state.pointer_y = 540; + wwarning("wl_backend: virtual pointer initialised at %d,%d", + wl_state.pointer_x, wl_state.pointer_y); + + /* Self-pipe shutdown: signal handler writes a byte; the event loop reads + * it here and calls wl_display_terminate() from within loop context, + * avoiding the async-signal-safety issues of calling wl_display_destroy() + * directly from a signal handler. */ + wl_state.shutdown_pipe[0] = wl_state.shutdown_pipe[1] = -1; + wl_state.shutdown_source = NULL; + wl_state.sigfd = -1; + wl_state.sigfd_source = NULL; + if (pipe(wl_state.shutdown_pipe) == 0) { + struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display); + /* Make read end non-blocking so the drain loop in the handler returns + * instead of blocking after all bytes are consumed. */ + fcntl(wl_state.shutdown_pipe[0], F_SETFL, O_NONBLOCK); + wl_state.shutdown_source = wl_event_loop_add_fd(loop, + wl_state.shutdown_pipe[0], + WL_EVENT_READABLE, + wl_shutdown_pipe_handler, + NULL); + } else { + wwarning("wl_backend: shutdown pipe creation failed; SIGTERM may crash"); + } + + /* Phase 20: if a real keyboard was found from the nested backend + * (captured in handle_backend_new_input during wlr_backend_start), + * switch the seat to it now so keyboard focus notifications use the + * correct keymap and modifier state from the host compositor. This + * must happen after the headless keyboard setup above (which may have + * called wlr_seat_set_keyboard with the headless device). */ + if (wl_state.real_keyboard_dev) + wlr_seat_set_keyboard(wl_state.seat, + wlr_keyboard_from_input_device(wl_state.real_keyboard_dev)); + + wl_state.started = 1; + + /* Block until XWayland fires the "ready" signal — pump the Wayland server + * event loop so the XWayland process can complete its handshake. + * handle_xwayland_ready() calls XOpenDisplay and stores the result in + * wl_state.x_display, then sets xwayland_ready = true to break the loop. + * If XWayland is not available, return NULL (pure Wayland path). */ + /* XWayland is lazy — it starts when the first X11 client connects. + * No need to wait for it at startup. Mark as ready so the rest of + * init proceeds without blocking. */ + if (wl_state.xwayland) { + wwarning("wl_backend: XWayland configured (eager mode) — ready at startup"); + wl_state.xwayland_ready = true; + if (wl_state.x_display) { + wwarning("wl_backend: XWayland bridge ready — Display*=%p, full WM UI enabled", + (void *)wl_state.x_display); + /* Start a pump thread to keep the Wayland event loop running + * while the main thread calls WMCreateScreenWithRContext() and + * other blocking X11 init in StartUp(). XWayland needs the + * Wayland compositor to be responsive to handle round-trips like + * XGetModifierMapping (which queries keyboard state from wlroots). + * The x11_event_source fd watcher is registered later in + * wl_event_loop_run() after this thread exits, so the pump + * thread never touches Xlib and there is no Xlib contention. */ + wl_state.startup_pump_running = 1; + if (pthread_create(&wl_state.startup_pump_thread, NULL, + wl_startup_pump_thread, NULL) != 0) { + wwarning("wl_backend: pthread_create for pump thread failed — " + "WINGs init may deadlock"); + wl_state.startup_pump_running = 0; + } + } else { + wwarning("wl_backend: XWayland ready but XOpenDisplay failed — pure Wayland mode"); + } + } + + return NULL; /* dpy=NULL so WM core skips Xlib calls */ +} + +static void +wl_display_close(void) +{ + if (!wl_state.started) + return; + + /* Remove the X11 event source and close the Xlib bridge connection + * before killing XWayland so we don't get stale readable events. */ + if (wl_state.x11_event_source) { + wl_event_source_remove(wl_state.x11_event_source); + wl_state.x11_event_source = NULL; + } + if (wl_state.x_display) { + XCloseDisplay(wl_state.x_display); + wl_state.x_display = NULL; + } + + /* Phase 17: shut down XWayland. + * + * wlroots 0.15 bug: wlr_xwayland_server_destroy() calls wl_client_destroy() + * on the XWayland Wayland client. After any Wayland client has connected + * and disconnected during the session, the XWayland server's wl_client ends + * up in an inconsistent state: the client struct is freed but server->client + * is NOT nulled. wlr_xwayland_server_destroy() then calls wl_client_destroy() + * on the freed struct and crashes in wl_list_remove(). + * + * Workaround: skip wlr_xwayland_destroy() entirely. The Xwayland process is + * a child of wmaker and will receive SIGHUP/SIGKILL when the parent exits. + * The X11 socket is in a tmpdir removed on exit. wl_display_destroy() (called + * below) handles all remaining Wayland client/resource cleanup. + */ + if (wl_state.xwayland) { + /* wlroots 0.15 bug: wlr_xwayland_server_destroy() (called from the + * display destroy signal below) sends SIGTERM to Xwayland and calls + * waitpid(). If Xwayland is blocked waiting for compositor events, + * waitpid() hangs indefinitely. + * + * Fix: pre-kill Xwayland with SIGKILL. SIGKILL cannot be caught or + * deferred, so the process dies immediately (kernel-synchronous). + * The zombie remains until wlroots' waitpid() reaps it — which now + * returns instantly. The Wayland client struct (server->client) is + * still valid because the event loop is stopped and the socket closure + * has not been dispatched, so wlroots' wl_client_destroy() succeeds. */ + if (wl_state.xwayland->server) { + int xdisp = wl_state.xwayland->server->display; + if (wl_state.xwayland->server->pid > 0) + kill(wl_state.xwayland->server->pid, SIGKILL); + /* wlroots 0.15 bug: after any Wayland client has connected and + * disconnected during the session, server->client holds a freed + * (stale) wl_client pointer. wlr_xwayland_server_destroy() (fired + * from the display destroy signal below) calls wl_client_destroy() + * on this freed pointer, which corrupts the list and triggers + * SIGABRT. Null the pointer so wlroots skips the destroy call. + * If the client is still valid, wl_display_destroy_clients() will + * clean it up; if it is stale, it has already been removed from + * the display's client list and nothing more is needed. */ + wl_state.xwayland->server->client = NULL; + /* Clean up X lock file and Unix socket left by SIGKILL'd Xwayland. + * Without this, the next run may find a stale lock file whose PID + * has been reused by an unrelated process, causing wlroots to + * refuse to reclaim the display number → "xcb connect failed". */ + { + char path[64]; + snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp); + unlink(path); + snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp); + unlink(path); + } + } else if (wl_state.xwayland->display_name && + wl_state.xwayland->display_name[0] == ':') { + /* Path 1: server exited early (e.g. stale lock) before wl_display_close + * ran — wlroots already freed server and set it to NULL. Clean up + * any lock file that Xwayland created before dying. */ + int xdisp = atoi(wl_state.xwayland->display_name + 1); + char path[64]; + snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp); + unlink(path); + snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp); + unlink(path); + } + wl_state.xwayland = NULL; + unsetenv("DISPLAY"); + } + /* Clean up the signalfd source (registered in wl_event_loop_run). */ + if (wl_state.sigfd_source) { + wl_event_source_remove(wl_state.sigfd_source); + wl_state.sigfd_source = NULL; + } + if (wl_state.sigfd >= 0) { + close(wl_state.sigfd); + wl_state.sigfd = -1; + } + /* Clean up the shutdown self-pipe (remove event source before display + * destruction so the fd is not left in the epoll set). */ + if (wl_state.shutdown_source) { + wl_event_source_remove(wl_state.shutdown_source); + wl_state.shutdown_source = NULL; + } + if (wl_state.shutdown_pipe[0] >= 0) { + close(wl_state.shutdown_pipe[0]); + wl_state.shutdown_pipe[0] = -1; + } + if (wl_state.shutdown_pipe[1] >= 0) { + close(wl_state.shutdown_pipe[1]); + wl_state.shutdown_pipe[1] = -1; + } + /* Remove all globally-registered wl_listener nodes before calling + * wl_display_destroy(). wl_display_destroy() frees wlroots objects + * (xdg_shell, backend, keyboard, pointer, output, etc.), which frees + * their signal structs. Any wl_listener.link that is still in one of + * those signal lists after the struct is freed becomes a dangling pointer. + * Subsequent wl_list operations (in wayland-server cleanup or by glibc's + * heap allocator detecting corruption) then trigger SIGABRT. + * Guard each removal with a link.next check: wayland-server zeroes the + * link after wl_list_remove(), so a non-NULL link means the listener is + * currently inserted in a signal list. */ +#define WL_LISTENER_REMOVE(l) \ + do { if ((l).link.next) wl_list_remove(&(l).link); } while (0) + + /* Output frame and new-output listeners */ + WL_LISTENER_REMOVE(wl_state.output_frame_listener); + WL_LISTENER_REMOVE(wl_state.backend_new_output_listener); + /* XDG shell and decoration listeners */ + WL_LISTENER_REMOVE(wl_state.xdg_new_surface_listener); + WL_LISTENER_REMOVE(wl_state.decoration_new_listener); + /* XWayland listeners (xwayland ptr already nulled in step 1, + * but listener links are still inserted in the (now-orphaned) xwayland + * object's signal lists — remove them to prevent heap corruption). */ + WL_LISTENER_REMOVE(wl_state.xwayland_ready_listener); + WL_LISTENER_REMOVE(wl_state.xwayland_new_surface_listener); + WL_LISTENER_REMOVE(wl_state.xwayland_server_destroy_listener); + /* Keyboard listeners */ + WL_LISTENER_REMOVE(wl_state.keyboard_key_listener); + WL_LISTENER_REMOVE(wl_state.keyboard_modifiers_listener); + /* Pointer listeners */ + WL_LISTENER_REMOVE(wl_state.pointer_motion_listener); + WL_LISTENER_REMOVE(wl_state.pointer_motion_abs_listener); + WL_LISTENER_REMOVE(wl_state.pointer_button_listener); + WL_LISTENER_REMOVE(wl_state.pointer_frame_listener); + /* Phase 20: backend new_input listener and real device listeners */ + WL_LISTENER_REMOVE(wl_state.backend_new_input_listener); + WL_LISTENER_REMOVE(wl_state.real_keyboard_key_listener); + WL_LISTENER_REMOVE(wl_state.real_keyboard_modifiers_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_motion_abs_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_button_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_frame_listener); + /* Phase 24/25: seat cursor + selection listeners */ + WL_LISTENER_REMOVE(wl_state.seat_request_cursor_listener); + WL_LISTENER_REMOVE(wl_state.seat_request_set_selection_listener); + /* Phase 26/28: primary selection + idle inhibitor listeners */ + WL_LISTENER_REMOVE(wl_state.seat_request_primary_selection_listener); + WL_LISTENER_REMOVE(wl_state.idle_new_inhibitor_listener); + /* Phase 29: axis listeners */ + WL_LISTENER_REMOVE(wl_state.pointer_axis_listener); + WL_LISTENER_REMOVE(wl_state.real_pointer_axis_listener); + /* Phase 30/31: pointer constraints + shortcuts inhibitor listeners */ + WL_LISTENER_REMOVE(wl_state.new_constraint_listener); + WL_LISTENER_REMOVE(wl_state.new_shortcuts_inhibitor_listener); + /* Phase 33: output management + output power listeners */ + WL_LISTENER_REMOVE(wl_state.output_mgr_apply_listener); + WL_LISTENER_REMOVE(wl_state.output_mgr_test_listener); + WL_LISTENER_REMOVE(wl_state.output_power_set_mode_listener); + /* Phase 34: text input, input method, virtual keyboard listeners */ + WL_LISTENER_REMOVE(wl_state.text_input_new_listener); + WL_LISTENER_REMOVE(wl_state.input_method_new_listener); + WL_LISTENER_REMOVE(wl_state.virtual_keyboard_new_listener); + /* Phase 35: xdg_activation listener */ + WL_LISTENER_REMOVE(wl_state.xdg_activation_request_listener); + /* Phase 36: layer shell new_surface listener */ + WL_LISTENER_REMOVE(wl_state.layer_shell_new_surface_listener); + /* Phase 37: virtual pointer listener */ + WL_LISTENER_REMOVE(wl_state.virtual_pointer_new_listener); +#undef WL_LISTENER_REMOVE + + /* Phase 23: destroy cursor and xcursor manager before display teardown. */ + if (wl_state.xcursor_mgr) { + wlr_xcursor_manager_destroy(wl_state.xcursor_mgr); + wl_state.xcursor_mgr = NULL; + } + if (wl_state.cursor) { + wlr_cursor_destroy(wl_state.cursor); + wl_state.cursor = NULL; + } + + /* Skip wl_display_destroy() and wl_display_destroy_clients(). + * + * wl_display_destroy() fires the display's destroy signal which triggers + * wlroots 0.15 internal object teardown. That teardown has a double- + * remove bug in the XWayland path: wlroots registers a listener on the + * Xwayland wl_client's destroy signal; that listener self-removes during + * client destruction, and wlr_xwayland_server_destroy() then tries to + * wl_list_remove() the already-nulled link, corrupting the heap + * (SIGABRT). + * + * wl_display_destroy_clients() also blocks: wlroots' XWayland client + * destroy handler performs blocking cleanup that can hang if Xwayland + * is in the middle of protocol dispatch. + * + * Both calls are unnecessary here because Exit() calls exit(0) immediately + * after wl_display_close() returns. The kernel reclaims all resources: + * - file descriptors closed → all Wayland clients get POLLHUP + * - memory freed by OS + * - Xwayland already SIGKILL'd in step 1 + * - event sources removed in step 2 + * - all our signal listeners removed in step 3 */ + unsetenv("WAYLAND_DISPLAY"); + wl_state.renderer = NULL; + wl_state.backend = NULL; + wl_state.headless_inner = NULL; + wl_state.wl_backend_inner = NULL; + wl_state.display = NULL; + wl_state.output = NULL; + wl_state.frame_timer = NULL; + wl_state.output_layout = NULL; + wl_state.scene_output = NULL; + wl_state.scene = NULL; + wl_state.allocator = NULL; + wl_state.started = 0; +} + +static void +wl_display_post_open(const char *display_name, int *multi_head_out) +{ + (void)display_name; + (void)multi_head_out; +} + +static int +wl_display_screen_count(void) +{ + return wl_state.started ? 1 : 0; +} + +static int +wl_display_default_screen(void) +{ + return 0; +} + +static unsigned long +wl_atom_intern(const char *name) +{ + /* The WM's own UI runs on XWayland — it needs real X11 atoms for + * property operations (WM_STATE, WM_PROTOCOLS, etc.). */ + if (wl_state.x_display) + return (unsigned long)XInternAtom(wl_state.x_display, name, False); + return 0; +} diff --git a/src/backend/wayland/wl_event.c b/src/backend/wayland/wl_event.c new file mode 100644 index 0000000..318faab --- /dev/null +++ b/src/backend/wayland/wl_event.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include "../../screen.h" +#include "wl_types.h" + + +void wl_event_flush(void) +{ + /* Flush pending Xlib requests to XWayland so drawing becomes visible. */ + if (wl_state.x_display) + XFlush(wl_state.x_display); +} +void wl_event_flush_expose(void) {} +void wl_event_sync(int discard) +{ + if (wl_state.x_display) + XSync(wl_state.x_display, discard ? True : False); +} +int wl_event_pending(void) { return 0; } +void wl_event_drain_motion(void) {} +void wl_event_discard_enter_leave_until(unsigned long timestamp) { (void)timestamp; } +void wl_event_handle_extensions(WMEvent *event) { (void)event; } +void wl_event_randr_update(WMEvent *event) { (void)event; } diff --git a/src/backend/wayland/wl_overlay.c b/src/backend/wayland/wl_overlay.c new file mode 100644 index 0000000..1586362 --- /dev/null +++ b/src/backend/wayland/wl_overlay.c @@ -0,0 +1,355 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include "../../screen.h" +#include "wl_types.h" +#include + +/* Forward declarations for helpers defined in wl_backend.c */ +struct wm_shm_surface *wm_shm_surface_create(int width, int height); +void wm_shm_surface_destroy(struct wm_shm_surface *s); +void wm_shm_surface_commit(struct wm_shm_surface *s, + struct wlr_scene_tree *parent, int x, int y); +void wm_shm_surface_fill(struct wm_shm_surface *s, + int x, int y, int w, int h, uint32_t argb); + +/* ------------------------------------------------------------------ */ +/* Phase 81: Overlay animation surface — XOR pixel engine + bitmap */ +/* font for geometry display (miniaturize zoom, transparent */ +/* move/resize frames, position/size readouts). */ +/* ------------------------------------------------------------------ */ + +/* + * XOR mask: 0xFFFFFFFF XOR 0x00000000 (transparent) = 0xFFFFFFFF (opaque + * white); XOR 0xFFFFFFFF again = 0x00000000 (transparent, i.e. erased). + * The overlay scene buffer is composited OVER all client windows via + * standard alpha blending, so transparent pixels show through to the + * desktop and opaque-white pixels appear on top. + */ +#define OVL_MASK 0xFFFFFFFFu + +/* + * 5-wide × 9-tall bitmap font for printable ASCII (0x20–0x7E). + * Encoding: bit 7 = column 0 (left), bit 3 = column 4 (right), bits 2–0 + * unused. Cell width = 6 px (glyph 5 + 1 gap); cell height = 10 px. + */ +#define OVL_GW 5 /* glyph pixel width */ +#define OVL_GH 9 /* glyph pixel height */ +#define OVL_CW 6 /* cell width (px) */ +#define OVL_CH 10 /* cell height (px) */ + +const uint8_t ovl_font[96][OVL_GH] = { + /*20 ' '*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /*21 '!'*/{0x20,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x00}, + /*22 '"'*/{0x50,0x50,0x50,0x00,0x00,0x00,0x00,0x00,0x00}, + /*23 '#'*/{0x50,0x50,0xF8,0x50,0xF8,0x50,0x50,0x00,0x00}, + /*24 '$'*/{0x20,0x70,0xA0,0x70,0x28,0x70,0x20,0x00,0x00}, + /*25 '%'*/{0xC0,0xC8,0x10,0x20,0x40,0x98,0x18,0x00,0x00}, + /*26 '&'*/{0x60,0x90,0x60,0xA0,0x98,0x90,0x68,0x00,0x00}, + /*27'\''*/ {0x60,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00}, + /*28 '('*/{0x10,0x20,0x40,0x40,0x40,0x20,0x10,0x00,0x00}, + /*29 ')'*/{0x40,0x20,0x10,0x10,0x10,0x20,0x40,0x00,0x00}, + /*2A '*'*/{0x00,0x88,0x50,0xF8,0x50,0x88,0x00,0x00,0x00}, + /*2B '+'*/{0x00,0x20,0x20,0xF8,0x20,0x20,0x00,0x00,0x00}, + /*2C ','*/{0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00}, + /*2D '-'*/{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00}, + /*2E '.'*/{0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00}, + /*2F '/'*/{0x08,0x08,0x10,0x20,0x40,0x80,0x80,0x00,0x00}, + /*30 '0'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*31 '1'*/{0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*32 '2'*/{0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00,0x00}, + /*33 '3'*/{0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00,0x00}, + /*34 '4'*/{0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00,0x00}, + /*35 '5'*/{0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00,0x00}, + /*36 '6'*/{0x38,0x40,0x80,0xF0,0x88,0x88,0x70,0x00,0x00}, + /*37 '7'*/{0xF8,0x08,0x10,0x20,0x20,0x20,0x20,0x00,0x00}, + /*38 '8'*/{0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00,0x00}, + /*39 '9'*/{0x70,0x88,0x88,0x78,0x08,0x08,0x70,0x00,0x00}, + /*3A ':'*/{0x00,0x60,0x60,0x00,0x60,0x60,0x00,0x00,0x00}, + /*3B ';'*/{0x00,0x60,0x60,0x00,0x60,0x20,0x40,0x00,0x00}, + /*3C '<'*/{0x10,0x20,0x40,0x80,0x40,0x20,0x10,0x00,0x00}, + /*3D '='*/{0x00,0x00,0xF8,0x00,0xF8,0x00,0x00,0x00,0x00}, + /*3E '>'*/{0x40,0x20,0x10,0x08,0x10,0x20,0x40,0x00,0x00}, + /*3F '?'*/{0x70,0x88,0x08,0x10,0x20,0x00,0x20,0x00,0x00}, + /*40 '@'*/{0x70,0x88,0x98,0xA8,0x98,0x80,0x70,0x00,0x00}, + /*41 'A'*/{0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00,0x00}, + /*42 'B'*/{0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00,0x00}, + /*43 'C'*/{0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00,0x00}, + /*44 'D'*/{0xE0,0x90,0x88,0x88,0x88,0x90,0xE0,0x00,0x00}, + /*45 'E'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00,0x00}, + /*46 'F'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00,0x00}, + /*47 'G'*/{0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00,0x00}, + /*48 'H'*/{0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00,0x00}, + /*49 'I'*/{0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*4A 'J'*/{0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x00}, + /*4B 'K'*/{0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00,0x00}, + /*4C 'L'*/{0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00}, + /*4D 'M'*/{0x88,0xD8,0xA8,0x88,0x88,0x88,0x88,0x00,0x00}, + /*4E 'N'*/{0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00,0x00}, + /*4F 'O'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*50 'P'*/{0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00,0x00}, + /*51 'Q'*/{0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00,0x00}, + /*52 'R'*/{0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00,0x00}, + /*53 'S'*/{0x70,0x88,0x80,0x70,0x08,0x88,0x70,0x00,0x00}, + /*54 'T'*/{0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x00}, + /*55 'U'*/{0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00}, + /*56 'V'*/{0x88,0x88,0x88,0x88,0x50,0x50,0x20,0x00,0x00}, + /*57 'W'*/{0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00,0x00}, + /*58 'X'*/{0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00,0x00}, + /*59 'Y'*/{0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00,0x00}, + /*5A 'Z'*/{0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00,0x00}, + /*5B '['*/{0x70,0x40,0x40,0x40,0x40,0x40,0x70,0x00,0x00}, + /*5C'\\'*/ {0x80,0x80,0x40,0x20,0x10,0x08,0x08,0x00,0x00}, + /*5D ']'*/{0x70,0x10,0x10,0x10,0x10,0x10,0x70,0x00,0x00}, + /*5E '^'*/{0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00}, + /*5F '_'*/{0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x00}, + /*60 '`'*/{0x40,0x20,0x10,0x00,0x00,0x00,0x00,0x00,0x00}, + /*61 'a'*/{0x00,0x00,0x70,0x08,0x78,0x88,0x78,0x00,0x00}, + /*62 'b'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0xF0,0x00,0x00}, + /*63 'c'*/{0x00,0x00,0x70,0x88,0x80,0x88,0x70,0x00,0x00}, + /*64 'd'*/{0x08,0x08,0x78,0x88,0x88,0x88,0x78,0x00,0x00}, + /*65 'e'*/{0x00,0x00,0x70,0x88,0xF8,0x80,0x70,0x00,0x00}, + /*66 'f'*/{0x30,0x48,0x40,0xE0,0x40,0x40,0x40,0x00,0x00}, + /*67 'g'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x70,0x00}, + /*68 'h'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, + /*69 'i'*/{0x20,0x00,0x60,0x20,0x20,0x20,0x70,0x00,0x00}, + /*6A 'j'*/{0x10,0x00,0x30,0x10,0x10,0x10,0x90,0x60,0x00}, + /*6B 'k'*/{0x80,0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00,0x00}, + /*6C 'l'*/{0x60,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00}, + /*6D 'm'*/{0x00,0x00,0xD8,0xA8,0xA8,0xA8,0x88,0x00,0x00}, + /*6E 'n'*/{0x00,0x00,0xF0,0x88,0x88,0x88,0x88,0x00,0x00}, + /*6F 'o'*/{0x00,0x00,0x70,0x88,0x88,0x88,0x70,0x00,0x00}, + /*70 'p'*/{0x00,0x00,0xF0,0x88,0x88,0xF0,0x80,0x80,0x00}, + /*71 'q'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x08,0x00}, + /*72 'r'*/{0x00,0x00,0xB0,0xC8,0x80,0x80,0x80,0x00,0x00}, + /*73 's'*/{0x00,0x00,0x70,0x80,0x70,0x08,0xF0,0x00,0x00}, + /*74 't'*/{0x40,0x40,0xF0,0x40,0x40,0x48,0x30,0x00,0x00}, + /*75 'u'*/{0x00,0x00,0x88,0x88,0x88,0x98,0x68,0x00,0x00}, + /*76 'v'*/{0x00,0x00,0x88,0x88,0x88,0x50,0x20,0x00,0x00}, + /*77 'w'*/{0x00,0x00,0x88,0x88,0xA8,0xD8,0x88,0x00,0x00}, + /*78 'x'*/{0x00,0x00,0x88,0x50,0x20,0x50,0x88,0x00,0x00}, + /*79 'y'*/{0x00,0x00,0x88,0x88,0x78,0x08,0x70,0x00,0x00}, + /*7A 'z'*/{0x00,0x00,0xF8,0x10,0x20,0x40,0xF8,0x00,0x00}, + /*7B '{'*/{0x18,0x20,0x20,0x40,0x20,0x20,0x18,0x00,0x00}, + /*7C '|'*/{0x20,0x20,0x20,0x00,0x20,0x20,0x20,0x00,0x00}, + /*7D '}'*/{0x60,0x10,0x10,0x08,0x10,0x10,0x60,0x00,0x00}, + /*7E '~'*/{0x40,0xA8,0x18,0x00,0x00,0x00,0x00,0x00,0x00}, +}; + +/* Lazily allocate (or resize) the full-screen overlay buffer. */ +void +overlay_ensure(WScreen *scr) +{ + int w, h; + if (scr && scr->scr_width > 0 && scr->scr_height > 0) { + w = scr->scr_width; + h = scr->scr_height; + } else if (wl_state.output) { + w = (int)wl_state.output->width; + h = (int)wl_state.output->height; + } else { + w = 1920; h = 1080; + } + if (wl_state.overlay_surf && + wl_state.overlay_surf->width == w && + wl_state.overlay_surf->height == h) + return; + if (wl_state.overlay_surf) { + wm_shm_surface_destroy(wl_state.overlay_surf); + wl_state.overlay_surf = NULL; + } + wl_state.overlay_surf = wm_shm_surface_create(w, h); +} + +/* XOR a single pixel on the overlay buffer (clipped). */ +static inline void +ovl_plot(struct wm_shm_surface *s, int x, int y) +{ + if ((unsigned)x < (unsigned)s->width && (unsigned)y < (unsigned)s->height) + s->data[y * (int)(s->stride >> 2) + x] ^= OVL_MASK; +} + +/* XOR a horizontal run of `len` pixels starting at (x, y). */ +void +ovl_hline(struct wm_shm_surface *s, int x, int y, int len) +{ + if ((unsigned)y >= (unsigned)s->height || len <= 0) return; + int x0 = x < 0 ? 0 : x; + int x1 = x + len; if (x1 > s->width) x1 = s->width; + if (x0 >= x1) return; + uint32_t *p = s->data + y * (int)(s->stride >> 2) + x0; + for (int i = x0; i < x1; i++) *p++ ^= OVL_MASK; +} + +/* XOR a vertical run of `len` pixels starting at (x, y). */ +void +ovl_vline(struct wm_shm_surface *s, int x, int y, int len) +{ + if ((unsigned)x >= (unsigned)s->width || len <= 0) return; + int y0 = y < 0 ? 0 : y; + int y1 = y + len; if (y1 > s->height) y1 = s->height; + int stride = (int)(s->stride >> 2); + for (int j = y0; j < y1; j++) + s->data[j * stride + x] ^= OVL_MASK; +} + +/* Bresenham line (with fast h/v paths). */ +void +ovl_line(struct wm_shm_surface *s, int x0, int y0, int x1, int y1) +{ + if (x0 == x1) { + int y = y0 < y1 ? y0 : y1; + ovl_vline(s, x0, y, abs(y1 - y0) + 1); + return; + } + if (y0 == y1) { + int x = x0 < x1 ? x0 : x1; + ovl_hline(s, x, y0, abs(x1 - x0) + 1); + return; + } + int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + for (;;) { + ovl_plot(s, x0, y0); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } +} + +/* Render one glyph at cell origin (cx, cy) using XOR. */ +void +ovl_draw_glyph(struct wm_shm_surface *s, int cx, int cy, unsigned char ch) +{ + if (ch < 0x20 || ch > 0x7E) ch = '?'; + const uint8_t *g = ovl_font[ch - 0x20]; + for (int row = 0; row < OVL_GH; row++) { + uint8_t bits = g[row]; + if (!bits) continue; + int py = cy + row; + if (py < 0 || py >= s->height) continue; + for (int col = 0; col < OVL_GW; col++) { + if (bits & (0x80u >> col)) + ovl_plot(s, cx + col, py); + } + } +} + +/* Push the modified overlay buffer to the scene and render a frame. */ +void +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. */ + /* 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) { + /* Ensure the node is visible for the animation frame. */ + wlr_scene_node_set_enabled(&s->scene_buf->node, true); + /* Notify wlroots that the pixel data has changed. */ + wlr_scene_buffer_set_buffer_with_damage(s->scene_buf, &s->wlr_buf, NULL); + } + /* Commit the scene immediately so the animation step is visible + * before the caller's wusleep() delay. */ + if (wl_state.scene_output && wl_state.allocator) + wlr_scene_output_commit(wl_state.scene_output, NULL); +} + +/* ------------------------------------------------------------------ */ +/* Server grab */ +/* ------------------------------------------------------------------ */ + +void wl_server_grab(void) {} /* no-op: Wayland double-buffers for us */ +void wl_server_ungrab(void) +{ + overlay_flush(); + /* Disable the overlay scene node so it doesn't intercept hit-testing. + * The XOR animation has already erased its pixels; leaving the node + * enabled blocks frame_buf_at (wlr_scene_node_at hits the overlay + * before any frame_buf). */ + if (wl_state.overlay_surf && wl_state.overlay_surf->scene_buf) + wlr_scene_node_set_enabled(&wl_state.overlay_surf->scene_buf->node, false); +} + +/* ------------------------------------------------------------------ */ +/* Overlays */ +/* ------------------------------------------------------------------ */ + +void +wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2) +{ + overlay_ensure(scr); + if (wl_state.overlay_surf) { + ovl_line(wl_state.overlay_surf, x1, y1, x2, y2); + overlay_flush(); + } +} + +void +wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || n < 2) return; + for (int i = 0; i < n - 1; i++) + ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y); +} + +void +wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || w <= 0 || h <= 0) return; + /* Matching XDrawRectangle semantics: corners are shared between + * adjacent edges and therefore XOR'd twice (become transparent). */ + ovl_hline(s, x, y, w + 1); /* top */ + ovl_hline(s, x, y + h, w + 1); /* bottom */ + ovl_vline(s, x, y, h + 1); /* left */ + ovl_vline(s, x + w, y, h + 1); /* right */ + overlay_flush(); +} + +void +wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s) return; + for (int i = 0; i < n; i++) + ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2); +} + +void +wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || !str) return; + /* y is the text baseline; render from the top of the cell. */ + int top = y - (OVL_GH - 2); + for (int i = 0; i < len; i++) + ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]); +} + +int +wl_overlay_text_width(WScreen *scr, const char *str, int n) +{ + (void)scr; (void)str; + return n * OVL_CW; +} diff --git a/src/backend/wayland/wl_props.c b/src/backend/wayland/wl_props.c new file mode 100644 index 0000000..5e81030 --- /dev/null +++ b/src/backend/wayland/wl_props.c @@ -0,0 +1,377 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../window.h" +#include "wl_types.h" + + if (fb) { + if (win_x) { *win_x = fb->x; } + if (win_y) { *win_y = fb->y; } + if (win_w) { *win_w = fb->width; } + if (win_h) { *win_h = fb->height; } + return 1; + } + /* Synthetic child IDs (titlebar, buttons) — return minimal geometry. */ + if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; } + if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; } + return 0; +} + +void wl_window_add_events(WNativeWindow win, unsigned long extra_mask) { (void)win; (void)extra_mask; } +void wl_window_select_events(WNativeWindow win, unsigned long mask) { (void)win; (void)mask; } + +void wl_window_shape_clear(WWindow *wwin) { (void)wwin; } +void wl_window_shape_set(WWindow *wwin) { (void)wwin; } + +/* ------------------------------------------------------------------ */ +/* Overlays */ +/* ------------------------------------------------------------------ */ + +void +wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2) +{ + overlay_ensure(scr); + if (wl_state.overlay_surf) { + ovl_line(wl_state.overlay_surf, x1, y1, x2, y2); + overlay_flush(); + } +} + +void +wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || n < 2) return; + for (int i = 0; i < n - 1; i++) + ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y); +} + +void +wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || w <= 0 || h <= 0) return; + /* Matching XDrawRectangle semantics: corners are shared between + * adjacent edges and therefore XOR'd twice (become transparent). */ + ovl_hline(s, x, y, w + 1); /* top */ + ovl_hline(s, x, y + h, w + 1); /* bottom */ + ovl_vline(s, x, y, h + 1); /* left */ + ovl_vline(s, x + w, y, h + 1); /* right */ + overlay_flush(); +} + +void +wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s) return; + for (int i = 0; i < n; i++) + ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2); +} + +void +wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || !str) return; + /* y is the text baseline; render from the top of the cell. */ + int top = y - (OVL_GH - 2); + for (int i = 0; i < len; i++) + ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]); +} + +int +wl_overlay_text_width(WScreen *scr, const char *str, int n) +{ + (void)scr; (void)str; + return n * OVL_CW; +} + +/* ------------------------------------------------------------------ */ +/* Client */ +/* ------------------------------------------------------------------ */ + +int +wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out) +{ + /* Phase 42: return real map_state and geometry from XDG surface. + * Phase 17: XWayland path uses xw_surface->mapped and width/height. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + memset(out, 0, sizeof(*out)); + if (v && v->xw_surface) { + if (v->xw_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640; + out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + } else if (v && v->xdg_surface && v->xdg_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xdg_surface->surface->current.width; + out->height = v->xdg_surface->surface->current.height; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + return 1; +} + +void +wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out) +{ + (void)win; + out->depth = 24; out->visual = NULL; out->colormap = 0; +} + +void +wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask) +{ (void)win; (void)event_mask; } + +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) { + /* Move client's scene tree under the frame's tree */ + if (parent_fb->tree) + wlr_scene_node_reparent(&v->scene_node->node, parent_fb->tree); + /* Position at relative offset within the tree */ + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + } + return; + } + + + + /* For WINGs internal windows: create a child frame_buf so the dialog + * content is visible in the scene graph. */ + if (client >= 0x20000000 && new_parent >= 0x10000000) { + /* client is a WINGs view ID, new_parent is a frame_buf */ + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + if (!parent_fb) return; + + /* Check if we already have a frame_buf for this client */ + struct wl_frame_buf *existing = frame_buf_find(client); + if (existing) { + /* Just update parent and position */ + existing->parent_id = new_parent; + existing->rel_x = x; + existing->rel_y = y; + existing->x = parent_fb->x + x; + existing->y = parent_fb->y + y; + if (existing->scene_buf) { + /* Ensure it's under the parent frame's tree, then position + * at relative offset within the tree. */ + if (parent_fb->tree && + existing->scene_buf->node.parent != parent_fb->tree) { + wlr_scene_node_reparent(&existing->scene_buf->node, + parent_fb->tree); + } + if (parent_fb->tree) + wlr_scene_node_set_position(&existing->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&existing->scene_buf->node, + existing->x, existing->y); + } + return; + } + + /* Create a new frame_buf for this WINGs view. + * Get size from the view itself. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)client); + int w = view ? view->size.width : parent_fb->width; + int h = view ? view->size.height : (parent_fb->height - y); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + if (!fb->image) { wfree(fb); return; } + + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + + fb->id = client; /* use the WINGs view ID directly */ + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + fb->width = w; + fb->height = h; + + if (wl_state.scene) { + /* Place the WINGs content under the parent frame's tree so it + * moves with the WM-managed dialog decorations. */ + struct wlr_scene_tree *_wp = parent_fb->tree + ? parent_fb->tree + : (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) { + fb_attach_scene_buf_listener(fb); + /* If under the frame tree, use relative coords; otherwise absolute. */ + if (parent_fb->tree) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + + /* Connect the WINGs view's backing store to this frame_buf's image */ + extern void W_SetViewBacking(unsigned long id, void *img); + W_SetViewBacking((unsigned long)client, fb->image); + } + /* Reparent an existing frame_buf into a new parent */ + else if (client >= 0x10000000 && new_parent >= 0x10000000) { + struct wl_frame_buf *fb = frame_buf_find(client); + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + fprintf(stderr, "reparent_framebuf: client=0x%lx fb=%p new_parent=0x%lx parent_fb=%p\n", + (unsigned long)client, (void*)fb, + (unsigned long)new_parent, (void*)parent_fb); + if (fb) fprintf(stderr, " fb: tree=%p scene_buf=%p pos=(%d,%d)\n", + (void*)fb->tree, (void*)fb->scene_buf, + fb->tree ? fb->tree->node.x : -1, + fb->tree ? fb->tree->node.y : -1); + if (parent_fb) fprintf(stderr, " parent_fb: tree=%p pos=(%d,%d)\n", + (void*)parent_fb->tree, + parent_fb->tree ? parent_fb->tree->node.x : -1, + parent_fb->tree ? parent_fb->tree->node.y : -1); + if (fb && parent_fb) { + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + if (fb->tree) { + /* This frame_buf has a tree (e.g. fake_leader): reparent + * the whole tree so all children move with it. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->tree->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->tree->node, x, y); + } else if (fb->scene_buf) { + /* Plain frame_buf (no tree): reparent scene_buf directly. */ + if (parent_fb->tree) + wlr_scene_node_reparent(&fb->scene_buf->node, parent_fb->tree); + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } + /* Update absolute tracking recursively for all descendants */ + frame_buf_update_children_abs(client); + } + } +} + +/* Called by WINGs after a view repaints — commit its frame_buf to scene graph */ +void wl_view_commit_backing_impl(unsigned long id) +{ + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id); + WNativeWindow saved_parent_id = 0; + int saved_rel_x = 0, saved_rel_y = 0; + /* If the view was resized, the old frame_buf's image is stale. */ + if (fb) { + extern void *W_GetViewBacking(unsigned long id); + void *current = W_GetViewBacking((unsigned long)id); + if (current != (void *)fb->image) { + /* Save parent relation before destroying — the new frame_buf + * must be positioned within the same parent tree so dialog + * content reparented via wl_client_reparent stays visible. */ + saved_parent_id = fb->parent_id; + saved_rel_x = fb->rel_x; + saved_rel_y = fb->rel_y; + wlr_scene_node_destroy(&fb->scene_buf->node); + wl_list_remove(&fb->link); + wlr_buffer_drop(&fb->base); + wfree(fb); + fb = NULL; + } + } + if (!fb) { + extern W_View *W_GetViewForWindow(unsigned long id); + extern void *W_GetViewBacking(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)id); + if (view && view->flags.topLevel) { + fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + fb->id = id; + fb->parent_id = saved_parent_id; + fb->rel_x = saved_rel_x; + fb->rel_y = saved_rel_y; + fb->x = view->pos.x; + fb->y = view->pos.y; + fb->width = view->size.width; + fb->height = view->size.height; + fb->image = (pixman_image_t *)W_GetViewBacking(id); + if (fb->image) { + pixman_image_ref(fb->image); + wlr_buffer_init(&fb->base, &frame_buf_impl, + pixman_image_get_width(fb->image), + pixman_image_get_height(fb->image)); + if (wl_state.scene) { + struct wlr_scene_tree *parent = NULL; + /* Re-parent under the saved parent's tree so + * dialogs reparented via wl_client_reparent stay + * correctly positioned within their frame. */ + if (saved_parent_id) { + struct wl_frame_buf *pfb = + frame_buf_find(saved_parent_id); + if (pfb && pfb->tree) + parent = pfb->tree; + } + if (!parent) + parent = 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(parent, &fb->base); + if (fb->scene_buf) { + fb_attach_scene_buf_listener(fb); + /* Position relative to parent's tree or absolute */ + if (parent != &wl_state.scene->tree && saved_parent_id) + wlr_scene_node_set_position(&fb->scene_buf->node, + saved_rel_x, saved_rel_y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, + fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + wl_list_insert(&wl_state.frame_list, &fb->link); + } else { + wfree(fb); + fb = NULL; + } + } + } + if (fb && fb->scene_buf) { + /* Sync position from the WINGs view — it may have moved since + * the frame_buf was created (e.g. switch panel is positioned + * after widget creation via WMMoveWidget). \ No newline at end of file diff --git a/src/backend/wayland/wl_stacking.c b/src/backend/wayland/wl_stacking.c new file mode 100644 index 0000000..fc73c46 --- /dev/null +++ b/src/backend/wayland/wl_stacking.c @@ -0,0 +1,175 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include "../../screen.h" +#include "wl_types.h" + +struct wl_frame_buf *frame_buf_find(WNativeWindow id); +struct wl_toplevel_view *wl_find_view_by_id(WNativeWindow id); + + +/* ------------------------------------------------------------------ */ +/* 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). */ +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. */ +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; +} + +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); +} + +void +wl_stacking_raise(WNativeWindow win) +{ + 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, &v->link); + if (v->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(v->frame_id); + if (fb && fb->tree) { + wlr_scene_node_raise_to_top(&fb->tree->node); + return; + } + } + if (v->scene_node) + wlr_scene_node_raise_to_top(&v->scene_node->node); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + 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); + } + return; + } +} + +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); + } +} + +void +wl_stacking_restack(WNativeWindow *windows, int count) +{ + /* 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) { + /* 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); + wl_list_insert(&wl_state.toplevel_list, &v->link); + if (v->scene_node) + wlr_scene_node_raise_to_top(&v->scene_node->node); + } + } +} + +int +wl_stacking_query(WNativeWindow root, + WNativeWindow **out, unsigned int *count) +{ + /* Phase 58: return synthetic IDs in Z-order (top to bottom). + * RemakeStackList will find no WM_CTX_STACK entries for them + * and set window_count=0, but returning 1 avoids the "could not + * get window list" warning. */ + struct wl_toplevel_view *v; + unsigned int n = 0, i = 0; + WNativeWindow *wins; + (void)root; + + wl_list_for_each(v, &wl_state.toplevel_list, link) n++; + if (n == 0) { *out = NULL; *count = 0; return 1; } + + wins = wmalloc(n * sizeof(WNativeWindow)); + wl_list_for_each(v, &wl_state.toplevel_list, link) wins[i++] = v->id; + *out = wins; *count = n; + return 1; +} diff --git a/src/backend/wayland/wl_types.h b/src/backend/wayland/wl_types.h index 9354d22..e261c2b 100644 --- a/src/backend/wayland/wl_types.h +++ b/src/backend/wayland/wl_types.h @@ -7,6 +7,17 @@ #include /* Phase 29+: WM-owned SHM surface buffers */ #include "wl_backend.h" +/* SHM surface — pixman-backed wlr_buffer for WM-owned rendering */ +struct wm_shm_surface { + struct wlr_buffer wlr_buf; + uint32_t *data; + int width; + int height; + size_t stride; + pixman_image_t *image; + struct wlr_scene_buffer *scene_buf; +}; + /* ------------------------------------------------------------------ */ /* Phase 38: per-toplevel view and context store */ /* ------------------------------------------------------------------ */ @@ -380,3 +391,35 @@ void * wl_context_find(WNativeWindow win, WContextKey key); int wl_context_delete(WNativeWindow win, WContextKey key); #endif /* WL_TYPES_H */ + +/* wl_event.c */ +void wl_event_flush(void); +void wl_event_flush_expose(void); +void wl_event_sync(int discard); +int wl_event_pending(void); +void wl_event_drain_motion(void); +void wl_event_discard_enter_leave_until(unsigned long timestamp); +void wl_event_handle_extensions(WMEvent *event); +void wl_event_randr_update(WMEvent *event); + +void wl_stacking_set_level(WNativeWindow win, int level); +void wl_stacking_raise(WNativeWindow win); +void wl_stacking_lower(WNativeWindow win); +void wl_stacking_restack(WNativeWindow *windows, int count); +int wl_stacking_query(WNativeWindow root, WNativeWindow **out, unsigned int *count); + +void wl_server_grab(void); +void wl_server_ungrab(void); +void wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2); +void wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n); +void wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h); +void wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n); +void wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len); +int wl_overlay_text_width(WScreen *scr, const char *str, int n); + +/* Overlay font metrics */ +#define OVL_GH 9 +#define OVL_CW 6 + +void wl_window_shape_set(WWindow *wwin); + diff --git a/src/backend/wayland/wl_window.c b/src/backend/wayland/wl_window.c new file mode 100644 index 0000000..3bfe30d --- /dev/null +++ b/src/backend/wayland/wl_window.c @@ -0,0 +1,598 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../window.h" +#include "../../framewin.h" +#include "wl_types.h" + + +int +wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out) +{ + /* Phase 42: return real map_state and geometry from XDG surface. + * Phase 17: XWayland path uses xw_surface->mapped and width/height. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + memset(out, 0, sizeof(*out)); + if (v && v->xw_surface) { + if (v->xw_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640; + out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + } else if (v && v->xdg_surface && v->xdg_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xdg_surface->surface->current.width; + out->height = v->xdg_surface->surface->current.height; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + return 1; +} + +void +wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out) +{ + (void)win; + out->depth = 24; out->visual = NULL; out->colormap = 0; +} + +void +wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask) +{ (void)win; (void)event_mask; } + +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) { + /* Move client's scene tree under the frame's tree */ + if (parent_fb->tree) + wlr_scene_node_reparent(&v->scene_node->node, parent_fb->tree); + /* Position at relative offset within the tree */ + wlr_scene_node_set_position(&v->scene_node->node, x, y); + } + } + return; + } + + + + /* For WINGs internal windows: create a child frame_buf so the dialog + * content is visible in the scene graph. */ + if (client >= 0x20000000 && new_parent >= 0x10000000) { + /* client is a WINGs view ID, new_parent is a frame_buf */ + struct wl_frame_buf *parent_fb = frame_buf_find(new_parent); + if (!parent_fb) return; + + /* Check if we already have a frame_buf for this client */ + struct wl_frame_buf *existing = frame_buf_find(client); + if (existing) { + /* Just update parent and position */ + existing->parent_id = new_parent; + existing->rel_x = x; + existing->rel_y = y; + existing->x = parent_fb->x + x; + existing->y = parent_fb->y + y; + if (existing->scene_buf) { + /* Ensure it's under the parent frame's tree, then position + * at relative offset within the tree. */ + if (parent_fb->tree && + existing->scene_buf->node.parent != parent_fb->tree) { + wlr_scene_node_reparent(&existing->scene_buf->node, + parent_fb->tree); + } + if (parent_fb->tree) + wlr_scene_node_set_position(&existing->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&existing->scene_buf->node, + existing->x, existing->y); + } + return; + } + + /* Create a new frame_buf for this WINGs view. + * Get size from the view itself. */ + extern W_View *W_GetViewForWindow(unsigned long id); + W_View *view = W_GetViewForWindow((unsigned long)client); + int w = view ? view->size.width : parent_fb->width; + int h = view ? view->size.height : (parent_fb->height - y); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + if (!fb->image) { wfree(fb); return; } + + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + + fb->id = client; /* use the WINGs view ID directly */ + fb->parent_id = new_parent; + fb->rel_x = x; + fb->rel_y = y; + fb->x = parent_fb->x + x; + fb->y = parent_fb->y + y; + fb->width = w; + fb->height = h; + + if (wl_state.scene) { + /* Place the WINGs content under the parent frame's tree so it + * moves with the WM-managed dialog decorations. */ + struct wlr_scene_tree *_wp = parent_fb->tree + ? parent_fb->tree + : (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) { + fb_attach_scene_buf_listener(fb); + /* If under the frame tree, use relative coords; otherwise absolute. */ + if (parent_fb->tree) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + else + wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y); + wlr_scene_node_set_enabled(&fb->scene_buf->node, false); + } + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + + /* Connect the WINGs view's backing store to this frame_buf's image */ + extern void W_SetViewBacking(unsigned long id, void *img); + W_SetViewBacking((unsigned long)client, fb->image); + } + /* Reparent an existing frame_buf into a new parent */ + +void +wl_window_map(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, true); + return; + } + if (v) + return; + /* Frame buffer: enable its scene node and children. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && fb->scene_buf) { + /* If this is a child, only enable if parent is enabled */ + if (fb->parent_id) { + struct wl_frame_buf *parent = frame_buf_find(fb->parent_id); + if (parent && parent->scene_buf && + !parent->scene_buf->node.enabled) + return; /* parent not visible yet */ + } + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + /* Raise the tree (raises entire window assembly together) */ + 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) + wlr_scene_node_raise_to_top(&fb->scene_buf->node); + /* Also enable children (and grandchildren) */ + 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, true); + struct wl_frame_buf *gc; + wl_list_for_each(gc, &wl_state.frame_list, link) { + if (gc->parent_id == child->id && gc->scene_buf) + wlr_scene_node_set_enabled(&gc->scene_buf->node, true); + } + } + } + return; + } +} + +void +wl_window_map_raised(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, true); + if (v->frame_id) { + struct wl_frame_buf *fb = frame_buf_find(v->frame_id); + if (fb && fb->tree) { + wlr_scene_node_raise_to_top(&fb->tree->node); + return; + } + } + wlr_scene_node_raise_to_top(&v->scene_node->node); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && fb->scene_buf) { + wlr_scene_node_set_enabled(&fb->scene_buf->node, true); + if (fb->tree) { + /* Top-level frame: enable tree (decoration children become visible). */ + wlr_scene_node_set_enabled(&fb->tree->node, true); + wlr_scene_node_raise_to_top(&fb->tree->node); + } else { + wlr_scene_node_raise_to_top(&fb->scene_buf->node); + } + return; + } +} +void wl_window_map_subwindows(WNativeWindow parent) +{ + (void)parent; + /* Child windows are sub-regions of the frame buffer — no-op. */ +} + +void +wl_window_unmap(WNativeWindow win) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v && v->scene_node) { + wlr_scene_node_set_enabled(&v->scene_node->node, false); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + 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); +} +void wl_window_move(WNativeWindow win, int x, int y) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v) { + 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) { + /* Scene node is a child of the frame tree — use relative coords */ + if (v->frame_id) + wlr_scene_node_set_position(&v->scene_node->node, x, y); + else + 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)v->x, (int16_t)v->y, + v->xw_surface->width, v->xw_surface->height); + /* Re-apply any popups whose parent is this toplevel so they + * track the parent's position. */ + struct wl_toplevel_view *p; + wl_list_for_each(p, &wl_state.toplevel_list, link) { + if (p->popup && p->popup_parent + && wl_find_view_by_xdg_surface(p->popup_parent->base) == v) { + wmaker_popup_apply(p); + } + } + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + fb->rel_x = x; fb->rel_y = y; + if (fb->parent_id) { + struct wl_frame_buf *parent = frame_buf_find(fb->parent_id); + if (parent) { + fb->x = parent->x + x; + fb->y = parent->y + y; + } else { + fb->x = x; fb->y = y; + } + /* Child frame_buf: reposition scene_buf at relative offset */ + if (fb->scene_buf) + wlr_scene_node_set_position(&fb->scene_buf->node, x, y); + } else { + fb->x = x; fb->y = y; + /* Top-level frame: move the tree (children follow automatically) */ + if (fb->tree) + wlr_scene_node_set_position(&fb->tree->node, x, y); + } + /* Update absolute position tracking for hit-testing — recursively + * for all descendants so grandchildren (e.g. WINGs panel inside + * a fake_leader inside a WMaker frame) stay in sync. */ + frame_buf_update_children_abs(win); + return; + } +} + +void wl_window_resize(WNativeWindow win, int w, int h) +{ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + if (v) { + if (w <= 0 || h <= 0) return; + if (v->xw_surface) { + wlr_xwayland_surface_configure(v->xw_surface, + (int16_t)v->x, (int16_t)v->y, (uint16_t)w, (uint16_t)h); + return; + } + if (!v->xdg_surface || !v->xdg_surface->toplevel) return; + wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)w, (uint32_t)h); + wlr_xdg_surface_schedule_configure(v->xdg_surface); + return; + } + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb && w > 0 && h > 0 && (w != fb->width || h != fb->height)) { + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + fb->width = w; + fb->height = h; + fb->base.width = w; + fb->base.height = h; + /* Update scene graph's cached buffer size for hit-testing. */ + if (fb->scene_buf) + wlr_scene_buffer_set_dest_size(fb->scene_buf, w, h); + } +} +void wl_window_set_border(WNativeWindow win, unsigned long pixel) +{ + (void)win; (void)pixel; /* borders are drawn into the frame buffer */ +} +void wl_window_set_border_width(WNativeWindow win, int width) +{ + (void)win; (void)width; /* no-op for native frames */ +} +void wl_window_set_save_under(WNativeWindow win, int save_under) { (void)win; (void)save_under; } +void wl_noop_window_set_background(WNativeWindow win, unsigned long pixel) { (void)win; (void)pixel; } +extern void *W_GetViewBacking(unsigned long id); + +void wl_window_set_background_pixmap(WNativeWindow win, unsigned long pixmap) +{ + if (pixmap == 0) return; + struct wl_frame_buf *fb = frame_buf_find(win); + pixman_image_t *dst_img = NULL; + + if (fb && fb->image) { + dst_img = fb->image; + fb->bg_pixmap = (pixman_image_t *)(uintptr_t)pixmap; + } else { + dst_img = (pixman_image_t *)W_GetViewBacking(win); + } + if (!dst_img) return; + + pixman_image_t *src = (pixman_image_t *)(uintptr_t)pixmap; + int src_w = pixman_image_get_width(src); + int src_h = pixman_image_get_height(src); + + /* Blit source pixmap into frame_buf's image */ + pixman_image_composite32(PIXMAN_OP_SRC, + src, /* src */ + NULL, /* mask */ + dst_img, /* dest */ + 0, 0, /* src x,y */ + 0, 0, /* mask x,y */ + 0, 0, /* dest x,y */ + src_w < pixman_image_get_width(dst_img) ? src_w : pixman_image_get_width(dst_img), + src_h < pixman_image_get_height(dst_img) ? src_h : pixman_image_get_height(dst_img)); + if (fb) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +void wl_pixmap_free(unsigned long pixmap) +{ + if (pixmap == 0) return; + pixman_image_t *img = (pixman_image_t *)(uintptr_t)pixmap; + pixman_image_unref(img); +} +void wl_window_clear(WNativeWindow win) +{ + struct wl_frame_buf *fb = frame_buf_find(win); + pixman_image_t *img = NULL; + + if (fb && fb->image) { + img = fb->image; + } else { + img = (pixman_image_t *)W_GetViewBacking(win); + } + if (!img) return; + + int width = pixman_image_get_width(img); + int height = pixman_image_get_height(img); + + /* If a background pixmap is set, repaint from it (like XClearWindow) */ + if (fb && fb->bg_pixmap) { + pixman_image_composite32(PIXMAN_OP_SRC, + fb->bg_pixmap, NULL, img, + 0, 0, 0, 0, 0, 0, + width, height); + } else { + /* No background pixmap — fill with transparent black */ + int stride = pixman_image_get_stride(img); + uint32_t *data = (uint32_t *)pixman_image_get_data(img); + memset(data, 0, stride * height); + } + + /* Notify the scene buffer of the update */ + if (fb) + wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL); +} + +int +wl_window_get_rect(WNativeWindow win, + int *win_x, int *win_y, int *win_w, int *win_h) +{ + /* Phase 55: return real geometry for synthetic toplevel IDs. */ + if (win == WNATIVE_NONE) { + if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; } + if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; } + return 0; /* invalid/none */ + } + struct wl_toplevel_view *v = wl_find_view_by_id(win); + /* Phase 17: XWayland geometry. */ + if (v && v->xw_surface) { + if (win_x) { *win_x = v->x; } + if (win_y) { *win_y = v->y; } + if (win_w) { *win_w = v->xw_surface->width > 0 ? v->xw_surface->width : 1; } + if (win_h) { *win_h = v->xw_surface->height > 0 ? v->xw_surface->height : 1; } + return 1; + } + if (v && v->xdg_surface && v->xdg_surface->surface) { + int w = v->xdg_surface->surface->current.width; + int h = v->xdg_surface->surface->current.height; + if (win_x) { *win_x = v->x; } + if (win_y) { *win_y = v->y; } + if (win_w) { *win_w = w > 0 ? w : 1; } + if (win_h) { *win_h = h > 0 ? h : 1; } + return 1; + } + /* Frame buffer geometry. */ + struct wl_frame_buf *fb = frame_buf_find(win); + if (fb) { + if (win_x) { *win_x = fb->x; } + if (win_y) { *win_y = fb->y; } + if (win_w) { *win_w = fb->width; } + if (win_h) { *win_h = fb->height; } + return 1; + } + /* Synthetic child IDs (titlebar, buttons) — return minimal geometry. */ + if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; } + if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; } + return 0; +} + +void wl_window_add_events(WNativeWindow win, unsigned long extra_mask) { (void)win; (void)extra_mask; } +void wl_window_select_events(WNativeWindow win, unsigned long mask) { (void)win; (void)mask; } + +void wl_window_shape_clear(WWindow *wwin) { (void)wwin; } +void wl_window_shape_set(WWindow *wwin) { (void)wwin; } + +/* ------------------------------------------------------------------ */ +/* Overlays */ +/* ------------------------------------------------------------------ */ + +void +wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2) +{ + overlay_ensure(scr); + if (wl_state.overlay_surf) { + ovl_line(wl_state.overlay_surf, x1, y1, x2, y2); + overlay_flush(); + } +} + +void +wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || n < 2) return; + for (int i = 0; i < n - 1; i++) + ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y); +} + +void +wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || w <= 0 || h <= 0) return; + /* Matching XDrawRectangle semantics: corners are shared between + * adjacent edges and therefore XOR'd twice (become transparent). */ + ovl_hline(s, x, y, w + 1); /* top */ + ovl_hline(s, x, y + h, w + 1); /* bottom */ + ovl_vline(s, x, y, h + 1); /* left */ + ovl_vline(s, x + w, y, h + 1); /* right */ + overlay_flush(); +} + +void +wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s) return; + for (int i = 0; i < n; i++) + ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2); +} + +void +wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len) +{ + overlay_ensure(scr); + struct wm_shm_surface *s = wl_state.overlay_surf; + if (!s || !str) return; + /* y is the text baseline; render from the top of the cell. */ + int top = y - (OVL_GH - 2); + for (int i = 0; i < len; i++) + ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]); +} + +int +wl_overlay_text_width(WScreen *scr, const char *str, int n) +{ + (void)scr; (void)str; + return n * OVL_CW; +} + +/* ------------------------------------------------------------------ */ +/* Client */ +/* ------------------------------------------------------------------ */ + +int +wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out) +{ + /* Phase 42: return real map_state and geometry from XDG surface. + * Phase 17: XWayland path uses xw_surface->mapped and width/height. */ + struct wl_toplevel_view *v = wl_find_view_by_id(win); + memset(out, 0, sizeof(*out)); + if (v && v->xw_surface) { + if (v->xw_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640; + out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + } else if (v && v->xdg_surface && v->xdg_surface->surface) { + out->map_state = WCLIENT_VIEWABLE; + out->width = v->xdg_surface->surface->current.width; + out->height = v->xdg_surface->surface->current.height; + } else { + out->map_state = WCLIENT_UNMAPPED; + } + return 1; +} + +void +wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out) +{ \ No newline at end of file