10068 lines
418 KiB
C
10068 lines
418 KiB
C
/*
|
||
* 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(®ion);
|
||
|
||
for (int y = 0; y < h; y++) {
|
||
int run_start = -1;
|
||
for (int x = 0; x < w; x++) {
|
||
uint32_t pixel = data[y * stride + x];
|
||
uint8_t alpha = (pixel >> 24) & 0xFF;
|
||
if (alpha > 0) {
|
||
if (run_start < 0) run_start = x;
|
||
} else {
|
||
if (run_start >= 0) {
|
||
pixman_region32_union_rect(®ion, ®ion, run_start, y, x - run_start, 1);
|
||
run_start = -1;
|
||
}
|
||
}
|
||
}
|
||
if (run_start >= 0) {
|
||
pixman_region32_union_rect(®ion, ®ion, run_start, y, w - run_start, 1);
|
||
}
|
||
}
|
||
|
||
/* Set opaque region for the scene buffer */
|
||
wlr_scene_buffer_set_opaque_region(fb->scene_buf, ®ion);
|
||
|
||
pixman_region32_fini(®ion);
|
||
}
|
||
static int
|
||
wl_catch_xwayland_error(Display *edpy, XErrorEvent *error)
|
||
{
|
||
(void)edpy;
|
||
/* BadMatch on X_SetInputFocus is benign: the target window may not be
|
||
* viewable yet (e.g. during XWayland startup or between map/unmap).
|
||
* Silence it so it does not reach Xlib's default handler. */
|
||
if (error->error_code == BadMatch &&
|
||
error->request_code == X_SetInputFocus)
|
||
return 0;
|
||
/* BadWindow is also common during window teardown; ignore silently. */
|
||
if (error->error_code == BadWindow)
|
||
return 0;
|
||
/* Everything else: print via wwarning and continue. */
|
||
char buf[256];
|
||
XGetErrorText(edpy, error->error_code, buf, sizeof(buf));
|
||
wwarning("wl_backend: X error from XWayland: %s (request %d, resource 0x%lx)",
|
||
buf, (int)error->request_code, (unsigned long)error->resourceid);
|
||
return 0;
|
||
}
|
||
|
||
/* _XDefaultError interposition -----------------------------------------------
|
||
* libX11's _XDefaultError (called when no user error handler is installed, or
|
||
* when the user handler IS _XDefaultError itself) calls exit(1) after printing.
|
||
* In the XWayland bridge the error handler can be temporarily displaced by
|
||
* WINGs/wrlib save-restore code (selection.c, dragcommon.c, texture.c) between
|
||
* the time a request is sent and the time its BadMatch reply is received.
|
||
*
|
||
* Because libX11 calls _XDefaultError via PLT (confirmed with objdump — it is
|
||
* a GLOBAL DEFAULT symbol with no version tag), our definition in the wmaker
|
||
* binary takes precedence over the library version via ELF symbol interposition.
|
||
* This is the last-resort safety net: even if _XErrorFunction is reset to
|
||
* _XDefaultError, we never exit on a recoverable X protocol error while the
|
||
* XWayland connection is open.
|
||
*/
|
||
int _XDefaultError(Display *dpy, XErrorEvent *event)
|
||
{
|
||
/* Only intercept errors on our XWayland Xlib connection. */
|
||
if (wl_state.x_display != NULL) {
|
||
char buf[256] = "unknown";
|
||
XGetErrorText(dpy, event->error_code, buf, sizeof(buf));
|
||
wwarning("wl_backend: X error reached _XDefaultError: "
|
||
"%s (request %d, resource 0x%lx) — suppressing exit",
|
||
buf, (int)event->request_code,
|
||
(unsigned long)event->resourceid);
|
||
return 0;
|
||
}
|
||
/* Pure X11 mode (no XWayland connection): behave as libX11 normally would.
|
||
* x11_backend installs x11_catch_error so _XDefaultError is only reached
|
||
* for truly unexpected errors — exiting here is the right thing. */
|
||
fprintf(stderr, "X Error of failed request: error_code=%d request_code=%d\n",
|
||
(int)event->error_code, (int)event->request_code);
|
||
exit(1);
|
||
}
|
||
|
||
static int
|
||
wl_catch_xwayland_io_error(Display *edpy)
|
||
{
|
||
(void)edpy;
|
||
/* XIO error = the X server closed the connection (e.g. XWayland crashed or
|
||
* its XWM connection died and it shut down). The default handler calls
|
||
* exit(1), which then makes XWayland print "(EE) failed to read Wayland
|
||
* events: Broken pipe" — our exit closes the Wayland socket that XWayland's
|
||
* client side reads. Intercept it so we can shut down gracefully. */
|
||
wwarning("wl_backend: X IO error — XWayland connection lost, terminating");
|
||
/* Trigger graceful shutdown via the event loop. */
|
||
wl_event_loop_terminate();
|
||
/* XIO error handlers must not return; call exit directly. */
|
||
exit(0);
|
||
}
|
||
|
||
static void
|
||
wl_register_error_handlers(void)
|
||
{
|
||
/* Install a permissive X error handler for the XWayland connection.
|
||
* The _XDefaultError interposition above provides a last-resort safety
|
||
* net if any code temporarily displaces this handler. */
|
||
XSetErrorHandler(wl_catch_xwayland_error);
|
||
/* Install an XIO error handler so we get a proper log message instead of
|
||
* the silent exit(1) from Xlib's default handler when XWayland disconnects. */
|
||
XSetIOErrorHandler(wl_catch_xwayland_io_error);
|
||
}
|
||
|
||
static void
|
||
wl_texture_alloc_color(WScreen *scr, WMColorRGB *color)
|
||
{
|
||
(void) scr; (void) color;
|
||
}
|
||
|
||
static void
|
||
wl_texture_free_colors(WScreen *scr, unsigned long *pixels, int count)
|
||
{
|
||
(void) scr; (void) pixels; (void) count;
|
||
}
|
||
|
||
static GC
|
||
wl_texture_create_gc(WScreen *scr, unsigned long pixel)
|
||
{
|
||
(void) scr; (void) pixel;
|
||
return NULL;
|
||
}
|
||
|
||
static void
|
||
wl_texture_free_gc(GC gc)
|
||
{
|
||
(void) gc;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Phase 80: no-op stubs for X11-only operations */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
static void wl_noop_frame_alloc_border_pixel(unsigned long colormap,
|
||
const char *color_name, unsigned long **pixel_out)
|
||
{ (void)colormap; (void)color_name; *pixel_out = NULL; }
|
||
|
||
static void wl_noop_frame_update_texture(struct WFrameWindow *fwin) { (void)fwin; }
|
||
|
||
static void wl_frame_paint_button(WCoreWindow *button, WTexture *texture,
|
||
unsigned long color, WPixmap *image, int pushed)
|
||
{
|
||
(void)texture; (void)image;
|
||
WFrameWindow *fwin = (WFrameWindow *)button->descriptor.parent;
|
||
if (!fwin || !fwin->core)
|
||
return;
|
||
|
||
struct wl_frame_buf *btn_fb = frame_buf_find(button->window);
|
||
if (!btn_fb || !btn_fb->image)
|
||
return;
|
||
|
||
int bw = btn_fb->width;
|
||
int bh = btn_fb->height;
|
||
|
||
/* Determine which icon to use */
|
||
RImage *icon = NULL;
|
||
if (button == fwin->right_button) {
|
||
if (!wl_state.btn_close_img)
|
||
wl_state.btn_close_img = RGetImageFromXPMData(
|
||
button->screen_ptr->rcontext, PRED_CLOSE_XPM);
|
||
icon = wl_state.btn_close_img;
|
||
} else if (button == fwin->left_button) {
|
||
if (!wl_state.btn_iconify_img)
|
||
wl_state.btn_iconify_img = RGetImageFromXPMData(
|
||
button->screen_ptr->rcontext, PRED_ICONIFY_XPM);
|
||
icon = wl_state.btn_iconify_img;
|
||
}
|
||
if (!icon)
|
||
return;
|
||
|
||
/* Get titlebar background color */
|
||
struct wl_frame_buf *tb_fb = fwin->titlebar ? frame_buf_find(fwin->titlebar->window) : NULL;
|
||
uint32_t bg_pixel = 0xFF333333;
|
||
if (tb_fb && tb_fb->image) {
|
||
uint32_t *tb_data = pixman_image_get_data(tb_fb->image);
|
||
int tb_stride = pixman_image_get_stride(tb_fb->image) / 4;
|
||
int sample_y = btn_fb->rel_y + bh / 2;
|
||
int sample_x = btn_fb->rel_x + bw / 2;
|
||
if (sample_y >= 0 && sample_y < tb_fb->height &&
|
||
sample_x >= 0 && sample_x < tb_fb->width)
|
||
bg_pixel = tb_data[sample_y * tb_stride + sample_x];
|
||
}
|
||
|
||
/* Foreground = title text color; when pushed, swap fg/bg */
|
||
uint32_t fg_pixel = 0xFF000000 | ((color >> 16 & 0xFF) << 16) |
|
||
((color >> 8 & 0xFF) << 8) | (color & 0xFF);
|
||
uint32_t fill_bg = pushed ? fg_pixel : bg_pixel;
|
||
uint32_t fill_fg = pushed ? bg_pixel : fg_pixel;
|
||
|
||
uint32_t *dst = pixman_image_get_data(btn_fb->image);
|
||
int dst_stride = pixman_image_get_stride(btn_fb->image) / 4;
|
||
|
||
/* Fill background */
|
||
for (int y = 0; y < bh; y++)
|
||
for (int x = 0; x < bw; x++)
|
||
dst[y * dst_stride + x] = fill_bg;
|
||
|
||
/* Draw border bevel derived from bg color (raised normal, sunken pushed) */
|
||
unsigned char bg_r = (bg_pixel >> 16) & 0xFF;
|
||
unsigned char bg_g = (bg_pixel >> 8) & 0xFF;
|
||
unsigned char bg_b = bg_pixel & 0xFF;
|
||
uint32_t hi_c = 0xFF000000 | ((uint32_t)(bg_r + (255-bg_r)/3) << 16) |
|
||
((uint32_t)(bg_g + (255-bg_g)/3) << 8) | (bg_b + (255-bg_b)/3);
|
||
uint32_t lo_c = 0xFF000000 | ((uint32_t)(bg_r * 2/3) << 16) |
|
||
((uint32_t)(bg_g * 2/3) << 8) | (bg_b * 2/3);
|
||
uint32_t hi = pushed ? lo_c : hi_c;
|
||
uint32_t lo = pushed ? hi_c : lo_c;
|
||
for (int x = 0; x < bw; x++) { dst[x] = hi; dst[(bh-1) * dst_stride + x] = lo; }
|
||
for (int y = 0; y < bh; y++) { dst[y * dst_stride] = hi; dst[y * dst_stride + bw - 1] = lo; }
|
||
|
||
/* Blit icon centered, using fill_fg color for opaque pixels */
|
||
int ix = (bw - icon->width) / 2 + (pushed ? 1 : 0);
|
||
int iy = (bh - icon->height) / 2 + (pushed ? 1 : 0);
|
||
|
||
for (int y = 0; y < icon->height; y++) {
|
||
int dy = iy + y;
|
||
if (dy < 0 || dy >= bh) continue;
|
||
uint32_t *dst_row = dst + dy * dst_stride;
|
||
for (int x = 0; x < icon->width; x++) {
|
||
int dx = ix + x;
|
||
if (dx < 0 || dx >= bw) continue;
|
||
unsigned char a;
|
||
if (icon->format == RRGBAFormat)
|
||
a = icon->data[(y * icon->width + x) * 4 + 3];
|
||
else
|
||
a = 255;
|
||
if (a == 0) continue;
|
||
dst_row[dx] = fill_fg;
|
||
}
|
||
}
|
||
|
||
/* Enable and commit the button's own scene_buf */
|
||
wlr_scene_buffer_set_buffer_with_damage(btn_fb->scene_buf, &btn_fb->base, NULL);
|
||
wlr_scene_node_set_enabled(&btn_fb->scene_buf->node, true);
|
||
wlr_scene_node_raise_to_top(&btn_fb->scene_buf->node);
|
||
}
|
||
|
||
static void wl_noop_netwm_init(WScreen *scr) { (void)scr; }
|
||
static void wl_noop_netwm_cleanup(WScreen *scr) { (void)scr; }
|
||
static void wl_noop_netwm_update_desktop(WScreen *scr) { (void)scr; }
|
||
static int wl_noop_netwm_get_current_desktop(WScreen *scr) { (void)scr; return -1; }
|
||
static void wl_netwm_update_opacity(WWindow *wwin)
|
||
{
|
||
if (!wwin->frame)
|
||
return;
|
||
|
||
struct wl_frame_buf *fb = frame_buf_find(wwin->frame->core->window);
|
||
if (!fb || !fb->scene_buf)
|
||
return;
|
||
|
||
float alpha = 1.0f;
|
||
if (wl_state.x_display && wwin->client_win) {
|
||
WMNativeAtom type;
|
||
int format;
|
||
unsigned long items, rest;
|
||
unsigned char *prop = NULL;
|
||
WMNativeAtom opacity_atom = XInternAtom(wl_state.x_display,
|
||
"_NET_WM_WINDOW_OPACITY", False);
|
||
if (XGetWindowProperty(wl_state.x_display, (WNativeWindow)wwin->client_win,
|
||
opacity_atom, 0L, 1L, False, AnyPropertyType,
|
||
&type, &format, &items, &rest, &prop) == Success
|
||
&& format == 32 && items == 1 && prop) {
|
||
unsigned long val = *(unsigned long *)prop;
|
||
alpha = (float)val / (float)0xFFFFFFFF;
|
||
}
|
||
if (prop)
|
||
XFree(prop);
|
||
}
|
||
|
||
wlr_scene_buffer_set_opacity(fb->scene_buf, alpha);
|
||
|
||
/* Apply to all child frame_bufs (titlebar buttons, resizebar) */
|
||
struct wl_frame_buf *child;
|
||
wl_list_for_each(child, &wl_state.frame_list, link) {
|
||
if (child->parent_id == fb->id && child->scene_buf)
|
||
wlr_scene_buffer_set_opacity(child->scene_buf, alpha);
|
||
}
|
||
|
||
/* Apply to the client surface */
|
||
struct wl_toplevel_view *v;
|
||
wl_list_for_each(v, &wl_state.toplevel_list, link) {
|
||
if (v->frame_id == fb->id && v->scene_node) {
|
||
struct wlr_scene_node *node;
|
||
wl_list_for_each(node, &v->scene_node->children, link) {
|
||
if (node->type == WLR_SCENE_NODE_BUFFER)
|
||
wlr_scene_buffer_set_opacity(
|
||
wlr_scene_buffer_from_node(node), alpha);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
static void wl_noop_netwm_update_actions(WWindow *wwin, int del) { (void)wwin; (void)del; }
|
||
static void wl_noop_netwm_check_client_hints(WWindow *wwin, int *layer, int *workspace)
|
||
{ (void)wwin; (void)layer; (void)workspace; }
|
||
static void wl_noop_netwm_check_initial_state(WWindow *wwin) { (void)wwin; }
|
||
static int wl_noop_netwm_get_pid(WNativeWindow window) { (void)window; return 0; }
|
||
static void wl_noop_netwm_cleanup_frame_extents(WWindow *wwin) { (void)wwin; }
|
||
static void wl_noop_netwm_set_frame_extents(WWindow *wwin) { (void)wwin; }
|
||
|
||
static void wl_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,
|
||
};
|