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

10068 lines
418 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 */
/* ------------------------------------------------------------------ */
struct wl_toplevel_view; /* defined below */
static void wm_dock_app_add(struct wl_toplevel_view *v);
static void wm_dock_app_remove(WNativeWindow id);
/* ------------------------------------------------------------------ */
/* Phase 38: per-toplevel view and context store */
/* ------------------------------------------------------------------ */
/*
* wl_toplevel_view -- one entry per XDG toplevel managed by the compositor.
* Holds the wlroots surface pointer, three lifecycle listeners, and a
* synthetic Window ID used as the policy-layer window handle.
*/
struct wl_toplevel_view {
struct wlr_xdg_surface *xdg_surface;
struct wl_listener map_listener;
struct wl_listener unmap_listener;
struct wl_listener destroy_listener;
struct wl_listener title_listener; /* Phase 46: xdg_toplevel set_title */
struct wl_listener request_move_listener; /* Phase 15 */
struct wl_listener request_resize_listener; /* Phase 15 */
struct wl_listener request_maximize_listener; /* Phase 16 */
struct wl_listener request_fullscreen_listener;/* Phase 16 */
struct wl_listener request_minimize_listener; /* Phase 16 */
struct wl_listener set_app_id_listener; /* Phase 16 */
struct wl_listener commit_listener; /* initial configure */
struct wl_listener surface_commit_listener; /* permanent commit listener */
/* Phase 17: XWayland — set for X11 windows, NULL for XDG toplevels. */
struct wlr_xwayland_surface *xw_surface;
/* Path 1: geometry listener — keeps scene node in sync when X11 window moves. */
struct wl_listener xw_geometry_listener;
struct wl_listener xw_surface_map_listener; /* fires when wlr_surface maps */
/* Phase 18: scene graph tree for this surface (NULL if scene unavailable). */
struct wlr_scene_tree *scene_node;
/* Listener fires when scene_node is destroyed (e.g. when a parent
* tree is destroyed by wl_frame_destroy as part of unmanage).
* Without it, view->scene_node becomes a dangling pointer and the
* later handle_toplevel_destroy crashes calling wlr_scene_node_destroy
* a second time. */
struct wl_listener scene_destroy_listener;
WNativeWindow id; /* synthetic handle, >= 0x10000000 */
WNativeWindow frame_id; /* frame_buf that owns this view (0 = none) */
int frame_rel_x, frame_rel_y; /* offset within frame */
int x, y; /* Phase 55: WM-assigned position */
/* wmaker-popup-v1: client-driven popup position (only valid if popup != NULL) */
struct wl_resource *popup; /* wmaker_popup_v1 resource or NULL */
int popup_x, popup_y;
struct wlr_xdg_toplevel *popup_parent; /* parent toplevel or NULL */
long *wm_state; /* Phase 56: saved WM state or NULL */
int wm_state_n; /* Phase 56: element count */
char **wm_command; /* Phase 57: WM_COMMAND argv or NULL */
int wm_command_n; /* Phase 57: argument count */
/* Phase 35: foreign toplevel handle — exposes this window to taskbars. */
struct wlr_foreign_toplevel_handle_v1 *ft_handle;
struct wl_list link; /* wl_state.toplevel_list */
};
/*
* wl_or_view — tracks an override_redirect XWayland surface (WM frames,
* dock icons, menus, tooltips). These are not "managed" by the WM in the
* ICCCM sense but must be composited by the scene graph.
*/
struct wl_or_view {
struct wlr_xwayland_surface *xw_surface;
struct wlr_scene_tree *scene_node;
struct wl_listener map_listener;
struct wl_listener unmap_listener;
struct wl_listener destroy_listener;
struct wl_listener set_geometry_listener;
struct wl_list link; /* wl_state.or_list */
};
/*
* wl_frame_buf — a pixman-backed scene buffer for WM frame windows.
* Each frame (titlebar + buttons + resizebar) is rendered into a single
* ARGB pixman image and displayed via a wlr_scene_buffer node.
*/
struct wl_frame_buf {
struct wlr_buffer base; /* wlr_buffer (must be first) */
pixman_image_t *image; /* ARGB32 pixel data */
struct wlr_scene_buffer *scene_buf; /* scene graph node */
struct wlr_scene_tree *tree; /* grouping tree (top-level frames only) */
/* Destroy listeners on scene_buf and tree so they get NULL'd when
* wlroots auto-destroys them (e.g. via cascade from parent tree
* destruction during wl_frame_destroy). Without these, code that
* later tries to wlr_scene_node_destroy them again segfaults inside
* wl_list_remove on already-zeroed list links. */
struct wl_listener scene_buf_destroy;
struct wl_listener tree_destroy;
WNativeWindow id; /* synthetic ID (>= 0x10000000) */
WNativeWindow parent_id; /* parent frame_buf ID (0 = root) */
int x, y; /* position in output coords */
int rel_x, rel_y; /* position relative to parent */
int width, height;
pixman_image_t *bg_pixmap; /* background pixmap for window_clear */
struct wl_list link; /* wl_state.frame_list */
};
/*
* Phase 36: wl_layer_view — one entry per mapped zwlr_layer_shell_v1 surface.
* Tracks the wlroots layer surface, its scene node, and lifecycle listeners.
*/
struct wl_layer_view {
struct wlr_layer_surface_v1 *layer_surface;
struct wlr_scene_tree *scene_node;
struct wl_listener map_listener;
struct wl_listener unmap_listener;
struct wl_listener destroy_listener;
struct wl_list link; /* wl_state.layer_list */
};
/*
* Context store: three per-key linked lists keyed by WNativeWindow.
* Backed by a flat list of (win, data) pairs — fine for the small
* number of windows a typical user has open.
*/
struct wl_ctx_entry {
WNativeWindow win;
void *data;
struct wl_list link;
};
/* ------------------------------------------------------------------ */
/* Global compositor state */
/* ------------------------------------------------------------------ */
struct wl_compositor_state {
struct wl_display *display;
struct wlr_backend *backend;
struct wlr_renderer *renderer;
struct wlr_output *output; /* headless output (screen 0) */
struct wlr_output_layout *output_layout; /* Phase 34 */
/* Phase 35: core Wayland protocol globals */
struct wlr_compositor *compositor;
struct wlr_seat *seat;
struct wlr_data_device_manager *data_device_mgr;
struct wlr_xdg_shell *xdg_shell;
/* wmaker-popup-v1 protocol global — client-driven popup positioning */
struct wl_global *wmaker_popup_manager;
/* Phase 64: XDG decoration manager — tell clients to use CSD */
struct wlr_xdg_decoration_manager_v1 *decoration_manager;
struct wl_listener decoration_new_listener;
/* Phase 36: XDG toplevel tracking */
struct wl_listener xdg_new_surface_listener;
int managed_surface_count; /* surfaces seen */
/* Phase 37: periodic timer that drives client frame callbacks */
struct wl_event_source *frame_timer;
/* Phase 38: per-toplevel view list and context store */
struct wl_list toplevel_list; /* wl_toplevel_view.link */
struct wl_list or_list; /* wl_or_view.link */
struct wl_list frame_list; /* wl_frame_buf.link */
struct wl_list layer_list; /* wl_layer_view.link */
struct wl_list ctx_lists[3]; /* one per WContextKey */
unsigned long next_synthetic_id; /* >= 0x10000000 */
/* Phase 43: first real keyboard device (set by handle_backend_new_input) */
struct wlr_input_device *keyboard_dev;
struct xkb_keymap *xkb_keymap; /* default keymap for keysym lookup */
struct wl_listener keyboard_key_listener; /* Phase 78 */
struct wl_listener keyboard_modifiers_listener; /* Phase 78 */
/* Phase 53: first real pointer device (set by handle_backend_new_input) */
struct wlr_input_device *pointer_dev;
/* Phase 54: virtual pointer position (screen coordinates) */
int pointer_x, pointer_y;
/* Phase 14: pointer event listeners wired to pointer_dev->pointer */
struct wl_listener pointer_motion_listener;
struct wl_listener pointer_motion_abs_listener;
struct wl_listener pointer_button_listener;
struct wl_listener pointer_frame_listener;
/* Phase 17: XWayland server for X11 client support */
struct wlr_xwayland *xwayland;
struct wl_listener xwayland_new_surface_listener;
struct wl_listener xwayland_ready_listener;
struct wl_listener xwayland_server_destroy_listener;
bool xwayland_ready; /* set by handle_xwayland_ready */
Display *x_display; /* WM's Xlib connection to own XWayland */
struct wl_event_source *x11_event_source; /* wl_event_loop fd watcher for x_display */
pthread_t startup_pump_thread; /* pumps Wayland event loop during StartUp() */
volatile int startup_pump_running; /* 1 while pump thread is active */
volatile int terminate; /* set to exit main loop */
/* Phase 20: real input devices from nested backend + their listeners */
struct wlr_input_device *real_keyboard_dev;
struct wlr_input_device *real_pointer_dev;
struct wl_listener backend_new_input_listener;
struct wl_listener real_keyboard_key_listener;
struct wl_listener real_keyboard_modifiers_listener;
struct wl_listener real_keyboard_destroy_listener;
struct wl_listener real_pointer_motion_abs_listener;
struct wl_listener real_pointer_button_listener;
struct wl_listener real_pointer_frame_listener;
/* Phase 18: nested Wayland + wlr_scene */
int is_nested; /* 1 if using wlr_wl_backend */
struct wlr_backend *headless_inner; /* headless backend (always) */
struct wlr_backend *wl_backend_inner; /* wl backend (nested only) */
struct wlr_session *session; /* DRM session (standalone only) */
struct wlr_allocator *allocator;
struct wlr_scene *scene;
struct wlr_scene_output *scene_output;
struct wl_listener backend_new_output_listener;
struct wl_listener output_frame_listener;
char socket_name[64]; /* Phase 34: e.g. "wayland-0" */
int started; /* non-zero after successful display_open */
/* Self-pipe for async-signal-safe shutdown: signal handler writes to [1],
* event loop reads [0] and calls wl_display_terminate(). */
int shutdown_pipe[2];
struct wl_event_source *shutdown_source;
/* signalfd-based signal handling: primary mechanism for catching
* SIGTERM/SIGINT/SIGHUP from within the event loop (avoids async-signal-
* safety issues with the traditional signal handler path). */
int sigfd;
struct wl_event_source *sigfd_source;
/* Phase 22: interactive move/resize grab state.
* grab_type: 0=none, 1=move, 2=resize, 3=WM policy grab.
* Type 3 is used by wl_pointer_grab for WM-internal drags (dock icons,
* app icons, menus). It suppresses wl_update_pointer_focus (so client
* surfaces don't steal pointer events) but does NOT intercept motion —
* WME_MOTION events flow normally to the WMMaskEvent loop.
* grab_view: the toplevel being moved/resized (NULL when grab_type==0 or 3).
* grab_start_ptr_{x,y}: pointer position when grab started.
* grab_start_win_{x,y,w,h}: window geometry when grab started.
* grab_resize_edges: WL_SHELL_SURFACE_RESIZE_* bitmask (resize only). */
int grab_type;
struct wl_toplevel_view *grab_view;
int grab_start_ptr_x, grab_start_ptr_y;
int grab_start_win_x, grab_start_win_y;
int grab_start_win_w, grab_start_win_h;
uint32_t grab_resize_edges;
/* Phase 23: hardware cursor and xcursor theme. */
struct wlr_cursor *cursor;
struct wlr_xcursor_manager *xcursor_mgr;
/* Phase 24: seat cursor-surface request listener. */
struct wl_listener seat_request_cursor_listener;
/* Phase 25: clipboard — seat selection request listener. */
struct wl_listener seat_request_set_selection_listener;
/* Phase 26: primary selection (middle-click paste). */
struct wlr_primary_selection_v1_device_manager *primary_selection_mgr;
struct wl_listener seat_request_primary_selection_listener;
/* Phase 27: xdg-output manager for logical output geometry. */
struct wlr_xdg_output_manager_v1 *xdg_output_mgr;
/* Phase 28: idle-inhibit manager for video players / fullscreen apps. */
struct wlr_idle_inhibit_manager_v1 *idle_inhibit_mgr;
struct wl_listener idle_new_inhibitor_listener;
/* Phase 29: wp_viewporter for hardware video scaling / subsurface damage. */
struct wlr_viewporter *viewporter;
/* Phase 30: relative pointer + pointer constraints for games. */
struct wlr_relative_pointer_manager_v1 *relative_pointer_mgr;
struct wlr_pointer_constraints_v1 *pointer_constraints;
struct wl_listener new_constraint_listener;
/* Phase 31: keyboard shortcuts inhibitor for fullscreen / remote desktop. */
struct wlr_keyboard_shortcuts_inhibit_manager_v1 *shortcuts_inhibit_mgr;
struct wl_listener new_shortcuts_inhibitor_listener;
/* Phase 32: GPU zero-copy buffer import, screencopy, GPU framebuffer export. */
struct wlr_linux_dmabuf_v1 *linux_dmabuf;
struct wlr_screencopy_manager_v1 *screencopy_mgr;
struct wlr_export_dmabuf_manager_v1 *export_dmabuf_mgr;
/* Phase 33: output management (randr), output power (DPMS), gamma control, presentation time. */
struct wlr_output_manager_v1 *output_mgr;
struct wl_listener output_mgr_apply_listener;
struct wl_listener output_mgr_test_listener;
struct wlr_output_power_manager_v1 *output_power_mgr;
struct wl_listener output_power_set_mode_listener;
struct wlr_gamma_control_manager_v1 *gamma_control_mgr;
struct wlr_presentation *presentation;
/* Phase 34: text input (IME client side), input method (IME server side), virtual keyboard. */
struct wlr_text_input_manager_v3 *text_input_mgr;
struct wl_listener text_input_new_listener;
struct wlr_input_method_manager_v2 *input_method_mgr;
struct wl_listener input_method_new_listener;
struct wlr_virtual_keyboard_manager_v1 *virtual_keyboard_mgr;
struct wl_listener virtual_keyboard_new_listener;
/* Phase 35: foreign toplevel management + xdg_activation (focus tokens). */
struct wlr_foreign_toplevel_manager_v1 *foreign_toplevel_mgr;
struct wlr_xdg_activation_v1 *xdg_activation;
struct wl_listener xdg_activation_request_listener;
/* Phase 36: layer shell — panels, docks, wallpapers, overlays. */
struct wlr_layer_shell_v1 *layer_shell;
struct wl_listener layer_shell_new_surface_listener;
/* Phase 37: clipboard manager, input inhibitor, virtual pointer, idle, xdg-foreign. */
struct wlr_data_control_manager_v1 *data_control_mgr;
struct wlr_virtual_pointer_manager_v1 *virtual_pointer_mgr;
struct wl_listener virtual_pointer_new_listener;
struct wlr_xdg_foreign_registry *xdg_foreign_registry;
struct wlr_xdg_foreign_v2 *xdg_foreign_v2;
/* Phase 29 axis listeners (headless pointer + real nested pointer). */
struct wl_listener pointer_axis_listener;
struct wl_listener real_pointer_axis_listener;
struct wl_listener real_pointer_destroy_listener;
/* Phase 30: WM dock panel (native Wayland rendering via wm_shm_surface). */
struct wm_shm_surface *dock_surface; /* NULL until screen_create_internals */
int dock_width; /* pixels reserved on right edge */
int dock_icon_sz; /* icon_sz saved by wm_dock_create */
int dock_n_slots; /* slot count saved by wm_dock_create */
/* Phase 30+: managed app list for dock icon rendering */
#define WM_DOCK_MAX_APPS 32
struct {
WNativeWindow id;
uint32_t *icon_argb; /* heap ARGB8888, or NULL */
int icon_w, icon_h;
} dock_apps[WM_DOCK_MAX_APPS];
int dock_app_count;
/* Stacking level trees — one wlr_scene_tree per layer, ordered
* bottom-to-top (index 0 rendered first = furthest back).
*
* Every scene node that participates in the compositor output is
* assigned to exactly one bucket so that creation order within the
* root tree never matters and there are no Z-order surprises.
*
* Named constants (WL_LAYER_*) map to bucket indices:
*
* 0 WL_LAYER_BACKGROUND background rect / wallpaper
* 1 WL_LAYER_DESKTOP WMDesktopLevel (level < -1)
* 2 WL_LAYER_SUNKEN WMSunkenLevel (level == -1)
* 3 WL_LAYER_NORMAL WMNormalLevel (0 <= level < 3)
* 4 WL_LAYER_FLOATING WMFloatingLevel (3 <= level < 20)
* 5 WL_LAYER_MENU WMMainMenuWindowLevel (20), OR-redirect
* 6 WL_LAYER_DOCK WMStatusWindowLevel / WMDockWindowLevel (21-49)
* also the dock panel wm_shm_surface
* 7 WL_LAYER_FULLSCREEN WMFullscreenLevel (50-99)
* 8 WL_LAYER_OVERLAY WMModalPanel / WMPopUp (>=100)
* also the XOR animation overlay surface
*/
#define WL_LAYER_BACKGROUND 0
#define WL_LAYER_DESKTOP 1
#define WL_LAYER_SUNKEN 2
#define WL_LAYER_NORMAL 3
#define WL_LAYER_FLOATING 4
#define WL_LAYER_MENU 5
#define WL_LAYER_DOCK 6
#define WL_LAYER_FULLSCREEN 7
#define WL_LAYER_OVERLAY 8
#define WL_NUM_LEVEL_BUCKETS 9
struct wlr_scene_tree *level_trees[WL_NUM_LEVEL_BUCKETS];
struct wlr_scene_rect *bg_rect; /* default solid background */
struct wlr_scene_buffer *bg_image; /* wallpaper image (if set) */
struct wlr_buffer *bg_wlr_buf; /* backing buffer for bg_image */
/* Phase 81: full-screen overlay surface for XOR animation drawing */
struct wm_shm_surface *overlay_surf; /* NULL until first draw */
/* Focus sink: small off-screen InputOnly X window that receives focus
* when no application window is focused. Mirrors what x11_backend does
* for no_focus_win so that XSetInputFocus calls on this window succeed. */
WNativeWindow no_focus_xwin; /* 0 until screen_create_internals */
/* wrlib RContext drawable — an ordinary InputOutput window that XWM must
* not manage (no WM_CLASS, no backing wl_surface). Stored here so
* handle_xwayland_new_surface can filter it before allocating a view. */
WNativeWindow rcontext_xwin; /* 0 until screen_alloc_gcs */
/* Deferred dock creation: set in screen_create_internals, consumed in
* wl_event_loop_run() after the startup pump thread stops (to avoid racing
* with the pump thread's frame-timer callback on the scene graph). */
WScreen *dock_pending_scr;
/* Button icon RImages (parsed from XPM on first use) */
RImage *btn_close_img;
RImage *btn_iconify_img;
RImage *btn_kill_img;
/* Tracks the frame_buf the pointer was last over (after button resolution),
* so motion handlers can emit synthetic WME_LEAVE/WME_ENTER events when
* the pointer crosses between WM frame sub-windows (titlebar, resizebar,
* buttons). buttonMouseDown's inner press-track loop depends on these. */
WNativeWindow last_hovered_id;
/* Focus guard: when true, the next focus-follows-pointer event is suppressed.
* Set by switchpanel_send_enter (called twice in wSwitchPanelDestroy) to prevent
* spurious focus changes when the Alt-Tab overlay is destroyed and the pointer
* re-enters the root window or a client surface underneath. */
bool ignore_focus_ffp;
/* Keyboard grab: when true, key events are queued for WMMaskEvent but not
* forwarded to Wayland clients (used by Alt-Tab cycling). */
bool keyboard_grabbed;
};
static struct wl_compositor_state wl_state;
/* Forward declarations for helpers used before their definition. */
static int level_to_bucket(int level);
static struct wlr_scene_tree *level_tree_for(int level);
static struct wl_frame_buf *frame_buf_find(WNativeWindow id);
/* ------------------------------------------------------------------ */
/* Phase A: WINGs event backend wiring helpers */
/* ------------------------------------------------------------------ */
/*
* Get the WMScreen (W_Screen) for screen 0. Returns NULL if StartUp()
* hasn't initialised the screen yet.
*/
static WMScreen *
wl_get_wmscreen(void)
{
if (w_global.screen_count == 0)
return NULL;
WScreen *scr = wScreenWithNumber(0);
return scr ? scr->wmscreen : NULL;
}
/*
* Dispatch callback for W_WL_SetLoopDispatch — ticks the Wayland compositor
* event loop so clients stay responsive during WINGs modal loops.
*/
static void
wl_wings_dispatch(int timeout_ms, void *ud)
{
(void)ud;
if (!wl_state.display)
return;
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
wl_display_flush_clients(wl_state.display);
wl_event_loop_dispatch(loop, timeout_ms);
}
/* ------------------------------------------------------------------ */
/* Phase 36 / 37 / 38: XDG surface lifecycle, frame loop, view mgmt */
/* ------------------------------------------------------------------ */
/*
* Phase 37: Wayland event loop timer — drives client frame callbacks.
*/
/* Called from within the event loop when the shutdown self-pipe becomes
* readable. Calling wl_display_terminate() here is safe because we are
* inside wl_event_loop_dispatch(), not in a signal handler. */
static int
wl_shutdown_pipe_handler(int fd, uint32_t mask, void *data)
{
char buf[16];
(void)mask; (void)data;
/* drain the pipe so the fd does not stay readable */
while (read(fd, buf, sizeof(buf)) > 0)
;
wl_state.terminate = 1;
return 0;
}
static int
frame_timer_cb(void *data)
{
(void)data;
if (!wl_state.startup_pump_running) {
if (!wl_state.is_nested && wl_state.output)
wlr_output_send_frame(wl_state.output);
}
if (wl_state.frame_timer)
wl_event_source_timer_update(wl_state.frame_timer, 16);
return 0;
}
/* Helper: recover the view from one of its embedded listeners. */
#define VIEW_FROM_LISTENER(ptr, field) \
((struct wl_toplevel_view *)((char *)(ptr) - offsetof(struct wl_toplevel_view, field)))
/* X error handler — defined near the vtable at the bottom of this file. */
static int wl_catch_xwayland_error(Display *edpy, XErrorEvent *error);
/* Phase 43: forward declaration — defined in the focus section below. */
static void wl_focus_set(WNativeWindow win, int revert_to, unsigned long timestamp);
/* Phase 50: forward declaration — defined in client section below. */
static int wl_client_get_pid(WNativeWindow win);
/* Phase 13: forward declarations — defined in pointer section below. */
static void wl_update_pointer_focus(uint32_t time_msec);
static void wl_pointer_warp_to(int x, int y);
/* wmaker-popup-v1 helper — defined in protocol section below. */
static void wmaker_popup_apply(struct wl_toplevel_view *view);
/* Phase 14: pointer event handler forward declarations. */
static void handle_pointer_motion(struct wl_listener *listener, void *data);
/* Phase 24: click-to-focus helper — defined in Phase 13 pointer section. */
static struct wlr_surface *view_surface_at(int px, int py, double *sx_out,
double *sy_out, struct wl_toplevel_view **view_out);
/* Phase 24: client cursor-surface request handler. */
static void handle_seat_request_cursor(struct wl_listener *listener, void *data);
/* Phase 25: client clipboard selection request handler. */
static void handle_seat_request_set_selection(struct wl_listener *listener, void *data);
/* Phase 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);
/* 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. */
static 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. */
static struct wl_toplevel_view *
wl_find_view_by_xdg_surface(struct wlr_xdg_surface *surf)
{
struct wl_toplevel_view *v;
wl_list_for_each(v, &wl_state.toplevel_list, link) {
if (v->xdg_surface == surf)
return v;
}
return NULL;
}
/*
* Phase 46: XDG toplevel title changed — update wwin->wm_instance.
*/
static void
handle_toplevel_title(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, title_listener);
(void)data;
const char *new_title = view->xdg_surface->toplevel->title;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (wwin->wm_instance)
wfree(wwin->wm_instance);
wwin->wm_instance = new_title ? strdup(new_title) : NULL;
if (wwin->wm_class)
wfree(wwin->wm_class);
const char *app_id = (view->xdg_surface->toplevel->app_id)
? view->xdg_surface->toplevel->app_id : "wayland-client";
wwin->wm_class = strdup(app_id);
wWindowUpdateName(wwin, new_title);
}
/*
* Phase 23: set the cursor image by name from the loaded xcursor theme.
* No-op if cursor or xcursor_mgr was not initialised.
*/
static void
wl_set_cursor_image(const char *name)
{
if (!wl_state.cursor || !wl_state.xcursor_mgr)
return;
wlr_cursor_set_xcursor(wl_state.cursor, wl_state.xcursor_mgr, name);
}
/*
* Phase 23: map WL_SHELL_SURFACE_RESIZE_* edge bitmask to an xcursor name.
*/
static const char *
resize_edges_to_cursor(uint32_t edges)
{
switch (edges & (WL_SHELL_SURFACE_RESIZE_TOP | WL_SHELL_SURFACE_RESIZE_BOTTOM |
WL_SHELL_SURFACE_RESIZE_LEFT | WL_SHELL_SURFACE_RESIZE_RIGHT)) {
case WL_SHELL_SURFACE_RESIZE_TOP: return "n-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM: return "s-resize";
case WL_SHELL_SURFACE_RESIZE_LEFT: return "w-resize";
case WL_SHELL_SURFACE_RESIZE_RIGHT: return "e-resize";
case WL_SHELL_SURFACE_RESIZE_TOP_LEFT: return "nw-resize";
case WL_SHELL_SURFACE_RESIZE_TOP_RIGHT: return "ne-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT: return "sw-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT: return "se-resize";
default: return "se-resize";
}
}
/*
* Phase 22: interactive grab helpers.
*
* wl_grab_begin_move — called from handle_toplevel_request_move to start
* a compositor-side move grab. Records the pointer position and window
* origin so that subsequent motion events can compute the displacement.
*
* wl_grab_begin_resize — same for resize; additionally records the window
* size and the edge bitmask so we know which sides to stretch.
*
* wl_grab_end — clears the grab state (called on button release).
*
* wl_grab_apply_motion — called from both pointer motion handlers to
* update the window position or size while the grab is active.
*/
static void
wl_grab_end(void)
{
wl_state.grab_type = 0;
wl_state.grab_view = NULL;
/* Phase 23: restore default arrow cursor when grab ends. */
wl_set_cursor_image("left_ptr");
}
static void
wl_grab_begin_move(struct wl_toplevel_view *view)
{
int w, h;
if (!view) return;
/* Retrieve current window geometry. */
if (view->xw_surface) {
w = view->xw_surface->width;
h = view->xw_surface->height;
} else if (view->xdg_surface && view->xdg_surface->surface) {
w = view->xdg_surface->surface->current.width;
h = view->xdg_surface->surface->current.height;
} else { w = 1; h = 1; }
wl_state.grab_type = 1; /* MOVE */
wl_state.grab_view = view;
wl_state.grab_start_ptr_x = wl_state.pointer_x;
wl_state.grab_start_ptr_y = wl_state.pointer_y;
wl_state.grab_start_win_x = view->x;
wl_state.grab_start_win_y = view->y;
wl_state.grab_start_win_w = w;
wl_state.grab_start_win_h = h;
wwarning("wl_backend: move grab started for id=0x%lx at ptr=%d,%d win=%d,%d",
view->id, wl_state.pointer_x, wl_state.pointer_y, view->x, view->y);
/* Clear seat pointer focus so motion events are not delivered to the
* client while we are repositioning it. */
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
/* Phase 23: show a move cursor during the grab. */
wl_set_cursor_image("fleur");
}
static void
wl_grab_begin_resize(struct wl_toplevel_view *view, uint32_t edges)
{
int w, h;
if (!view) return;
if (view->xw_surface) {
w = view->xw_surface->width;
h = view->xw_surface->height;
} else if (view->xdg_surface && view->xdg_surface->surface) {
w = view->xdg_surface->surface->current.width;
h = view->xdg_surface->surface->current.height;
} else { w = 1; h = 1; }
wl_state.grab_type = 2; /* RESIZE */
wl_state.grab_view = view;
wl_state.grab_start_ptr_x = wl_state.pointer_x;
wl_state.grab_start_ptr_y = wl_state.pointer_y;
wl_state.grab_start_win_x = view->x;
wl_state.grab_start_win_y = view->y;
wl_state.grab_start_win_w = w;
wl_state.grab_start_win_h = h;
wl_state.grab_resize_edges = edges;
wwarning("wl_backend: resize grab started for id=0x%lx edges=0x%x size=%dx%d",
view->id, edges, w, h);
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
/* Phase 23: show an appropriate resize cursor during the grab. */
wl_set_cursor_image(resize_edges_to_cursor(edges));
}
/* Called from both pointer-motion handlers (relative + absolute). */
static void
wl_grab_apply_motion(void)
{
if (!wl_state.grab_type || !wl_state.grab_view)
return;
struct wl_toplevel_view *v = wl_state.grab_view;
int dx = wl_state.pointer_x - wl_state.grab_start_ptr_x;
int dy = wl_state.pointer_y - wl_state.grab_start_ptr_y;
if (wl_state.grab_type == 1) {
/* MOVE: simply reposition the window. */
int nx = wl_state.grab_start_win_x + dx;
int ny = wl_state.grab_start_win_y + dy;
v->x = nx; v->y = ny;
if (v->frame_id) {
/* Framed window: move the frame tree. The client scene_node
* is a child of the tree and moves with it automatically. */
struct wl_frame_buf *fb = frame_buf_find(v->frame_id);
if (fb && fb->tree) {
int frame_x = nx - v->frame_rel_x;
int frame_y = ny - v->frame_rel_y;
fb->x = frame_x;
fb->y = frame_y;
wlr_scene_node_set_position(&fb->tree->node, frame_x, frame_y);
/* Update child absolute tracking */
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
if (child->parent_id == v->frame_id) {
child->x = fb->x + child->rel_x;
child->y = fb->y + child->rel_y;
}
}
}
} else if (v->scene_node) {
wlr_scene_node_set_position(&v->scene_node->node, nx, ny);
}
if (v->xw_surface)
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)nx, (int16_t)ny,
v->xw_surface->width, v->xw_surface->height);
} else if (wl_state.grab_type == 2) {
/* RESIZE: compute new size (and origin for left/top edges). */
uint32_t edges = wl_state.grab_resize_edges;
int nx = wl_state.grab_start_win_x;
int ny = wl_state.grab_start_win_y;
int nw = wl_state.grab_start_win_w;
int nh = wl_state.grab_start_win_h;
if (edges & WL_SHELL_SURFACE_RESIZE_RIGHT)
nw = wl_state.grab_start_win_w + dx;
if (edges & WL_SHELL_SURFACE_RESIZE_LEFT) {
nw = wl_state.grab_start_win_w - dx;
nx = wl_state.grab_start_win_x + dx;
}
if (edges & WL_SHELL_SURFACE_RESIZE_BOTTOM)
nh = wl_state.grab_start_win_h + dy;
if (edges & WL_SHELL_SURFACE_RESIZE_TOP) {
nh = wl_state.grab_start_win_h - dy;
ny = wl_state.grab_start_win_y + dy;
}
if (nw < 32) { nw = 32; }
if (nh < 32) { nh = 32; }
/* Use wWindowConfigure to update the WM frame (titlebar/resizebar)
* along with the client surface. */
WObjDescriptor *desc = wm_backend->context_find(v->id, WM_CTX_CLIENT_WIN);
if (desc && desc->parent_type == WCLASS_WINDOW) {
WWindow *wwin = (WWindow *)desc->parent;
/* Ensure client scene_node is parented under frame tree */
if (v->scene_node && v->frame_id) {
struct wl_frame_buf *ffb = frame_buf_find(v->frame_id);
if (ffb && ffb->tree) {
if (v->scene_node->node.parent != ffb->tree) {
wwarning("REPARENT FIX: scene_node parent=%p tree=%p",
(void*)v->scene_node->node.parent, (void*)ffb->tree);
wlr_scene_node_reparent(&v->scene_node->node, ffb->tree);
wlr_scene_node_set_position(&v->scene_node->node,
v->frame_rel_x, v->frame_rel_y);
}
}
}
int frame_x = nx;
int frame_y = ny;
if (wwin->frame)
frame_y = ny - wwin->frame->top_width;
wWindowConfigure(wwin, frame_x, frame_y, nw, nh);
} else {
/* Fallback for unmanaged surfaces: direct scene manipulation. */
v->x = nx; v->y = ny;
if (v->scene_node)
wlr_scene_node_set_position(&v->scene_node->node, nx, ny);
if (v->xw_surface) {
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)nx, (int16_t)ny, (uint16_t)nw, (uint16_t)nh);
} else if (v->xdg_surface) {
wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)nw, (uint32_t)nh);
wlr_xdg_surface_schedule_configure(v->xdg_surface);
}
}
}
}
/*
* Phase 15/22: xdg_toplevel request_move — client requests interactive move.
* Phase 22: starts a compositor-side move grab.
*/
static void
handle_toplevel_request_move(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_move_listener);
(void)data;
wl_grab_begin_move(view);
}
/*
* Phase 15/22: xdg_toplevel request_resize — client requests interactive resize.
* Phase 22: starts a compositor-side resize grab.
*/
static void
handle_toplevel_request_resize(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_resize_listener);
struct wlr_xdg_toplevel_resize_event *ev = data;
wl_grab_begin_resize(view, ev ? ev->edges : WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT);
}
/*
* Phase 16: xdg_toplevel request_maximize — toggle maximize state.
*/
static void
handle_toplevel_request_maximize(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_maximize_listener);
(void)data;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (wwin->flags.maximized)
wUnmaximizeWindow(wwin);
else
wMaximizeWindow(wwin, MAX_VERTICAL | MAX_HORIZONTAL, wGetHeadForWindow(wwin));
}
/*
* Phase 16: xdg_toplevel request_fullscreen.
*/
static void
handle_toplevel_request_fullscreen(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_fullscreen_listener);
(void)data;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (!wwin->flags.fullscreen)
wFullscreenWindow(wwin);
}
/*
* Phase 16: xdg_toplevel request_minimize — iconify the window.
*/
static void
handle_toplevel_request_minimize(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_minimize_listener);
(void)data;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (!wwin->flags.miniaturized)
wIconifyWindow(wwin);
}
/*
* Phase 16: xdg_toplevel set_app_id — update wm_class.
*/
static void
handle_toplevel_set_app_id(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, set_app_id_listener);
(void)data;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
const char *app_id = view->xdg_surface->toplevel->app_id;
if (!app_id)
return;
if (wwin->wm_class)
wfree(wwin->wm_class);
wwin->wm_class = strdup(app_id);
}
/*
* Phase 38: toplevel map — surface is ready to be managed.
*/
static void
handle_toplevel_scene_destroy(struct wl_listener *listener, void *data)
{
(void)data;
struct wl_toplevel_view *view =
VIEW_FROM_LISTENER(listener, scene_destroy_listener);
/* wlroots is destroying view->scene_node (typically because its
* parent tree — the WMaker frame's fb->tree — is being destroyed
* as part of unmanage). NULL the pointer so the later
* handle_toplevel_destroy doesn't try to redestroy it. */
view->scene_node = NULL;
wl_list_remove(&view->scene_destroy_listener.link);
}
static void
view_attach_scene_destroy_listener(struct wl_toplevel_view *view)
{
if (!view || !view->scene_node) return;
view->scene_destroy_listener.notify = handle_toplevel_scene_destroy;
wl_signal_add(&view->scene_node->node.events.destroy,
&view->scene_destroy_listener);
}
static void
handle_toplevel_map(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, map_listener);
(void)data;
wwarning("wl_backend: toplevel mapped id=0x%lx app_id=%s title=%s",
view->id,
view->xdg_surface->toplevel->app_id ? view->xdg_surface->toplevel->app_id : "(none)",
view->xdg_surface->toplevel->title ? view->xdg_surface->toplevel->title : "(none)");
/* Detect popup-style WINGs windows (menu editor items, etc.) — these
* need borderless presentation, no taskbar entry, and free positioning. */
const char *app_id_check = view->xdg_surface->toplevel->app_id;
int is_popup = (app_id_check && strcmp(app_id_check, "wings-popup") == 0);
/* Phase 39/41: register the surface as a managed window. */
{
WScreen *scr = wScreenWithNumber(0);
if (scr) {
WWindow *wwin = wManageWindow(scr, view->id);
if (wwin) {
if (is_popup) {
/* Set borderless + skip-taskbar attributes */
wwin->client_flags.no_border = 1;
wwin->client_flags.no_titlebar = 1;
wwin->client_flags.no_closable = 1;
wwin->client_flags.no_miniaturizable = 1;
wwin->client_flags.no_resizable = 1;
wwin->client_flags.no_close_button = 1;
wwin->client_flags.no_miniaturize_button = 1;
wwin->client_flags.no_resizebar = 1;
wwin->client_flags.skip_window_list = 1;
wwin->client_flags.skip_switchpanel = 1;
wwin->user_flags = wwin->client_flags;
/* Rebuild frame without decorations */
extern void wWindowConfigureBorders(WWindow *wwin);
wWindowConfigureBorders(wwin);
/* If the client requested a position via wmaker-popup-v1,
* apply it now that the frame is set up. */
if (view->popup)
wmaker_popup_apply(view);
}
int pid = wl_client_get_pid(view->id);
/* Phase 62: focus delivered by wManageWindow via wSetFocusTo
* (auto_focus=1 set in wl_screen_create_internals); the
* direct wl_focus_set() call is no longer needed here. */
/* Phase 18: add XDG surface to the scene graph.
*
* ORDERING: wManageWindow (above) calls wl_client_reparent
* which sets view->frame_id but cannot reparent the scene
* node because it does not exist yet. We create it here
* and immediately do the deferred reparent if frame_id is
* already set, so the client surface ends up inside
* fb->tree and moves with it during stacking operations.
* Without this the client surface would sit as a flat
* sibling of fb->tree in WL_LAYER_NORMAL and CommitStacking
* raises would leave it behind the frame decorations of
* subsequently-opened windows. */
if (wl_state.scene && !view->scene_node) {
struct wlr_scene_tree *_vp = wl_state.level_trees[WL_LAYER_NORMAL]
? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree;
view->scene_node = wlr_scene_subsurface_tree_create(
_vp, view->xdg_surface->surface);
if (view->scene_node) {
view_attach_scene_destroy_listener(view);
/* Deferred reparent: move client surface under its
* frame grouping tree now that the node exists. */
if (view->frame_id) {
struct wl_frame_buf *_pfb = frame_buf_find(view->frame_id);
if (_pfb && _pfb->tree)
wlr_scene_node_reparent(&view->scene_node->node,
_pfb->tree);
/* Position at relative offset within frame tree */
wlr_scene_node_set_position(&view->scene_node->node,
view->frame_rel_x, view->frame_rel_y);
} else {
wlr_scene_node_set_position(&view->scene_node->node,
view->x, view->y);
}
}
}
/* Phase 13: warp virtual pointer to the centre of the
* newly managed window so pointer focus is immediately
* delivered to its surface. The warp calls
* wl_update_pointer_focus() internally. */
{
int cx = view->x + wwin->client.width / 2;
int cy = view->y + wwin->client.height / 2;
wl_pointer_warp_to(cx, cy);
}
/* Phase 35: create a foreign toplevel handle so taskbars
* and docks (e.g. wlr-taskbar, waybar) can enumerate and
* control this window. */
if (wl_state.foreign_toplevel_mgr && !view->ft_handle) {
view->ft_handle =
wlr_foreign_toplevel_handle_v1_create(
wl_state.foreign_toplevel_mgr);
if (view->ft_handle) {
const char *title = wwin->wm_class ? wwin->wm_class : "";
const char *app_id = wwin->wm_instance ? wwin->wm_instance : "";
wlr_foreign_toplevel_handle_v1_set_title(view->ft_handle, title);
wlr_foreign_toplevel_handle_v1_set_app_id(view->ft_handle, app_id);
if (wl_state.output)
wlr_foreign_toplevel_handle_v1_output_enter(
view->ft_handle, wl_state.output);
wlr_foreign_toplevel_handle_v1_set_activated(view->ft_handle, true);
}
}
/* Phase 30+: add to dock icon list */
wm_dock_app_add(view);
}
}
}
}
/*
* Phase 40: toplevel unmap — surface hidden or destroyed; unmanage the window.
*/
static void
handle_toplevel_unmap(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, unmap_listener);
(void)data;
wwarning("wl_backend: toplevel unmapped id=0x%lx", view->id);
/* Phase 35: destroy the foreign toplevel handle so taskbars stop
* showing this window in their list. */
if (view->ft_handle) {
wlr_foreign_toplevel_handle_v1_destroy(view->ft_handle);
view->ft_handle = NULL;
}
/* Phase 30+: remove from dock icon list */
wm_dock_app_remove(view->id);
/* Phase 40: remove the WWindow from the WM's managed list. */
WScreen *scr = wScreenWithNumber(0);
if (scr) {
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (desc && desc->parent_type == WCLASS_WINDOW) {
WWindow *wwin = (WWindow *)desc->parent;
wUnmanageWindow(wwin, False, True);
}
}
}
/*
* Phase 38: toplevel destroy — free the view.
*/
static void
handle_toplevel_destroy(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener);
(void)data;
/* Remove all listeners registered with xdg_surface / xdg_toplevel signals.
* Failure to remove a listener before freeing the view leaves a dangling
* pointer in wlroots' signal list; when wlroots later emits or cleans up
* those signals (e.g. during wl_display_destroy) it corrupts the heap. */
wl_list_remove(&view->map_listener.link);
wl_list_remove(&view->unmap_listener.link);
wl_list_remove(&view->destroy_listener.link);
wl_list_remove(&view->title_listener.link);
wl_list_remove(&view->request_move_listener.link);
wl_list_remove(&view->request_resize_listener.link);
wl_list_remove(&view->request_maximize_listener.link);
wl_list_remove(&view->request_fullscreen_listener.link);
wl_list_remove(&view->request_minimize_listener.link);
wl_list_remove(&view->set_app_id_listener.link);
wl_list_remove(&view->commit_listener.link);
wl_list_remove(&view->surface_commit_listener.link);
wl_list_remove(&view->link);
/* Phase 18: remove scene node. */
if (view->scene_node) {
/* Remove our destroy listener BEFORE destroying so the listener
* doesn't fire and try to remove itself from a list we're
* already cleaning up. */
wl_list_remove(&view->scene_destroy_listener.link);
wlr_scene_node_destroy(&view->scene_node->node);
view->scene_node = NULL;
}
wl_state.managed_surface_count--;
if (view->wm_state) wfree(view->wm_state); /* Phase 56 */
if (view->wm_command) { /* Phase 57 */
int ci;
for (ci = 0; ci < view->wm_command_n; ci++)
wfree(view->wm_command[ci]);
wfree(view->wm_command);
}
wfree(view);
}
/*
* Phase 64: new_toplevel_decoration listener.
* Respond with CLIENT_SIDE so clients draw their own decorations.
* Must defer until the surface is initialized (wlroots 0.19).
*/
struct wl_deferred_decoration {
struct wl_listener commit_listener;
struct wlr_xdg_toplevel_decoration_v1 *deco;
};
static void
handle_deferred_decoration_commit(struct wl_listener *listener, void *data)
{
struct wl_deferred_decoration *dd =
wl_container_of(listener, dd, commit_listener);
struct wlr_xdg_surface *surface = dd->deco->toplevel->base;
(void)data;
if (!surface->initial_commit)
return;
wl_list_remove(&dd->commit_listener.link);
wlr_xdg_toplevel_decoration_v1_set_mode(dd->deco,
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
wfree(dd);
}
static void
handle_new_toplevel_decoration(struct wl_listener *listener, void *data)
{
struct wlr_xdg_toplevel_decoration_v1 *deco = data;
(void)listener;
struct wlr_xdg_surface *surface = deco->toplevel->base;
if (surface->initialized) {
wlr_xdg_toplevel_decoration_v1_set_mode(deco,
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
} else {
struct wl_deferred_decoration *dd = wmalloc(sizeof(*dd));
dd->deco = deco;
dd->commit_listener.notify = handle_deferred_decoration_commit;
wl_signal_add(&surface->surface->events.commit, &dd->commit_listener);
}
}
/* ------------------------------------------------------------------ */
/* wmaker-popup-v1 protocol — client-driven popup positioning */
/* ------------------------------------------------------------------ */
static void
wmaker_popup_handle_destroy(struct wl_client *client,
struct wl_resource *resource)
{
(void)client;
wl_resource_destroy(resource);
}
static void
wmaker_popup_handle_set_parent(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *parent_resource)
{
(void)client;
struct wl_toplevel_view *view = wl_resource_get_user_data(resource);
if (!view) return;
if (parent_resource) {
view->popup_parent = wlr_xdg_toplevel_from_resource(parent_resource);
} else {
view->popup_parent = NULL;
}
wwarning("wmaker_popup: set_parent view=%p parent=%p", (void*)view,
(void*)view->popup_parent);
}
static void
wmaker_popup_handle_set_position(struct wl_client *client,
struct wl_resource *resource,
int32_t x, int32_t y)
{
(void)client;
struct wl_toplevel_view *view = wl_resource_get_user_data(resource);
wwarning("wmaker_popup: set_position view=%p (%d,%d)", (void*)view, x, y);
if (!view) return;
view->popup_x = (int)x;
view->popup_y = (int)y;
wmaker_popup_apply(view);
}
static const struct wmaker_popup_v1_interface wmaker_popup_impl = {
.destroy = wmaker_popup_handle_destroy,
.set_parent = wmaker_popup_handle_set_parent,
.set_position = wmaker_popup_handle_set_position,
};
static void
wmaker_popup_resource_destroy(struct wl_resource *resource)
{
struct wl_toplevel_view *view = wl_resource_get_user_data(resource);
if (view && view->popup == resource)
view->popup = NULL;
}
static void
wmaker_popup_apply(struct wl_toplevel_view *view)
{
if (!view || !view->popup) return;
/* If a parent was set, translate the requested position into screen
* coordinates by adding the parent toplevel's current position. */
int target_x = view->popup_x;
int target_y = view->popup_y;
if (view->popup_parent) {
struct wl_toplevel_view *parent_view =
wl_find_view_by_xdg_surface(view->popup_parent->base);
if (parent_view) {
target_x += parent_view->x;
target_y += parent_view->y;
}
}
int dx = target_x - view->x;
int dy = target_y - view->y;
wwarning("wmaker_popup_apply: view=%p (%d,%d)->(%d,%d) frame_id=0x%lx scene_node=%p",
(void*)view, view->x, view->y, target_x, target_y,
(unsigned long)view->frame_id, (void*)view->scene_node);
view->x = target_x;
view->y = target_y;
/* If a frame_buf wraps this view, move it via the WM's window-move
* path so the frame and its tree node update consistently. */
if (view->frame_id) {
struct wl_frame_buf *fb = frame_buf_find(view->frame_id);
if (fb) {
fb->x += dx;
fb->y += dy;
if (fb->tree) {
wlr_scene_node_set_position(&fb->tree->node, fb->x, fb->y);
/* Raise above siblings so popups appear over their parent */
wlr_scene_node_raise_to_top(&fb->tree->node);
wwarning("popup_apply: raised frame tree id=0x%lx to top, parent=%p",
(unsigned long)view->id, (void*)fb->tree->node.parent);
} else if (fb->scene_buf) {
wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y);
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
}
view->frame_rel_x = 0;
view->frame_rel_y = 0;
}
} else if (view->scene_node) {
wlr_scene_node_set_position(&view->scene_node->node,
view->x, view->y);
wlr_scene_node_raise_to_top(&view->scene_node->node);
wwarning("popup_apply: raised scene_node id=0x%lx to top",
(unsigned long)view->id);
}
}
static void
wmaker_popup_manager_handle_destroy(struct wl_client *client,
struct wl_resource *resource)
{
(void)client;
wl_resource_destroy(resource);
}
static void
wmaker_popup_manager_handle_get_popup(struct wl_client *client,
struct wl_resource *manager_resource,
uint32_t id,
struct wl_resource *toplevel_resource)
{
(void)manager_resource;
struct wlr_xdg_toplevel *xdg_top =
wlr_xdg_toplevel_from_resource(toplevel_resource);
if (!xdg_top) {
wl_resource_post_error(manager_resource, 0,
"wmaker_popup: invalid xdg_toplevel resource");
return;
}
struct wl_toplevel_view *view =
wl_find_view_by_xdg_surface(xdg_top->base);
struct wl_resource *resource = wl_resource_create(client,
&wmaker_popup_v1_interface, 1, id);
if (!resource) {
wl_client_post_no_memory(client);
return;
}
wl_resource_set_implementation(resource, &wmaker_popup_impl,
view, wmaker_popup_resource_destroy);
if (view)
view->popup = resource;
}
static const struct wmaker_popup_manager_v1_interface wmaker_popup_manager_impl = {
.destroy = wmaker_popup_manager_handle_destroy,
.get_popup = wmaker_popup_manager_handle_get_popup,
};
static void
wmaker_popup_manager_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *resource = wl_resource_create(client,
&wmaker_popup_manager_v1_interface,
version > 1 ? 1 : (int)version, id);
if (!resource) {
wl_client_post_no_memory(client);
return;
}
wl_resource_set_implementation(resource, &wmaker_popup_manager_impl,
NULL, NULL);
}
/* ------------------------------------------------------------------ */
/* Phase 17: XWayland surface lifecycle */
/* ------------------------------------------------------------------ */
/* --- Override-redirect (unmanaged) XWayland surface handlers --- */
static void
handle_or_map(struct wl_listener *listener, void *data)
{
struct wl_or_view *ov = wl_container_of(listener, ov, map_listener);
struct wlr_xwayland_surface *xw = ov->xw_surface;
(void)data;
/* Skip scene operations while the startup pump thread is running —
* wlroots is not thread-safe. The scene node will be created when
* the surface is next committed after the pump thread exits. */
if (wl_state.startup_pump_running)
return;
if (wl_state.scene && !ov->scene_node && xw->surface) {
struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU]
? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree;
ov->scene_node = wlr_scene_subsurface_tree_create(_op, xw->surface);
if (ov->scene_node)
wlr_scene_node_set_position(&ov->scene_node->node, xw->x, xw->y);
}
}
static void
handle_or_unmap(struct wl_listener *listener, void *data)
{
struct wl_or_view *ov = wl_container_of(listener, ov, unmap_listener);
(void)data;
if (ov->scene_node) {
wlr_scene_node_destroy(&ov->scene_node->node);
ov->scene_node = NULL;
}
}
static void
handle_or_destroy(struct wl_listener *listener, void *data)
{
struct wl_or_view *ov = wl_container_of(listener, ov, destroy_listener);
(void)data;
wl_list_remove(&ov->map_listener.link);
wl_list_remove(&ov->unmap_listener.link);
wl_list_remove(&ov->destroy_listener.link);
wl_list_remove(&ov->set_geometry_listener.link);
wl_list_remove(&ov->link);
if (ov->scene_node)
wlr_scene_node_destroy(&ov->scene_node->node);
wfree(ov);
}
static void
handle_or_set_geometry(struct wl_listener *listener, void *data)
{
struct wl_or_view *ov = wl_container_of(listener, ov, set_geometry_listener);
struct wlr_xwayland_surface *xw = ov->xw_surface;
(void)data;
if (ov->scene_node)
wlr_scene_node_set_position(&ov->scene_node->node, xw->x, xw->y);
}
/* --- Managed XWayland surface handlers --- */
/*
* Path 1: handle_xwayland_set_geometry — X11 window moved/resized.
* Keeps the scene node in sync with the X11-authoritative position.
*/
static void
handle_xwayland_set_geometry(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view =
wl_container_of(listener, view, xw_geometry_listener);
struct wlr_xwayland_surface *xw = view->xw_surface;
(void)data;
if (view->scene_node) {
if (view->frame_id) {
/* Framed: position relative to frame tree */
wlr_scene_node_set_position(&view->scene_node->node,
view->frame_rel_x, view->frame_rel_y);
} else {
wlr_scene_node_set_position(&view->scene_node->node, xw->x, xw->y);
}
}
}
/*
* handle_xwayland_surface_map — wlr_surface has mapped (committed first buffer).
* Now we can add to scene graph and manage as a WM client.
*/
static void
handle_xwayland_surface_map(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view =
wl_container_of(listener, view, xw_surface_map_listener);
(void)data;
struct wlr_xwayland_surface *xw = view->xw_surface;
wwarning("wl_backend: xwayland surface mapped id=0x%lx class=%s title=%s",
view->id,
xw->class ? xw->class : "(none)",
xw->title ? xw->title : "(none)");
/* Add to scene graph for compositing. */
if (wl_state.scene && !view->scene_node && xw->surface) {
struct wlr_scene_tree *_vp = wl_state.level_trees[WL_LAYER_NORMAL]
? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree;
view->scene_node = wlr_scene_subsurface_tree_create(_vp, xw->surface);
if (view->scene_node) {
view_attach_scene_destroy_listener(view);
wlr_scene_node_set_position(&view->scene_node->node, view->x, view->y);
}
}
/* Manage as WM client. */
if (w_global.screen_count == 0)
return;
WScreen *scr = wScreenWithNumber(0);
if (scr) {
WWindow *wwin = wManageWindow(scr, view->id);
if (wwin) {
wwarning("wl_backend: wManageWindow(xwayland) succeeded id=0x%lx "
"class=%s name=%s geo=%dx%d pid=%d",
view->id,
wwin->wm_class ? wwin->wm_class : "(none)",
wwin->wm_instance ? wwin->wm_instance : "(none)",
wwin->client.width, wwin->client.height, (int)xw->pid);
int cx = view->x + wwin->client.width / 2;
int cy = view->y + wwin->client.height / 2;
wl_pointer_warp_to(cx, cy);
wm_dock_app_add(view);
}
}
}
/*
* handle_xwayland_associate — wlr_surface attached to XWayland surface.
* Hook surface->events.map to know when it actually has content.
*/
static void
handle_xwayland_associate(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, map_listener);
(void)data;
struct wlr_xwayland_surface *xw = view->xw_surface;
wwarning("wl_backend: xwayland associate id=0x%lx surface=%p",
view->id, (void*)xw->surface);
if (xw->surface) {
view->xw_surface_map_listener.notify = handle_xwayland_surface_map;
wl_signal_add(&xw->surface->events.map, &view->xw_surface_map_listener);
}
}
/*
* handle_xwayland_unmap — X11 window hidden.
* Path 1: just remove the scene node; WindowMaker handles unmanagement.
* Pure Wayland: unmanage via wUnmanageWindow.
*/
static void
handle_xwayland_unmap(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, unmap_listener);
(void)data;
wwarning("wl_backend: xwayland unmapped id=0x%lx", view->id);
/* Remove surface map listener since the surface is being dissociated */
if (view->xw_surface_map_listener.link.next &&
view->xw_surface_map_listener.link.next != &view->xw_surface_map_listener.link)
wl_list_remove(&view->xw_surface_map_listener.link);
wl_list_init(&view->xw_surface_map_listener.link);
/* Phase 30+: remove from dock icon list */
wm_dock_app_remove(view->id);
if (view->scene_node) {
wlr_scene_node_destroy(&view->scene_node->node);
view->scene_node = NULL;
}
if (w_global.screen_count == 0)
return;
WScreen *scr = wScreenWithNumber(0);
if (scr) {
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (desc && desc->parent_type == WCLASS_WINDOW) {
WWindow *wwin = (WWindow *)desc->parent;
wUnmanageWindow(wwin, False, True);
}
}
}
/*
* handle_xwayland_destroy — X11 window destroyed; free the view.
*/
static void
handle_xwayland_destroy(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener);
(void)data;
wl_list_remove(&view->map_listener.link);
wl_list_remove(&view->unmap_listener.link);
wl_list_remove(&view->destroy_listener.link);
wl_list_remove(&view->title_listener.link);
if (view->xw_surface_map_listener.link.next &&
view->xw_surface_map_listener.link.next != &view->xw_surface_map_listener.link)
wl_list_remove(&view->xw_surface_map_listener.link);
if (view->xw_geometry_listener.link.next)
wl_list_remove(&view->xw_geometry_listener.link);
wl_list_remove(&view->link);
/* Phase 18: remove scene node (may already be gone from unmap). */
if (view->scene_node) {
wlr_scene_node_destroy(&view->scene_node->node);
view->scene_node = NULL;
}
wl_state.managed_surface_count--;
if (view->wm_state) wfree(view->wm_state);
if (view->wm_command) {
int ci;
for (ci = 0; ci < view->wm_command_n; ci++)
wfree(view->wm_command[ci]);
wfree(view->wm_command);
}
wfree(view);
}
/*
* handle_xwayland_set_class — X11 window WM_CLASS changed.
*/
static void
handle_xwayland_set_class(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, title_listener);
(void)data;
struct wlr_xwayland_surface *xw = view->xw_surface;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (wwin->wm_class) wfree(wwin->wm_class);
if (wwin->wm_instance) wfree(wwin->wm_instance);
wwin->wm_class = xw->class ? strdup(xw->class) : NULL;
wwin->wm_instance = xw->instance ? strdup(xw->instance) : NULL;
}
/*
* Path 1: handle_xwayland_server_destroy — fired when the XWayland server
* process exits before or without firing the ready signal (startup failure).
* Set the failure flag so the pump loop can exit early and clean up.
*/
static void
handle_xwayland_server_destroy(struct wl_listener *listener, void *data)
{
(void)listener;
(void)data;
wwarning("wl_backend: XWayland server exited");
wl_state.xwayland_ready = true; /* break the wait loop */
}
static void
handle_xwayland_ready(struct wl_listener *listener, void *data)
{
(void)listener;
(void)data;
if (!wl_state.xwayland || !wl_state.xwayland->display_name) {
wwarning("wl_backend: XWayland ready but display_name unavailable");
wl_state.xwayland_ready = true;
return;
}
/* Advertise the XWayland display so X11 client apps can connect. */
setenv("DISPLAY", wl_state.xwayland->display_name, 1);
wwarning("wl_backend: XWayland ready on %s — opening WM Xlib connection",
wl_state.xwayland->display_name);
/* Open the WM's own Xlib connection to its XWayland instance.
* This Display* is returned from wl_display_open so WINGs, title bars,
* root menus, and the dock all work via the X11 code path. */
wl_state.x_display = XOpenDisplay(wl_state.xwayland->display_name);
if (!wl_state.x_display)
wwarning("wl_backend: XOpenDisplay(%s) failed — WM UI limited to native Wayland",
wl_state.xwayland->display_name);
wl_state.xwayland_ready = true;
}
/*
* handle_xwayland_new_surface — called when a new X11 window connects.
* Allocates a wl_toplevel_view, uses window_id as the Window ID,
* and wires the lifecycle listeners to XWayland signals.
*/
static void
handle_xwayland_new_surface(struct wl_listener *listener, void *data)
{
struct wlr_xwayland_surface *xw = data;
(void)listener;
/* Override_redirect windows (WM frames, dock icons, menus, tooltips):
* not managed as ICCCM clients but must be composited. Track them in
* or_list and add to the scene graph on map. */
if (xw->override_redirect) {
/* Skip the rcontext drawable — it's an offscreen pixmap holder. */
if (wl_state.rcontext_xwin &&
xw->window_id == (xcb_window_t)wl_state.rcontext_xwin)
return;
struct wl_or_view *ov = wmalloc(sizeof(*ov));
memset(ov, 0, sizeof(*ov));
ov->xw_surface = xw;
ov->map_listener.notify = handle_or_map;
ov->unmap_listener.notify = handle_or_unmap;
ov->destroy_listener.notify = handle_or_destroy;
ov->set_geometry_listener.notify = handle_or_set_geometry;
wl_signal_add(&xw->events.destroy, &ov->destroy_listener);
wl_signal_add(&xw->events.associate, &ov->map_listener);
wl_signal_add(&xw->events.dissociate, &ov->unmap_listener);
wl_signal_add(&xw->events.set_geometry, &ov->set_geometry_listener);
wl_list_insert(&wl_state.or_list, &ov->link);
/* If already associated (surface exists), add to scene now. */
if (xw->surface) {
if (wl_state.scene) {
struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU]
? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree;
ov->scene_node = wlr_scene_subsurface_tree_create(_op, xw->surface);
if (ov->scene_node)
wlr_scene_node_set_position(&ov->scene_node->node,
xw->x, xw->y);
}
}
return;
}
/* Skip the wrlib RContext drawable window. It is created by RCreateContext
* without override_redirect, but is purely an internal offscreen drawable
* with no WM_CLASS and no backing wl_surface. XWM managing it triggers
* RENDER picture operations that fail (BadDrawable/RenderBadPicture) and
* corrupt XWM state → broken pipe. This check must come before the
* screen_count guard because we record rcontext_xwin in screen_alloc_gcs
* (called during wScreenInit at screen_count==0), and XWM may emit the
* new_surface event for it after screen_count becomes 1. */
if (wl_state.rcontext_xwin && xw->window_id == (xcb_window_t)wl_state.rcontext_xwin) {
wwarning("wl_backend: xwayland rcontext drawable 0x%x skipped",
(unsigned)xw->window_id);
return;
}
/* Guard: during startup the pump thread dispatches Wayland events while
* WMCreateScreenWithRContext runs on the main thread. WINGs creates
* internal X windows then, triggering this callback from the pump thread.
* wlroots is not thread-safe — calling wlr_xwayland_surface_configure
* from a non-main thread crashes. Real app windows arrive only after
* screen_count > 0. */
if (w_global.screen_count == 0) {
wwarning("wl_backend: xwayland new surface 0x%x during startup — skipped",
(unsigned)xw->window_id);
return;
}
/* Skip windows with no WM_CLASS and no title: these are internal
* WINGs/wrlib helper windows that XWM has processed late (after
* screen_count > 0 due to pump-thread timing). Managing them causes
* frame-window creation on a null WNativeWindow → BadDrawable cascade
* that corrupts XWayland state and eventually triggers an XIO error.
* Real application windows always have WM_CLASS set before they map.
* If WM_CLASS is set later, the set_class signal will NOT re-trigger
* management — apps that misbehave this way simply won't be managed,
* which is better than a broken-pipe crash. */
if (!xw->class && !xw->title) {
wwarning("wl_backend: xwayland surface 0x%x has no class/title yet — deferring",
(unsigned)xw->window_id);
}
struct wl_toplevel_view *view = wmalloc(sizeof(*view));
memset(view, 0, sizeof(*view));
view->xw_surface = xw;
view->id = (WNativeWindow)xw->window_id;
view->map_listener.notify = handle_xwayland_associate;
view->unmap_listener.notify = handle_xwayland_unmap;
view->destroy_listener.notify = handle_xwayland_destroy;
view->title_listener.notify = handle_xwayland_set_class;
view->xw_geometry_listener.notify = handle_xwayland_set_geometry;
wl_signal_add(&xw->events.associate, &view->map_listener);
wl_signal_add(&xw->events.dissociate, &view->unmap_listener);
wl_signal_add(&xw->events.destroy, &view->destroy_listener);
wl_signal_add(&xw->events.set_class, &view->title_listener);
wl_signal_add(&xw->events.set_geometry, &view->xw_geometry_listener);
wl_list_insert(&wl_state.toplevel_list, &view->link);
wl_state.managed_surface_count++;
view->x = (wl_state.output ? (int)wl_state.output->width : 1920) / 4;
view->y = (wl_state.output ? (int)wl_state.output->height : 1080) / 4;
/* If the surface is already associated (has a wlr_surface), we missed
* the associate event. Hook surface->events.map directly. */
if (xw->surface) {
view->xw_surface_map_listener.notify = handle_xwayland_surface_map;
wl_signal_add(&xw->surface->events.map, &view->xw_surface_map_listener);
}
wwarning("wl_backend: new XWayland surface id=0x%lx class=%s title=%s pos=%d,%d",
view->id,
xw->class ? xw->class : "(none)",
xw->title ? xw->title : "(none)",
xw->x, xw->y);
/* Send initial configure so the X11 client gets a position and size.
* Use the client's requested size if reasonable, otherwise default. */
{
int cw = (xw->width >= 64) ? xw->width : 640;
int ch = (xw->height >= 64) ? xw->height : 480;
wlr_xwayland_surface_configure(xw,
(int16_t)view->x, (int16_t)view->y, cw, ch);
}
}
/*
* handle_xdg_toplevel_initial_commit -- wlroots 0.19 requires that the
* initial configure is sent in response to the client's first (empty) commit,
* not at new_surface time. The surface is only "initialized" after this
* commit, so schedule_configure is safe here.
*/
static void
handle_xdg_toplevel_initial_commit(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view =
wl_container_of(listener, view, commit_listener);
(void)data;
if (!view->xdg_surface || !view->xdg_surface->initial_commit)
return;
/* Only need this once — remove ourselves. */
wl_list_remove(&view->commit_listener.link);
wl_list_init(&view->commit_listener.link);
/* Send initial configure so the client enters the configure/ack cycle.
* Size 0,0 means "choose your own size" per the XDG spec. */
wlr_xdg_toplevel_set_activated(view->xdg_surface->toplevel, true);
wlr_xdg_toplevel_set_size(view->xdg_surface->toplevel, 0, 0);
wlr_xdg_surface_schedule_configure(view->xdg_surface);
}
static void
handle_toplevel_commit(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view =
wl_container_of(listener, view, surface_commit_listener);
(void)data;
/* Ensure client scene_node is parented under frame tree */
if (view->scene_node && view->frame_id) {
struct wl_frame_buf *ffb = frame_buf_find(view->frame_id);
if (ffb && ffb->tree && view->scene_node->node.parent != ffb->tree) {
wlr_scene_node_reparent(&view->scene_node->node, ffb->tree);
wlr_scene_node_set_position(&view->scene_node->node,
view->frame_rel_x, view->frame_rel_y);
}
}
/* When the client commits a buffer at a new size (e.g. after interactive
* resize), update the WM frame to match. */
if (view->xdg_surface && view->xdg_surface->surface && view->frame_id) {
int cw = view->xdg_surface->surface->current.width;
int ch = view->xdg_surface->surface->current.height;
if (cw > 0 && ch > 0) {
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (desc && desc->parent_type == WCLASS_WINDOW) {
WWindow *wwin = (WWindow *)desc->parent;
if (wwin->frame &&
(cw != wwin->client.width || ch != wwin->client.height)) {
wwin->client.width = cw;
wwin->client.height = ch;
int h = ch + wwin->frame->top_width + wwin->frame->bottom_width;
wFrameWindowResize(wwin->frame, cw, h);
}
}
}
}
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
}
/*
* Phase 36 / 38: new XDG toplevel listener (wlroots 0.19+).
* Fires when the client calls get_toplevel on an xdg_surface.
* Allocates a wl_toplevel_view, assigns a synthetic Window ID,
* wires lifecycle listeners, and sets up the initial commit handler.
*/
static void
handle_xdg_new_toplevel(struct wl_listener *listener, void *data)
{
struct wlr_xdg_toplevel *toplevel = data;
struct wlr_xdg_surface *xdg_surface = toplevel->base;
struct wl_toplevel_view *view;
(void)listener;
view = wmalloc(sizeof(*view));
memset(view, 0, sizeof(*view));
view->xdg_surface = xdg_surface;
view->id = wl_state.next_synthetic_id++;
view->map_listener.notify = handle_toplevel_map;
view->unmap_listener.notify = handle_toplevel_unmap;
view->destroy_listener.notify = handle_toplevel_destroy;
view->title_listener.notify = handle_toplevel_title;
view->request_move_listener.notify = handle_toplevel_request_move;
view->request_resize_listener.notify = handle_toplevel_request_resize;
view->request_maximize_listener.notify = handle_toplevel_request_maximize;
view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen;
view->request_minimize_listener.notify = handle_toplevel_request_minimize;
view->set_app_id_listener.notify = handle_toplevel_set_app_id;
wl_signal_add(&xdg_surface->surface->events.map, &view->map_listener);
wl_signal_add(&xdg_surface->surface->events.unmap, &view->unmap_listener);
wl_signal_add(&toplevel->events.destroy, &view->destroy_listener);
wl_signal_add(&toplevel->events.set_title, &view->title_listener);
wl_signal_add(&toplevel->events.request_move, &view->request_move_listener);
wl_signal_add(&toplevel->events.request_resize, &view->request_resize_listener);
wl_signal_add(&toplevel->events.request_maximize, &view->request_maximize_listener);
wl_signal_add(&toplevel->events.request_fullscreen, &view->request_fullscreen_listener);
wl_signal_add(&toplevel->events.request_minimize, &view->request_minimize_listener);
wl_signal_add(&toplevel->events.set_app_id, &view->set_app_id_listener);
/* wlroots 0.19: send initial configure on the client's first commit,
* when the surface becomes initialized. */
view->commit_listener.notify = handle_xdg_toplevel_initial_commit;
wl_signal_add(&xdg_surface->surface->events.commit, &view->commit_listener);
view->surface_commit_listener.notify = handle_toplevel_commit;
wl_signal_add(&xdg_surface->surface->events.commit, &view->surface_commit_listener);
wl_list_insert(&wl_state.toplevel_list, &view->link);
wl_state.managed_surface_count++;
wwarning("wl_backend: new XDG toplevel id=0x%lx app_id=%s title=%s",
view->id,
toplevel->app_id ? toplevel->app_id : "(none)",
toplevel->title ? toplevel->title : "(none)");
}
/* ------------------------------------------------------------------ */
/* Phase 78: keyboard event handlers */
/* ------------------------------------------------------------------ */
/*
* handle_keyboard_key — fires for every key press/release on the headless
* keyboard. On key-press, dispatches WM shortcuts via wWaylandKeyPress();
* then notifies the focused Wayland client via wlr_seat_keyboard_notify_key.
* (The headless backend never actually fires these in practice, but the
* listener is wired so it works immediately when a real backend is used.)
*/
static void
handle_keyboard_key(struct wl_listener *listener, void *data)
{
struct wlr_keyboard_key_event *event = data;
(void)listener;
if (!wl_state.seat)
return;
struct wlr_keyboard *kb = wl_state.seat->keyboard_state.keyboard;
if (!kb) return;
/* Update modifier state */
wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers);
uint32_t keycode = event->keycode + 8; /* evdev → XKB keycode */
uint32_t modifiers = wlr_keyboard_get_modifiers(kb);
/* VT switching: Ctrl+Alt+Fn */
if (wl_state.session && event->state == WL_KEYBOARD_KEY_STATE_PRESSED &&
(modifiers & (WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT)) ==
(WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT)) {
xkb_keysym_t sym = xkb_state_key_get_one_sym(kb->xkb_state, keycode);
if (sym >= XKB_KEY_XF86Switch_VT_1 && sym <= XKB_KEY_XF86Switch_VT_12) {
wlr_session_change_vt(wl_state.session, sym - XKB_KEY_XF86Switch_VT_1 + 1);
return;
}
}
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
/* Try WM shortcut dispatch first */
if (!wl_state.keyboard_grabbed && wWaylandKeyPress((unsigned int)keycode, (unsigned int)modifiers))
return; /* consumed — do not forward to client */
}
/* Phase A: enqueue native WMEvent for WINGs widgets */
WMScreen *wmscr = wl_get_wmscreen();
if (wmscr) {
WMEvent ev;
memset(&ev, 0, sizeof(ev));
ev.type = (event->state == WL_KEYBOARD_KEY_STATE_PRESSED)
? WME_KEY_PRESS : WME_KEY_RELEASE;
/* Route to focused view */
W_FocusInfo *finfo = wmscr->focusInfo;
W_View *target = (finfo && finfo->focused) ? finfo->focused
: (finfo && finfo->toplevel) ? finfo->toplevel : NULL;
if (!target && wmscr->modalView)
target = wmscr->modalView;
ev.window = target ? (WNativeWindow)target->window : WNATIVE_NONE;
ev.u.key.keycode = keycode;
ev.u.key.state = modifiers;
ev.u.key.time = event->time_msec;
ev.u.key.x = wl_state.pointer_x;
ev.u.key.y = wl_state.pointer_y;
ev.u.key.x_root = wl_state.pointer_x;
ev.u.key.y_root = wl_state.pointer_y;
/* Translate keycode → keysym + UTF-8 via xkb */
if (kb->xkb_state) {
ev.u.key.keysym = xkb_state_key_get_one_sym(kb->xkb_state, keycode);
ev.u.key.utf8_len = xkb_state_key_get_utf8(kb->xkb_state, keycode,
ev.u.key.utf8, sizeof(ev.u.key.utf8));
if (ev.u.key.utf8_len >= (int)sizeof(ev.u.key.utf8))
ev.u.key.utf8_len = 0;
} else {
wwarning("handle_keyboard_key: kb->xkb_state is NULL, dropping key event (keycode=%u)", keycode);
return;
}
W_WL_EnqueueEvent(wmscr, &ev, NULL);
}
/* Forward non-shortcut key events to focused Wayland client (if any),
* but not during a keyboard grab (Alt-Tab cycling). */
if (!wl_state.keyboard_grabbed && wl_state.seat->keyboard_state.focused_surface)
wlr_seat_keyboard_notify_key(wl_state.seat,
event->time_msec, event->keycode, event->state);
}
static void
handle_keyboard_modifiers(struct wl_listener *listener, void *data)
{
(void)listener; (void)data;
if (!wl_state.seat) return;
struct wlr_keyboard *kb = wl_state.seat->keyboard_state.keyboard;
if (!kb) return;
wlr_seat_keyboard_notify_modifiers(wl_state.seat, &kb->modifiers);
}
/* ------------------------------------------------------------------ */
/* Phase 20: real input device forwarding for nested mode */
/* ------------------------------------------------------------------ */
/*
* handle_backend_new_input — fired for every input device added to any
* sub-backend. Headless synthetic devices (which never generate events)
* are skipped; the explicit setup code in wl_display_open handles those.
* Real devices from wlr_wl_backend (the nested compositor path) carry
* events from the host compositor's seat and must be wired so key presses
* and mouse motion reach focused Wayland clients.
*/
static void
handle_keyboard_destroy(struct wl_listener *listener, void *data)
{
(void)listener; (void)data;
wl_list_remove(&wl_state.real_keyboard_key_listener.link);
wl_list_remove(&wl_state.real_keyboard_modifiers_listener.link);
wl_list_remove(&wl_state.real_keyboard_destroy_listener.link);
wl_list_init(&wl_state.real_keyboard_key_listener.link);
wl_list_init(&wl_state.real_keyboard_modifiers_listener.link);
wl_list_init(&wl_state.real_keyboard_destroy_listener.link);
wl_state.real_keyboard_dev = NULL;
wl_state.keyboard_dev = NULL;
}
static void
handle_pointer_destroy(struct wl_listener *listener, void *data)
{
(void)listener; (void)data;
wl_list_remove(&wl_state.pointer_motion_listener.link);
wl_list_remove(&wl_state.real_pointer_motion_abs_listener.link);
wl_list_remove(&wl_state.real_pointer_button_listener.link);
wl_list_remove(&wl_state.real_pointer_frame_listener.link);
wl_list_remove(&wl_state.real_pointer_axis_listener.link);
wl_list_remove(&wl_state.real_pointer_destroy_listener.link);
wl_list_init(&wl_state.pointer_motion_listener.link);
wl_list_init(&wl_state.real_pointer_motion_abs_listener.link);
wl_list_init(&wl_state.real_pointer_button_listener.link);
wl_list_init(&wl_state.real_pointer_frame_listener.link);
wl_list_init(&wl_state.real_pointer_axis_listener.link);
wl_list_init(&wl_state.real_pointer_destroy_listener.link);
wl_state.real_pointer_dev = NULL;
wl_state.pointer_dev = NULL;
}
static void
handle_backend_new_input(struct wl_listener *listener, void *data)
{
struct wlr_input_device *dev = data;
(void)listener;
/* In wlroots 0.19 the headless backend has no input devices, so every
* device that arrives here is a real device from the nested Wayland
* backend (or a virtual device). No need to skip headless devices. */
switch (dev->type) {
case WLR_INPUT_DEVICE_KEYBOARD: {
struct wlr_keyboard *kb = wlr_keyboard_from_input_device(dev);
if (wl_state.xkb_keymap && !kb->xkb_state)
wlr_keyboard_set_keymap(kb, wl_state.xkb_keymap);
wl_state.real_keyboard_dev = dev;
wl_state.keyboard_dev = dev;
if (wl_state.seat)
wlr_seat_set_keyboard(wl_state.seat, kb);
wl_state.real_keyboard_key_listener.notify = handle_keyboard_key;
wl_signal_add(&kb->events.key,
&wl_state.real_keyboard_key_listener);
wl_state.real_keyboard_modifiers_listener.notify = handle_keyboard_modifiers;
wl_signal_add(&kb->events.modifiers,
&wl_state.real_keyboard_modifiers_listener);
wl_state.real_keyboard_destroy_listener.notify = handle_keyboard_destroy;
wl_signal_add(&dev->events.destroy,
&wl_state.real_keyboard_destroy_listener);
wwarning("wl_backend: real keyboard wired");
break;
}
case WLR_INPUT_DEVICE_POINTER: {
struct wlr_pointer *ptr = wlr_pointer_from_input_device(dev);
wl_state.real_pointer_dev = dev;
wl_state.pointer_dev = dev;
wl_state.pointer_motion_listener.notify = handle_pointer_motion;
wl_signal_add(&ptr->events.motion,
&wl_state.pointer_motion_listener);
wl_state.real_pointer_motion_abs_listener.notify =
handle_pointer_motion_absolute;
wl_signal_add(&ptr->events.motion_absolute,
&wl_state.real_pointer_motion_abs_listener);
wl_state.real_pointer_button_listener.notify = handle_pointer_button;
wl_signal_add(&ptr->events.button,
&wl_state.real_pointer_button_listener);
wl_state.real_pointer_frame_listener.notify = handle_pointer_frame;
wl_signal_add(&ptr->events.frame,
&wl_state.real_pointer_frame_listener);
wl_state.real_pointer_axis_listener.notify = handle_pointer_axis;
wl_signal_add(&ptr->events.axis,
&wl_state.real_pointer_axis_listener);
wl_state.real_pointer_destroy_listener.notify = handle_pointer_destroy;
wl_signal_add(&dev->events.destroy,
&wl_state.real_pointer_destroy_listener);
wwarning("wl_backend: real pointer wired");
break;
}
default:
break;
}
}
/* ------------------------------------------------------------------ */
/* Phase 18: output frame rendering and backend new-output handler */
/* ------------------------------------------------------------------ */
/*
* handle_output_frame — fires on every output vsync/frame event.
* Calls wlr_scene_output_commit which renders the scene graph (all mapped
* surfaces at their current positions) and sends Wayland frame callbacks
* to clients so they know they can submit the next buffer.
*/
static void
handle_output_frame(struct wl_listener *listener, void *data)
{
(void)listener; (void)data;
if (wl_state.startup_pump_running)
return;
if (wl_state.session && !wl_state.session->active)
return;
if (!wl_state.scene_output)
return;
static int frame_count = 0;
++frame_count;
if (wl_state.allocator) {
/* Full scene commit — renders client surfaces and fires
* wl_surface.frame callbacks so clients can submit next buffer.
* Also send frame_done unconditionally so clients aren't stalled. */
wlr_scene_output_commit(wl_state.scene_output, NULL);
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(wl_state.scene_output, &now);
} else {
/* No GPU allocator available (e.g. headless without DRI access).
* Skip rendering but still fire wl_surface.frame callbacks so
* clients are not stalled waiting for frame pacing. Without
* this, weston-simple-shm would never submit its second buffer. */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(wl_state.scene_output, &now);
}
}
/*
* handle_backend_new_output — fires when a new output is added to the backend.
* For the headless backend this fires synchronously after wlr_headless_add_output;
* for the nested Wayland backend it fires when the host compositor creates the
* output window. Initialises the render pipeline and wires the frame handler.
*/
static void
handle_backend_new_output(struct wl_listener *listener, void *data)
{
struct wlr_output *output = data;
(void)listener;
if (wl_state.output)
return; /* we only manage one output */
wl_state.output = output;
/* Initialise the render pipeline for this output. */
if (wl_state.allocator && wl_state.renderer)
wlr_output_init_render(output, wl_state.allocator, wl_state.renderer);
/* Enable the output: select preferred mode, then commit via state API.
* Nested Wayland outputs: sway's initial xdg-shell configure sends 0×0
* ("compositor defers to client"), so output->width/height stay 0 after
* the configure busy-loop. Without an explicit mode the swapchain tries
* to allocate a 0×0 GBM buffer which crashes. Fall back to 1920×1080 so
* the commit succeeds; sway will send a real configure once the window is
* visible and we handle it in the frame/configure signal path. */
{
struct wlr_output_state state;
wlr_output_state_init(&state);
wlr_output_state_set_enabled(&state, true);
struct wlr_output_mode *mode = wlr_output_preferred_mode(output);
if (mode) {
wlr_output_state_set_mode(&state, mode);
} else {
/* Default size for nested mode (or headless with no preferred mode). */
wlr_output_state_set_custom_mode(&state, 1920, 1080, 0);
}
if (!wlr_output_commit_state(output, &state))
wwarning("wl_backend: initial output commit failed");
wlr_output_state_finish(&state);
}
/* Register with the output layout. */
if (wl_state.output_layout)
wlr_output_layout_add_auto(wl_state.output_layout, output);
/* Phase 23: apply cursor image now that this output is in the layout.
* wlr_xcursor_manager_set_cursor_image iterates the output layout and
* sets the hardware cursor on each output; for the nested backend this
* sends wl_pointer.set_cursor to the host compositor, making the cursor
* visible. Also warp to the current virtual pointer position so the
* cursor appears at the right location from the first frame. */
if (wl_state.cursor && wl_state.xcursor_mgr) {
wlr_xcursor_manager_load(wl_state.xcursor_mgr, output->scale);
wlr_cursor_set_xcursor(wl_state.cursor, wl_state.xcursor_mgr, "left_ptr");
wlr_cursor_warp(wl_state.cursor, NULL,
(double)wl_state.pointer_x,
(double)wl_state.pointer_y);
wwarning("wl_backend: cursor image set to 'left_ptr' on output %s",
output->name);
}
/* Set window title when running as a nested compositor. */
if (wl_state.is_nested && wl_state.wl_backend_inner &&
wlr_backend_is_wl(wl_state.wl_backend_inner))
wlr_wl_output_set_title(output, "Window Maker");
/* Create scene output and wire the frame renderer. */
if (wl_state.scene) {
wl_state.scene_output = wlr_scene_output_create(wl_state.scene, output);
wl_state.output_frame_listener.notify = handle_output_frame;
wl_signal_add(&output->events.frame, &wl_state.output_frame_listener);
wwarning("wl_backend: scene output created for %s (%dx%d)",
output->name, output->width, output->height);
/* Phase 20: root background rectangle.
* Inserted first in the scene tree so it is always below client
* surfaces. Default WindowMaker desktop colour: rgb:50/50/75. */
{
float bg[4] = { 0x50/255.0f, 0x50/255.0f, 0x75/255.0f, 1.0f };
wl_state.bg_rect = wlr_scene_rect_create(
wl_state.level_trees[WL_LAYER_BACKGROUND],
(int)output->width, (int)output->height, bg);
wwarning("wl_backend: root background created (%dx%d)",
(int)output->width, (int)output->height);
}
}
wwarning("wl_backend: new output %s (%dx%d)",
output->name, output->width, output->height);
}
/* ------------------------------------------------------------------ */
/* Display lifecycle (Phase 32 / Phase 34) */
/* ------------------------------------------------------------------ */
/* Startup pump thread: keeps the Wayland event loop running while the main
* thread initialises WINGs/RContext during StartUp(). XWayland needs the
* Wayland compositor event loop to be responsive so it can answer X11
* round-trips like XGetModifierMapping (which queries keyboard state from
* wlroots via Wayland protocol). The thread does NOT touch Xlib — the
* x11_event_source fd watcher is registered AFTER this thread exits in
* wl_event_loop_run(), preventing any concurrent Xlib access. */
static void *
wl_startup_pump_thread(void *arg)
{
(void)arg;
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
while (wl_state.startup_pump_running) {
wl_display_flush_clients(wl_state.display);
wl_event_loop_dispatch(loop, 1);
}
return NULL;
}
/* Called by the Wayland event loop whenever the X11 connection fd is readable.
* Drains all pending X11 events and dispatches them through WINGs so that
* menus, dialogs, and other WINGs widgets work during wl_display_run(). */
static int
wl_handle_x11_events(int fd, uint32_t mask, void *data)
{
(void)fd; (void)mask; (void)data;
if (!wl_state.x_display)
return 0;
XEvent event;
while (XPending(wl_state.x_display)) {
XNextEvent(wl_state.x_display, &event);
/* Guard: WINGs screen is not initialised until StartUp() completes. */
if (w_global.screen_count > 0) {
/* Translate XEvent → WMEvent at the XWayland boundary.
* _x11_raw carries the original XEvent so WMHandleEvent can
* do X11-specific operations (view lookup, XFilterEvent, etc.) */
WMEvent wev;
memset(&wev, 0, sizeof(wev));
wev._x11_raw = &event;
wev.window = (WNativeWindow)event.xany.window;
switch (event.type) {
case KeyPress: wev.type = WME_KEY_PRESS; break;
case KeyRelease: wev.type = WME_KEY_RELEASE; break;
case ButtonPress: wev.type = WME_BUTTON_PRESS; break;
case ButtonRelease: wev.type = WME_BUTTON_RELEASE; break;
case MotionNotify: wev.type = WME_MOTION; break;
case EnterNotify: wev.type = WME_ENTER; break;
case LeaveNotify: wev.type = WME_LEAVE; break;
case Expose: wev.type = WME_EXPOSE; break;
case ConfigureNotify: wev.type = WME_CONFIGURE; break;
case MapNotify: wev.type = WME_MAP; break;
case UnmapNotify: wev.type = WME_UNMAP; break;
case DestroyNotify: wev.type = WME_DESTROY; break;
case FocusIn: wev.type = WME_FOCUS_IN; break;
case FocusOut: wev.type = WME_FOCUS_OUT; break;
case PropertyNotify: wev.type = WME_PROPERTY; break;
case ClientMessage: wev.type = WME_CLIENT_MESSAGE; break;
case SelectionNotify: wev.type = WME_SELECTION; break;
case SelectionRequest: wev.type = WME_SELECTION_REQUEST; break;
case SelectionClear: wev.type = WME_SELECTION_CLEAR; break;
case MappingNotify: wev.type = WME_MAPPING; break;
case ColormapNotify: wev.type = WME_COLORMAP; break;
case VisibilityNotify: wev.type = WME_VISIBILITY; break;
case ReparentNotify: wev.type = WME_REPARENT; break;
case ConfigureRequest: wev.type = WME_CONFIGURE_REQUEST; break;
case MapRequest: wev.type = WME_MAP_REQUEST; break;
default: wev.type = WME_NONE; break;
}
WMHandleEvent(&wev);
}
}
return 0;
}
/* Public accessor for the XWayland Display* — used by screen.c to create
* an RContext for texture rendering without exposing wl_state globally. */
Display *wl_get_x_display(void)
{
return wl_state.x_display;
}
static void *
wl_display_open(const char *name)
{
const char *sock;
(void)name; /* Wayland compositor picks its own socket name */
/* Initialize listener links so wl_list_remove is safe before first add */
wl_list_init(&wl_state.real_keyboard_key_listener.link);
wl_list_init(&wl_state.real_keyboard_modifiers_listener.link);
wl_list_init(&wl_state.real_keyboard_destroy_listener.link);
wl_list_init(&wl_state.pointer_motion_listener.link);
wl_list_init(&wl_state.real_pointer_motion_abs_listener.link);
wl_list_init(&wl_state.real_pointer_button_listener.link);
wl_list_init(&wl_state.real_pointer_frame_listener.link);
wl_list_init(&wl_state.real_pointer_axis_listener.link);
wl_list_init(&wl_state.real_pointer_destroy_listener.link);
XInitThreads();
/* Install X error handler immediately: Mesa/EGL may open a connection to
* DISPLAY (e.g. the host compositor's XWayland) during wlr_backend_start,
* which happens before StartUp() calls register_error_handlers().
* Installing here prevents Xlib's default handler from aborting the
* process on the benign BadMatch that XWayland generates on SetInputFocus
* during WM startup. */
XSetErrorHandler(wl_catch_xwayland_error);
wlr_log_init(WLR_ERROR, NULL);
wl_state.display = wl_display_create();
if (!wl_state.display) {
wwarning("wl_backend: failed to create wl_display");
return NULL;
}
/* Phase 18: choose backend — nested Wayland if WMAKER_WAYLAND_NESTED=1
* is set (run inside an existing Wayland compositor), otherwise headless.
* Both modes always include a headless backend (headless_inner) so that
* wlr_headless_add_input_device works regardless of nesting. */
/* Auto-detect nested mode: if WAYLAND_DISPLAY is set we are running inside
* an existing Wayland session and should use wlr_wl_backend so the
* compositor appears as a window on the host desktop.
* WMAKER_WAYLAND_NESTED=0 can be used to force headless even when
* WAYLAND_DISPLAY is present (e.g. for automated testing). */
{
const char *nested_env = getenv("WMAKER_WAYLAND_NESTED");
const char *wayland_display = getenv("WAYLAND_DISPLAY");
if (nested_env && nested_env[0] == '0')
wl_state.is_nested = 0; /* explicit override: force headless */
else if (nested_env && nested_env[0])
wl_state.is_nested = 1; /* explicit override: force nested */
else
wl_state.is_nested = (wayland_display && wayland_display[0]) ? 1 : 0;
}
/* Always create a headless backend for the output in headless mode. */
wl_state.headless_inner = wlr_headless_backend_create(
wl_display_get_event_loop(wl_state.display));
if (!wl_state.headless_inner) {
wwarning("wl_backend: failed to create headless backend");
wl_display_destroy(wl_state.display);
wl_state.display = NULL;
return NULL;
}
if (wl_state.is_nested) {
/* Nested: multi-backend wrapping nested Wayland + headless. */
wl_state.wl_backend_inner = wlr_wl_backend_create(
wl_display_get_event_loop(wl_state.display), NULL);
if (!wl_state.wl_backend_inner) {
wwarning("wl_backend: failed to create nested Wayland backend; falling back to standalone");
wl_state.is_nested = 0;
}
}
if (wl_state.is_nested && wl_state.wl_backend_inner) {
struct wlr_backend *multi = wlr_multi_backend_create(
wl_display_get_event_loop(wl_state.display));
wlr_multi_backend_add(multi, wl_state.wl_backend_inner);
wlr_multi_backend_add(multi, wl_state.headless_inner);
wl_state.backend = multi;
wwarning("wl_backend: nested Wayland compositor mode");
} else {
/* Standalone: use autocreate to get DRM/KMS + libinput on real hardware.
* Clear DISPLAY and WAYLAND_DISPLAY so wlroots goes straight to DRM. */
unsetenv("DISPLAY");
unsetenv("WAYLAND_DISPLAY");
struct wlr_session *session = NULL;
wl_state.backend = wlr_backend_autocreate(
wl_display_get_event_loop(wl_state.display), &session);
wl_state.session = session;
if (!wl_state.backend) {
wwarning("wl_backend: wlr_backend_autocreate failed; falling back to headless");
wl_state.backend = wl_state.headless_inner;
} else {
wwarning("wl_backend: standalone DRM/KMS compositor mode");
}
}
/* Renderer: use the appropriate backend as source. */
{
struct wlr_backend *render_src;
if (wl_state.wl_backend_inner)
render_src = wl_state.wl_backend_inner;
else
render_src = wl_state.backend;
wl_state.renderer = wlr_renderer_autocreate(render_src);
}
if (!wl_state.renderer) {
wwarning("wl_backend: failed to create renderer");
wlr_backend_destroy(wl_state.backend);
wl_display_destroy(wl_state.display);
wl_state.backend = NULL;
wl_state.display = NULL;
return NULL;
}
/* Allocator — needed for wlr_output_init_render. */
{
struct wlr_backend *alloc_src = wl_state.wl_backend_inner
? wl_state.wl_backend_inner : wl_state.backend;
wl_state.allocator = wlr_allocator_autocreate(alloc_src, wl_state.renderer);
}
if (!wl_state.allocator)
wwarning("wl_backend: allocator unavailable; wlr_output_init_render will be skipped");
/* Phase 34: output layout for multi-head geometry tracking.
* Must be created before backend start so handle_backend_new_output
* can add the output to the layout when it fires. */
wl_state.output_layout = wlr_output_layout_create(wl_state.display);
/* Phase 23: create hardware cursor tracker and load xcursor theme.
* wlr_cursor tracks the logical cursor position across the output
* layout and communicates cursor image changes to each output.
* In nested Wayland mode this calls wl_pointer.set_cursor on the host
* compositor so the cursor is visible in the window.
* Theme and image are applied in handle_backend_new_output after each
* output is added to the layout, ensuring the scale factor is known. */
wl_state.cursor = wlr_cursor_create();
if (wl_state.cursor) {
wlr_cursor_attach_output_layout(wl_state.cursor, wl_state.output_layout);
{
const char *theme = getenv("XCURSOR_THEME");
const char *size_str = getenv("XCURSOR_SIZE");
unsigned int size = size_str ? (unsigned int)atoi(size_str) : 24;
if (size == 0) size = 24;
wl_state.xcursor_mgr = wlr_xcursor_manager_create(theme, size);
}
if (wl_state.xcursor_mgr)
wlr_xcursor_manager_load(wl_state.xcursor_mgr, 1.0f);
wwarning("wl_backend: cursor created, xcursor theme loaded");
} else {
wwarning("wl_backend: wlr_cursor_create failed; cursor will be invisible");
}
/* Phase 18: scene graph — MUST be created before wlr_backend_start so
* that handle_backend_new_output can create the scene_output immediately
* when the output fires (for nested Wayland, new_output fires during
* wlr_backend_start via the host compositor roundtrip). */
wl_state.scene = wlr_scene_create();
/* Register commit callback for WINGs views (defined later in file) */
{
extern void (*W_ViewCommitBackingFn)(unsigned long id);
extern void wl_view_commit_backing_impl(unsigned long id);
extern void wl_view_destroy_backing_impl(unsigned long id);
W_ViewCommitBackingFn = wl_view_commit_backing_impl;
extern void (*W_ViewDestroyBackingFn)(unsigned long id);
W_ViewDestroyBackingFn = wl_view_destroy_backing_impl;
extern void (*W_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;
}
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");
}
/* 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, true);
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 (lazy mode) — starts on first X11 client");
wl_state.xwayland_ready = true;
if (wl_state.x_display) {
wwarning("wl_backend: XWayland bridge ready — Display*=%p, full WM UI enabled",
(void *)wl_state.x_display);
/* Start a pump thread to keep the Wayland event loop running
* while the main thread calls WMCreateScreenWithRContext() and
* other blocking X11 init in StartUp(). XWayland needs the
* Wayland compositor to be responsive to handle round-trips like
* XGetModifierMapping (which queries keyboard state from wlroots).
* The x11_event_source fd watcher is registered later in
* wl_event_loop_run() after this thread exits, so the pump
* thread never touches Xlib and there is no Xlib contention. */
wl_state.startup_pump_running = 1;
if (pthread_create(&wl_state.startup_pump_thread, NULL,
wl_startup_pump_thread, NULL) != 0) {
wwarning("wl_backend: pthread_create for pump thread failed — "
"WINGs init may deadlock");
wl_state.startup_pump_running = 0;
}
} else {
wwarning("wl_backend: XWayland ready but XOpenDisplay failed — pure Wayland mode");
}
}
return NULL; /* dpy=NULL so WM core skips Xlib calls */
}
static void
wl_display_close(void)
{
if (!wl_state.started)
return;
/* Remove the X11 event source and close the Xlib bridge connection
* before killing XWayland so we don't get stale readable events. */
if (wl_state.x11_event_source) {
wl_event_source_remove(wl_state.x11_event_source);
wl_state.x11_event_source = NULL;
}
if (wl_state.x_display) {
XCloseDisplay(wl_state.x_display);
wl_state.x_display = NULL;
}
/* Phase 17: shut down XWayland.
*
* wlroots 0.15 bug: wlr_xwayland_server_destroy() calls wl_client_destroy()
* on the XWayland Wayland client. After any Wayland client has connected
* and disconnected during the session, the XWayland server's wl_client ends
* up in an inconsistent state: the client struct is freed but server->client
* is NOT nulled. wlr_xwayland_server_destroy() then calls wl_client_destroy()
* on the freed struct and crashes in wl_list_remove().
*
* Workaround: skip wlr_xwayland_destroy() entirely. The Xwayland process is
* a child of wmaker and will receive SIGHUP/SIGKILL when the parent exits.
* The X11 socket is in a tmpdir removed on exit. wl_display_destroy() (called
* below) handles all remaining Wayland client/resource cleanup.
*/
if (wl_state.xwayland) {
/* wlroots 0.15 bug: wlr_xwayland_server_destroy() (called from the
* display destroy signal below) sends SIGTERM to Xwayland and calls
* waitpid(). If Xwayland is blocked waiting for compositor events,
* waitpid() hangs indefinitely.
*
* Fix: pre-kill Xwayland with SIGKILL. SIGKILL cannot be caught or
* deferred, so the process dies immediately (kernel-synchronous).
* The zombie remains until wlroots' waitpid() reaps it — which now
* returns instantly. The Wayland client struct (server->client) is
* still valid because the event loop is stopped and the socket closure
* has not been dispatched, so wlroots' wl_client_destroy() succeeds. */
if (wl_state.xwayland->server) {
int xdisp = wl_state.xwayland->server->display;
if (wl_state.xwayland->server->pid > 0)
kill(wl_state.xwayland->server->pid, SIGKILL);
/* wlroots 0.15 bug: after any Wayland client has connected and
* disconnected during the session, server->client holds a freed
* (stale) wl_client pointer. wlr_xwayland_server_destroy() (fired
* from the display destroy signal below) calls wl_client_destroy()
* on this freed pointer, which corrupts the list and triggers
* SIGABRT. Null the pointer so wlroots skips the destroy call.
* If the client is still valid, wl_display_destroy_clients() will
* clean it up; if it is stale, it has already been removed from
* the display's client list and nothing more is needed. */
wl_state.xwayland->server->client = NULL;
/* Clean up X lock file and Unix socket left by SIGKILL'd Xwayland.
* Without this, the next run may find a stale lock file whose PID
* has been reused by an unrelated process, causing wlroots to
* refuse to reclaim the display number → "xcb connect failed". */
{
char path[64];
snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp);
unlink(path);
snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp);
unlink(path);
}
} else if (wl_state.xwayland->display_name &&
wl_state.xwayland->display_name[0] == ':') {
/* Path 1: server exited early (e.g. stale lock) before wl_display_close
* ran — wlroots already freed server and set it to NULL. Clean up
* any lock file that Xwayland created before dying. */
int xdisp = atoi(wl_state.xwayland->display_name + 1);
char path[64];
snprintf(path, sizeof(path), "/tmp/.X%d-lock", xdisp);
unlink(path);
snprintf(path, sizeof(path), "/tmp/.X11-unix/X%d", xdisp);
unlink(path);
}
wl_state.xwayland = NULL;
unsetenv("DISPLAY");
}
/* Clean up the signalfd source (registered in wl_event_loop_run). */
if (wl_state.sigfd_source) {
wl_event_source_remove(wl_state.sigfd_source);
wl_state.sigfd_source = NULL;
}
if (wl_state.sigfd >= 0) {
close(wl_state.sigfd);
wl_state.sigfd = -1;
}
/* Clean up the shutdown self-pipe (remove event source before display
* destruction so the fd is not left in the epoll set). */
if (wl_state.shutdown_source) {
wl_event_source_remove(wl_state.shutdown_source);
wl_state.shutdown_source = NULL;
}
if (wl_state.shutdown_pipe[0] >= 0) {
close(wl_state.shutdown_pipe[0]);
wl_state.shutdown_pipe[0] = -1;
}
if (wl_state.shutdown_pipe[1] >= 0) {
close(wl_state.shutdown_pipe[1]);
wl_state.shutdown_pipe[1] = -1;
}
/* Remove all globally-registered wl_listener nodes before calling
* wl_display_destroy(). wl_display_destroy() frees wlroots objects
* (xdg_shell, backend, keyboard, pointer, output, etc.), which frees
* their signal structs. Any wl_listener.link that is still in one of
* those signal lists after the struct is freed becomes a dangling pointer.
* Subsequent wl_list operations (in wayland-server cleanup or by glibc's
* heap allocator detecting corruption) then trigger SIGABRT.
* Guard each removal with a link.next check: wayland-server zeroes the
* link after wl_list_remove(), so a non-NULL link means the listener is
* currently inserted in a signal list. */
#define WL_LISTENER_REMOVE(l) \
do { if ((l).link.next) wl_list_remove(&(l).link); } while (0)
/* Output frame and new-output listeners */
WL_LISTENER_REMOVE(wl_state.output_frame_listener);
WL_LISTENER_REMOVE(wl_state.backend_new_output_listener);
/* XDG shell and decoration listeners */
WL_LISTENER_REMOVE(wl_state.xdg_new_surface_listener);
WL_LISTENER_REMOVE(wl_state.decoration_new_listener);
/* XWayland listeners (xwayland ptr already nulled in step 1,
* but listener links are still inserted in the (now-orphaned) xwayland
* object's signal lists — remove them to prevent heap corruption). */
WL_LISTENER_REMOVE(wl_state.xwayland_ready_listener);
WL_LISTENER_REMOVE(wl_state.xwayland_new_surface_listener);
WL_LISTENER_REMOVE(wl_state.xwayland_server_destroy_listener);
/* Keyboard listeners */
WL_LISTENER_REMOVE(wl_state.keyboard_key_listener);
WL_LISTENER_REMOVE(wl_state.keyboard_modifiers_listener);
/* Pointer listeners */
WL_LISTENER_REMOVE(wl_state.pointer_motion_listener);
WL_LISTENER_REMOVE(wl_state.pointer_motion_abs_listener);
WL_LISTENER_REMOVE(wl_state.pointer_button_listener);
WL_LISTENER_REMOVE(wl_state.pointer_frame_listener);
/* Phase 20: backend new_input listener and real device listeners */
WL_LISTENER_REMOVE(wl_state.backend_new_input_listener);
WL_LISTENER_REMOVE(wl_state.real_keyboard_key_listener);
WL_LISTENER_REMOVE(wl_state.real_keyboard_modifiers_listener);
WL_LISTENER_REMOVE(wl_state.real_pointer_motion_abs_listener);
WL_LISTENER_REMOVE(wl_state.real_pointer_button_listener);
WL_LISTENER_REMOVE(wl_state.real_pointer_frame_listener);
/* Phase 24/25: seat cursor + selection listeners */
WL_LISTENER_REMOVE(wl_state.seat_request_cursor_listener);
WL_LISTENER_REMOVE(wl_state.seat_request_set_selection_listener);
/* Phase 26/28: primary selection + idle inhibitor listeners */
WL_LISTENER_REMOVE(wl_state.seat_request_primary_selection_listener);
WL_LISTENER_REMOVE(wl_state.idle_new_inhibitor_listener);
/* Phase 29: axis listeners */
WL_LISTENER_REMOVE(wl_state.pointer_axis_listener);
WL_LISTENER_REMOVE(wl_state.real_pointer_axis_listener);
/* Phase 30/31: pointer constraints + shortcuts inhibitor listeners */
WL_LISTENER_REMOVE(wl_state.new_constraint_listener);
WL_LISTENER_REMOVE(wl_state.new_shortcuts_inhibitor_listener);
/* Phase 33: output management + output power listeners */
WL_LISTENER_REMOVE(wl_state.output_mgr_apply_listener);
WL_LISTENER_REMOVE(wl_state.output_mgr_test_listener);
WL_LISTENER_REMOVE(wl_state.output_power_set_mode_listener);
/* Phase 34: text input, input method, virtual keyboard listeners */
WL_LISTENER_REMOVE(wl_state.text_input_new_listener);
WL_LISTENER_REMOVE(wl_state.input_method_new_listener);
WL_LISTENER_REMOVE(wl_state.virtual_keyboard_new_listener);
/* Phase 35: xdg_activation listener */
WL_LISTENER_REMOVE(wl_state.xdg_activation_request_listener);
/* Phase 36: layer shell new_surface listener */
WL_LISTENER_REMOVE(wl_state.layer_shell_new_surface_listener);
/* Phase 37: virtual pointer listener */
WL_LISTENER_REMOVE(wl_state.virtual_pointer_new_listener);
#undef WL_LISTENER_REMOVE
/* Phase 23: destroy cursor and xcursor manager before display teardown. */
if (wl_state.xcursor_mgr) {
wlr_xcursor_manager_destroy(wl_state.xcursor_mgr);
wl_state.xcursor_mgr = NULL;
}
if (wl_state.cursor) {
wlr_cursor_destroy(wl_state.cursor);
wl_state.cursor = NULL;
}
/* Skip wl_display_destroy() and wl_display_destroy_clients().
*
* wl_display_destroy() fires the display's destroy signal which triggers
* wlroots 0.15 internal object teardown. That teardown has a double-
* remove bug in the XWayland path: wlroots registers a listener on the
* Xwayland wl_client's destroy signal; that listener self-removes during
* client destruction, and wlr_xwayland_server_destroy() then tries to
* wl_list_remove() the already-nulled link, corrupting the heap
* (SIGABRT).
*
* wl_display_destroy_clients() also blocks: wlroots' XWayland client
* destroy handler performs blocking cleanup that can hang if Xwayland
* is in the middle of protocol dispatch.
*
* Both calls are unnecessary here because Exit() calls exit(0) immediately
* after wl_display_close() returns. The kernel reclaims all resources:
* - file descriptors closed → all Wayland clients get POLLHUP
* - memory freed by OS
* - Xwayland already SIGKILL'd in step 1
* - event sources removed in step 2
* - all our signal listeners removed in step 3 */
unsetenv("WAYLAND_DISPLAY");
wl_state.renderer = NULL;
wl_state.backend = NULL;
wl_state.headless_inner = NULL;
wl_state.wl_backend_inner = NULL;
wl_state.display = NULL;
wl_state.output = NULL;
wl_state.frame_timer = NULL;
wl_state.output_layout = NULL;
wl_state.scene_output = NULL;
wl_state.scene = NULL;
wl_state.allocator = NULL;
wl_state.started = 0;
}
static void
wl_display_post_open(const char *display_name, int *multi_head_out)
{
(void)display_name;
(void)multi_head_out;
}
static int
wl_display_screen_count(void)
{
return wl_state.started ? 1 : 0;
}
static int
wl_display_default_screen(void)
{
return 0;
}
static unsigned long
wl_atom_intern(const char *name)
{
/* The WM's own UI runs on XWayland — it needs real X11 atoms for
* property operations (WM_STATE, WM_PROTOCOLS, etc.). */
if (wl_state.x_display)
return (unsigned long)XInternAtom(wl_state.x_display, name, False);
return 0;
}
/* ------------------------------------------------------------------ */
/* Phase 29+: signalfd-based signal handling */
/* ------------------------------------------------------------------ */
/*
* wl_signalfd_handler -- called from within wl_event_loop_dispatch() when
* SIGTERM, SIGINT, or SIGHUP is pending on the signalfd.
*
* Running inside the event loop means we can safely call
* wl_display_terminate() without risking a re-entrant call into the Wayland
* event loop machinery, which is the failure mode of the traditional
* signal-handler → DispatchEvent → wl_display_terminate path.
*/
static int
wl_signalfd_handler(int fd, uint32_t mask, void *data)
{
struct signalfd_siginfo info;
ssize_t n;
(void)mask; (void)data;
n = read(fd, &info, sizeof(info));
if (n != (ssize_t)sizeof(info))
return 0;
wwarning("wl_backend: signalfd received signal %u - terminating event loop",
(unsigned)info.ssi_signo);
/* Set the exit state flag so post-loop cleanup paths see it. */
SIG_WCHANGE_STATE(WSTATE_NEED_EXIT);
/* Terminate the event loop. */
wl_state.terminate = 1;
return 0;
}
#ifdef HAVE_INOTIFY
static void wl_handle_inotify_events(void)
{
ssize_t eventQLength;
size_t i = 0;
char buff[(sizeof(struct inotify_event) + NAME_MAX + 1) * 5];
int oneShotFlag = 0;
eventQLength = read(w_global.inotify.fd_event_queue, buff, sizeof(buff));
if (eventQLength < 0) {
wwarning(_("read problem when trying to get INotify event: %s"), strerror(errno));
return;
}
while (i < (size_t)eventQLength) {
struct inotify_event *pevent = (struct inotify_event *)&buff[i];
if (pevent->mask & IN_DELETE_SELF) {
wwarning(_("the defaults database has been deleted!"));
if (w_global.inotify.fd_event_queue >= 0) {
close(w_global.inotify.fd_event_queue);
w_global.inotify.fd_event_queue = -1;
}
}
if (pevent->mask & IN_UNMOUNT) {
wwarning(_("the unit containing the defaults database has been unmounted."));
if (w_global.inotify.fd_event_queue >= 0) {
close(w_global.inotify.fd_event_queue);
w_global.inotify.fd_event_queue = -1;
}
wPreferences.flags.noupdates = 1;
}
/* React on IN_MOVED_TO (atomic rename — WMWritePropListToFile pattern)
* or IN_CLOSE_WRITE for direct writes. Skip temp files (they have
* a dot in their name like 'WindowMaker.XXXXXX'). */
if (oneShotFlag == 0 && pevent->len > 0) {
const char *name = pevent->name;
int is_tempfile = (strchr(name, '.') != NULL);
if (((pevent->mask & IN_MOVED_TO) ||
((pevent->mask & IN_CLOSE_WRITE) && !is_tempfile))) {
wDefaultsCheckDomains(NULL);
oneShotFlag = 1;
}
}
i += sizeof(struct inotify_event) + pevent->len;
}
}
#endif
static void
wl_event_loop_run(void)
{
if (!wl_state.display)
return;
/*
* Install signalfd as the primary SIGTERM/SIGINT/SIGHUP handler.
*
* The traditional path (startup.c handleExitSig → DispatchEvent →
* event_loop_terminate → pipe write) works in headless/smoke-test mode
* because epoll_wait with SA_RESTART sees the pipe immediately after the
* signal handler returns. In nested-Wayland mode the wlroots nested
* backend processes parent-compositor events alongside our own clients,
* and the combination of SA_RESTART + epoll ordering can cause the pipe
* write to be missed until the next event, leaving SIGTERM apparently
* ignored.
*
* signalfd is the compositor-standard solution: block async delivery,
* route signals through a regular fd in the epoll set, and handle them
* synchronously from within the dispatch loop. The pipe-based path
* (wl_event_loop_terminate) is retained as a fallback for code paths
* that call it before wl_event_loop_run() returns.
*/
{
sigset_t ss;
sigemptyset(&ss);
sigaddset(&ss, SIGTERM);
sigaddset(&ss, SIGINT);
sigaddset(&ss, SIGHUP);
wl_state.sigfd = signalfd(-1, &ss, SFD_NONBLOCK | SFD_CLOEXEC);
if (wl_state.sigfd >= 0) {
/* Block async delivery so signals only arrive via the fd. */
sigprocmask(SIG_BLOCK, &ss, NULL);
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
wl_state.sigfd_source = wl_event_loop_add_fd(loop,
wl_state.sigfd,
WL_EVENT_READABLE,
wl_signalfd_handler,
NULL);
if (wl_state.sigfd_source) {
wwarning("wl_backend: signalfd installed for SIGTERM/SIGINT/SIGHUP");
/* Disable the old signal handlers — signalfd owns these now */
signal(SIGTERM, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGHUP, SIG_DFL);
} else {
wwarning("wl_backend: wl_event_loop_add_fd failed; falling back to pipe");
close(wl_state.sigfd);
wl_state.sigfd = -1;
/* Restore async delivery so the old handler still works. */
sigprocmask(SIG_UNBLOCK, &ss, NULL);
}
} else {
wwarning("wl_backend: signalfd(2) failed (%s); SIGTERM via pipe fallback",
strerror(errno));
}
}
/* Stop the startup pump thread — wl_display_run() takes over Wayland
* event dispatching from here. Join before registering x11_event_source
* so there is no concurrent access to the event loop. */
if (wl_state.startup_pump_running) {
wl_state.startup_pump_running = 0;
pthread_join(wl_state.startup_pump_thread, NULL);
wwarning("wl_backend: startup pump thread stopped");
}
/* Deferred dock creation — safe now that the pump thread has stopped.
* wm_dock_create calls wlr_scene_buffer_create which must not race with
* the pump thread's frame-timer wlr_scene_output_commit. */
if (wl_state.dock_pending_scr) {
wm_dock_create(wl_state.dock_pending_scr);
wl_state.dock_pending_scr = NULL;
}
/* Create scene nodes for any OR views whose associate signal fired
* during the pump thread (when scene ops were unsafe). */
{
struct wl_or_view *ov;
wl_list_for_each(ov, &wl_state.or_list, link) {
if (!ov->scene_node && ov->xw_surface->surface && wl_state.scene) {
struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU]
? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree;
ov->scene_node = wlr_scene_subsurface_tree_create(_op, ov->xw_surface->surface);
if (ov->scene_node)
wlr_scene_node_set_position(&ov->scene_node->node,
ov->xw_surface->x, ov->xw_surface->y);
}
}
}
/* Register the X11 fd watcher now that StartUp() has completed and
* WINGs is initialised. Doing this here (not in wl_display_open) ensures
* the pump thread never touches Xlib. */
if (wl_state.x_display && !wl_state.x11_event_source) {
wl_state.x11_event_source = wl_event_loop_add_fd(
wl_display_get_event_loop(wl_state.display),
ConnectionNumber(wl_state.x_display),
WL_EVENT_READABLE,
wl_handle_x11_events, NULL);
if (wl_state.x11_event_source)
wwarning("wl_backend: X11 event source registered for WINGs dispatch");
}
wwarning("wl_backend: DISPLAY=%s WAYLAND_DISPLAY=%s",
getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)",
getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(unset)");
/* Phase A: register compositor dispatch callback so WINGs modal loops
* (WMRunModalLoop, color panels, etc.) can tick the event loop. */
{
WMScreen *wmscr = wl_get_wmscreen();
if (wmscr) {
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
int loop_fd = wl_event_loop_get_fd(loop);
W_WL_SetLoopDispatch(wmscr, loop_fd, wl_wings_dispatch, NULL);
wwarning("wl_backend: WINGs loop dispatch registered (fd=%d)", loop_fd);
}
}
wwarning("wl_backend: event loop running");
/* Custom event loop: pump wlroots, then drain WINGs events.
* We can't use wl_display_run() because WM event handlers (OpenRootMenu,
* etc.) call WMMaskEvent which re-enters the wlroots loop. By draining
* WINGs events BETWEEN dispatches (not inside a timer callback), modal
* loops in WMMaskEvent can safely call wl_event_loop_dispatch. */
{
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
while (!wl_state.terminate) {
wl_display_flush_clients(wl_state.display);
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
wl_event_loop_dispatch(loop, 16);
/* Commit scene after dispatch to pick up any client surface
* updates that arrived during this iteration. */
if (wl_state.scene_output && wl_state.allocator)
wlr_scene_output_commit(wl_state.scene_output, NULL);
/* 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
/* 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;
while (be->pending(ectx) && limit-- > 0) {
be->fetch(ectx, &wev);
WMHandleEvent(&wev);
}
}
}
}
wwarning("wl_backend: event loop exited");
/* Remove pointer listeners before wlroots destroys the pointer device */
if (wl_state.pointer_dev) {
wl_list_remove(&wl_state.real_pointer_motion_abs_listener.link);
wl_list_remove(&wl_state.real_pointer_button_listener.link);
wl_list_remove(&wl_state.real_pointer_frame_listener.link);
wl_list_remove(&wl_state.real_pointer_axis_listener.link);
}
}
static void
wl_event_loop_terminate(void)
{
/* Write a byte to the self-pipe. write(2) is async-signal-safe, so this
* can safely be called from a signal handler. The event loop will read
* the pipe and call wl_display_terminate() from within loop context,
* avoiding the double-free that results from calling wl_display_destroy()
* while wl_event_loop_dispatch() is active. */
if (wl_state.shutdown_pipe[1] >= 0) {
char c = 'T';
(void)write(wl_state.shutdown_pipe[1], &c, 1);
} else if (wl_state.display) {
/* fallback if pipe creation failed at startup */
wl_display_terminate(wl_state.display);
}
}
/* ================================================================== */
/* Phase 29: WM-owned SHM surfaces (pixman rendering → wlroots scene) */
/* ================================================================== */
/*
* wm_shm_surface — a compositor-owned pixel buffer that plugs into the
* wlroots scene graph.
*
* Memory layout:
* wm_shm_surface embeds a wlr_buffer so wlroots can reference-count and
* upload its pixels as a GPU texture. The pixel array (ARGB8888) is
* malloc'd and wrapped by a pixman_image_t for drawing.
*
* Lifecycle:
* wm_shm_surface_create — alloc + pixman init
* wm_shm_surface_fill — draw into the pixman image (no upload yet)
* wm_shm_surface_commit — create/reposition the wlr_scene_buffer node
* wm_shm_surface_destroy — tear down scene node + drop wlr_buffer ref
* (shm_buf_destroy frees memory when ref = 0)
*
* Update model (Phase 29): surfaces are static after first commit.
* A re-draw path (drop + recreate scene node) will be added in Phase 30
* once the dock and title-bar code drives it.
*/
struct wm_shm_surface {
/* wlr_buffer interface — MUST be first so wl_container_of works */
struct wlr_buffer wlr_buf;
/* Pixel store */
uint32_t *data; /* ARGB8888, row-major */
int width;
int height;
size_t stride; /* bytes per row = width * 4 */
/* pixman drawing surface wrapping data[] */
pixman_image_t *image;
/* scene graph presence */
struct wlr_scene_buffer *scene_buf; /* NULL until first commit */
};
/* ---------- wlr_buffer implementation -------------------------------- */
static void
shm_buf_destroy(struct wlr_buffer *buf)
{
struct wm_shm_surface *s = wl_container_of(buf, s, wlr_buf);
if (s->image)
pixman_image_unref(s->image);
free(s->data);
free(s);
}
static bool
shm_buf_begin_data_ptr(struct wlr_buffer *buf, uint32_t flags,
void **out_data, uint32_t *out_fmt, size_t *out_stride)
{
struct wm_shm_surface *s = wl_container_of(buf, s, wlr_buf);
(void)flags;
*out_data = s->data;
*out_fmt = DRM_FORMAT_ARGB8888;
*out_stride = s->stride;
return true;
}
static void
shm_buf_end_data_ptr(struct wlr_buffer *buf)
{
(void)buf;
}
static const struct wlr_buffer_impl wm_shm_buf_impl = {
.destroy = shm_buf_destroy,
.begin_data_ptr_access = shm_buf_begin_data_ptr,
.end_data_ptr_access = shm_buf_end_data_ptr,
};
/* ---------- surface API ---------------------------------------------- */
static struct wm_shm_surface *
wm_shm_surface_create(int width, int height)
{
if (width <= 0 || height <= 0)
return NULL;
struct wm_shm_surface *s = calloc(1, sizeof(*s));
if (!s)
return NULL;
s->width = width;
s->height = height;
s->stride = (size_t)width * 4;
s->data = calloc(1, (size_t)height * s->stride);
if (!s->data) {
free(s);
return NULL;
}
/* wlr_buffer init — producer holds one reference */
wlr_buffer_init(&s->wlr_buf, &wm_shm_buf_impl, width, height);
/* pixman image wrapping the same memory */
s->image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
width, height,
(uint32_t *)s->data,
(int)s->stride);
if (!s->image) {
/* wlr_buffer_drop triggers shm_buf_destroy → frees data + s */
wlr_buffer_drop(&s->wlr_buf);
return NULL;
}
return s;
}
/*
* Fill a rectangle with a solid ARGB colour.
* argb: 0xAARRGGBB — e.g. 0xFF4080C0 for opaque steel-blue.
*/
static void
wm_shm_surface_fill(struct wm_shm_surface *s,
int x, int y, int w, int h,
uint32_t argb)
{
if (!s || !s->image)
return;
/* clamp to surface bounds */
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > s->width) w = s->width - x;
if (y + h > s->height) h = s->height - y;
if (w <= 0 || h <= 0)
return;
pixman_color_t c = {
.alpha = (uint16_t)(((argb >> 24) & 0xff) * 0x0101),
.red = (uint16_t)(((argb >> 16) & 0xff) * 0x0101),
.green = (uint16_t)(((argb >> 8) & 0xff) * 0x0101),
.blue = (uint16_t)(((argb >> 0) & 0xff) * 0x0101),
};
pixman_image_t *fill = pixman_image_create_solid_fill(&c);
if (!fill)
return;
pixman_image_composite32(PIXMAN_OP_SRC, fill, NULL, s->image,
0, 0, 0, 0, x, y, w, h);
pixman_image_unref(fill);
}
/*
* Commit the surface to the scene graph.
*
* parent — scene-graph parent node (e.g. &wl_state.scene->tree.node)
* x, y — position in the compositor coordinate space
*
* First call creates the scene_buffer node; subsequent calls just
* reposition it (pixel updates are uploaded lazily by wlroots on the
* next wlr_scene_output_commit).
*/
static void
wm_shm_surface_commit(struct wm_shm_surface *s,
struct wlr_scene_tree *parent,
int x, int y)
{
if (!s)
return;
if (!s->scene_buf) {
s->scene_buf = wlr_scene_buffer_create(parent, &s->wlr_buf);
if (!s->scene_buf) {
wwarning("wm_shm_surface_commit: wlr_scene_buffer_create failed");
return;
}
wwarning("wl_backend: SHM surface %dx%d committed to scene at %d,%d",
s->width, s->height, x, y);
}
wlr_scene_node_set_position(&s->scene_buf->node, x, y);
}
/*
* Destroy a surface and remove it from the scene graph.
* After this call s is invalid — the memory is freed asynchronously
* by shm_buf_destroy once the scene node releases its lock.
*/
static void
wm_shm_surface_destroy(struct wm_shm_surface *s)
{
if (!s)
return;
if (s->scene_buf) {
wlr_scene_node_destroy(&s->scene_buf->node);
s->scene_buf = NULL;
}
wlr_buffer_drop(&s->wlr_buf); /* producer done; shm_buf_destroy frees s */
}
/* ================================================================== */
/* Phase 30: WM dock panel (layer-anchored, pixman-rendered) */
/* ================================================================== */
/*
* The dock is a vertical strip anchored to the right edge of the primary
* output. It is rendered into a wm_shm_surface whose pixels flow through
* the wlroots scene graph, exactly like any other WM UI element.
*
* Layout (right edge, full screen height):
*
* ┌──────────────────────────────────────────────────┬────────┐
* │ │ ██████ │
* │ desktop / client windows │ ██████ │
* │ (constrained to usable area) │ ██████ │
* │ │ dock │
* └──────────────────────────────────────────────────┴────────┘
* ← dw →
*
* The usable area (where windows are placed) is shrunk by dock_width so
* clients never overlap the panel.
*/
/* Default width when wPreferences.icon_size is not yet set. */
#define WM_DOCK_DEFAULT_WIDTH 68 /* 64 px icon + 4 px border */
/* Colours (ARGB8888, fully opaque) */
#define WM_DOCK_BG 0xFF1C1C1C /* dark charcoal background */
#define WM_DOCK_HIGHLIGHT 0xFF404040 /* 1-px highlight on left edge */
#define WM_DOCK_TOPSEP 0xFF303030 /* 1-px separator at top */
#define WM_DOCK_TILE_BG 0xFF252525 /* icon-slot background */
#define WM_DOCK_TILE_HI 0xFF353535 /* icon-slot top/left highlight */
#define WM_DOCK_TILE_SH 0xFF151515 /* icon-slot bottom/right shadow */
/* wm_dock_render_tile — paint one icon slot starting at (x, y).
* Slots are icon_size × icon_size with a 1-px inset bevel. */
static void
wm_dock_render_tile(struct wm_shm_surface *s,
int x, int y, int tile_sz)
{
/* Background */
wm_shm_surface_fill(s, x, y, tile_sz, tile_sz, WM_DOCK_TILE_BG);
/* Top highlight */
wm_shm_surface_fill(s, x, y, tile_sz, 1, WM_DOCK_TILE_HI);
/* Left highlight */
wm_shm_surface_fill(s, x, y, 1, tile_sz, WM_DOCK_TILE_HI);
/* Bottom shadow */
wm_shm_surface_fill(s, x, y + tile_sz - 1, tile_sz, 1, WM_DOCK_TILE_SH);
/* Right shadow */
wm_shm_surface_fill(s, x + tile_sz - 1, y, 1, tile_sz, WM_DOCK_TILE_SH);
}
/* wm_dock_render — paint the full dock panel.
* Draws the background, left-edge highlight, top separator, and a column
* of empty icon slots to give visual structure before real icons arrive. */
static void
wm_dock_render(struct wm_shm_surface *s, int icon_sz, int n_slots)
{
int w = s->width;
int h = s->height;
int tile_sz = icon_sz;
int pad = (w - tile_sz) / 2; /* horizontal centering margin */
/* Background */
wm_shm_surface_fill(s, 0, 0, w, h, WM_DOCK_BG);
/* Left-edge highlight (the dock's inner border with the desktop) */
wm_shm_surface_fill(s, 0, 0, 1, h, WM_DOCK_HIGHLIGHT);
/* Top separator */
wm_shm_surface_fill(s, 0, 0, w, 1, WM_DOCK_TOPSEP);
/* Icon-slot placeholders */
for (int i = 0; i < n_slots; i++) {
int ty = 2 + i * (tile_sz + 2);
if (ty + tile_sz > h)
break;
wm_dock_render_tile(s, pad, ty, tile_sz);
}
}
/* wm_dock_create — allocate the dock surface, render it, attach to scene,
* and shrink the head-0 usable area by dock_width pixels on the right. */
static void
wm_dock_create(WScreen *scr)
{
if (!wl_state.scene || !wl_state.output)
return;
int sw = (int)wl_state.output->width;
int sh = (int)wl_state.output->height;
int icon_sz = (wPreferences.icon_size > 0)
? wPreferences.icon_size : 64;
int dw = icon_sz + 4; /* dock width = icon + 2px pad each side */
int n_slots = (sh - 4) / (icon_sz + 2); /* how many slots fit */
struct wm_shm_surface *s = wm_shm_surface_create(dw, sh);
if (!s) {
wwarning("wl_backend: wm_dock_create: failed to allocate surface");
return;
}
wm_dock_render(s, icon_sz, n_slots);
/* Dock panel belongs in WL_LAYER_DOCK so it renders at the same
* depth as the dock icon frame_bufs (which wl_stacking_set_level
* places there for WMStatusWindowLevel=21). */
{
struct wlr_scene_tree *_dp = wl_state.level_trees[WL_LAYER_DOCK]
? wl_state.level_trees[WL_LAYER_DOCK] : &wl_state.scene->tree;
wm_shm_surface_commit(s, _dp, sw - dw, 0);
}
wl_state.dock_surface = s;
wl_state.dock_width = dw;
wl_state.dock_icon_sz = icon_sz;
wl_state.dock_n_slots = n_slots;
wl_state.dock_app_count = 0;
/* Shrink head-0 usable area so window placement avoids the dock. */
if (scr->usableArea) {
int right = scr->usableArea[0].x2 - dw;
if (right < scr->usableArea[0].x1)
right = scr->usableArea[0].x1;
scr->usableArea[0].x2 = right;
scr->totalUsableArea[0].x2 = right;
}
wwarning("wl_backend: dock panel created at %d,0 (%dx%d, %d slots)",
sw - dw, dw, sh, n_slots);
}
/* ------------------------------------------------------------------ */
/* Phase 30+: dock icon management */
/* ------------------------------------------------------------------ */
/*
* wm_shm_surface_repaint — after updating the pixman pixel data,
* force wlroots to re-upload the buffer to the GPU.
*
* wlroots 0.15 caches a texture per wlr_buffer pointer. Since our
* buffer is the same C object the cache entry stays valid even when
* pixel data changes. The reliable solution is to destroy the
* scene_buffer node and recreate it; wlroots then creates a fresh
* texture from the updated pixel data on the next frame.
*/
static void
wm_shm_surface_repaint(struct wm_shm_surface *s)
{
if (!s || !s->scene_buf) return;
int x = s->scene_buf->node.x;
int y = s->scene_buf->node.y;
struct wlr_scene_tree *parent = s->scene_buf->node.parent;
wlr_scene_node_destroy(&s->scene_buf->node);
s->scene_buf = NULL;
if (parent) {
s->scene_buf = wlr_scene_buffer_create(parent, &s->wlr_buf);
if (s->scene_buf)
wlr_scene_node_set_position(&s->scene_buf->node, x, y);
}
}
/*
* wm_dock_load_icon — read _NET_WM_ICON from an XWayland window via
* wrlib and return a heap-allocated ARGB8888 pixel array.
* Returns NULL for XDG native clients (no X11 icon available) or on
* failure; caller must free() the returned pointer when done.
*/
static uint32_t *
wm_dock_load_icon(struct wl_toplevel_view *v, int *out_w, int *out_h)
{
*out_w = *out_h = 0;
if (!v->xw_surface || !wl_state.x_display)
return NULL; /* XDG native: no X11 icon path */
RImage *ri = get_window_image_from_x11(wl_state.x_display, (WNativeWindow)v->id);
if (!ri)
return NULL;
int w = ri->width, h = ri->height;
int bpp = (ri->format == RRGBAFormat) ? 4 : 3;
uint32_t *argb = malloc((size_t)(w * h) * 4);
if (!argb) { RReleaseImage(ri); return NULL; }
unsigned char *p = ri->data;
for (int i = 0; i < w * h; i++, p += bpp) {
uint8_t r = p[0], g = p[1], b = p[2];
uint8_t a = (bpp == 4) ? p[3] : 255;
argb[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) |
((uint32_t)g << 8) | (uint32_t)b;
}
RReleaseImage(ri);
*out_w = w;
*out_h = h;
return argb;
}
/*
* wm_dock_refresh — repaint the dock surface with empty tiles, then
* overlay each managed app's icon into its slot, and re-upload to GPU.
*/
static void
wm_dock_refresh(void)
{
struct wm_shm_surface *s = wl_state.dock_surface;
if (!s) return;
int icon_sz = wl_state.dock_icon_sz;
int n_slots = wl_state.dock_n_slots;
int w = s->width;
int tile_sz = icon_sz;
int pad = (w - tile_sz) / 2;
/* Repaint background and all placeholder tiles */
wm_dock_render(s, icon_sz, n_slots);
/* Composite each app icon into its slot */
for (int i = 0; i < wl_state.dock_app_count && i < n_slots; i++) {
int iw = wl_state.dock_apps[i].icon_w;
int ih = wl_state.dock_apps[i].icon_h;
uint32_t *px = wl_state.dock_apps[i].icon_argb;
if (!px || iw <= 0 || ih <= 0)
continue;
pixman_image_t *src = pixman_image_create_bits(
PIXMAN_a8r8g8b8, iw, ih, px, iw * 4);
if (!src) continue;
/* Scale into the inset area (1px bevel border) */
int dst_sz = tile_sz - 2;
if (dst_sz < 1) dst_sz = 1;
pixman_transform_t xf;
pixman_transform_init_scale(&xf,
pixman_double_to_fixed((double)iw / dst_sz),
pixman_double_to_fixed((double)ih / dst_sz));
pixman_image_set_transform(src, &xf);
pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0);
int ty = 2 + i * (tile_sz + 2);
if (ty + tile_sz > s->height) { pixman_image_unref(src); break; }
pixman_image_composite32(PIXMAN_OP_OVER,
src, NULL, s->image,
0, 0, 0, 0,
pad + 1, ty + 1,
dst_sz, dst_sz);
pixman_image_unref(src);
}
/* Re-upload to GPU */
wm_shm_surface_repaint(s);
}
/*
* wm_dock_app_add — called when a window is managed; adds it to the
* dock list, attempts to load its icon, and refreshes the dock panel.
*/
static void
wm_dock_app_add(struct wl_toplevel_view *v)
{
if (!wl_state.dock_surface) return;
if (wl_state.dock_app_count >= WM_DOCK_MAX_APPS) return;
int slot = wl_state.dock_app_count;
wl_state.dock_apps[slot].id = v->id;
wl_state.dock_apps[slot].icon_argb =
wm_dock_load_icon(v, &wl_state.dock_apps[slot].icon_w,
&wl_state.dock_apps[slot].icon_h);
wl_state.dock_app_count++;
wm_dock_refresh();
wwarning("wl_backend: dock: add id=0x%lx icon=%s slot=%d",
v->id,
wl_state.dock_apps[slot].icon_argb ? "loaded" : "none",
slot);
}
/*
* wm_dock_app_remove — called when a window is unmanaged; removes it
* from the dock list and refreshes the panel.
*/
static void
wm_dock_app_remove(WNativeWindow id)
{
if (!wl_state.dock_surface) return;
for (int i = 0; i < wl_state.dock_app_count; i++) {
if (wl_state.dock_apps[i].id != id)
continue;
free(wl_state.dock_apps[i].icon_argb);
int tail = wl_state.dock_app_count - i - 1;
if (tail > 0)
memmove(&wl_state.dock_apps[i],
&wl_state.dock_apps[i + 1],
(size_t)tail * sizeof(wl_state.dock_apps[0]));
wl_state.dock_app_count--;
wm_dock_refresh();
wwarning("wl_backend: dock: remove id=0x%lx total=%d",
id, wl_state.dock_app_count);
return;
}
}
/* ------------------------------------------------------------------ */
/* Screen open/close */
/* ------------------------------------------------------------------ */
static void
wl_screen_open(WScreen *scr)
{
/* Set geometry from the headless output created in display_open(). */
scr->scr_width = wl_state.output ?
(int)wl_state.output->width : 1920;
scr->scr_height = wl_state.output ?
(int)wl_state.output->height : 1080;
scr->depth = 24;
scr->root_win = (WNativeWindow)0x0FFFFFFF; /* synthetic root window ID */
scr->colormap = 0;
/* Wayland has no X11-style window border; the frame_buf renders the
* full decoration. Set border width to 0 so policy code does not
* offset the client position by a non-existent border. */
scr->frame_border_width = 0;
/* XWayland bridge: fill in real X11 screen values so RCreateContext
* and WINGs can use the display connection for drawing and fonts. */
if (wl_state.x_display) {
int snum = DefaultScreen(wl_state.x_display);
scr->root_win = DefaultRootWindow(wl_state.x_display);
scr->depth = DefaultDepth(wl_state.x_display, snum);
scr->colormap = DefaultColormap(wl_state.x_display, snum);
}
}
static void wl_screen_close(WScreen *scr) { (void)scr; }
static int
wl_screen_init_display(WScreen *scr, int screen_number)
{
(void)screen_number;
scr->w_depth = 32;
scr->rcontext = RCreateContextWayland(scr->scr_width, scr->scr_height, NULL);
/* WINGs widget backends are selected via W_SetNextScreenKind — the
* compositor uses the Wayland-mode set even though wmaker itself is
* the compositor (rendering goes through pixman → wlroots scenes). */
W_SetNextScreenKind(WM_SCREEN_KIND_WAYLAND);
scr->wmscreen = WMCreateScreenWithRContext(NULL, 0, scr->rcontext);
if (!scr->wmscreen) {
wfatal(_("could not initialize WINGs widget set"));
return 0;
}
scr->black = WMBlackColor(scr->wmscreen);
scr->white = WMWhiteColor(scr->wmscreen);
scr->gray = WMGrayColor(scr->wmscreen);
scr->darkGray = WMDarkGrayColor(scr->wmscreen);
scr->black_pixel = scr->black ? WMColorPixel(scr->black) : 0xFF000000;
scr->white_pixel = scr->white ? WMColorPixel(scr->white) : 0xFFFFFFFF;
scr->light_pixel = scr->gray ? WMColorPixel(scr->gray) : 0xFFAAAAAA;
scr->dark_pixel = scr->darkGray ? WMColorPixel(scr->darkGray) : 0xFF555555;
return 1;
}
static int
wl_show_crash_dialog(int sig)
{
/* Wayland: showing a UI from a crashed compositor's child process is
* not possible (the child would need to connect to the dead parent
* for its compositor). Default to auto-restart, which is the most
* useful behavior for a daily-driver compositor — the user's session
* recovers on its own. The crash signal is logged separately by
* the monitor parent. */
(void)sig;
return WMRestart;
}
/* ------------------------------------------------------------------ */
/* Frame buffer (pixman + wlr_scene_buffer) */
/* ------------------------------------------------------------------ */
static void frame_buf_destroy(struct wlr_buffer *wlr_buf)
{
struct wl_frame_buf *fb = wl_container_of(wlr_buf, fb, base);
if (fb->image)
pixman_image_unref(fb->image);
wfree(fb);
}
static bool frame_buf_begin_data_ptr_access(struct wlr_buffer *wlr_buf,
uint32_t flags, void **data, uint32_t *format, size_t *stride)
{
struct wl_frame_buf *fb = wl_container_of(wlr_buf, fb, base);
(void)flags;
*data = pixman_image_get_data(fb->image);
*format = DRM_FORMAT_ARGB8888;
*stride = (size_t)pixman_image_get_stride(fb->image);
return true;
}
static void frame_buf_end_data_ptr_access(struct wlr_buffer *wlr_buf)
{
(void)wlr_buf;
}
static const struct wlr_buffer_impl frame_buf_impl = {
.destroy = frame_buf_destroy,
.begin_data_ptr_access = frame_buf_begin_data_ptr_access,
.end_data_ptr_access = frame_buf_end_data_ptr_access,
};
static struct wl_frame_buf *
frame_buf_find(WNativeWindow id)
{
struct wl_frame_buf *fb;
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (fb->id == id) return fb;
}
return NULL;
}
/*
* fb_scene_buf_attach / fb_tree_attach
*
* Register destroy listeners on a scene_buf or tree so that fb->scene_buf
* (or fb->tree) is NULL'd when wlroots auto-destroys the node — typically
* via cascade when an ancestor scene_tree is destroyed. Without this,
* follow-up cleanup paths (wl_view_destroy_backing_impl, wl_frame_destroy)
* call wlr_scene_node_destroy on a freed node and segfault inside
* wl_list_remove against a zeroed list link.
*/
static void fb_scene_buf_destroy_handler(struct wl_listener *l, void *d)
{
(void)d;
struct wl_frame_buf *fb = wl_container_of(l, fb, scene_buf_destroy);
fb->scene_buf = NULL;
wl_list_remove(&fb->scene_buf_destroy.link);
}
static void fb_tree_destroy_handler(struct wl_listener *l, void *d)
{
(void)d;
struct wl_frame_buf *fb = wl_container_of(l, fb, tree_destroy);
fb->tree = NULL;
/* The tree's destroy cascades into scene_buf — its destroy handler
* will fire too and NULL fb->scene_buf separately. */
wl_list_remove(&fb->tree_destroy.link);
}
static void fb_attach_scene_buf_listener(struct wl_frame_buf *fb)
{
if (!fb || !fb->scene_buf) return;
fb->scene_buf_destroy.notify = fb_scene_buf_destroy_handler;
wl_signal_add(&fb->scene_buf->node.events.destroy,
&fb->scene_buf_destroy);
}
static void fb_attach_tree_listener(struct wl_frame_buf *fb)
{
if (!fb || !fb->tree) return;
fb->tree_destroy.notify = fb_tree_destroy_handler;
wl_signal_add(&fb->tree->node.events.destroy, &fb->tree_destroy);
}
/* Recursively update fb->x, fb->y for all descendants of `parent`, used
* to keep absolute-position tracking in sync after the parent moves.
* Hit-testing (frame_buf_at, frame_buf_button_at) and click coordinate
* translation depend on these fields being current. */
static void
frame_buf_update_children_abs(WNativeWindow parent_id)
{
struct wl_frame_buf *parent = frame_buf_find(parent_id);
if (!parent) return;
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
if (child->parent_id == parent_id) {
child->x = parent->x + child->rel_x;
child->y = parent->y + child->rel_y;
/* Recurse into grandchildren */
frame_buf_update_children_abs(child->id);
}
}
/* Also update toplevel views parented to this frame */
struct wl_toplevel_view *cv;
wl_list_for_each(cv, &wl_state.toplevel_list, link) {
if (cv->frame_id == parent_id) {
cv->x = parent->x + cv->frame_rel_x;
cv->y = parent->y + cv->frame_rel_y;
}
}
}
/* Hit-test: find the frame_buf under (px, py) that should receive the event.
*
* Returns the most-specific (innermost) frame_buf whose rendered pixels
* are topmost at (px, py), so the WM event layer receives the correct
* sub-window ID (titlebar, resizebar, button) needed to trigger move/resize.
*
* Scene-graph structure (level-tree model):
*
* scene->tree
* level_trees[N] <- level bucket
* fb->tree <- top-level frame grouping tree
* fb->scene_buf <- full-frame decoration pixels
* child_fb->scene_buf <- titlebar / resizebar sub-frames
* v->scene_node <- client surface (returns NULL)
*
* Strategy:
* 1. wlr_scene_node_at gives the topmost rendered node.
* 2. First check if that exact node is a child frame_buf's scene_buf
* (titlebar, resizebar, button) — if so, return it directly.
* 3. Walk up to the fb->tree level (parent is a level_tree bucket).
* 4. Match that window-root node to a top-level frame_buf's tree.
* 5. If it was a client surface (v->scene_node), return NULL. */
static struct wl_frame_buf *
frame_buf_at(int px, int py)
{
if (!wl_state.scene) return NULL;
double sx, sy;
struct wlr_scene_node *node = wlr_scene_node_at(
&wl_state.scene->tree.node, (double)px, (double)py, &sx, &sy);
if (!node) return NULL;
struct wl_frame_buf *fb;
/* Step 2: exact match — is the topmost node a child frame_buf's
* scene_buf (no tree wrapper)? This covers titlebars, resizebars,
* and button sub-frames, which are what the WM needs for move/resize. */
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (!fb->tree && fb->scene_buf && &fb->scene_buf->node == node)
return fb;
}
/* Step 3: walk up until the parent is a level_tree bucket.
* The node we stop at is the fb->tree of a top-level frame, or
* a v->scene_node sitting directly in a level tree (unframed). */
struct wlr_scene_node *n = node;
while (n->parent
&& &n->parent->node != &wl_state.scene->tree.node) {
int _at_level = 0, _li;
for (_li = 0; _li < WL_NUM_LEVEL_BUCKETS; _li++) {
if (wl_state.level_trees[_li] &&
n->parent == wl_state.level_trees[_li]) {
_at_level = 1; break;
}
}
if (_at_level) break;
n = &n->parent->node;
}
/* Step 4: match window-root node to a top-level frame_buf. */
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (fb->tree && &fb->tree->node == n)
return fb;
}
/* Step 5: match a tree-less frame_buf (WINGs dialog) whose scene_buf
* is a direct child of a level_tree bucket. */
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (!fb->tree && fb->scene_buf && &fb->scene_buf->node == n)
return fb;
}
/* Step 6: topmost node was a client surface — not a WM frame. */
return NULL;
}
/*
* frame_buf_button_at — coordinate-based hit-test for virtual button frame_bufs.
*
* Buttons are composited into the titlebar buffer: their scene_buf nodes are
* in the scene graph but carry no committed buffer (scene_buf->buffer == NULL)
* and are therefore invisible to wlr_scene_node_at. We find them by walking
* the frame_list and comparing absolute screen coordinates.
*
* Returns the button fb whose absolute rect encloses (px,py) with the
* smallest area (most specific), or NULL if no button is under the pointer.
*/
static struct wl_frame_buf *
frame_buf_button_at(int px, int py)
{
struct wl_frame_buf *best = NULL;
struct wl_frame_buf *fb;
wl_list_for_each(fb, &wl_state.frame_list, link) {
if (!fb->parent_id) continue; /* top-level root */
if (!fb->scene_buf) continue; /* no scene node */
if (!fb->scene_buf->node.enabled) continue; /* hidden */
/* Walk up the scene tree — if any ancestor is disabled, this
* frame_buf is not actually visible (just its node enabled flag
* is set, but a parent tree was disabled e.g. by window_unmap). */
struct wlr_scene_node *n = fb->scene_buf->node.parent
? &fb->scene_buf->node.parent->node : NULL;
int ancestor_disabled = 0;
while (n) {
if (!n->enabled) { ancestor_disabled = 1; break; }
n = n->parent ? &n->parent->node : NULL;
}
if (ancestor_disabled) continue;
/* Buttons are small children of the titlebar with enabled scene_bufs.
* Skip large nodes (titlebars, resizebars) by size threshold. */
if (px < fb->x || px >= fb->x + fb->width) continue;
if (py < fb->y || py >= fb->y + fb->height) continue;
/* Prefer the smallest (most specific) enclosing fb. */
if (!best || (fb->width * fb->height) < (best->width * best->height))
best = fb;
}
return best;
}
/*
* frame_buf_hit — resolve the WM sub-window at (px,py).
*
* Tries button hit-testing first (buttons are invisible to the scene graph
* because their pixels are composited into the titlebar buffer), then falls
* back to the scene-graph-based frame_buf_at for titlebars, resizebars, and
* top-level frames.
*/
static struct wl_frame_buf *
frame_buf_hit(int px, int py)
{
struct wl_frame_buf *fb = frame_buf_button_at(px, py);
return fb ? fb : frame_buf_at(px, py);
}
/* ------------------------------------------------------------------ */
/* Frame / window lifecycle */
/* ------------------------------------------------------------------ */
static WNativeWindow
wl_frame_create_toplevel(WScreen *scr,
int x, int y, int width, int height,
int border_width,
int depth, void *visual,
unsigned long colormap,
WMPixel border_pixel)
{
(void)border_width; (void)depth; (void)visual;
(void)colormap; (void)border_pixel; (void)scr;
if (width <= 0) width = 1;
if (height <= 0) height = 1;
struct wl_frame_buf *fb = wmalloc(sizeof(*fb));
memset(fb, 0, sizeof(*fb));
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
width, height, NULL, 0);
if (!fb->image) {
wfree(fb);
return WNATIVE_NONE;
}
wlr_buffer_init(&fb->base, &frame_buf_impl, width, height);
fb->id = wl_state.next_synthetic_id++;
fb->x = x;
fb->y = y;
fb->width = width;
fb->height = height;
/* Create scene buffer node under a per-window tree.
* The tree is positioned at (x,y) so all children use local offsets.
* Default to WL_LAYER_NORMAL; wl_stacking_set_level() will reparent
* to the correct bucket when AddToStackList() fires immediately after. */
if (wl_state.scene) {
struct wlr_scene_tree *_parent = wl_state.level_trees[WL_LAYER_NORMAL]
? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree;
fb->tree = wlr_scene_tree_create(_parent);
if (fb->tree) {
fb_attach_tree_listener(fb);
wlr_scene_node_set_position(&fb->tree->node, x, y);
fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base);
if (fb->scene_buf) {
fb_attach_scene_buf_listener(fb);
wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0);
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
/* Tree starts disabled — wl_window_map enables it when the
* frame is logically mapped. Without this, decoration
* children (titlebar, buttons) of unmapped menus/dialogs
* appear as orphans because window_map_raised on those
* children enables their scene_bufs while the frame itself
* has not been mapped. */
wlr_scene_node_set_enabled(&fb->tree->node, false);
}
}
wl_list_insert(&wl_state.frame_list, &fb->link);
/* Register this frame_buf image in the WINGs backing table so
* WMDrawString() can resolve the window ID to a pixman_image_t
* (needed for miniwindow title drawing in update_icon_title). */
W_RegisterBacking(fb->id, fb->image);
return fb->id;
}
static WNativeWindow
wl_frame_create_child(WNativeWindow parent_win, WScreen *scr,
int x, int y, int width, int height)
{
(void)scr;
if (width <= 0) width = 1;
if (height <= 0) height = 1;
/* Compute absolute position from parent */
int abs_x = x, abs_y = y;
struct wl_frame_buf *parent = frame_buf_find(parent_win);
if (parent) {
abs_x = parent->x + x;
abs_y = parent->y + y;
}
struct wl_frame_buf *fb = wmalloc(sizeof(*fb));
memset(fb, 0, sizeof(*fb));
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
width, height, NULL, 0);
if (!fb->image) {
wfree(fb);
return WNATIVE_NONE;
}
wlr_buffer_init(&fb->base, &frame_buf_impl, width, height);
fb->id = wl_state.next_synthetic_id++;
fb->parent_id = parent_win;
fb->rel_x = x;
fb->rel_y = y;
fb->x = abs_x;
fb->y = abs_y;
fb->width = width;
fb->height = height;
if (wl_state.scene) {
struct wlr_scene_tree *ptree = (parent && parent->tree)
? parent->tree : &wl_state.scene->tree;
fb->scene_buf = wlr_scene_buffer_create(ptree, &fb->base);
if (fb->scene_buf) {
fb_attach_scene_buf_listener(fb);
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
}
wl_list_insert(&wl_state.frame_list, &fb->link);
W_RegisterBacking(fb->id, fb->image);
return fb->id;
}
static void
wl_frame_destroy(WScreen *scr, WNativeWindow win)
{
(void)scr;
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb) return;
W_UnregisterBacking(fb->id);
/* Destroy the scene_buf and (if any) the tree. Without destroying
* the tree, leaked tree nodes accumulate in the scene graph and
* can cause stale rendering or hit-testing issues (e.g. ghost
* titlebars after a menu is closed). */
if (fb->scene_buf)
wlr_scene_node_destroy(&fb->scene_buf->node);
if (fb->tree)
wlr_scene_node_destroy(&fb->tree->node);
wl_list_remove(&fb->link);
wlr_buffer_drop(&fb->base);
}
static void
wl_frame_configure(WNativeWindow win, int x, int y, int width, int height)
{
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb) return;
/* Update position tracking */
fb->rel_x = x;
fb->rel_y = y;
if (fb->parent_id) {
struct wl_frame_buf *parent = frame_buf_find(fb->parent_id);
if (parent) {
fb->x = parent->x + x;
fb->y = parent->y + y;
} else {
fb->x = x;
fb->y = y;
}
/* Child frame_buf: reposition scene_buf at relative offset within parent tree */
if (fb->scene_buf)
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
} else {
fb->x = x;
fb->y = y;
/* Top-level frame: move the tree (children follow automatically) */
if (fb->tree)
wlr_scene_node_set_position(&fb->tree->node, x, y);
}
/* Resize: reallocate the pixman image if dimensions changed. */
if (width > 0 && height > 0 && (width != fb->width || height != fb->height)) {
if (fb->image)
pixman_image_unref(fb->image);
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
width, height, NULL, 0);
fb->width = width;
fb->height = height;
fb->base.width = width;
fb->base.height = height;
/* Notify scene graph of new size — required for hit-testing.
* wlroots caches buffer dimensions; without this, clicks beyond
* the old size would miss this scene_buf. */
if (fb->scene_buf)
wlr_scene_buffer_set_dest_size(fb->scene_buf, width, height);
}
/* Update absolute position tracking — recursively for all descendants. */
frame_buf_update_children_abs(win);
}
static void wl_frame_paint_button(WCoreWindow *button, WTexture *texture,
unsigned long color, WPixmap *image, int pushed);
/*
* wl_frame_paint — render the entire frame decoration into the pixman buffer.
*
* Uses wTextureRenderImage() (pure CPU) for titlebar/resizebar backgrounds,
* pixman for bevel lines, and the WINGs font backend for title text.
* Returns 1 (handled) so wFrameWindowPaint skips the Xlib code path.
*/
static int
wl_frame_paint(struct WFrameWindow *fwin)
{
struct wl_frame_buf *fb = frame_buf_find(fwin->core->window);
if (!fb || !fb->image)
return 0;
int state = fwin->flags.state;
int total_w = fb->width;
int total_h = fb->height;
/* Clear the buffer to transparent black. */
pixman_color_t clear = {0, 0, 0, 0}; /* transparent */
pixman_image_fill_rectangles(PIXMAN_OP_SRC, fb->image, &clear,
1, &(pixman_rectangle16_t){0, 0, total_w, total_h});
/* --- Titlebar --- */
if (fwin->titlebar && fwin->top_width > 0) {
struct wl_frame_buf *tb_fb = frame_buf_find(fwin->titlebar->window);
pixman_image_t *tb_img = tb_fb ? tb_fb->image : fb->image;
int tb_w = total_w; /* always full frame width */
int tb_h = fwin->top_width;
/* In Wayland we composite buttons into the titlebar buffer, so
* the titlebar must be full-width and positioned at the frame origin
* regardless of what updateTitlebar() configured for X11 layout. */
if (tb_fb) {
tb_fb->rel_x = 0;
tb_fb->rel_y = 0;
tb_fb->x = fb->x;
tb_fb->y = fb->y;
if (tb_fb->scene_buf)
wlr_scene_node_set_position(&tb_fb->scene_buf->node, 0, 0);
}
/* Ensure titlebar buffer is full width */
if (tb_fb && tb_fb->width < tb_w) {
pixman_image_unref(tb_fb->image);
tb_fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8,
tb_w, tb_h, NULL, 0);
tb_fb->width = tb_w;
tb_fb->height = tb_h;
tb_fb->base.width = tb_w;
tb_fb->base.height = tb_h;
tb_img = tb_fb->image;
/* Notify scene graph so hit-testing covers the new size. */
if (tb_fb->scene_buf)
wlr_scene_buffer_set_dest_size(tb_fb->scene_buf, tb_w, tb_h);
}
WTexture *tex = fwin->title_texture[state];
/* Clear titlebar buffer */
if (tb_fb) {
pixman_color_t clear = {0, 0, 0, 0};
pixman_image_fill_rectangles(PIXMAN_OP_SRC, tb_img, &clear,
1, &(pixman_rectangle16_t){0, 0, tb_w, tb_h});
}
/* Render texture to RImage. */
RImage *img = wTextureRenderImage(tex, tb_w, tb_h, WREL_RAISED);
if (img) {
uint32_t *dst = pixman_image_get_data(tb_img);
int dst_stride = pixman_image_get_stride(tb_img) / 4;
int x, y;
if (img->format == RRGBAFormat) {
for (y = 0; y < tb_h && y < img->height; y++) {
unsigned char *src_row = img->data + y * img->width * 4;
uint32_t *dst_row = dst + y * dst_stride;
for (x = 0; x < tb_w && x < img->width; x++) {
unsigned char r = src_row[x*4+0];
unsigned char g = src_row[x*4+1];
unsigned char b = src_row[x*4+2];
unsigned char a = src_row[x*4+3];
dst_row[x] = ((uint32_t)a << 24) | ((uint32_t)r << 16) |
((uint32_t)g << 8) | (uint32_t)b;
}
}
} else {
/* RGB format */
for (y = 0; y < tb_h && y < img->height; y++) {
unsigned char *src_row = img->data + y * img->width * 3;
uint32_t *dst_row = dst + y * dst_stride;
for (x = 0; x < tb_w && x < img->width; x++) {
unsigned char r = src_row[x*3+0];
unsigned char g = src_row[x*3+1];
unsigned char b = src_row[x*3+2];
dst_row[x] = 0xFF000000U | ((uint32_t)r << 16) |
((uint32_t)g << 8) | (uint32_t)b;
}
}
}
RReleaseImage(img);
}
/* Draw title text. */
if (fwin->title && *fwin->font && fwin->title_color[state]) {
int lofs = 6, rofs = 6;
int titlelen, tw, tx, ty, th;
char *title;
if (fwin->left_button && !fwin->flags.hide_left_button &&
!fwin->flags.lbutton_dont_fit)
lofs += fwin->left_button->width + 3;
if (fwin->right_button && !fwin->flags.hide_right_button &&
!fwin->flags.rbutton_dont_fit)
rofs += fwin->right_button->width + 3;
title = ShrinkString(*fwin->font, fwin->title, tb_w - lofs - rofs);
titlelen = strlen(title);
tw = WMWidthOfString(*fwin->font, title, titlelen);
th = WMFontHeight(*fwin->font);
switch (fwin->flags.justification) {
case WTJ_LEFT: tx = lofs; break;
case WTJ_RIGHT: tx = tb_w - tw - rofs; break;
default: tx = (tb_w - tw) / 2; break;
}
ty = (tb_h - th) / 2;
WMDrawString(fwin->screen_ptr->wmscreen, (WMDrawable)tb_img,
fwin->title_color[state], *fwin->font,
tx, ty, title, titlelen);
wfree(title);
}
/* Commit titlebar buffer */
if (tb_fb && tb_fb->scene_buf) {
wlr_scene_buffer_set_buffer_with_damage(tb_fb->scene_buf, &tb_fb->base, NULL);
}
}
/* --- Titlebar buttons --- */
if (fwin->titlebar && fwin->top_width > 0) {
if (fwin->left_button && !fwin->flags.hide_left_button &&
!fwin->flags.lbutton_dont_fit)
wl_frame_paint_button(fwin->left_button,
fwin->title_texture[state],
WMColorPixel(fwin->title_color[state]),
fwin->lbutton_image, False);
if (fwin->right_button && !fwin->flags.hide_right_button &&
!fwin->flags.rbutton_dont_fit)
wl_frame_paint_button(fwin->right_button,
fwin->title_texture[state],
WMColorPixel(fwin->title_color[state]),
fwin->rbutton_image, False);
}
/* --- Resizebar --- */
/* Skip resizebar when frame is shaded (height ≤ titlebar). */
bool show_resizebar = fwin->resizebar && fwin->bottom_width > 0
&& total_h > (int)fwin->top_width;
if (show_resizebar) {
int rb_y = total_h - fwin->bottom_width;
int rb_w = total_w;
int rb_h = fwin->bottom_width;
WTexture *tex = fwin->resizebar_texture[0];
RImage *img = wTextureRenderImage(tex, rb_w, rb_h, WREL_RAISED);
if (img) {
uint32_t *dst = pixman_image_get_data(fb->image);
int dst_stride = pixman_image_get_stride(fb->image) / 4;
int x, y;
if (img->format == RRGBAFormat) {
for (y = 0; y < rb_h && y < img->height; y++) {
unsigned char *src_row = img->data + y * img->width * 4;
uint32_t *dst_row = dst + (rb_y + y) * dst_stride;
for (x = 0; x < rb_w && x < img->width; x++) {
unsigned char r = src_row[x*4+0];
unsigned char g = src_row[x*4+1];
unsigned char b = src_row[x*4+2];
unsigned char a = src_row[x*4+3];
dst_row[x] = ((uint32_t)a << 24) | ((uint32_t)r << 16) |
((uint32_t)g << 8) | (uint32_t)b;
}
}
} else {
for (y = 0; y < rb_h && y < img->height; y++) {
unsigned char *src_row = img->data + y * img->width * 3;
uint32_t *dst_row = dst + (rb_y + y) * dst_stride;
for (x = 0; x < rb_w && x < img->width; x++) {
unsigned char r = src_row[x*3+0];
unsigned char g = src_row[x*3+1];
unsigned char b = src_row[x*3+2];
dst_row[x] = 0xFF000000U | ((uint32_t)r << 16) |
((uint32_t)g << 8) | (uint32_t)b;
}
}
}
RReleaseImage(img);
}
}
/* Hide the resizebar's own scene node when shaded. */
if (fwin->resizebar) {
struct wl_frame_buf *rb_fb = frame_buf_find(fwin->resizebar->window);
if (rb_fb && rb_fb->scene_buf)
wlr_scene_node_set_enabled(&rb_fb->scene_buf->node, show_resizebar);
}
/* Commit the buffer to the scene graph only if it has content (resizebar). */
if (fb->scene_buf) {
if (show_resizebar) {
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
} else {
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
}
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
return 1;
}
/* ------------------------------------------------------------------ */
/* Context (window → object lookup table) — Phase 38 */
/* ------------------------------------------------------------------ */
static void
wl_context_save(WNativeWindow win, WContextKey key, void *data)
{
struct wl_list *list = &wl_state.ctx_lists[(int)key];
struct wl_ctx_entry *e;
/* update existing entry if present */
wl_list_for_each(e, list, link) {
if (e->win == win) { e->data = data; return; }
}
/* new entry */
e = wmalloc(sizeof(*e));
e->win = win;
e->data = data;
wl_list_insert(list, &e->link);
}
static void *
wl_context_find(WNativeWindow win, WContextKey key)
{
struct wl_list *list = &wl_state.ctx_lists[(int)key];
struct wl_ctx_entry *e;
wl_list_for_each(e, list, link) {
if (e->win == win) return e->data;
}
return NULL;
}
static int
wl_context_delete(WNativeWindow win, WContextKey key)
{
struct wl_list *list = &wl_state.ctx_lists[(int)key];
struct wl_ctx_entry *e, *tmp;
wl_list_for_each_safe(e, tmp, list, link) {
if (e->win == win) {
wl_list_remove(&e->link);
wfree(e);
return 1;
}
}
return 0; /* not found */
}
/* ------------------------------------------------------------------ */
/* 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;
W_WL_EnqueueEvent(wmscr, &wev, NULL);
/* 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 (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;
}
/*
* 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 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;
}
}
W_WL_EnqueueEvent(wmscr, &wev, target);
}
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)
wl_set_cursor_image("left_ptr");
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;
}
}
/* ------------------------------------------------------------------ */
/* 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;
wl_set_cursor_image("left_ptr");
}
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);
/* Restore WM cursor — the client may have set its own cursor
* surface which becomes invalid once pointer focus leaves. */
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;
*root_x = wl_state.pointer_x;
*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)cursor_id; (void)timestamp; }
/* ------------------------------------------------------------------ */
/* Keyboard grabs */
/* ------------------------------------------------------------------ */
static int
wl_keyboard_grab(WNativeWindow win)
{
(void)win;
wl_state.keyboard_grabbed = true;
return 0;
}
static void
wl_keyboard_ungrab(void)
{
wl_state.keyboard_grabbed = false;
}
/* ------------------------------------------------------------------ */
/* Phase 81: Overlay animation surface — XOR pixel engine + bitmap */
/* font for geometry display (miniaturize zoom, transparent */
/* move/resize frames, position/size readouts). */
/* ------------------------------------------------------------------ */
/*
* XOR mask: 0xFFFFFFFF XOR 0x00000000 (transparent) = 0xFFFFFFFF (opaque
* white); XOR 0xFFFFFFFF again = 0x00000000 (transparent, i.e. erased).
* The overlay scene buffer is composited OVER all client windows via
* standard alpha blending, so transparent pixels show through to the
* desktop and opaque-white pixels appear on top.
*/
#define OVL_MASK 0xFFFFFFFFu
/*
* 5-wide × 9-tall bitmap font for printable ASCII (0x20–0x7E).
* Encoding: bit 7 = column 0 (left), bit 3 = column 4 (right), bits 2–0
* unused. Cell width = 6 px (glyph 5 + 1 gap); cell height = 10 px.
*/
#define OVL_GW 5 /* glyph pixel width */
#define OVL_GH 9 /* glyph pixel height */
#define OVL_CW 6 /* cell width (px) */
#define OVL_CH 10 /* cell height (px) */
static const uint8_t ovl_font[96][OVL_GH] = {
/*20 ' '*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/*21 '!'*/{0x20,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x00},
/*22 '"'*/{0x50,0x50,0x50,0x00,0x00,0x00,0x00,0x00,0x00},
/*23 '#'*/{0x50,0x50,0xF8,0x50,0xF8,0x50,0x50,0x00,0x00},
/*24 '$'*/{0x20,0x70,0xA0,0x70,0x28,0x70,0x20,0x00,0x00},
/*25 '%'*/{0xC0,0xC8,0x10,0x20,0x40,0x98,0x18,0x00,0x00},
/*26 '&'*/{0x60,0x90,0x60,0xA0,0x98,0x90,0x68,0x00,0x00},
/*27'\''*/ {0x60,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00},
/*28 '('*/{0x10,0x20,0x40,0x40,0x40,0x20,0x10,0x00,0x00},
/*29 ')'*/{0x40,0x20,0x10,0x10,0x10,0x20,0x40,0x00,0x00},
/*2A '*'*/{0x00,0x88,0x50,0xF8,0x50,0x88,0x00,0x00,0x00},
/*2B '+'*/{0x00,0x20,0x20,0xF8,0x20,0x20,0x00,0x00,0x00},
/*2C ','*/{0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00},
/*2D '-'*/{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00},
/*2E '.'*/{0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00},
/*2F '/'*/{0x08,0x08,0x10,0x20,0x40,0x80,0x80,0x00,0x00},
/*30 '0'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00},
/*31 '1'*/{0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00,0x00},
/*32 '2'*/{0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00,0x00},
/*33 '3'*/{0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00,0x00},
/*34 '4'*/{0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00,0x00},
/*35 '5'*/{0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00,0x00},
/*36 '6'*/{0x38,0x40,0x80,0xF0,0x88,0x88,0x70,0x00,0x00},
/*37 '7'*/{0xF8,0x08,0x10,0x20,0x20,0x20,0x20,0x00,0x00},
/*38 '8'*/{0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00,0x00},
/*39 '9'*/{0x70,0x88,0x88,0x78,0x08,0x08,0x70,0x00,0x00},
/*3A ':'*/{0x00,0x60,0x60,0x00,0x60,0x60,0x00,0x00,0x00},
/*3B ';'*/{0x00,0x60,0x60,0x00,0x60,0x20,0x40,0x00,0x00},
/*3C '<'*/{0x10,0x20,0x40,0x80,0x40,0x20,0x10,0x00,0x00},
/*3D '='*/{0x00,0x00,0xF8,0x00,0xF8,0x00,0x00,0x00,0x00},
/*3E '>'*/{0x40,0x20,0x10,0x08,0x10,0x20,0x40,0x00,0x00},
/*3F '?'*/{0x70,0x88,0x08,0x10,0x20,0x00,0x20,0x00,0x00},
/*40 '@'*/{0x70,0x88,0x98,0xA8,0x98,0x80,0x70,0x00,0x00},
/*41 'A'*/{0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00,0x00},
/*42 'B'*/{0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00,0x00},
/*43 'C'*/{0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00,0x00},
/*44 'D'*/{0xE0,0x90,0x88,0x88,0x88,0x90,0xE0,0x00,0x00},
/*45 'E'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00,0x00},
/*46 'F'*/{0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00,0x00},
/*47 'G'*/{0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00,0x00},
/*48 'H'*/{0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00,0x00},
/*49 'I'*/{0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00},
/*4A 'J'*/{0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x00},
/*4B 'K'*/{0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00,0x00},
/*4C 'L'*/{0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00,0x00},
/*4D 'M'*/{0x88,0xD8,0xA8,0x88,0x88,0x88,0x88,0x00,0x00},
/*4E 'N'*/{0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00,0x00},
/*4F 'O'*/{0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00},
/*50 'P'*/{0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00,0x00},
/*51 'Q'*/{0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00,0x00},
/*52 'R'*/{0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00,0x00},
/*53 'S'*/{0x70,0x88,0x80,0x70,0x08,0x88,0x70,0x00,0x00},
/*54 'T'*/{0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x00},
/*55 'U'*/{0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00},
/*56 'V'*/{0x88,0x88,0x88,0x88,0x50,0x50,0x20,0x00,0x00},
/*57 'W'*/{0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00,0x00},
/*58 'X'*/{0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00,0x00},
/*59 'Y'*/{0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00,0x00},
/*5A 'Z'*/{0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00,0x00},
/*5B '['*/{0x70,0x40,0x40,0x40,0x40,0x40,0x70,0x00,0x00},
/*5C'\\'*/ {0x80,0x80,0x40,0x20,0x10,0x08,0x08,0x00,0x00},
/*5D ']'*/{0x70,0x10,0x10,0x10,0x10,0x10,0x70,0x00,0x00},
/*5E '^'*/{0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00},
/*5F '_'*/{0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x00},
/*60 '`'*/{0x40,0x20,0x10,0x00,0x00,0x00,0x00,0x00,0x00},
/*61 'a'*/{0x00,0x00,0x70,0x08,0x78,0x88,0x78,0x00,0x00},
/*62 'b'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0xF0,0x00,0x00},
/*63 'c'*/{0x00,0x00,0x70,0x88,0x80,0x88,0x70,0x00,0x00},
/*64 'd'*/{0x08,0x08,0x78,0x88,0x88,0x88,0x78,0x00,0x00},
/*65 'e'*/{0x00,0x00,0x70,0x88,0xF8,0x80,0x70,0x00,0x00},
/*66 'f'*/{0x30,0x48,0x40,0xE0,0x40,0x40,0x40,0x00,0x00},
/*67 'g'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x70,0x00},
/*68 'h'*/{0x80,0x80,0xF0,0x88,0x88,0x88,0x88,0x00,0x00},
/*69 'i'*/{0x20,0x00,0x60,0x20,0x20,0x20,0x70,0x00,0x00},
/*6A 'j'*/{0x10,0x00,0x30,0x10,0x10,0x10,0x90,0x60,0x00},
/*6B 'k'*/{0x80,0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00,0x00},
/*6C 'l'*/{0x60,0x20,0x20,0x20,0x20,0x20,0x70,0x00,0x00},
/*6D 'm'*/{0x00,0x00,0xD8,0xA8,0xA8,0xA8,0x88,0x00,0x00},
/*6E 'n'*/{0x00,0x00,0xF0,0x88,0x88,0x88,0x88,0x00,0x00},
/*6F 'o'*/{0x00,0x00,0x70,0x88,0x88,0x88,0x70,0x00,0x00},
/*70 'p'*/{0x00,0x00,0xF0,0x88,0x88,0xF0,0x80,0x80,0x00},
/*71 'q'*/{0x00,0x00,0x78,0x88,0x88,0x78,0x08,0x08,0x00},
/*72 'r'*/{0x00,0x00,0xB0,0xC8,0x80,0x80,0x80,0x00,0x00},
/*73 's'*/{0x00,0x00,0x70,0x80,0x70,0x08,0xF0,0x00,0x00},
/*74 't'*/{0x40,0x40,0xF0,0x40,0x40,0x48,0x30,0x00,0x00},
/*75 'u'*/{0x00,0x00,0x88,0x88,0x88,0x98,0x68,0x00,0x00},
/*76 'v'*/{0x00,0x00,0x88,0x88,0x88,0x50,0x20,0x00,0x00},
/*77 'w'*/{0x00,0x00,0x88,0x88,0xA8,0xD8,0x88,0x00,0x00},
/*78 'x'*/{0x00,0x00,0x88,0x50,0x20,0x50,0x88,0x00,0x00},
/*79 'y'*/{0x00,0x00,0x88,0x88,0x78,0x08,0x70,0x00,0x00},
/*7A 'z'*/{0x00,0x00,0xF8,0x10,0x20,0x40,0xF8,0x00,0x00},
/*7B '{'*/{0x18,0x20,0x20,0x40,0x20,0x20,0x18,0x00,0x00},
/*7C '|'*/{0x20,0x20,0x20,0x00,0x20,0x20,0x20,0x00,0x00},
/*7D '}'*/{0x60,0x10,0x10,0x08,0x10,0x10,0x60,0x00,0x00},
/*7E '~'*/{0x40,0xA8,0x18,0x00,0x00,0x00,0x00,0x00,0x00},
};
/* Lazily allocate (or resize) the full-screen overlay buffer. */
static void
overlay_ensure(WScreen *scr)
{
int w, h;
if (scr && scr->scr_width > 0 && scr->scr_height > 0) {
w = scr->scr_width;
h = scr->scr_height;
} else if (wl_state.output) {
w = (int)wl_state.output->width;
h = (int)wl_state.output->height;
} else {
w = 1920; h = 1080;
}
if (wl_state.overlay_surf &&
wl_state.overlay_surf->width == w &&
wl_state.overlay_surf->height == h)
return;
if (wl_state.overlay_surf) {
wm_shm_surface_destroy(wl_state.overlay_surf);
wl_state.overlay_surf = NULL;
}
wl_state.overlay_surf = wm_shm_surface_create(w, h);
}
/* XOR a single pixel on the overlay buffer (clipped). */
static inline void
ovl_plot(struct wm_shm_surface *s, int x, int y)
{
if ((unsigned)x < (unsigned)s->width && (unsigned)y < (unsigned)s->height)
s->data[y * (int)(s->stride >> 2) + x] ^= OVL_MASK;
}
/* XOR a horizontal run of `len` pixels starting at (x, y). */
static void
ovl_hline(struct wm_shm_surface *s, int x, int y, int len)
{
if ((unsigned)y >= (unsigned)s->height || len <= 0) return;
int x0 = x < 0 ? 0 : x;
int x1 = x + len; if (x1 > s->width) x1 = s->width;
if (x0 >= x1) return;
uint32_t *p = s->data + y * (int)(s->stride >> 2) + x0;
for (int i = x0; i < x1; i++) *p++ ^= OVL_MASK;
}
/* XOR a vertical run of `len` pixels starting at (x, y). */
static void
ovl_vline(struct wm_shm_surface *s, int x, int y, int len)
{
if ((unsigned)x >= (unsigned)s->width || len <= 0) return;
int y0 = y < 0 ? 0 : y;
int y1 = y + len; if (y1 > s->height) y1 = s->height;
int stride = (int)(s->stride >> 2);
for (int j = y0; j < y1; j++)
s->data[j * stride + x] ^= OVL_MASK;
}
/* Bresenham line (with fast h/v paths). */
static void
ovl_line(struct wm_shm_surface *s, int x0, int y0, int x1, int y1)
{
if (x0 == x1) {
int y = y0 < y1 ? y0 : y1;
ovl_vline(s, x0, y, abs(y1 - y0) + 1);
return;
}
if (y0 == y1) {
int x = x0 < x1 ? x0 : x1;
ovl_hline(s, x, y0, abs(x1 - x0) + 1);
return;
}
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
for (;;) {
ovl_plot(s, x0, y0);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
/* Render one glyph at cell origin (cx, cy) using XOR. */
static void
ovl_draw_glyph(struct wm_shm_surface *s, int cx, int cy, unsigned char ch)
{
if (ch < 0x20 || ch > 0x7E) ch = '?';
const uint8_t *g = ovl_font[ch - 0x20];
for (int row = 0; row < OVL_GH; row++) {
uint8_t bits = g[row];
if (!bits) continue;
int py = cy + row;
if (py < 0 || py >= s->height) continue;
for (int col = 0; col < OVL_GW; col++) {
if (bits & (0x80u >> col))
ovl_plot(s, cx + col, py);
}
}
}
/* Push the modified overlay buffer to the scene and render a frame. */
static void
overlay_flush(void)
{
struct wm_shm_surface *s = wl_state.overlay_surf;
if (!s || !wl_state.scene) return;
/* First call creates the scene_buffer node; subsequent calls reposition. */
/* Overlay surface lives in WL_LAYER_OVERLAY so it is structurally
* above every window and dock node without needing raise_to_top. */
{
struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_OVERLAY]
? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree;
wm_shm_surface_commit(s, _op, 0, 0);
}
if (s->scene_buf) {
/* Ensure the node is visible for the animation frame. */
wlr_scene_node_set_enabled(&s->scene_buf->node, true);
/* Notify wlroots that the pixel data has changed. */
wlr_scene_buffer_set_buffer_with_damage(s->scene_buf, &s->wlr_buf, NULL);
}
/* Commit the scene immediately so the animation step is visible
* before the caller's wusleep() delay. */
if (wl_state.scene_output && wl_state.allocator)
wlr_scene_output_commit(wl_state.scene_output, NULL);
}
/* ------------------------------------------------------------------ */
/* Server grab */
/* ------------------------------------------------------------------ */
static void wl_server_grab(void) {} /* no-op: Wayland double-buffers for us */
static void wl_server_ungrab(void)
{
overlay_flush();
/* Disable the overlay scene node so it doesn't intercept hit-testing.
* The XOR animation has already erased its pixels; leaving the node
* enabled blocks frame_buf_at (wlr_scene_node_at hits the overlay
* before any frame_buf). */
if (wl_state.overlay_surf && wl_state.overlay_surf->scene_buf)
wlr_scene_node_set_enabled(&wl_state.overlay_surf->scene_buf->node, false);
}
/* ------------------------------------------------------------------ */
/* Stacking */
/* ------------------------------------------------------------------ */
/* Map a WM window level integer to a level_trees[] bucket index.
* Buckets are ordered 0=lowest (WL_LAYER_BACKGROUND) to 8=highest
* (WL_LAYER_OVERLAY), matching the bottom-to-top creation order of
* level_trees[]. Does not map WL_LAYER_BACKGROUND (no WM level). */
static int
level_to_bucket(int level)
{
if (level < -1) return WL_LAYER_DESKTOP;
if (level < 0) return WL_LAYER_SUNKEN;
if (level < 3) return WL_LAYER_NORMAL;
if (level < 20) return WL_LAYER_FLOATING;
if (level < 21) return WL_LAYER_MENU;
if (level < 50) return WL_LAYER_DOCK;
if (level < 100) return WL_LAYER_FULLSCREEN;
return WL_LAYER_OVERLAY;
}
/* Return the level_trees[] entry for a given WM level integer.
* Falls back to the root scene tree only if the scene is not yet
* initialised, which cannot happen during normal window management. */
static struct wlr_scene_tree *
level_tree_for(int level)
{
int b = level_to_bucket(level);
if (wl_state.level_trees[b])
return wl_state.level_trees[b];
/* Scene not yet initialised — this window will be invisible. */
return &wl_state.scene->tree;
}
static void
wl_stacking_set_level(WNativeWindow win, int level)
{
/* Move the frame's grouping tree into the correct level bucket so
* that the scene graph structurally enforces level ordering. All
* other nodes (background rect, dock panel, OR views, overlays)
* are already placed in their fixed buckets at creation time. */
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb || !fb->tree) return;
struct wlr_scene_tree *target = level_tree_for(level);
if (fb->tree->node.parent != target)
wlr_scene_node_reparent(&fb->tree->node, target);
}
static void
wl_stacking_raise(WNativeWindow win)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
wl_list_remove(&v->link);
wl_list_insert(&wl_state.toplevel_list, &v->link);
if (v->frame_id) {
struct wl_frame_buf *fb = frame_buf_find(v->frame_id);
if (fb && fb->tree) {
wlr_scene_node_raise_to_top(&fb->tree->node);
return;
}
}
if (v->scene_node)
wlr_scene_node_raise_to_top(&v->scene_node->node);
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb) {
if (fb->tree) {
wlr_scene_node_raise_to_top(&fb->tree->node);
} else if (fb->scene_buf) {
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
}
return;
}
}
static void
wl_stacking_lower(WNativeWindow win)
{
/* Frame-buf path: operate on the grouping tree, not the inner buffer
* node. Lowering fb->tree moves the whole window (frame pixels +
* reparented client surface) together, within its level bucket. */
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb) {
if (fb->tree)
wlr_scene_node_lower_to_bottom(&fb->tree->node);
else if (fb->scene_buf)
wlr_scene_node_lower_to_bottom(&fb->scene_buf->node);
return;
}
/* Unframed toplevel view (no frame_buf wrapper). */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
wl_list_remove(&v->link);
wl_list_insert(wl_state.toplevel_list.prev, &v->link);
if (v->scene_node)
wlr_scene_node_lower_to_bottom(&v->scene_node->node);
}
}
static void
wl_stacking_restack(WNativeWindow *windows, int count)
{
/* Reorder scene graph to match the WM stacking order.
*
* windows[] is ordered top-to-bottom (index 0 = topmost).
* Process bottom-to-top: last raise_to_top call wins, so the
* window at index 0 ends up on top within its level bucket.
*
* Each window is represented by its toplevel frame_buf ID. We
* raise fb->tree — the grouping tree that owns both the frame
* pixel buffer and the reparented client surface — so the entire
* window moves as one atomic unit. Child frame_bufs (titlebar,
* resizebar) are already children of fb->tree and move with it. */
int i;
for (i = count - 1; i >= 0; i--) {
WNativeWindow win = windows[i];
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb) {
/* Raise the grouping tree: moves frame + client together. */
if (fb->tree)
wlr_scene_node_raise_to_top(&fb->tree->node);
else if (fb->scene_buf)
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
continue;
}
/* Fallback: unframed toplevel view (no frame_buf). */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
wl_list_remove(&v->link);
wl_list_insert(&wl_state.toplevel_list, &v->link);
if (v->scene_node)
wlr_scene_node_raise_to_top(&v->scene_node->node);
}
}
}
static int
wl_stacking_query(WNativeWindow root,
WNativeWindow **out, unsigned int *count)
{
/* Phase 58: return synthetic IDs in Z-order (top to bottom).
* RemakeStackList will find no WM_CTX_STACK entries for them
* and set window_count=0, but returning 1 avoids the "could not
* get window list" warning. */
struct wl_toplevel_view *v;
unsigned int n = 0, i = 0;
WNativeWindow *wins;
(void)root;
wl_list_for_each(v, &wl_state.toplevel_list, link) n++;
if (n == 0) { *out = NULL; *count = 0; return 1; }
wins = wmalloc(n * sizeof(WNativeWindow));
wl_list_for_each(v, &wl_state.toplevel_list, link) wins[i++] = v->id;
*out = wins; *count = n;
return 1;
}
/* ------------------------------------------------------------------ */
/* Events */
/* ------------------------------------------------------------------ */
static void wl_event_flush(void)
{
/* Flush pending Xlib requests to XWayland so drawing becomes visible. */
if (wl_state.x_display)
XFlush(wl_state.x_display);
}
static void wl_event_flush_expose(void) {}
static void wl_event_sync(int discard)
{
if (wl_state.x_display)
XSync(wl_state.x_display, discard ? True : False);
}
static int wl_event_pending(void) { return 0; }
static void wl_event_drain_motion(void) {}
static void wl_event_discard_enter_leave_until(unsigned long timestamp) { (void)timestamp; }
static void wl_event_handle_extensions(WMEvent *event) { (void)event; }
static void wl_event_randr_update(WMEvent *event) { (void)event; }
/* ------------------------------------------------------------------ */
/* Properties */
/* ------------------------------------------------------------------ */
static int
wl_prop_get_normal_hints(WNativeWindow win, WNormalHints *out, int *pre_icccm)
{
/* Phase 47: populate size constraints from XDG toplevel.
* Phase 17: also handles XWayland size_hints. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
memset(out, 0, sizeof(*out));
*pre_icccm = 0;
if (!v) return 0;
/* Phase 17: XWayland size_hints path. */
if (v->xw_surface) {
xcb_size_hints_t *sh = v->xw_surface->size_hints;
if (sh) {
if (sh->min_width > 0 || sh->min_height > 0) {
out->flags |= WNH_P_MIN_SIZE;
out->min_width = sh->min_width > 0 ? sh->min_width : 1;
out->min_height = sh->min_height > 0 ? sh->min_height : 1;
}
if (sh->max_width > 0 || sh->max_height > 0) {
out->flags |= WNH_P_MAX_SIZE;
out->max_width = sh->max_width;
out->max_height = sh->max_height;
}
}
if (v->xw_surface->width > 0 && v->xw_surface->height > 0) {
out->flags |= WNH_P_SIZE;
out->width = v->xw_surface->width;
out->height = v->xw_surface->height;
}
return (out->flags != 0) ? 1 : 0;
}
if (!v->xdg_surface || !v->xdg_surface->toplevel)
return 0;
struct wlr_xdg_toplevel *tl = v->xdg_surface->toplevel;
if (tl->current.min_width > 0 || tl->current.min_height > 0) {
out->flags |= WNH_P_MIN_SIZE;
out->min_width = (int)tl->current.min_width;
out->min_height = (int)tl->current.min_height;
}
if (tl->current.max_width > 0 || tl->current.max_height > 0) {
out->flags |= WNH_P_MAX_SIZE;
out->max_width = (int)tl->current.max_width;
out->max_height = (int)tl->current.max_height;
}
/* Report the current size as the base/initial size. */
if (v->xdg_surface->surface) {
int w = v->xdg_surface->surface->current.width;
int h = v->xdg_surface->surface->current.height;
if (w > 0 && h > 0) {
out->flags |= WNH_P_SIZE;
out->width = w;
out->height = h;
}
}
return (out->flags != 0) ? 1 : 0;
}
static WWMHints *
wl_prop_get_wm_hints(WNativeWindow win) { (void)win; return NULL; }
static int
wl_prop_get_wm_class(WNativeWindow win,
char **wm_class, char **wm_instance)
{
/* Phase 41: use XDG app_id / Phase 17: use XWayland WM_CLASS. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v && v->xw_surface) {
*wm_class = strdup(v->xw_surface->class ? v->xw_surface->class : "x11-client");
*wm_instance = strdup(v->xw_surface->instance ? v->xw_surface->instance : "x11-client");
return 0;
}
const char *app_id = (v && v->xdg_surface && v->xdg_surface->toplevel &&
v->xdg_surface->toplevel->app_id)
? v->xdg_surface->toplevel->app_id : "wayland-client";
*wm_class = strdup(app_id);
*wm_instance = strdup(app_id);
return 0;
}
static Bool
wl_window_fetch_name(WNativeWindow win, char **name_out)
{
/* Phase 41: XDG title / Phase 17: XWayland title. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
const char *title = NULL;
if (v && v->xw_surface)
title = v->xw_surface->title;
else if (v && v->xdg_surface && v->xdg_surface->toplevel)
title = v->xdg_surface->toplevel->title;
if (title) {
*name_out = strdup(title);
return True;
}
*name_out = NULL;
return False;
}
static Bool
wl_window_get_icon_name(WNativeWindow win, char **name_out)
{
/* Phase 60: return title for icon labels; Phase 17: XWayland path. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
const char *title = NULL;
if (v && v->xw_surface)
title = v->xw_surface->title;
else if (v && v->xdg_surface && v->xdg_surface->toplevel)
title = v->xdg_surface->toplevel->title;
if (title) {
*name_out = strdup(title);
return True;
}
*name_out = NULL;
return False;
}
static Bool
wl_noop_screen_get_head_usable_area(WScreen *scr, int head,
int *x1, int *y1, int *x2, int *y2)
{
(void)scr; (void)head; (void)x1; (void)y1; (void)x2; (void)y2;
/* Layer-shell strut tracking not yet wired; full head is usable. */
return False;
}
static void *
wl_window_get_icon_image(WNativeWindow win)
{
/* For XWayland clients, parse _NET_WM_ICON via the Xwayland display.
* For native Wayland clients there is no equivalent (the upcoming
* xdg-toplevel-icon protocol is not yet wired). */
if (!wl_state.x_display) return NULL;
return get_window_image_from_x11(wl_state.x_display, (WNativeWindow)win);
}
static int
wl_prop_get_wmaker_menu(WNativeWindow win,
char ***list_out, int *count_out)
{
(void)win;
*list_out = NULL; *count_out = 0;
return 0;
}
static WNativeWindow
wl_prop_get_transient_for(WNativeWindow win)
{
/* Phase 65: return the synthetic ID of the parent XDG toplevel, or
* WNATIVE_NONE if this surface has no parent. The wlroots XDG
* implementation stores the parent as xdg_surface->toplevel->parent
* (a wlr_xdg_surface *). We walk the view list to find its ID. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v) return WNATIVE_NONE;
/* Phase 17: XWayland transient_for via parent window_id. */
if (v->xw_surface) {
struct wlr_xwayland_surface *parent = v->xw_surface->parent;
if (!parent) return WNATIVE_NONE;
return (WNativeWindow)parent->window_id;
}
if (!v->xdg_surface || !v->xdg_surface->toplevel)
return WNATIVE_NONE;
struct wlr_xdg_toplevel *ptl = v->xdg_surface->toplevel->parent;
if (!ptl || !ptl->base)
return WNATIVE_NONE;
struct wl_toplevel_view *pv = wl_find_view_by_xdg_surface(ptl->base);
if (!pv)
return WNATIVE_NONE;
return pv->id;
}
static int wl_prop_has_wmaker_menu(WNativeWindow win) { (void)win; return 0; }
static int wl_prop_has_gtk_object_path(WNativeWindow win) { (void)win; return 0; }
static void
wl_prop_set_wm_state(WNativeWindow win, const long *data, int n)
{
/* Phase 56: store WM state in the view struct for later retrieval. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v || !data || n <= 0) return;
if (v->wm_state) wfree(v->wm_state);
v->wm_state = wmalloc((size_t)n * sizeof(long));
memcpy(v->wm_state, data, (size_t)n * sizeof(long));
v->wm_state_n = n;
}
static int
wl_prop_get_wm_state(WNativeWindow win, long **data_out)
{
/* Phase 56: return a heap copy of the stored WM state (caller frees). */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v || !v->wm_state) { *data_out = NULL; return 0; }
*data_out = wmalloc((size_t)v->wm_state_n * sizeof(long));
memcpy(*data_out, v->wm_state, (size_t)v->wm_state_n * sizeof(long));
return v->wm_state_n;
}
/* ------------------------------------------------------------------ */
/* WNativeWindow map / unmap / geometry */
/* ------------------------------------------------------------------ */
static void
wl_window_map(WNativeWindow win)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v && v->scene_node) {
wlr_scene_node_set_enabled(&v->scene_node->node, true);
return;
}
if (v)
return;
/* Frame buffer: enable its scene node and children. */
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb && fb->scene_buf) {
/* If this is a child, only enable if parent is enabled */
if (fb->parent_id) {
struct wl_frame_buf *parent = frame_buf_find(fb->parent_id);
if (parent && parent->scene_buf &&
!parent->scene_buf->node.enabled)
return; /* parent not visible yet */
}
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
/* Raise the tree (raises entire window assembly together) */
if (fb->tree) {
wlr_scene_node_set_enabled(&fb->tree->node, true);
wlr_scene_node_raise_to_top(&fb->tree->node);
} else if (!fb->parent_id)
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
/* Also enable children (and grandchildren) */
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
if (child->parent_id == win && child->scene_buf) {
wlr_scene_node_set_enabled(&child->scene_buf->node, true);
struct wl_frame_buf *gc;
wl_list_for_each(gc, &wl_state.frame_list, link) {
if (gc->parent_id == child->id && gc->scene_buf)
wlr_scene_node_set_enabled(&gc->scene_buf->node, true);
}
}
}
return;
}
}
static void
wl_window_map_raised(WNativeWindow win)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v && v->scene_node) {
wlr_scene_node_set_enabled(&v->scene_node->node, true);
if (v->frame_id) {
struct wl_frame_buf *fb = frame_buf_find(v->frame_id);
if (fb && fb->tree) {
wlr_scene_node_raise_to_top(&fb->tree->node);
return;
}
}
wlr_scene_node_raise_to_top(&v->scene_node->node);
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb && fb->scene_buf) {
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
if (fb->tree) {
/* Top-level frame: enable tree (decoration children become visible). */
wlr_scene_node_set_enabled(&fb->tree->node, true);
wlr_scene_node_raise_to_top(&fb->tree->node);
} else {
wlr_scene_node_raise_to_top(&fb->scene_buf->node);
}
return;
}
}
static void wl_window_map_subwindows(WNativeWindow parent)
{
(void)parent;
/* Child windows are sub-regions of the frame buffer — no-op. */
}
static void
wl_window_unmap(WNativeWindow win)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v && v->scene_node) {
wlr_scene_node_set_enabled(&v->scene_node->node, false);
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb) return;
/* For top-level frames, disable the whole tree. This hides the frame
* buffer, all child scene_bufs (titlebar, resizebar, buttons), and the
* client surface reparented into fb->tree — atomically and irrevocably
* until the frame is explicitly mapped again. Disabling individual
* child nodes is insufficient because a subsequent wl_frame_paint can
* re-enable them while the window is still iconified. */
if (fb->tree) {
wlr_scene_node_set_enabled(&fb->tree->node, false);
return;
}
/* Child frame_buf: disable its scene_buf node only. */
if (fb->scene_buf)
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
static void wl_window_move(WNativeWindow win, int x, int y)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
if (v->frame_id) {
/* Client is a child of a frame — coords are relative to frame */
v->frame_rel_x = x;
v->frame_rel_y = y;
struct wl_frame_buf *fb = frame_buf_find(v->frame_id);
if (fb) {
v->x = fb->x + x;
v->y = fb->y + y;
}
} else {
v->x = x; v->y = y;
}
if (v->scene_node) {
/* Scene node is a child of the frame tree — use relative coords */
if (v->frame_id)
wlr_scene_node_set_position(&v->scene_node->node, x, y);
else
wlr_scene_node_set_position(&v->scene_node->node, v->x, v->y);
}
if (v->xw_surface)
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)v->x, (int16_t)v->y,
v->xw_surface->width, v->xw_surface->height);
/* Re-apply any popups whose parent is this toplevel so they
* track the parent's position. */
struct wl_toplevel_view *p;
wl_list_for_each(p, &wl_state.toplevel_list, link) {
if (p->popup && p->popup_parent
&& wl_find_view_by_xdg_surface(p->popup_parent->base) == v) {
wmaker_popup_apply(p);
}
}
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb) {
fb->rel_x = x; fb->rel_y = y;
if (fb->parent_id) {
struct wl_frame_buf *parent = frame_buf_find(fb->parent_id);
if (parent) {
fb->x = parent->x + x;
fb->y = parent->y + y;
} else {
fb->x = x; fb->y = y;
}
/* Child frame_buf: reposition scene_buf at relative offset */
if (fb->scene_buf)
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
} else {
fb->x = x; fb->y = y;
/* Top-level frame: move the tree (children follow automatically) */
if (fb->tree)
wlr_scene_node_set_position(&fb->tree->node, x, y);
}
/* Update absolute position tracking for hit-testing — recursively
* for all descendants so grandchildren (e.g. WINGs panel inside
* a fake_leader inside a WMaker frame) stay in sync. */
frame_buf_update_children_abs(win);
return;
}
}
static void wl_window_resize(WNativeWindow win, int w, int h)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v) {
if (w <= 0 || h <= 0) return;
if (v->xw_surface) {
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)v->x, (int16_t)v->y, (uint16_t)w, (uint16_t)h);
return;
}
if (!v->xdg_surface || !v->xdg_surface->toplevel) return;
wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)w, (uint32_t)h);
wlr_xdg_surface_schedule_configure(v->xdg_surface);
return;
}
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb && w > 0 && h > 0 && (w != fb->width || h != fb->height)) {
pixman_image_unref(fb->image);
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0);
fb->width = w;
fb->height = h;
fb->base.width = w;
fb->base.height = h;
/* Update scene graph's cached buffer size for hit-testing. */
if (fb->scene_buf)
wlr_scene_buffer_set_dest_size(fb->scene_buf, w, h);
}
}
static void wl_window_set_border(WNativeWindow win, unsigned long pixel)
{
(void)win; (void)pixel; /* borders are drawn into the frame buffer */
}
static void wl_window_set_border_width(WNativeWindow win, int width)
{
(void)win; (void)width; /* no-op for native frames */
}
static void wl_window_set_save_under(WNativeWindow win, int save_under) { (void)win; (void)save_under; }
static void wl_noop_window_set_background(WNativeWindow win, unsigned long pixel) { (void)win; (void)pixel; }
extern void *W_GetViewBacking(unsigned long id);
static void wl_window_set_background_pixmap(WNativeWindow win, unsigned long pixmap)
{
if (pixmap == 0) return;
struct wl_frame_buf *fb = frame_buf_find(win);
pixman_image_t *dst_img = NULL;
if (fb && fb->image) {
dst_img = fb->image;
fb->bg_pixmap = (pixman_image_t *)(uintptr_t)pixmap;
} else {
dst_img = (pixman_image_t *)W_GetViewBacking(win);
}
if (!dst_img) return;
pixman_image_t *src = (pixman_image_t *)(uintptr_t)pixmap;
int src_w = pixman_image_get_width(src);
int src_h = pixman_image_get_height(src);
/* Blit source pixmap into frame_buf's image */
pixman_image_composite32(PIXMAN_OP_SRC,
src, /* src */
NULL, /* mask */
dst_img, /* dest */
0, 0, /* src x,y */
0, 0, /* mask x,y */
0, 0, /* dest x,y */
src_w < pixman_image_get_width(dst_img) ? src_w : pixman_image_get_width(dst_img),
src_h < pixman_image_get_height(dst_img) ? src_h : pixman_image_get_height(dst_img));
if (fb)
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
}
static void wl_pixmap_free(unsigned long pixmap)
{
if (pixmap == 0) return;
pixman_image_t *img = (pixman_image_t *)(uintptr_t)pixmap;
pixman_image_unref(img);
}
static void wl_window_clear(WNativeWindow win)
{
struct wl_frame_buf *fb = frame_buf_find(win);
pixman_image_t *img = NULL;
if (fb && fb->image) {
img = fb->image;
} else {
img = (pixman_image_t *)W_GetViewBacking(win);
}
if (!img) return;
int width = pixman_image_get_width(img);
int height = pixman_image_get_height(img);
/* If a background pixmap is set, repaint from it (like XClearWindow) */
if (fb && fb->bg_pixmap) {
pixman_image_composite32(PIXMAN_OP_SRC,
fb->bg_pixmap, NULL, img,
0, 0, 0, 0, 0, 0,
width, height);
} else {
/* No background pixmap — fill with transparent black */
int stride = pixman_image_get_stride(img);
uint32_t *data = (uint32_t *)pixman_image_get_data(img);
memset(data, 0, stride * height);
}
/* Notify the scene buffer of the update */
if (fb)
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
}
static int
wl_window_get_rect(WNativeWindow win,
int *win_x, int *win_y, int *win_w, int *win_h)
{
/* Phase 55: return real geometry for synthetic toplevel IDs. */
if (win == WNATIVE_NONE) {
if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; }
if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; }
return 0; /* invalid/none */
}
struct wl_toplevel_view *v = wl_find_view_by_id(win);
/* Phase 17: XWayland geometry. */
if (v && v->xw_surface) {
if (win_x) { *win_x = v->x; }
if (win_y) { *win_y = v->y; }
if (win_w) { *win_w = v->xw_surface->width > 0 ? v->xw_surface->width : 1; }
if (win_h) { *win_h = v->xw_surface->height > 0 ? v->xw_surface->height : 1; }
return 1;
}
if (v && v->xdg_surface && v->xdg_surface->surface) {
int w = v->xdg_surface->surface->current.width;
int h = v->xdg_surface->surface->current.height;
if (win_x) { *win_x = v->x; }
if (win_y) { *win_y = v->y; }
if (win_w) { *win_w = w > 0 ? w : 1; }
if (win_h) { *win_h = h > 0 ? h : 1; }
return 1;
}
/* Frame buffer geometry. */
struct wl_frame_buf *fb = frame_buf_find(win);
if (fb) {
if (win_x) { *win_x = fb->x; }
if (win_y) { *win_y = fb->y; }
if (win_w) { *win_w = fb->width; }
if (win_h) { *win_h = fb->height; }
return 1;
}
/* Synthetic child IDs (titlebar, buttons) — return minimal geometry. */
if (win_x) { *win_x = 0; } if (win_y) { *win_y = 0; }
if (win_w) { *win_w = 1; } if (win_h) { *win_h = 1; }
return 0;
}
static void wl_window_add_events(WNativeWindow win, unsigned long extra_mask) { (void)win; (void)extra_mask; }
static void wl_window_select_events(WNativeWindow win, unsigned long mask) { (void)win; (void)mask; }
static void wl_window_shape_clear(WWindow *wwin) { (void)wwin; }
static void wl_window_shape_set(WWindow *wwin) { (void)wwin; }
/* ------------------------------------------------------------------ */
/* Overlays */
/* ------------------------------------------------------------------ */
static void
wl_overlay_draw_line(WScreen *scr, int x1, int y1, int x2, int y2)
{
overlay_ensure(scr);
if (wl_state.overlay_surf)
ovl_line(wl_state.overlay_surf, x1, y1, x2, y2);
}
static void
wl_overlay_draw_lines(WScreen *scr, WPoint *pts, int n)
{
overlay_ensure(scr);
struct wm_shm_surface *s = wl_state.overlay_surf;
if (!s || n < 2) return;
for (int i = 0; i < n - 1; i++)
ovl_line(s, pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y);
}
static void
wl_overlay_draw_rect(WScreen *scr, int x, int y, int w, int h)
{
overlay_ensure(scr);
struct wm_shm_surface *s = wl_state.overlay_surf;
if (!s || w <= 0 || h <= 0) return;
/* Matching XDrawRectangle semantics: corners are shared between
* adjacent edges and therefore XOR'd twice (become transparent). */
ovl_hline(s, x, y, w + 1); /* top */
ovl_hline(s, x, y + h, w + 1); /* bottom */
ovl_vline(s, x, y, h + 1); /* left */
ovl_vline(s, x + w, y, h + 1); /* right */
}
static void
wl_overlay_draw_segments(WScreen *scr, WSegment *segs, int n)
{
overlay_ensure(scr);
struct wm_shm_surface *s = wl_state.overlay_surf;
if (!s) return;
for (int i = 0; i < n; i++)
ovl_line(s, segs[i].x1, segs[i].y1, segs[i].x2, segs[i].y2);
}
static void
wl_overlay_draw_string(WScreen *scr, int x, int y, const char *str, int len)
{
overlay_ensure(scr);
struct wm_shm_surface *s = wl_state.overlay_surf;
if (!s || !str) return;
/* y is the text baseline; render from the top of the cell. */
int top = y - (OVL_GH - 2);
for (int i = 0; i < len; i++)
ovl_draw_glyph(s, x + i * OVL_CW, top, (unsigned char)str[i]);
}
static int
wl_overlay_text_width(WScreen *scr, const char *str, int n)
{
(void)scr; (void)str;
return n * OVL_CW;
}
/* ------------------------------------------------------------------ */
/* Client */
/* ------------------------------------------------------------------ */
static int
wl_window_get_client_attrs(WNativeWindow win, WClientAttributes *out)
{
/* Phase 42: return real map_state and geometry from XDG surface.
* Phase 17: XWayland path uses xw_surface->mapped and width/height. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
memset(out, 0, sizeof(*out));
if (v && v->xw_surface) {
if (v->xw_surface->surface) {
out->map_state = WCLIENT_VIEWABLE;
out->width = v->xw_surface->width >= 64 ? v->xw_surface->width : 640;
out->height = v->xw_surface->height >= 64 ? v->xw_surface->height : 480;
} else {
out->map_state = WCLIENT_UNMAPPED;
}
} else if (v && v->xdg_surface && v->xdg_surface->surface) {
out->map_state = WCLIENT_VIEWABLE;
out->width = v->xdg_surface->surface->current.width;
out->height = v->xdg_surface->surface->current.height;
} else {
out->map_state = WCLIENT_UNMAPPED;
}
return 1;
}
static void
wl_client_get_visual_attrs(WNativeWindow win, WClientVisual *out)
{
(void)win;
out->depth = 24; out->visual = NULL; out->colormap = 0;
}
static void
wl_client_adopt_attributes(WNativeWindow win, unsigned long event_mask)
{ (void)win; (void)event_mask; }
static void
wl_client_reparent(WNativeWindow client, WNativeWindow new_parent, int x, int y)
{
/* Toplevel view reparented into a frame_buf — record the association
* so wl_window_move can keep them in sync. */
struct wl_toplevel_view *v = wl_find_view_by_id(client);
if (v) {
v->frame_id = new_parent;
v->frame_rel_x = x;
v->frame_rel_y = y;
/* Position the client surface relative to the frame */
struct wl_frame_buf *parent_fb = frame_buf_find(new_parent);
if (parent_fb) {
v->x = parent_fb->x + x;
v->y = parent_fb->y + y;
if (v->scene_node) {
/* Move client's scene tree under the frame's tree */
if (parent_fb->tree)
wlr_scene_node_reparent(&v->scene_node->node, parent_fb->tree);
/* Position at relative offset within the tree */
wlr_scene_node_set_position(&v->scene_node->node, x, y);
}
}
return;
}
/* For WINGs internal windows: create a child frame_buf so the dialog
* content is visible in the scene graph. */
if (client >= 0x20000000 && new_parent >= 0x10000000) {
/* client is a WINGs view ID, new_parent is a frame_buf */
struct wl_frame_buf *parent_fb = frame_buf_find(new_parent);
if (!parent_fb) return;
/* Check if we already have a frame_buf for this client */
struct wl_frame_buf *existing = frame_buf_find(client);
if (existing) {
/* Just update parent and position */
existing->parent_id = new_parent;
existing->rel_x = x;
existing->rel_y = y;
existing->x = parent_fb->x + x;
existing->y = parent_fb->y + y;
if (existing->scene_buf) {
/* Ensure it's under the parent frame's tree, then position
* at relative offset within the tree. */
if (parent_fb->tree &&
existing->scene_buf->node.parent != parent_fb->tree) {
wlr_scene_node_reparent(&existing->scene_buf->node,
parent_fb->tree);
}
if (parent_fb->tree)
wlr_scene_node_set_position(&existing->scene_buf->node, x, y);
else
wlr_scene_node_set_position(&existing->scene_buf->node,
existing->x, existing->y);
}
return;
}
/* Create a new frame_buf for this WINGs view.
* Get size from the view itself. */
extern W_View *W_GetViewForWindow(unsigned long id);
W_View *view = W_GetViewForWindow((unsigned long)client);
int w = view ? view->size.width : parent_fb->width;
int h = view ? view->size.height : (parent_fb->height - y);
if (w <= 0) w = 1;
if (h <= 0) h = 1;
struct wl_frame_buf *fb = wmalloc(sizeof(*fb));
memset(fb, 0, sizeof(*fb));
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0);
if (!fb->image) { wfree(fb); return; }
wlr_buffer_init(&fb->base, &frame_buf_impl, w, h);
fb->id = client; /* use the WINGs view ID directly */
fb->parent_id = new_parent;
fb->rel_x = x;
fb->rel_y = y;
fb->x = parent_fb->x + x;
fb->y = parent_fb->y + y;
fb->width = w;
fb->height = h;
if (wl_state.scene) {
/* Place the WINGs content under the parent frame's tree so it
* moves with the WM-managed dialog decorations. */
struct wlr_scene_tree *_wp = parent_fb->tree
? parent_fb->tree
: (wl_state.level_trees[WL_LAYER_FLOATING]
? wl_state.level_trees[WL_LAYER_FLOATING]
: &wl_state.scene->tree);
fb->scene_buf = wlr_scene_buffer_create(_wp, &fb->base);
if (fb->scene_buf) {
fb_attach_scene_buf_listener(fb);
/* If under the frame tree, use relative coords; otherwise absolute. */
if (parent_fb->tree)
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
else
wlr_scene_node_set_position(&fb->scene_buf->node, fb->x, fb->y);
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
}
wl_list_insert(&wl_state.frame_list, &fb->link);
/* Connect the WINGs view's backing store to this frame_buf's image */
extern void W_SetViewBacking(unsigned long id, void *img);
W_SetViewBacking((unsigned long)client, fb->image);
}
/* Reparent an existing frame_buf into a new parent */
else if (client >= 0x10000000 && new_parent >= 0x10000000) {
struct wl_frame_buf *fb = frame_buf_find(client);
struct wl_frame_buf *parent_fb = frame_buf_find(new_parent);
if (fb && parent_fb) {
fb->parent_id = new_parent;
fb->rel_x = x;
fb->rel_y = y;
fb->x = parent_fb->x + x;
fb->y = parent_fb->y + y;
if (fb->tree) {
/* This frame_buf has a tree (e.g. fake_leader): reparent
* the whole tree so all children move with it. */
if (parent_fb->tree)
wlr_scene_node_reparent(&fb->tree->node, parent_fb->tree);
wlr_scene_node_set_position(&fb->tree->node, x, y);
} else if (fb->scene_buf) {
/* Plain frame_buf (no tree): reparent scene_buf directly. */
if (parent_fb->tree)
wlr_scene_node_reparent(&fb->scene_buf->node, parent_fb->tree);
wlr_scene_node_set_position(&fb->scene_buf->node, x, y);
}
/* Update absolute tracking recursively for all descendants */
frame_buf_update_children_abs(client);
}
}
}
/* Called by WINGs after a view repaints — commit its frame_buf to scene graph */
void wl_view_commit_backing_impl(unsigned long id)
{
struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id);
if (fb && fb->scene_buf) {
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
/* Enable the node — it has content now */
wlr_scene_node_set_enabled(&fb->scene_buf->node, true);
}
}
void wl_view_destroy_backing_impl(unsigned long id)
{
struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)id);
if (!fb) return;
wl_list_remove(&fb->link);
/* fb->scene_buf may have been auto-destroyed by wlroots already if a
* parent tree was destroyed earlier (e.g. wl_frame_destroy on the
* surrounding WMaker frame). The destroy listener NULLs scene_buf
* in that case. */
if (fb->scene_buf)
wlr_scene_node_destroy(&fb->scene_buf->node);
wlr_buffer_drop(&fb->base);
}
static void
wl_client_synth_configure(WNativeWindow client_win,
int x, int y, int width, int height,
int border_width, WNativeWindow above)
{
/* Phase 44: send an XDG configure with the new size to the client.
* Phase 13: also record the WM-assigned position in the view so
* pointer hit-testing (view_surface_at) uses current coordinates.
* Phase 17: XWayland uses wlr_xwayland_surface_configure instead. */
(void)border_width; (void)above;
struct wl_toplevel_view *v = wl_find_view_by_id(client_win);
if (!v) return;
v->x = x;
v->y = y;
/* Update relative offset so frame_configure keeps us in sync. */
if (v->frame_id) {
struct wl_frame_buf *pfb = frame_buf_find(v->frame_id);
if (pfb) {
v->frame_rel_x = x - pfb->x;
v->frame_rel_y = y - pfb->y;
}
/* Scene node is child of frame tree — use relative coords */
if (v->scene_node)
wlr_scene_node_set_position(&v->scene_node->node,
v->frame_rel_x, v->frame_rel_y);
} else {
/* No frame — use absolute coords */
if (v->scene_node)
wlr_scene_node_set_position(&v->scene_node->node, x, y);
}
if (v->xw_surface) {
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)x, (int16_t)y,
width > 0 ? (uint16_t)width : v->xw_surface->width,
height > 0 ? (uint16_t)height : v->xw_surface->height);
return;
}
if (!v->xdg_surface || !v->xdg_surface->toplevel) return;
if (width > 0 && height > 0)
wlr_xdg_toplevel_set_size(v->xdg_surface->toplevel, (uint32_t)width, (uint32_t)height);
wlr_xdg_surface_schedule_configure(v->xdg_surface);
}
static void wl_client_add_to_save_set(WNativeWindow win) { (void)win; }
static void wl_client_remove_from_save_set(WNativeWindow win) { (void)win; }
static void
wl_client_close(WNativeWindow win)
{
/* Phase 46: ask the surface to close.
* Phase 17: XWayland uses wlr_xwayland_surface_close. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v) return;
if (v->xw_surface)
wlr_xwayland_surface_close(v->xw_surface);
else if (v->xdg_surface && v->xdg_surface->toplevel)
wlr_xdg_toplevel_send_close(v->xdg_surface->toplevel);
}
static void wl_client_query_shaped(WNativeWindow win, int *shaped_out)
{ (void)win; *shaped_out = 0; }
static int
wl_client_get_gnustep_attrs(WNativeWindow win, GNUstepWMAttributes **attr_out)
{ (void)win; *attr_out = NULL; return 0; }
static int wl_client_get_pid(WNativeWindow win)
{
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (!v) return 0;
/* Phase 17: XWayland surfaces have a direct pid field. */
if (v->xw_surface)
return (int)v->xw_surface->pid;
if (!v->xdg_surface || !v->xdg_surface->resource)
return 0;
struct wl_client *client = wl_resource_get_client(v->xdg_surface->resource);
if (!client) return 0;
pid_t pid = 0; uid_t uid = 0; gid_t gid = 0;
wl_client_get_credentials(client, &pid, &uid, &gid);
return (int)pid;
}
static RImage *
wl_client_snapshot(WScreen *scr, WNativeWindow client_win, WNativeWindow frame_win)
{ (void)scr; (void)client_win; (void)frame_win; return NULL; }
static int
wl_coords_translate(WNativeWindow src, WNativeWindow dst,
int src_x, int src_y, int *dst_x, int *dst_y)
{
(void)src; (void)dst;
*dst_x = src_x; *dst_y = src_y;
return 0;
}
/* ------------------------------------------------------------------ */
/* ICCCM */
/* ------------------------------------------------------------------ */
static void wl_icccm_set_wm_hints(WNativeWindow win, const WWMHints *hints) { (void)win; (void)hints; }
static void wl_icccm_set_class_hint(WNativeWindow win, const char *wm_class, const char *wm_instance) { (void)win; (void)wm_class; (void)wm_instance; }
static void wl_icccm_set_class_hint_if_absent(WNativeWindow win, const char *wm_class, const char *wm_instance) { (void)win; (void)wm_class; (void)wm_instance; }
static void
wl_icccm_set_command(WNativeWindow win, char **argv, int argc)
{
/* Phase 57: store WM_COMMAND in the view struct. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
int i;
if (!v || !argv || argc <= 0) return;
/* free previous command if any */
if (v->wm_command) {
for (i = 0; i < v->wm_command_n; i++) wfree(v->wm_command[i]);
wfree(v->wm_command);
}
v->wm_command = wmalloc((size_t)argc * sizeof(char *));
for (i = 0; i < argc; i++)
v->wm_command[i] = wstrdup(argv[i] ? argv[i] : "");
v->wm_command_n = argc;
}
static int
wl_icccm_get_command(WNativeWindow win, char ***argv_out, int *argc_out)
{
/* Phase 57: return a NULL-terminated copy of the stored WM_COMMAND.
* Caller frees with wl_icccm_free_command. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
int i;
if (!v || !v->wm_command) { *argv_out = NULL; *argc_out = 0; return 0; }
/* Allocate n+1 entries (NULL-terminated for free_command iteration). */
*argv_out = wmalloc((size_t)(v->wm_command_n + 1) * sizeof(char *));
for (i = 0; i < v->wm_command_n; i++)
(*argv_out)[i] = wstrdup(v->wm_command[i]);
(*argv_out)[v->wm_command_n] = NULL;
*argc_out = v->wm_command_n;
return 1;
}
static void
wl_icccm_free_command(char **argv)
{
/* Phase 57: free NULL-terminated argv returned by wl_icccm_get_command. */
int i;
if (!argv) return;
for (i = 0; argv[i] != NULL; i++) wfree(argv[i]);
wfree(argv);
}
/* ------------------------------------------------------------------ */
/* Fake leader */
/* ------------------------------------------------------------------ */
static WNativeWindow wl_fake_leader_create(WScreen *scr)
{
(void)scr;
/* Create a frame_buf with a tree to act as the dialog's content
* container. The tree is essential — content reparented into the
* fake_leader (e.g. WINGs panel views) becomes a child of the tree
* and follows it when the fake_leader itself is reparented into
* a WMaker frame. */
struct wl_frame_buf *fb = wmalloc(sizeof(*fb));
memset(fb, 0, sizeof(*fb));
fb->id = wl_state.next_synthetic_id++;
fb->parent_id = 0;
fb->width = 1;
fb->height = 1;
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, 1, 1, NULL, 0);
wlr_buffer_init(&fb->base, &frame_buf_impl, 1, 1);
if (wl_state.scene) {
struct wlr_scene_tree *_pp = wl_state.level_trees[WL_LAYER_NORMAL]
? wl_state.level_trees[WL_LAYER_NORMAL] : &wl_state.scene->tree;
fb->tree = wlr_scene_tree_create(_pp);
if (fb->tree) {
fb_attach_tree_listener(fb);
wlr_scene_node_set_position(&fb->tree->node, 0, 0);
fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base);
if (fb->scene_buf) {
fb_attach_scene_buf_listener(fb);
wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0);
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
wlr_scene_node_set_enabled(&fb->tree->node, true);
}
}
wl_list_insert(&wl_state.frame_list, &fb->link);
return fb->id;
}
static void wl_fake_leader_destroy(WNativeWindow win) { (void)win; }
/* ------------------------------------------------------------------ */
/* Screen internals */
/* ------------------------------------------------------------------ */
static int wl_screen_replace_wm(WScreen *scr) { (void)scr; return 1; }
static int wl_screen_select_input(WScreen *scr, long mask) { (void)scr; (void)mask; return 1; }
static void wl_screen_set_root_cursor(WScreen *scr, int cursor_id) { (void)scr; (void)cursor_id; }
static void wl_screen_set_icon_sizes(WScreen *scr, int min_size, int max_size) { (void)scr; (void)min_size; (void)max_size; }
static void wl_screen_setup_noticeboard(WScreen *scr) { (void)scr; }
static void
wl_screen_alloc_gcs(WScreen *scr)
{
XGCValues gcv;
int gcm;
static const char stipple_data[] = { 0x02, 0x01 };
/* Pre-initialise menu text colors to black so that setMenuTextColor /
* setMenuDisabledColor in wReadDefaults can safely compare them before
* both have been set from the defaults DB. */
scr->menu_title_color[0] = WMRetainColor(scr->white);
scr->mtext_color = WMCreateRGBColor(scr->wmscreen, 0, 0, 0, True);
scr->dtext_color = WMCreateRGBColor(scr->wmscreen, 0, 0, 0, True);
/* Create X11 GCs on the XWayland connection. wmaker's rendering code
* (framewin, icon, menu) uses these regardless of the display backend.
* Mirrors x11_screen_alloc_gcs exactly; without these, CopyArea and
* XRender calls use GC/drawable 0 → BadDrawable → XWayland broken pipe. */
if (!wl_state.x_display)
return;
/* Mark the wrlib drawable as override_redirect so XWayland's XWM does not
* manage it as a client window. XWM reads override_redirect from the
* CreateNotify event (already gone), but any subsequent ConfigureNotify
* updates the flag on the wlr_xwayland_surface it created. Combined with
* the explicit window-ID filter in handle_xwayland_new_surface, this
* prevents BadDrawable/RenderBadPicture cascade and the resulting broken
* pipe from the nested Wayland backend. */
if (scr->w_win) {
wl_state.rcontext_xwin = (WNativeWindow)scr->w_win;
XSetWindowAttributes or_attr;
or_attr.override_redirect = True;
XChangeWindowAttributes(wl_state.x_display, (WNativeWindow)scr->w_win, CWOverrideRedirect, &or_attr);
XFlush(wl_state.x_display);
}
scr->stipple_bitmap = XCreateBitmapFromData(wl_state.x_display, (WNativeWindow)scr->w_win,
stipple_data, 2, 2);
gcv.stipple = scr->stipple_bitmap;
gcv.foreground = scr->white_pixel;
gcv.fill_style = FillStippled;
gcv.graphics_exposures = False;
gcm = GCForeground | GCStipple | GCFillStyle | GCGraphicsExposures;
scr->stipple_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, gcm, &gcv);
gcv.function = GXcopy;
gcv.foreground = scr->white_pixel;
gcv.background = scr->black_pixel;
gcv.line_width = 1;
gcv.line_style = LineDoubleDash;
gcv.fill_style = FillSolid;
gcv.dash_offset = 0;
gcv.dashes = 4;
gcv.graphics_exposures = False;
gcm = GCFunction | GCGraphicsExposures | GCForeground | GCBackground
| GCLineWidth | GCLineStyle | GCFillStyle | GCDashOffset | GCDashList;
scr->icon_select_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, gcm, &gcv);
gcv.function = GXxor;
gcv.plane_mask = AllPlanes;
gcv.foreground = scr->white_pixel;
if (gcv.foreground == 0)
gcv.foreground = 1;
gcv.line_width = DEF_FRAME_THICKNESS;
gcv.subwindow_mode = IncludeInferiors;
gcv.graphics_exposures = False;
scr->frame_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->root_win,
GCForeground | GCGraphicsExposures | GCFunction
| GCSubwindowMode | GCLineWidth | GCPlaneMask, &gcv);
gcv.foreground = scr->white_pixel;
if (gcv.foreground == 0)
gcv.foreground = 17;
gcv.function = GXxor;
gcv.subwindow_mode = IncludeInferiors;
gcv.line_width = 1;
gcv.cap_style = CapRound;
gcv.graphics_exposures = False;
gcm = GCForeground | GCFunction | GCSubwindowMode | GCLineWidth
| GCCapStyle | GCGraphicsExposures;
scr->line_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->root_win, gcm, &gcv);
scr->line_pixel = gcv.foreground;
gcv.foreground = scr->white_pixel;
gcv.background = scr->black_pixel;
gcv.graphics_exposures = False;
scr->copy_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win,
GCForeground | GCBackground | GCGraphicsExposures, &gcv);
gcv.graphics_exposures = False;
scr->draw_gc = XCreateGC(wl_state.x_display, (WNativeWindow)scr->w_win, GCGraphicsExposures, &gcv);
scr->mono_gc = XCreateGC(wl_state.x_display, scr->stipple_bitmap, GCGraphicsExposures, &gcv);
}
static void
wl_screen_create_internals(WScreen *scr)
{
/* Create a real off-screen InputOnly X window that absorbs keyboard focus
* when no application window holds it. This mirrors x11_screen_create_internals
* exactly: without a mapped, viewable focus-sink window, any XSetInputFocus
* call targeting it (serial ~1626 during manageAllWindows) returns BadMatch. */
if (wl_state.x_display) {
XSetWindowAttributes attribs;
int vmask = CWEventMask | CWOverrideRedirect;
attribs.event_mask = WM_EVMASK_KEY_PRESS | WM_EVMASK_FOCUS_CHANGE;
attribs.override_redirect = True;
WNativeWindow xwin = XCreateWindow(wl_state.x_display, DefaultRootWindow(wl_state.x_display),
-10, -10, 4, 4, 0,
0, InputOnly, CopyFromParent, vmask, &attribs);
if (xwin) {
XSelectInput(wl_state.x_display, xwin, WM_EVMASK_KEY_PRESS | WM_EVMASK_KEY_RELEASE);
XMapWindow(wl_state.x_display, xwin);
/* Do NOT call XSetInputFocus here: the window may not be viewable
* yet in XWayland (map is async), triggering BadMatch that perturbs
* XWayland state and causes a Wayland broken-pipe cascade.
* wl_focus_set() will clear Wayland seat focus via
* wlr_seat_keyboard_clear_focus when this window is the target. */
wl_state.no_focus_xwin = xwin;
scr->no_focus_win = (WNativeWindow)xwin;
}
}
wPreferences.auto_focus = 1;
if (wl_state.seat)
wlr_seat_keyboard_clear_focus(wl_state.seat);
wSetModifierMasks(WM_MOD_MOD1, WM_MOD_MOD1, WM_MOD_MOD4, 0);
/* XWayland bridge: create a real X11 helper window for EWMH
* (_NET_SUPPORTING_WM_CHECK) and clipboard selection ownership.
* override_redirect=True prevents XWayland's XWM from managing it as a
* client window (otherwise it appears as class=(none) and generates
* BadDrawable errors from wlroots when wManageWindow tries to reparent it). */
if (wl_state.x_display) {
XSetWindowAttributes iattr;
iattr.override_redirect = True;
WNativeWindow xwin = XCreateWindow(wl_state.x_display, DefaultRootWindow(wl_state.x_display),
-2, -2, 1, 1, 0,
0, InputOutput, CopyFromParent,
CWOverrideRedirect, &iattr);
if (xwin)
scr->info_window = (WNativeWindow)xwin;
}
/* Create dock_shadow: scene_buffer used as ghost overlay during icon drag */
if (wl_state.scene) {
int sz = wPreferences.icon_size;
struct wl_frame_buf *dsfb = wmalloc(sizeof(*dsfb));
memset(dsfb, 0, sizeof(*dsfb));
dsfb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, sz, sz, NULL, sz * 4);
if (dsfb->image) {
wlr_buffer_init(&dsfb->base, &frame_buf_impl, sz, sz);
dsfb->id = wl_state.next_synthetic_id++;
dsfb->width = sz;
dsfb->height = sz;
struct wlr_scene_tree *ds_parent = wl_state.level_trees[WL_LAYER_OVERLAY]
? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree;
dsfb->tree = wlr_scene_tree_create(ds_parent);
if (dsfb->tree) {
dsfb->scene_buf = wlr_scene_buffer_create(dsfb->tree, &dsfb->base);
if (dsfb->scene_buf)
wlr_scene_node_set_enabled(&dsfb->scene_buf->node, false);
wlr_scene_node_set_enabled(&dsfb->tree->node, false);
}
wl_list_insert(&wl_state.frame_list, &dsfb->link);
scr->dock_shadow = dsfb->id;
} else {
wfree(dsfb);
}
}
/* Phase 30: dock/clip now uses XWayland windows for rendering.
* The WM's dock logic (dock.c) creates X11 windows on the XWayland
* Display* and draws on them with Xlib — the compositor composites
* them like any other XWayland surface. */
/* wPreferences.flags.noclip = 1; */
/* wPreferences.flags.nodock = 1; */
/* Phase 30: dock creation is deferred to wl_event_loop_run() (after the
* startup pump thread stops). wm_dock_create() calls wlr_scene_buffer_create
* which is not thread-safe — calling it here races with the pump thread's
* frame-timer callback that calls wlr_scene_output_commit on the same scene.
* The dock_scr pointer tells wl_event_loop_run() to create the dock once the
* compositor's single-threaded event loop is running.
* NOTE: native dock panel disabled — WM dock uses XWayland windows now. */
/* wl_state.dock_pending_scr = scr; */
/* Wayland has no X11-style window border; force to 0 so policy code
* does not offset client positions by a non-existent border. */
scr->frame_border_width = 0;
/* Create workspace name badge overlay (initially 10x10, resized on use) */
if (wl_state.scene) {
struct wl_frame_buf *fb = wmalloc(sizeof(*fb));
memset(fb, 0, sizeof(*fb));
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, 10, 10, NULL, 0);
if (fb->image) {
wlr_buffer_init(&fb->base, &frame_buf_impl, 10, 10);
fb->id = wl_state.next_synthetic_id++;
fb->width = 10;
fb->height = 10;
struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY]
? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree;
fb->tree = wlr_scene_tree_create(parent);
if (fb->tree) {
fb_attach_tree_listener(fb);
fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base);
if (fb->scene_buf) {
fb_attach_scene_buf_listener(fb);
wlr_scene_node_set_enabled(&fb->scene_buf->node, false);
}
wlr_scene_node_set_enabled(&fb->tree->node, false);
}
wl_list_insert(&wl_state.frame_list, &fb->link);
W_RegisterBacking(fb->id, fb->image);
scr->workspace_name = fb->id;
} else {
wfree(fb);
}
}
}
static void wl_screen_create_pixmaps(WScreen *scr) { (void)scr; }
static void
wl_screen_load_tech_font(WScreen *scr)
{
if (!scr) return;
/* Wayland mode has no native font handle (overlay text is drawn via
* the embedded 5×9 bitmap font in wl_overlay_draw_string). We just
* publish the metric fields so moveres.c gets correct geometry. */
scr->tech_draw_font = NULL;
scr->tech_draw_font_ascent = OVL_GH - 2; /* 7 */
scr->tech_draw_font_descent = 2;
}
static void wl_screen_show_mini_screenshot(WScreen *scr, RImage *img) { (void)scr; (void)img; }
static RImage *wl_screen_capture_area(WScreen *scr) { (void)scr; return NULL; }
static RImage *wl_screen_capture_window(WScreen *scr, WWindow *wwin) { (void)scr; (void)wwin; return NULL; }
static void wl_refresh_desktop(WScreen *scr) { (void)scr; }
/* --- Desktop background rendering --- */
/* Simple wlr_buffer implementation backed by a pixman_image_t */
struct wl_bg_buffer {
struct wlr_buffer base;
pixman_image_t *image;
};
static void bg_buffer_destroy(struct wlr_buffer *wlr_buf)
{
struct wl_bg_buffer *buf = wl_container_of(wlr_buf, buf, base);
if (buf->image)
pixman_image_unref(buf->image);
free(buf);
}
static bool bg_buffer_begin_data_ptr_access(struct wlr_buffer *wlr_buf,
uint32_t flags, void **data, uint32_t *format, size_t *stride)
{
(void)flags;
struct wl_bg_buffer *buf = wl_container_of(wlr_buf, buf, base);
*data = pixman_image_get_data(buf->image);
*format = DRM_FORMAT_ARGB8888;
*stride = pixman_image_get_stride(buf->image);
return true;
}
static void bg_buffer_end_data_ptr_access(struct wlr_buffer *wlr_buf)
{
(void)wlr_buf;
}
static const struct wlr_buffer_impl bg_buffer_impl = {
.destroy = bg_buffer_destroy,
.begin_data_ptr_access = bg_buffer_begin_data_ptr_access,
.end_data_ptr_access = bg_buffer_end_data_ptr_access,
};
static int parse_color_to_rcolor(WScreen *scr, const char *name, RColor *out)
{
WMColorRGB wmc;
if (!name || !wGetColor(scr, name, &wmc))
return 0;
out->red = wmc.red >> 8;
out->green = wmc.green >> 8;
out->blue = wmc.blue >> 8;
out->alpha = 255;
return 1;
}
static void wl_set_desktop_background(WScreen *scr, const char *texture_desc)
{
if (!scr || !texture_desc || !wl_state.output)
return;
int width = wl_state.output->width;
int height = wl_state.output->height;
/* Parse the texture proplist */
WMPropList *texpl = WMCreatePropListFromDescription(texture_desc);
if (!texpl)
return;
/* Render the texture to an RImage using wrlib */
RContext *rc = scr->rcontext;
RImage *bg = NULL;
if (WMIsPLArray(texpl) && WMGetPropListItemCount(texpl) > 0) {
WMPropList *type_pl = WMGetFromPLArray(texpl, 0);
const char *type = type_pl ? WMGetFromPLString(type_pl) : "";
if (strcasecmp(type, "solid") == 0 && WMGetPropListItemCount(texpl) >= 2) {
const char *colorstr = WMGetFromPLString(WMGetFromPLArray(texpl, 1));
RColor color = {0x50, 0x50, 0x75, 0xFF};
if (colorstr)
parse_color_to_rcolor(scr, colorstr, &color);
/* For solid colors, just update the scene rect directly */
if (wl_state.bg_rect) {
float c[4] = { color.red/255.0f, color.green/255.0f, color.blue/255.0f, 1.0f };
wlr_scene_rect_set_color(wl_state.bg_rect, c);
wlr_scene_node_set_enabled(&wl_state.bg_rect->node, true);
if (wl_state.bg_image)
wlr_scene_node_set_enabled(&wl_state.bg_image->node, false);
}
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
WMReleasePropList(texpl);
return;
} else if ((strcasecmp(type, "vgradient") == 0 ||
strcasecmp(type, "hgradient") == 0 ||
strcasecmp(type, "dgradient") == 0) &&
WMGetPropListItemCount(texpl) >= 3) {
const char *c1str = WMGetFromPLString(WMGetFromPLArray(texpl, 1));
const char *c2str = WMGetFromPLString(WMGetFromPLArray(texpl, 2));
RColor color1 = {0, 0, 0, 255}, color2 = {0, 0, 0, 255};
if (c1str) parse_color_to_rcolor(scr, c1str, &color1);
if (c2str) parse_color_to_rcolor(scr, c2str, &color2);
int style = (type[0] == 'v' || type[0] == 'V') ? RVerticalGradient :
(type[0] == 'h' || type[0] == 'H') ? RHorizontalGradient :
RDiagonalGradient;
bg = RRenderGradient(width, height, &color1, &color2, style);
} else if ((strcasecmp(type, "mvgradient") == 0 ||
strcasecmp(type, "mhgradient") == 0 ||
strcasecmp(type, "mdgradient") == 0) &&
WMGetPropListItemCount(texpl) >= 3) {
int ncolors = WMGetPropListItemCount(texpl) - 1;
RColor **colors = wmalloc((ncolors + 1) * sizeof(RColor *));
RColor *cbuf = wmalloc(ncolors * sizeof(RColor));
int ci;
for (ci = 0; ci < ncolors; ci++) {
const char *cs = WMGetFromPLString(WMGetFromPLArray(texpl, ci + 1));
cbuf[ci] = (RColor){0, 0, 0, 255};
if (cs) parse_color_to_rcolor(scr, cs, &cbuf[ci]);
colors[ci] = &cbuf[ci];
}
colors[ncolors] = NULL;
int style = (type[1] == 'v' || type[1] == 'V') ? RVerticalGradient :
(type[1] == 'h' || type[1] == 'H') ? RHorizontalGradient :
RDiagonalGradient;
bg = RRenderMultiGradient(width, height, colors, style);
wfree(cbuf);
wfree(colors);
} else if ((strcasecmp(type, "pixmap") == 0 ||
strcasecmp(type, "spixmap") == 0 ||
strcasecmp(type, "mpixmap") == 0 ||
strcasecmp(type, "cpixmap") == 0 ||
strcasecmp(type, "fpixmap") == 0 ||
strcasecmp(type, "tpixmap") == 0) &&
WMGetPropListItemCount(texpl) >= 2) {
const char *path = WMGetFromPLString(WMGetFromPLArray(texpl, 1));
/* Background color (3rd argument, optional) */
RColor bgcolor = {0, 0, 0, 255};
if (WMGetPropListItemCount(texpl) >= 3) {
const char *cstr = WMGetFromPLString(WMGetFromPLArray(texpl, 2));
if (cstr) parse_color_to_rcolor(scr, cstr, &bgcolor);
}
if (path) {
RImage *img = RLoadImage(rc, path, 0);
if (img) {
/* Combine with background color if image has alpha */
if (img->format == RRGBAFormat)
RCombineImageWithColor(img, &bgcolor);
char mode = (type[0] >= 'a' && type[0] <= 'z') ? type[0] - 32 : type[0];
if (mode == 'S') {
/* Scaled: stretch to exact screen size */
bg = RSmoothScaleImage(img, width, height);
} else if (mode == 'F') {
/* Filled: scale preserving aspect, fill screen (crop) */
int w, h;
if (img->width * height > img->height * width) {
w = (height * img->width) / img->height;
h = height;
} else {
w = width;
h = (width * img->height) / img->width;
}
RImage *scaled = RSmoothScaleImage(img, w, h);
if (scaled) {
bg = RCreateImage(width, height, 0);
if (bg) {
RClearImage(bg, &bgcolor);
int sx = (scaled->width - width) / 2;
int sy = (scaled->height - height) / 2;
RCopyArea(bg, scaled, sx, sy, width, height, 0, 0);
}
RReleaseImage(scaled);
}
} else if (mode == 'M') {
/* Maximized: scale preserving aspect, fit inside (letterbox) */
int w, h;
if (img->width * height > img->height * width) {
w = width;
h = (width * img->height) / img->width;
} else {
w = (height * img->width) / img->height;
h = height;
}
RImage *scaled = RSmoothScaleImage(img, w, h);
if (scaled) {
bg = RCreateImage(width, height, 0);
if (bg) {
RClearImage(bg, &bgcolor);
int dx = (width - scaled->width) / 2;
int dy = (height - scaled->height) / 2;
RCombineArea(bg, scaled, 0, 0, scaled->width, scaled->height, dx, dy);
}
RReleaseImage(scaled);
}
} else if (mode == 'C') {
/* Centered: place at center, no scaling */
bg = RCreateImage(width, height, 0);
if (bg) {
RClearImage(bg, &bgcolor);
int dx = (width - img->width) / 2;
int dy = (height - img->height) / 2;
/* Handle images larger than screen */
int sx = 0, sy = 0, cw = img->width, ch = img->height;
if (dx < 0) { sx = -dx; dx = 0; cw = width; }
if (dy < 0) { sy = -dy; dy = 0; ch = height; }
RCombineArea(bg, img, sx, sy, cw, ch, dx, dy);
}
} else if (mode == 'T') {
/* Tiled: repeat the image */
bg = RCreateImage(width, height, 0);
if (bg) {
for (int ty = 0; ty < height; ty += img->height) {
for (int tx = 0; tx < width; tx += img->width) {
int cw = img->width;
int ch = img->height;
if (tx + cw > width) cw = width - tx;
if (ty + ch > height) ch = height - ty;
RCombineArea(bg, img, 0, 0, cw, ch, tx, ty);
}
}
}
} else {
/* Default (plain "pixmap"): scale to fill */
bg = RSmoothScaleImage(img, width, height);
}
RReleaseImage(img);
}
}
}
}
WMReleasePropList(texpl);
if (!bg) {
return;
}
/* Convert RImage to pixman_image and set as scene buffer */
struct wl_bg_buffer *buf = calloc(1, sizeof(*buf));
if (!buf) { RReleaseImage(bg); return; }
buf->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL, width * 4);
if (!buf->image) { free(buf); RReleaseImage(bg); return; }
/* Copy RImage (RGBA or RGB) into ARGB32 pixman buffer */
uint32_t *dst = (uint32_t *)pixman_image_get_data(buf->image);
unsigned char *src = bg->data;
int npixels = width * height;
if (bg->format == RRGBAFormat) {
for (int i = 0; i < npixels; i++) {
dst[i] = ((uint32_t)src[3] << 24) | ((uint32_t)src[0] << 16) |
((uint32_t)src[1] << 8) | (uint32_t)src[2];
src += 4;
}
} else {
for (int i = 0; i < npixels; i++) {
dst[i] = 0xFF000000 | ((uint32_t)src[0] << 16) |
((uint32_t)src[1] << 8) | (uint32_t)src[2];
src += 3;
}
}
RReleaseImage(bg);
wlr_buffer_init(&buf->base, &bg_buffer_impl, width, height);
/* Hide the solid-color rect */
if (wl_state.bg_rect)
wlr_scene_node_set_enabled(&wl_state.bg_rect->node, false);
/* Replace or create the background scene buffer */
if (wl_state.bg_image) {
wlr_scene_buffer_set_buffer(wl_state.bg_image, &buf->base);
wlr_scene_node_set_enabled(&wl_state.bg_image->node, true);
} else {
wl_state.bg_image = wlr_scene_buffer_create(
wl_state.level_trees[WL_LAYER_BACKGROUND], &buf->base);
}
/* Release old buffer, keep new one */
if (wl_state.bg_wlr_buf)
wlr_buffer_drop(wl_state.bg_wlr_buf);
wl_state.bg_wlr_buf = &buf->base;
/* Schedule a frame to display the new background immediately */
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
}
/* ------------------------------------------------------------------ */
static void
wl_monitors_query(WScreen *scr, WHeadGeometry **heads_out, int *count_out)
{
(void)scr;
if (!wl_state.output) {
*heads_out = NULL;
*count_out = 0;
return;
}
/* Phase 34: return the single headless output as head 0. */
WHeadGeometry *heads = wmalloc(sizeof(WHeadGeometry));
if (wl_state.output_layout) {
struct wlr_box box = {0};
wlr_output_layout_get_box(wl_state.output_layout, wl_state.output, &box);
if (box.width > 0 || box.height > 0) {
heads[0].x = box.x;
heads[0].y = box.y;
heads[0].width = box.width;
heads[0].height = box.height;
} else {
heads[0].x = heads[0].y = 0;
heads[0].width = (int)wl_state.output->width;
heads[0].height = (int)wl_state.output->height;
}
} else {
heads[0].x = heads[0].y = 0;
heads[0].width = (int)wl_state.output->width;
heads[0].height = (int)wl_state.output->height;
}
*heads_out = heads;
*count_out = 1;
}
static void wl_monitors_select_events(WScreen *scr) { (void)scr; }
/* ------------------------------------------------------------------ */
/* Workspace badge */
/* ------------------------------------------------------------------ */
static RImage *
wl_workspace_badge_render(WScreen *scr, WNativeWindow badge_win,
const char *name, int name_len,
int width, int height)
{
struct wl_frame_buf *fb = frame_buf_find(badge_win);
if (!fb) return NULL;
/* Resize pixman buffer if needed */
if (width > 0 && height > 0 && (width != fb->width || height != fb->height)) {
W_UnregisterBacking(fb->id);
pixman_image_unref(fb->image);
fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL, 0);
if (!fb->image) return NULL;
fb->width = width;
fb->height = height;
fb->base.width = width;
fb->base.height = height;
W_RegisterBacking(fb->id, fb->image);
}
/* Fill black */
pixman_image_t *black_fill = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL, 0);
if (black_fill) {
uint32_t *data = pixman_image_get_data(black_fill);
for (int i = 0; i < width * height; i++)
data[i] = 0xFF000000;
pixman_image_composite32(PIXMAN_OP_SRC, black_fill, NULL, fb->image,
0, 0, 0, 0, 0, 0, width, height);
pixman_image_unref(black_fill);
}
/* Draw text (shadow passes + final) */
for (int x = 0; x <= 4; x++)
for (int y = 0; y <= 4; y++)
WMDrawString(scr->wmscreen, (WMDrawable)badge_win, scr->white,
scr->workspace_name_font, x, y, name, name_len);
/* Clear and redraw final text centered */
uint32_t *px = pixman_image_get_data(fb->image);
for (int i = 0; i < width * height; i++)
px[i] = 0xFF000000;
WMDrawString(scr->wmscreen, (WMDrawable)badge_win, scr->white,
scr->workspace_name_font, 2, 2, name, name_len);
/* Update scene buffer */
if (fb->scene_buf)
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
/* Return RImage copy for fade animation */
RImage *result = RCreateImage(width, height, False);
if (result) {
uint32_t *src = pixman_image_get_data(fb->image);
for (int i = 0; i < width * height; i++) {
uint32_t p = src[i];
result->data[i * 3 + 0] = (p >> 16) & 0xFF; /* R */
result->data[i * 3 + 1] = (p >> 8) & 0xFF; /* G */
result->data[i * 3 + 2] = p & 0xFF; /* B */
}
}
return result;
}
static RImage *
wl_workspace_badge_get_background(WScreen *scr, int x, int y, int width, int height)
{
(void)scr; (void)x; (void)y;
RImage *img = RCreateImage(width, height, False);
if (img)
memset(img->data, 8, width * height * 3);
return img;
}
static void
wl_workspace_badge_update(WScreen *scr, WNativeWindow badge_win, RImage *img)
{
(void)scr;
if (!img) return;
struct wl_frame_buf *fb = frame_buf_find(badge_win);
if (!fb || !fb->image) return;
int w = img->width;
int h = img->height;
if (w > fb->width) w = fb->width;
if (h > fb->height) h = fb->height;
/* Blit RImage (RGB or RGBA) into pixman ARGB32 buffer */
uint32_t *dst = pixman_image_get_data(fb->image);
int bpp = img->format == RRGBAFormat ? 4 : 3;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int si = (y * img->width + x) * bpp;
uint8_t r = img->data[si];
uint8_t g = img->data[si + 1];
uint8_t b = img->data[si + 2];
dst[y * fb->width + x] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
}
if (fb->scene_buf)
wlr_scene_buffer_set_buffer_with_damage(fb->scene_buf, &fb->base, NULL);
if (wl_state.output)
wlr_output_schedule_frame(wl_state.output);
}
/* ------------------------------------------------------------------ */
/* XKB */
/* ------------------------------------------------------------------ */
static WKeyCode
wl_keysym_to_keycode(unsigned long keysym)
{
/* Phase 77: scan the XKB keymap to find the first keycode that produces
* the given keysym at any level/layout. The XKB keycode ==
* evdev_scancode + 8, which equals the X11 keycode — so the values stored
* in wKeyBindings are directly compatible.
* Use the real keyboard's keymap if available; fall back to the default
* keymap created at startup (before any real keyboard arrives). */
struct xkb_keymap *km = wl_state.xkb_keymap;
if (wl_state.keyboard_dev) {
struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev);
if (kb && kb->keymap) km = kb->keymap;
}
if (!km)
return 0;
xkb_keycode_t min_kc = xkb_keymap_min_keycode(km);
xkb_keycode_t max_kc = xkb_keymap_max_keycode(km);
for (xkb_keycode_t kc = min_kc; kc <= max_kc; kc++) {
xkb_layout_index_t nlayouts = xkb_keymap_num_layouts_for_key(km, kc);
xkb_layout_index_t layout;
for (layout = 0; layout < nlayouts; layout++) {
xkb_level_index_t nlevels = xkb_keymap_num_levels_for_key(km, kc, layout);
xkb_level_index_t level;
for (level = 0; level < nlevels; level++) {
const xkb_keysym_t *syms = NULL;
int n = xkb_keymap_key_get_syms_by_level(km, kc, layout, level, &syms);
int si;
for (si = 0; si < n; si++) {
if (syms[si] == (xkb_keysym_t)keysym)
return (WKeyCode)kc;
}
}
}
}
return 0;
}
static unsigned long
wl_keysym_from_keycode(unsigned int keycode, unsigned int index)
{
/* Phase 77: reverse lookup — return the keysym at the given keycode/level. */
struct xkb_keymap *km = wl_state.xkb_keymap;
if (wl_state.keyboard_dev) {
struct wlr_keyboard *kb = wlr_keyboard_from_input_device(wl_state.keyboard_dev);
if (kb && kb->keymap) km = kb->keymap;
}
if (!km)
return 0;
const xkb_keysym_t *syms = NULL;
int n = xkb_keymap_key_get_syms_by_level(km, (xkb_keycode_t)keycode,
0, (xkb_level_index_t)index, &syms);
if (n > 0 && syms)
return (unsigned long)syms[0];
return 0;
}
static void wl_xkb_lock_group(int group) { (void)group; }
static int wl_xkb_get_group(void) { return 0; }
/* ------------------------------------------------------------------ */
/* Extensions / cursors / error handlers */
/* ------------------------------------------------------------------ */
static void wl_extensions_init(void) {}
static void wl_cursors_init(void) {}
static WNativeWindow wl_cursor_create_named(const char *name) { (void)name; return WNATIVE_NONE; }
static void
wl_shape_combine_mask(WNativeWindow win, WNativeWindow mask)
{
struct wl_frame_buf *fb = frame_buf_find(win);
if (!fb) return;
pixman_image_t *mask_img = (pixman_image_t *)mask;
if (!mask_img) {
/* No mask: clear opaque region (accept all input) */
wlr_scene_buffer_set_opaque_region(fb->scene_buf, NULL);
return;
}
/* Build a pixman region from the mask image (non-transparent pixels) */
int w = pixman_image_get_width(mask_img);
int h = pixman_image_get_height(mask_img);
uint32_t *data = (uint32_t *)pixman_image_get_data(mask_img);
int stride = pixman_image_get_stride(mask_img) / 4;
pixman_region32_t region;
pixman_region32_init(&region);
for (int y = 0; y < h; y++) {
int run_start = -1;
for (int x = 0; x < w; x++) {
uint32_t pixel = data[y * stride + x];
uint8_t alpha = (pixel >> 24) & 0xFF;
if (alpha > 0) {
if (run_start < 0) run_start = x;
} else {
if (run_start >= 0) {
pixman_region32_union_rect(&region, &region, run_start, y, x - run_start, 1);
run_start = -1;
}
}
}
if (run_start >= 0) {
pixman_region32_union_rect(&region, &region, run_start, y, w - run_start, 1);
}
}
/* Set opaque region for the scene buffer */
wlr_scene_buffer_set_opaque_region(fb->scene_buf, &region);
pixman_region32_fini(&region);
}
static int
wl_catch_xwayland_error(Display *edpy, XErrorEvent *error)
{
(void)edpy;
/* BadMatch on X_SetInputFocus is benign: the target window may not be
* viewable yet (e.g. during XWayland startup or between map/unmap).
* Silence it so it does not reach Xlib's default handler. */
if (error->error_code == BadMatch &&
error->request_code == X_SetInputFocus)
return 0;
/* BadWindow is also common during window teardown; ignore silently. */
if (error->error_code == BadWindow)
return 0;
/* Everything else: print via wwarning and continue. */
char buf[256];
XGetErrorText(edpy, error->error_code, buf, sizeof(buf));
wwarning("wl_backend: X error from XWayland: %s (request %d, resource 0x%lx)",
buf, (int)error->request_code, (unsigned long)error->resourceid);
return 0;
}
/* _XDefaultError interposition -----------------------------------------------
* libX11's _XDefaultError (called when no user error handler is installed, or
* when the user handler IS _XDefaultError itself) calls exit(1) after printing.
* In the XWayland bridge the error handler can be temporarily displaced by
* WINGs/wrlib save-restore code (selection.c, dragcommon.c, texture.c) between
* the time a request is sent and the time its BadMatch reply is received.
*
* Because libX11 calls _XDefaultError via PLT (confirmed with objdump — it is
* a GLOBAL DEFAULT symbol with no version tag), our definition in the wmaker
* binary takes precedence over the library version via ELF symbol interposition.
* This is the last-resort safety net: even if _XErrorFunction is reset to
* _XDefaultError, we never exit on a recoverable X protocol error while the
* XWayland connection is open.
*/
int _XDefaultError(Display *dpy, XErrorEvent *event)
{
/* Only intercept errors on our XWayland Xlib connection. */
if (wl_state.x_display != NULL) {
char buf[256] = "unknown";
XGetErrorText(dpy, event->error_code, buf, sizeof(buf));
wwarning("wl_backend: X error reached _XDefaultError: "
"%s (request %d, resource 0x%lx) — suppressing exit",
buf, (int)event->request_code,
(unsigned long)event->resourceid);
return 0;
}
/* Pure X11 mode (no XWayland connection): behave as libX11 normally would.
* x11_backend installs x11_catch_error so _XDefaultError is only reached
* for truly unexpected errors — exiting here is the right thing. */
fprintf(stderr, "X Error of failed request: error_code=%d request_code=%d\n",
(int)event->error_code, (int)event->request_code);
exit(1);
}
static int
wl_catch_xwayland_io_error(Display *edpy)
{
(void)edpy;
/* XIO error = the X server closed the connection (e.g. XWayland crashed or
* its XWM connection died and it shut down). The default handler calls
* exit(1), which then makes XWayland print "(EE) failed to read Wayland
* events: Broken pipe" — our exit closes the Wayland socket that XWayland's
* client side reads. Intercept it so we can shut down gracefully. */
wwarning("wl_backend: X IO error — XWayland connection lost, terminating");
/* Trigger graceful shutdown via the event loop. */
wl_event_loop_terminate();
/* XIO error handlers must not return; call exit directly. */
exit(0);
}
static void
wl_register_error_handlers(void)
{
/* Install a permissive X error handler for the XWayland connection.
* The _XDefaultError interposition above provides a last-resort safety
* net if any code temporarily displaces this handler. */
XSetErrorHandler(wl_catch_xwayland_error);
/* Install an XIO error handler so we get a proper log message instead of
* the silent exit(1) from Xlib's default handler when XWayland disconnects. */
XSetIOErrorHandler(wl_catch_xwayland_io_error);
}
static void
wl_texture_alloc_color(WScreen *scr, WMColorRGB *color)
{
(void) scr; (void) color;
}
static void
wl_texture_free_colors(WScreen *scr, unsigned long *pixels, int count)
{
(void) scr; (void) pixels; (void) count;
}
static GC
wl_texture_create_gc(WScreen *scr, unsigned long pixel)
{
(void) scr; (void) pixel;
return NULL;
}
static void
wl_texture_free_gc(GC gc)
{
(void) gc;
}
/* ------------------------------------------------------------------ */
/* Phase 80: no-op stubs for X11-only operations */
/* ------------------------------------------------------------------ */
static void wl_noop_frame_alloc_border_pixel(unsigned long colormap,
const char *color_name, unsigned long **pixel_out)
{ (void)colormap; (void)color_name; *pixel_out = NULL; }
static void wl_noop_frame_update_texture(struct WFrameWindow *fwin) { (void)fwin; }
static void wl_frame_paint_button(WCoreWindow *button, WTexture *texture,
unsigned long color, WPixmap *image, int pushed)
{
(void)texture; (void)image;
WFrameWindow *fwin = (WFrameWindow *)button->descriptor.parent;
if (!fwin || !fwin->core)
return;
struct wl_frame_buf *btn_fb = frame_buf_find(button->window);
if (!btn_fb || !btn_fb->image)
return;
int bw = btn_fb->width;
int bh = btn_fb->height;
/* Determine which icon to use */
RImage *icon = NULL;
if (button == fwin->right_button) {
if (!wl_state.btn_close_img)
wl_state.btn_close_img = RGetImageFromXPMData(
button->screen_ptr->rcontext, PRED_CLOSE_XPM);
icon = wl_state.btn_close_img;
} else if (button == fwin->left_button) {
if (!wl_state.btn_iconify_img)
wl_state.btn_iconify_img = RGetImageFromXPMData(
button->screen_ptr->rcontext, PRED_ICONIFY_XPM);
icon = wl_state.btn_iconify_img;
}
if (!icon)
return;
/* Get titlebar background color */
struct wl_frame_buf *tb_fb = fwin->titlebar ? frame_buf_find(fwin->titlebar->window) : NULL;
uint32_t bg_pixel = 0xFF333333;
if (tb_fb && tb_fb->image) {
uint32_t *tb_data = pixman_image_get_data(tb_fb->image);
int tb_stride = pixman_image_get_stride(tb_fb->image) / 4;
int sample_y = btn_fb->rel_y + bh / 2;
int sample_x = btn_fb->rel_x + bw / 2;
if (sample_y >= 0 && sample_y < tb_fb->height &&
sample_x >= 0 && sample_x < tb_fb->width)
bg_pixel = tb_data[sample_y * tb_stride + sample_x];
}
/* Foreground = title text color; when pushed, swap fg/bg */
uint32_t fg_pixel = 0xFF000000 | ((color >> 16 & 0xFF) << 16) |
((color >> 8 & 0xFF) << 8) | (color & 0xFF);
uint32_t fill_bg = pushed ? fg_pixel : bg_pixel;
uint32_t fill_fg = pushed ? bg_pixel : fg_pixel;
uint32_t *dst = pixman_image_get_data(btn_fb->image);
int dst_stride = pixman_image_get_stride(btn_fb->image) / 4;
/* Fill background */
for (int y = 0; y < bh; y++)
for (int x = 0; x < bw; x++)
dst[y * dst_stride + x] = fill_bg;
/* Draw border bevel derived from bg color (raised normal, sunken pushed) */
unsigned char bg_r = (bg_pixel >> 16) & 0xFF;
unsigned char bg_g = (bg_pixel >> 8) & 0xFF;
unsigned char bg_b = bg_pixel & 0xFF;
uint32_t hi_c = 0xFF000000 | ((uint32_t)(bg_r + (255-bg_r)/3) << 16) |
((uint32_t)(bg_g + (255-bg_g)/3) << 8) | (bg_b + (255-bg_b)/3);
uint32_t lo_c = 0xFF000000 | ((uint32_t)(bg_r * 2/3) << 16) |
((uint32_t)(bg_g * 2/3) << 8) | (bg_b * 2/3);
uint32_t hi = pushed ? lo_c : hi_c;
uint32_t lo = pushed ? hi_c : lo_c;
for (int x = 0; x < bw; x++) { dst[x] = hi; dst[(bh-1) * dst_stride + x] = lo; }
for (int y = 0; y < bh; y++) { dst[y * dst_stride] = hi; dst[y * dst_stride + bw - 1] = lo; }
/* Blit icon centered, using fill_fg color for opaque pixels */
int ix = (bw - icon->width) / 2 + (pushed ? 1 : 0);
int iy = (bh - icon->height) / 2 + (pushed ? 1 : 0);
for (int y = 0; y < icon->height; y++) {
int dy = iy + y;
if (dy < 0 || dy >= bh) continue;
uint32_t *dst_row = dst + dy * dst_stride;
for (int x = 0; x < icon->width; x++) {
int dx = ix + x;
if (dx < 0 || dx >= bw) continue;
unsigned char a;
if (icon->format == RRGBAFormat)
a = icon->data[(y * icon->width + x) * 4 + 3];
else
a = 255;
if (a == 0) continue;
dst_row[dx] = fill_fg;
}
}
/* Enable and commit the button's own scene_buf */
wlr_scene_buffer_set_buffer_with_damage(btn_fb->scene_buf, &btn_fb->base, NULL);
wlr_scene_node_set_enabled(&btn_fb->scene_buf->node, true);
wlr_scene_node_raise_to_top(&btn_fb->scene_buf->node);
}
static void wl_noop_netwm_init(WScreen *scr) { (void)scr; }
static void wl_noop_netwm_cleanup(WScreen *scr) { (void)scr; }
static void wl_noop_netwm_update_desktop(WScreen *scr) { (void)scr; }
static int wl_noop_netwm_get_current_desktop(WScreen *scr) { (void)scr; return -1; }
static void wl_netwm_update_opacity(WWindow *wwin)
{
if (!wwin->frame)
return;
struct wl_frame_buf *fb = frame_buf_find(wwin->frame->core->window);
if (!fb || !fb->scene_buf)
return;
float alpha = 1.0f;
if (wl_state.x_display && wwin->client_win) {
WMNativeAtom type;
int format;
unsigned long items, rest;
unsigned char *prop = NULL;
WMNativeAtom opacity_atom = XInternAtom(wl_state.x_display,
"_NET_WM_WINDOW_OPACITY", False);
if (XGetWindowProperty(wl_state.x_display, (WNativeWindow)wwin->client_win,
opacity_atom, 0L, 1L, False, AnyPropertyType,
&type, &format, &items, &rest, &prop) == Success
&& format == 32 && items == 1 && prop) {
unsigned long val = *(unsigned long *)prop;
alpha = (float)val / (float)0xFFFFFFFF;
}
if (prop)
XFree(prop);
}
wlr_scene_buffer_set_opacity(fb->scene_buf, alpha);
/* Apply to all child frame_bufs (titlebar buttons, resizebar) */
struct wl_frame_buf *child;
wl_list_for_each(child, &wl_state.frame_list, link) {
if (child->parent_id == fb->id && child->scene_buf)
wlr_scene_buffer_set_opacity(child->scene_buf, alpha);
}
/* Apply to the client surface */
struct wl_toplevel_view *v;
wl_list_for_each(v, &wl_state.toplevel_list, link) {
if (v->frame_id == fb->id && v->scene_node) {
struct wlr_scene_node *node;
wl_list_for_each(node, &v->scene_node->children, link) {
if (node->type == WLR_SCENE_NODE_BUFFER)
wlr_scene_buffer_set_opacity(
wlr_scene_buffer_from_node(node), alpha);
}
}
}
}
static void wl_noop_netwm_update_actions(WWindow *wwin, int del) { (void)wwin; (void)del; }
static void wl_noop_netwm_check_client_hints(WWindow *wwin, int *layer, int *workspace)
{ (void)wwin; (void)layer; (void)workspace; }
static void wl_noop_netwm_check_initial_state(WWindow *wwin) { (void)wwin; }
static int wl_noop_netwm_get_pid(WNativeWindow window) { (void)window; return 0; }
static void wl_noop_netwm_cleanup_frame_extents(WWindow *wwin) { (void)wwin; }
static void wl_noop_netwm_set_frame_extents(WWindow *wwin) { (void)wwin; }
static void wl_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 */
uint32_t bg;
if (selected) {
bg = 0xFF4A6984;
} 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 */
if (entry->cascade >= 0) {
int ay = y + h / 2;
for (int i = 0; i < 5; i++)
for (int j = -i; j <= i; j++)
if (w - 12 + i < w && ay + j >= 0 && ay + j < fb->height)
dst[(ay + j) * stride + (w - 12 + i)] = selected ? 0xFFFFFFFF : 0xFF000000;
}
/* 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)
{ (void)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,
};