windowmaker-wl/src/backend/wayland/wl_backend.c

7550 lines
315 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <sys/select.h>
#ifdef HAVE_INOTIFY
#include <sys/inotify.h>
#include <limits.h>
#endif
#include <time.h>
#include <pthread.h>
#include <wayland-server-core.h>
#include <wlr/backend.h>
#include <wlr/backend/headless.h>
#include <wlr/backend/session.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/interfaces/wlr_output.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_subcompositor.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_output_layout.h>
#include <wlr/types/wlr_seat.h>
#include <wlr/types/wlr_keyboard.h>
#include <wlr/types/wlr_xdg_shell.h>
#include "wmaker-popup-v1-server-protocol.h"
#include <wlr/types/wlr_xdg_decoration_v1.h>
#include <wlr/xwayland.h> /* Phase 17: XWayland support */
#include <X11/Xproto.h> /* X_SetInputFocus and other request codes */
#include <wlr/backend/wayland.h> /* Phase 18: nested Wayland backend */
#include <wlr/backend/multi.h> /* Phase 18: multi-backend */
#include <wlr/types/wlr_scene.h> /* Phase 18: scene graph compositing */
#include <wlr/render/allocator.h> /* Phase 18: buffer allocator */
#include <wlr/types/wlr_buffer.h> /* Phase 29+: WM-owned SHM surface buffers */
#include <wlr/interfaces/wlr_buffer.h> /* wlr_buffer_init/wlr_buffer_impl */
#include <libdrm/drm_fourcc.h> /* Phase 29+: DRM_FORMAT_ARGB8888 */
#include <pixman.h> /* Phase 29+: software pixel rendering */
#include <wlr/types/wlr_cursor.h> /* Phase 23: hardware cursor tracking */
#include <wlr/types/wlr_xcursor_manager.h> /* Phase 23: xcursor theme */
#include <wlr/types/wlr_data_device.h> /* Phase 25: clipboard / selection */
#include <wlr/types/wlr_primary_selection.h> /* Phase 26: primary selection (middle-click) */
#include <wlr/types/wlr_primary_selection_v1.h> /* Phase 26: zwp_primary_selection_device_manager_v1 */
#include <wlr/types/wlr_xdg_output_v1.h> /* Phase 27: xdg-output for panel geometry */
#include <wlr/types/wlr_idle_inhibit_v1.h> /* Phase 28: idle inhibit for video/fullscreen */
#include <wlr/types/wlr_viewporter.h> /* Phase 29: wp_viewporter for video scaling */
#include <wlr/types/wlr_relative_pointer_v1.h> /* 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 <wlr/types/wlr_pointer_constraints_v1.h> /* Phase 30: pointer lock for games */
#include <wlr/types/wlr_keyboard_shortcuts_inhibit_v1.h> /* Phase 31: shortcut inhibit for games */
#include <wlr/types/wlr_linux_dmabuf_v1.h> /* Phase 32: zwp_linux_dmabuf_v1 for GPU zero-copy */
#include <wlr/types/wlr_screencopy_v1.h> /* Phase 32: zwlr_screencopy_manager_v1 for screenshots */
#include <wlr/types/wlr_export_dmabuf_v1.h> /* Phase 32: zwlr_export_dmabuf_manager_v1 for screen recorders */
#include <wlr/types/wlr_output_management_v1.h> /* 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 <wlr/types/wlr_output_power_management_v1.h> /* Phase 33: zwlr_output_power_management_v1 (DPMS) */
#include <wlr/types/wlr_gamma_control_v1.h> /* Phase 33: zwlr_gamma_control_manager_v1 (Night Light) */
#include <wlr/types/wlr_presentation_time.h> /* Phase 33: wp_presentation (smooth frame timing) */
#include <wlr/types/wlr_text_input_v3.h> /* Phase 34: zwp_text_input_manager_v3 (IME client) */
#include <wlr/types/wlr_input_method_v2.h> /* Phase 34: zwp_input_method_manager_v2 (IME server) */
#include <wlr/types/wlr_virtual_keyboard_v1.h> /* Phase 34: zwp_virtual_keyboard_manager_v1 */
#include <wlr/types/wlr_foreign_toplevel_management_v1.h> /* Phase 35: zwlr_foreign_toplevel_management_v1 */
#include <wlr/types/wlr_xdg_activation_v1.h> /* 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 <wlr/types/wlr_layer_shell_v1.h> /* Phase 36: zwlr_layer_shell_v1 (panels, docks) */
#include <wlr/types/wlr_data_control_v1.h> /* Phase 37: zwlr_data_control_manager_v1 (clipboard mgr) */
#include <wlr/types/wlr_virtual_pointer_v1.h> /* 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 <wlr/types/wlr_xdg_foreign_registry.h> /* Phase 37: xdg-foreign registry (shared state) */
#include <wlr/types/wlr_xdg_foreign_v2.h> /* Phase 37: zxdg_exporter_v2 / zxdg_importer_v2 */
#include <wlr/util/box.h>
/* Generated xdg-shell protocol implementation (Wayland glue code) */
#include "protocols/xdg-shell-protocol.c"
#include <wlr/util/log.h>
/* Phase 77: XKB for keysym→keycode translation and modifier mask setup */
#include <xkbcommon/xkbcommon.h>
#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 <WINGs/WINGsP.h>
/* ------------------------------------------------------------------ */
#include <WINGs/WMEvent.h>
#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. */
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 */
/* ------------------------------------------------------------------ */
/*
* 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. */
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. */
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;
}
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;
}
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);
}
}
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 now in wl_types.h */
/* ---------- 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 ---------------------------------------------- */
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.
*/
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).
*/
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.
*/
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) */
/* ------------------------------------------------------------------ */
/* 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;
}
/* ------------------------------------------------------------------ */
/* 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; }
/* ------------------------------------------------------------------ */
/* 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; }
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_prop_get_protocols(WNativeWindow window, WProtocols *prots)
{
(void)window;
memset(prots, 0, sizeof(WProtocols));
prots->DELETE_WINDOW = 1; /* All XDG toplevels support close */
}
static unsigned char *wl_noop_prop_get_check_property(WNativeWindow window,
unsigned long hint, unsigned long type, int format, int count, int *ret_count)
{ (void)window; (void)hint; (void)type; (void)format; (void)count;
if (ret_count) *ret_count = 0; return NULL; }
static void wl_noop_prop_set_wmaker_protocols(WNativeWindow root) { (void)root; }
static void wl_noop_prop_set_icon_tile(WScreen *scr, RImage *image) { (void)scr; (void)image; }
static void wl_noop_prop_cleanup(WNativeWindow root) { (void)root; }
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 WNativeWindow wl_noop_prop_get_client_leader(WNativeWindow window) { (void)window; return WNATIVE_NONE; }
static int wl_noop_prop_get_window_state(WNativeWindow window) { (void)window; return -1; }
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_window_get_command(WNativeWindow win, char ***argv_out, int *argc_out)
{
/* Wayland native clients: return [app_id] as a single-arg command.
* Most app_ids match the binary name (e.g. "foot", "firefox") so this
* is sufficient for session restore via PATH lookup or a desktop-file
* Exec= line.
*
* XWayland clients: prefer WM_COMMAND (read from wl_state.x_display)
* and fall back to xw_surface->instance / class. */
*argv_out = NULL;
*argc_out = 0;
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v) return 0;
const char *cmd = NULL;
if (v->xw_surface) {
/* XWayland: try WM_COMMAND first via the X server. */
if (wl_state.x_display && v->xw_surface->window_id) {
char **cmd_argv = NULL;
int cmd_argc = 0;
if (XGetCommand(wl_state.x_display, v->xw_surface->window_id,
&cmd_argv, &cmd_argc) && cmd_argc > 0 && cmd_argv) {
/* Copy to wmaker's allocator */
*argv_out = wmalloc(sizeof(char *) * cmd_argc);
for (int i = 0; i < cmd_argc; i++)
(*argv_out)[i] = wstrdup(cmd_argv[i]);
*argc_out = cmd_argc;
XFreeStringList(cmd_argv);
return 1;
}
}
cmd = v->xw_surface->instance ? v->xw_surface->instance :
v->xw_surface->class;
} else if (v->xdg_surface && v->xdg_surface->toplevel) {
cmd = v->xdg_surface->toplevel->app_id;
}
if (!cmd || !*cmd) return 0;
*argv_out = wmalloc(sizeof(char *));
(*argv_out)[0] = wstrdup(cmd);
*argc_out = 1;
return 1;
}
static int wl_noop_window_get_command(WNativeWindow win, char ***a, int *c) { (void)win; (void)a; *c=0; return 0; }
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);
}
/* ------------------------------------------------------------------ */
/* 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,
};