2328 lines
101 KiB
C
2328 lines
101 KiB
C
/*
|
||
* wl_backend.c -- wlroots-based Wayland compositor backend for Window Maker
|
||
*/
|
||
#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>
|
||
#include <X11/Xproto.h> /* X_SetInputFocus and other request codes */
|
||
#include <wlr/backend/wayland.h>
|
||
#include <wlr/backend/multi.h>
|
||
#include <wlr/types/wlr_scene.h>
|
||
#include <wlr/render/allocator.h>
|
||
#include <wlr/types/wlr_buffer.h>
|
||
#include <wlr/interfaces/wlr_buffer.h> /* wlr_buffer_init/wlr_buffer_impl */
|
||
#include <libdrm/drm_fourcc.h>
|
||
#include <pixman.h>
|
||
#include <wlr/types/wlr_cursor.h>
|
||
#include <wlr/types/wlr_xcursor_manager.h>
|
||
#include <wlr/types/wlr_data_device.h>
|
||
#include <wlr/types/wlr_primary_selection.h>
|
||
#include <wlr/types/wlr_primary_selection_v1.h>
|
||
#include <wlr/types/wlr_xdg_output_v1.h>
|
||
#include <wlr/types/wlr_idle_inhibit_v1.h>
|
||
#include <wlr/types/wlr_viewporter.h>
|
||
#include <wlr/types/wlr_relative_pointer_v1.h>
|
||
/* 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>
|
||
#include <wlr/types/wlr_keyboard_shortcuts_inhibit_v1.h>
|
||
#include <wlr/types/wlr_linux_dmabuf_v1.h>
|
||
#include <wlr/types/wlr_screencopy_v1.h>
|
||
#include <wlr/types/wlr_export_dmabuf_v1.h>
|
||
#include <wlr/types/wlr_output_management_v1.h>
|
||
/* 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>
|
||
#include <wlr/types/wlr_gamma_control_v1.h>
|
||
#include <wlr/types/wlr_presentation_time.h>
|
||
#include <wlr/types/wlr_text_input_v3.h>
|
||
#include <wlr/types/wlr_input_method_v2.h>
|
||
#include <wlr/types/wlr_virtual_keyboard_v1.h>
|
||
#include <wlr/types/wlr_foreign_toplevel_management_v1.h>
|
||
#include <wlr/types/wlr_xdg_activation_v1.h>
|
||
/* 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>
|
||
#include <wlr/types/wlr_data_control_v1.h>
|
||
#include <wlr/types/wlr_virtual_pointer_v1.h>
|
||
/* 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>
|
||
#include <wlr/types/wlr_xdg_foreign_v2.h>
|
||
#include <wlr/util/box.h>
|
||
/* Generated xdg-shell protocol implementation (Wayland glue code) */
|
||
#include "protocols/xdg-shell-protocol.c"
|
||
#include <wlr/util/log.h>
|
||
#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"
|
||
#include "../../stacking.h"
|
||
#include "../../xinerama.h"
|
||
#include "../../xmodifier.h"
|
||
#include "../../event.h"
|
||
#include "../x11/wmspec.h"
|
||
#include "../../icon.h" /* WIcon, TILE_* */
|
||
#include "../../workspace.h" /* WWorkspace */
|
||
#include "../../dock.h" /* WDock (for clip->collapsed) */
|
||
#include "../../appicon.h" /* WAppIcon */
|
||
#include "../../defaults.h" /* wDefaultsCheckDomains */
|
||
#include "../../resources.h" /* wGetColor */
|
||
#include "../../menu.h" /* WMenu, WMenuEntry */
|
||
#include "../../balloon.h" /* WBalloon */
|
||
#include "../../dialog.h" /* WMRestart, WMAbort */
|
||
#include "../backend.h"
|
||
#include "wl_backend.h"
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* WINGs private header — needed for W_EventBackend, W_Screen fields */
|
||
#include <WINGs/WINGsP.h>
|
||
#include <WINGs/WMEvent.h>
|
||
#include "../../def_pixmaps.h"
|
||
struct W_Screen;
|
||
struct W_View;
|
||
void W_WL_EnqueueEvent(struct W_Screen *scr, const WMEvent *ev, struct W_View *target);
|
||
void W_WL_SetLoopDispatch(struct W_Screen *scr,
|
||
int loop_fd,
|
||
void (*dispatch_fn)(int timeout_ms, void *ud),
|
||
void *userdata);
|
||
void W_RegisterBacking(unsigned long id, void *img);
|
||
void W_UnregisterBacking(unsigned long id);
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Forward declarations */
|
||
|
||
#include "wl_types.h"
|
||
extern void wm_dock_app_add(struct wl_toplevel_view *v);
|
||
extern void wm_dock_app_remove(WNativeWindow id);
|
||
|
||
struct wl_compositor_state wl_state;
|
||
|
||
/* Forward declarations for helpers used before their definition. */
|
||
int level_to_bucket(int level);
|
||
struct wlr_scene_tree *level_tree_for(int level);
|
||
struct wl_frame_buf *frame_buf_find(WNativeWindow id);
|
||
|
||
|
||
/*
|
||
* 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)))
|
||
|
||
#if USE_XWAYLAND
|
||
/* X error handler — defined near the vtable at the bottom of this file. */
|
||
int wl_catch_xwayland_error(Display *edpy, XErrorEvent *error);
|
||
#endif
|
||
|
||
void wl_focus_set(WNativeWindow win, int revert_to, unsigned long timestamp);
|
||
int wl_client_get_pid(WNativeWindow win);
|
||
void wl_update_pointer_focus(uint32_t time_msec);
|
||
void wl_pointer_warp_to(int x, int y);
|
||
/* wmaker-popup-v1 helper — defined in protocol section below. */
|
||
void wmaker_popup_apply(struct wl_toplevel_view *view);
|
||
extern void wmaker_popup_manager_bind(struct wl_client *client, void *data, uint32_t version, uint32_t id);
|
||
extern void handle_pointer_motion(struct wl_listener *listener, void *data);
|
||
extern struct wlr_surface *view_surface_at(int px, int py, double *sx_out,
|
||
double *sy_out, struct wl_toplevel_view **view_out);
|
||
extern void handle_seat_request_cursor(struct wl_listener *listener, void *data);
|
||
static void handle_seat_request_set_selection(struct wl_listener *listener, void *data);
|
||
void handle_seat_request_start_drag(struct wl_listener *listener, void *data);
|
||
static void handle_seat_request_primary_selection(struct wl_listener *listener, void *data);
|
||
/* Compositor-internal clipboard hooks for WINGs. */
|
||
void wl_clipboard_store_to_seat(const char *text, int len);
|
||
char *wl_clipboard_fetch_from_seat(int *len_out);
|
||
|
||
/* Drag icon overlay for WINGs compositor-internal DND. */
|
||
void wl_drag_icon_show(struct W_View *view, int x, int y);
|
||
void wl_drag_icon_move(int x, int y);
|
||
void wl_drag_icon_hide(void);
|
||
void wl_drag_cursor_feedback(Bool accepted);
|
||
static void handle_idle_new_inhibitor(struct wl_listener *listener, void *data);
|
||
extern void handle_pointer_axis(struct wl_listener *listener, void *data);
|
||
static void handle_new_pointer_constraint(struct wl_listener *listener, void *data);
|
||
static void handle_new_shortcuts_inhibitor(struct wl_listener *listener, void *data);
|
||
extern void handle_output_mgr_apply(struct wl_listener *listener, void *data);
|
||
extern void handle_output_mgr_test(struct wl_listener *listener, void *data);
|
||
extern void handle_output_power_set_mode(struct wl_listener *listener, void *data);
|
||
extern void handle_text_input_new(struct wl_listener *listener, void *data);
|
||
extern void handle_input_method_new(struct wl_listener *listener, void *data);
|
||
extern void handle_virtual_keyboard_new(struct wl_listener *listener, void *data);
|
||
extern void handle_xdg_activation_request(struct wl_listener *listener, void *data);
|
||
static void handle_layer_shell_new_surface(struct wl_listener *listener, void *data);
|
||
extern void handle_xdg_new_toplevel(struct wl_listener *listener, void *data);
|
||
extern void handle_new_toplevel_decoration(struct wl_listener *listener, void *data);
|
||
extern void handle_virtual_pointer_new(struct wl_listener *listener, void *data);
|
||
#ifdef USE_XWAYLAND
|
||
extern void handle_xwayland_new_surface(struct wl_listener *listener, void *data);
|
||
extern void handle_xwayland_ready(struct wl_listener *listener, void *data);
|
||
extern void handle_xwayland_server_destroy(struct wl_listener *listener, void *data);
|
||
#endif
|
||
extern void wm_dock_create(WScreen *scr);
|
||
extern void handle_backend_new_input(struct wl_listener *listener, void *data);
|
||
extern void handle_backend_new_output(struct wl_listener *listener, void *data);
|
||
extern void handle_output_frame(struct wl_listener *listener, void *data);
|
||
extern void handle_pointer_motion_absolute(struct wl_listener *listener, void *data);
|
||
extern void handle_pointer_button(struct wl_listener *listener, void *data);
|
||
extern void handle_keyboard_key(struct wl_listener *listener, void *data);
|
||
extern void handle_keyboard_modifiers(struct wl_listener *listener, void *data);
|
||
extern void handle_pointer_frame(struct wl_listener *listener, void *data);
|
||
extern void wl_grab_begin_move(struct wl_toplevel_view *view);
|
||
extern void wl_grab_begin_resize(struct wl_toplevel_view *view, uint32_t edges);
|
||
extern void wl_grab_end(void);
|
||
extern const char *resize_edges_to_cursor(uint32_t edges);
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/* 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;
|
||
}
|
||
|
||
#if USE_XWAYLAND
|
||
/* Called by the Wayland event loop whenever the X11 connection fd is readable. */
|
||
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;
|
||
}
|
||
#else
|
||
static int
|
||
wl_handle_x11_events(int fd, uint32_t mask, void *data)
|
||
{ (void)fd; (void)mask; (void)data; return 0; }
|
||
#endif
|
||
|
||
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);
|
||
|
||
#if USE_XWAYLAND
|
||
XInitThreads();
|
||
/* Install X error handler: Mesa/EGL via XWayland */
|
||
XSetErrorHandler(wl_catch_xwayland_error);
|
||
#endif
|
||
|
||
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;
|
||
}
|
||
|
||
/* 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");
|
||
|
||
/* 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);
|
||
|
||
/* 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");
|
||
}
|
||
|
||
/* scene graph — MUST be created before wlr_backend_start so
|
||
* that handle_backend_new_output can create the scene_output immediately
|
||
* when the output fires (for nested Wayland, new_output fires during
|
||
* wlr_backend_start via the host compositor roundtrip). */
|
||
wl_state.scene = wlr_scene_create();
|
||
/* Register commit callback for WINGs views (defined later in file) */
|
||
{
|
||
extern void (*W_ViewCommitBackingFn)(unsigned long id);
|
||
extern void wl_view_commit_backing_impl(unsigned long id);
|
||
extern void wl_view_destroy_backing_impl(unsigned long id);
|
||
W_ViewCommitBackingFn = wl_view_commit_backing_impl;
|
||
extern void (*W_ViewDestroyBackingFn)(unsigned long id);
|
||
W_ViewDestroyBackingFn = wl_view_destroy_backing_impl;
|
||
|
||
extern void (*W_ViewSceneNodeSetEnabledFn)(unsigned long id, int enabled);
|
||
extern void wl_view_scene_node_set_enabled(unsigned long id, int enabled);
|
||
W_ViewSceneNodeSetEnabledFn = wl_view_scene_node_set_enabled;
|
||
|
||
extern void (*W_ClipboardStoreFn)(const char *text, int len);
|
||
extern char *(*W_ClipboardFetchFn)(int *len_out);
|
||
W_ClipboardStoreFn = wl_clipboard_store_to_seat;
|
||
W_ClipboardFetchFn = wl_clipboard_fetch_from_seat;
|
||
|
||
extern void (*W_DragIconShowFn)(struct W_View *, int, int);
|
||
extern void (*W_DragIconMoveFn)(int, int);
|
||
extern void (*W_DragIconHideFn)(void);
|
||
W_DragIconShowFn = wl_drag_icon_show;
|
||
W_DragIconMoveFn = wl_drag_icon_move;
|
||
W_DragIconHideFn = wl_drag_icon_hide;
|
||
|
||
extern void (*W_DragCursorFeedbackFn)(Bool);
|
||
W_DragCursorFeedbackFn = wl_drag_cursor_feedback;
|
||
}
|
||
if (wl_state.scene) {
|
||
wwarning("wl_backend: scene graph created");
|
||
/* Create the 9 level-bucket trees in bottom-to-top order
|
||
* (WL_LAYER_BACKGROUND first, WL_LAYER_OVERLAY last).
|
||
* Every subsequent scene node is parented to one of these
|
||
* trees, never directly to scene->tree, so Z-ordering is
|
||
* determined entirely by bucket index + intra-bucket order. */
|
||
int _li;
|
||
for (_li = 0; _li < WL_NUM_LEVEL_BUCKETS; _li++)
|
||
wl_state.level_trees[_li] = wlr_scene_tree_create(&wl_state.scene->tree);
|
||
} else
|
||
wwarning("wl_backend: wlr_scene_create failed (no compositing)");
|
||
|
||
/* 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);
|
||
|
||
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)");
|
||
|
||
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);
|
||
|
||
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);
|
||
}
|
||
/* handle clipboard set-selection requests on the seat so
|
||
* Ctrl-C / copy operations from clients propagate to the seat selection
|
||
* and become available for paste in other clients. */
|
||
if (wl_state.seat) {
|
||
wl_state.seat_request_set_selection_listener.notify =
|
||
handle_seat_request_set_selection;
|
||
wl_signal_add(&wl_state.seat->events.request_set_selection,
|
||
&wl_state.seat_request_set_selection_listener);
|
||
wwarning("wl_backend: clipboard selection wired");
|
||
|
||
wl_state.seat_request_start_drag_listener.notify =
|
||
handle_seat_request_start_drag;
|
||
wl_signal_add(&wl_state.seat->events.request_start_drag,
|
||
&wl_state.seat_request_start_drag_listener);
|
||
wwarning("wl_backend: DND start_drag wired");
|
||
}
|
||
|
||
/* 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");
|
||
}
|
||
|
||
/* 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");
|
||
}
|
||
|
||
/* 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");
|
||
}
|
||
|
||
/* 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");
|
||
|
||
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");
|
||
}
|
||
|
||
/* 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");
|
||
}
|
||
/* 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)");
|
||
|
||
/* 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)");
|
||
|
||
/* 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)");
|
||
}
|
||
|
||
/* 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)");
|
||
}
|
||
|
||
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)");
|
||
}
|
||
|
||
/* 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");
|
||
|
||
/* 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");
|
||
}
|
||
|
||
wlr_renderer_init_wl_shm(wl_state.renderer, wl_state.display);
|
||
/* wl_output global is created in handle_backend_new_output. */
|
||
|
||
/* 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);
|
||
|
||
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;
|
||
|
||
wl_state.xdg_new_surface_listener.notify = handle_xdg_new_toplevel;
|
||
wl_signal_add(&wl_state.xdg_shell->events.new_toplevel,
|
||
&wl_state.xdg_new_surface_listener);
|
||
|
||
/* Path 1: XWayland bridge — start Xwayland eagerly (lazy=false) so that
|
||
* the compositor can open a Display* from its socket and pass it back to
|
||
* main.c. WindowMaker's own X11 UI (title bars, root menus, dock) then
|
||
* renders through XWayland, making the WM visible on-screen.
|
||
* The ready signal fires once Xwayland's socket accepts connections. */
|
||
wl_state.xwayland = wlr_xwayland_create(wl_state.display,
|
||
wl_state.compositor, false);
|
||
#ifdef USE_XWAYLAND
|
||
if (wl_state.xwayland) {
|
||
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);
|
||
}
|
||
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);
|
||
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)");
|
||
}
|
||
#endif
|
||
|
||
/* 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);
|
||
}
|
||
|
||
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");
|
||
}
|
||
|
||
/* if a real keyboard was found from the nested backend
|
||
* (captured in handle_backend_new_input during wlr_backend_start),
|
||
* switch the seat to it now so keyboard focus notifications use the
|
||
* correct keymap and modifier state from the host compositor. This
|
||
* must happen after the headless keyboard setup above (which may have
|
||
* called wlr_seat_set_keyboard with the headless device). */
|
||
if (wl_state.real_keyboard_dev)
|
||
wlr_seat_set_keyboard(wl_state.seat,
|
||
wlr_keyboard_from_input_device(wl_state.real_keyboard_dev));
|
||
|
||
wl_state.started = 1;
|
||
|
||
/* Block until XWayland fires the "ready" signal — pump the Wayland server
|
||
* event loop so the XWayland process can complete its handshake.
|
||
* handle_xwayland_ready() calls XOpenDisplay and stores the result in
|
||
* wl_state.x_display, then sets xwayland_ready = true to break the loop.
|
||
* If XWayland is not available, return NULL (pure Wayland path). */
|
||
/* XWayland is lazy — it starts when the first X11 client connects.
|
||
* No need to wait for it at startup. Mark as ready so the rest of
|
||
* init proceeds without blocking. */
|
||
if (wl_state.xwayland) {
|
||
wwarning("wl_backend: XWayland configured (eager mode) — ready at startup");
|
||
wl_state.xwayland_ready = true;
|
||
if (wl_state.x_display) {
|
||
wwarning("wl_backend: XWayland bridge ready — Display*=%p, full WM UI enabled",
|
||
(void *)wl_state.x_display);
|
||
/* Start a pump thread to keep the Wayland event loop running
|
||
* while the main thread calls WMCreateScreenWithRContext() and
|
||
* other blocking X11 init in StartUp(). XWayland needs the
|
||
* Wayland compositor to be responsive to handle round-trips like
|
||
* XGetModifierMapping (which queries keyboard state from wlroots).
|
||
* The x11_event_source fd watcher is registered later in
|
||
* wl_event_loop_run() after this thread exits, so the pump
|
||
* thread never touches Xlib and there is no Xlib contention. */
|
||
wl_state.startup_pump_running = 1;
|
||
if (pthread_create(&wl_state.startup_pump_thread, NULL,
|
||
wl_startup_pump_thread, NULL) != 0) {
|
||
wwarning("wl_backend: pthread_create for pump thread failed — "
|
||
"WINGs init may deadlock");
|
||
wl_state.startup_pump_running = 0;
|
||
}
|
||
} else {
|
||
wwarning("wl_backend: XWayland ready but XOpenDisplay failed — pure Wayland mode");
|
||
}
|
||
}
|
||
|
||
return NULL; /* dpy=NULL so WM core skips Xlib calls */
|
||
}
|
||
|
||
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 USE_XWAYLAND
|
||
if (wl_state.x_display) {
|
||
XCloseDisplay(wl_state.x_display);
|
||
wl_state.x_display = NULL;
|
||
}
|
||
#endif
|
||
|
||
/* 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);
|
||
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);
|
||
WL_LISTENER_REMOVE(wl_state.seat_request_cursor_listener);
|
||
WL_LISTENER_REMOVE(wl_state.seat_request_set_selection_listener);
|
||
WL_LISTENER_REMOVE(wl_state.seat_request_primary_selection_listener);
|
||
WL_LISTENER_REMOVE(wl_state.idle_new_inhibitor_listener);
|
||
WL_LISTENER_REMOVE(wl_state.pointer_axis_listener);
|
||
WL_LISTENER_REMOVE(wl_state.real_pointer_axis_listener);
|
||
WL_LISTENER_REMOVE(wl_state.new_constraint_listener);
|
||
WL_LISTENER_REMOVE(wl_state.new_shortcuts_inhibitor_listener);
|
||
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);
|
||
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);
|
||
WL_LISTENER_REMOVE(wl_state.xdg_activation_request_listener);
|
||
WL_LISTENER_REMOVE(wl_state.layer_shell_new_surface_listener);
|
||
WL_LISTENER_REMOVE(wl_state.virtual_pointer_new_listener);
|
||
#undef WL_LISTENER_REMOVE
|
||
|
||
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;
|
||
}
|
||
|
||
void
|
||
wl_display_post_open(const char *display_name, int *multi_head_out)
|
||
{
|
||
(void)display_name;
|
||
(void)multi_head_out;
|
||
}
|
||
|
||
int
|
||
wl_display_screen_count(void)
|
||
{
|
||
return wl_state.started ? 1 : 0;
|
||
}
|
||
|
||
int
|
||
wl_display_default_screen(void)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
|
||
void
|
||
wl_event_loop_run(void)
|
||
{
|
||
if (!wl_state.display)
|
||
return;
|
||
|
||
/*
|
||
* Install signalfd as the primary SIGTERM/SIGINT/SIGHUP handler.
|
||
*
|
||
* The traditional path (startup.c handleExitSig → DispatchEvent →
|
||
* event_loop_terminate → pipe write) works in headless/smoke-test mode
|
||
* because epoll_wait with SA_RESTART sees the pipe immediately after the
|
||
* signal handler returns. In nested-Wayland mode the wlroots nested
|
||
* backend processes parent-compositor events alongside our own clients,
|
||
* and the combination of SA_RESTART + epoll ordering can cause the pipe
|
||
* write to be missed until the next event, leaving SIGTERM apparently
|
||
* ignored.
|
||
*
|
||
* signalfd is the compositor-standard solution: block async delivery,
|
||
* route signals through a regular fd in the epoll set, and handle them
|
||
* synchronously from within the dispatch loop. The pipe-based path
|
||
* (wl_event_loop_terminate) is retained as a fallback for code paths
|
||
* that call it before wl_event_loop_run() returns.
|
||
*/
|
||
{
|
||
sigset_t ss;
|
||
sigemptyset(&ss);
|
||
sigaddset(&ss, SIGTERM);
|
||
sigaddset(&ss, SIGINT);
|
||
sigaddset(&ss, SIGHUP);
|
||
|
||
wl_state.sigfd = signalfd(-1, &ss, SFD_NONBLOCK | SFD_CLOEXEC);
|
||
if (wl_state.sigfd >= 0) {
|
||
/* Block async delivery so signals only arrive via the fd. */
|
||
sigprocmask(SIG_BLOCK, &ss, NULL);
|
||
|
||
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
|
||
wl_state.sigfd_source = wl_event_loop_add_fd(loop,
|
||
wl_state.sigfd,
|
||
WL_EVENT_READABLE,
|
||
wl_signalfd_handler,
|
||
NULL);
|
||
if (wl_state.sigfd_source) {
|
||
wwarning("wl_backend: signalfd installed for SIGTERM/SIGINT/SIGHUP");
|
||
/* Disable the old signal handlers — signalfd owns these now */
|
||
signal(SIGTERM, SIG_DFL);
|
||
signal(SIGINT, SIG_DFL);
|
||
signal(SIGHUP, SIG_DFL);
|
||
} else {
|
||
wwarning("wl_backend: wl_event_loop_add_fd failed; falling back to pipe");
|
||
close(wl_state.sigfd);
|
||
wl_state.sigfd = -1;
|
||
/* Restore async delivery so the old handler still works. */
|
||
sigprocmask(SIG_UNBLOCK, &ss, NULL);
|
||
}
|
||
} else {
|
||
wwarning("wl_backend: signalfd(2) failed (%s); SIGTERM via pipe fallback",
|
||
strerror(errno));
|
||
}
|
||
}
|
||
|
||
/* Stop the startup pump thread — wl_display_run() takes over Wayland
|
||
* event dispatching from here. Join before registering x11_event_source
|
||
* so there is no concurrent access to the event loop. */
|
||
if (wl_state.startup_pump_running) {
|
||
wl_state.startup_pump_running = 0;
|
||
pthread_join(wl_state.startup_pump_thread, NULL);
|
||
wwarning("wl_backend: startup pump thread stopped");
|
||
}
|
||
|
||
/* Deferred dock creation — safe now that the pump thread has stopped.
|
||
* wm_dock_create calls wlr_scene_buffer_create which must not race with
|
||
* the pump thread's frame-timer wlr_scene_output_commit. */
|
||
if (wl_state.dock_pending_scr) {
|
||
wm_dock_create(wl_state.dock_pending_scr);
|
||
wl_state.dock_pending_scr = NULL;
|
||
}
|
||
|
||
/* Create scene nodes for any OR views whose associate signal fired
|
||
* during the pump thread (when scene ops were unsafe). */
|
||
{
|
||
struct wl_or_view *ov;
|
||
wl_list_for_each(ov, &wl_state.or_list, link) {
|
||
if (!ov->scene_node && ov->xw_surface->surface && wl_state.scene) {
|
||
struct wlr_scene_tree *_op = wl_state.level_trees[WL_LAYER_MENU]
|
||
? wl_state.level_trees[WL_LAYER_MENU] : &wl_state.scene->tree;
|
||
ov->scene_node = wlr_scene_subsurface_tree_create(_op, ov->xw_surface->surface);
|
||
if (ov->scene_node)
|
||
wlr_scene_node_set_position(&ov->scene_node->node,
|
||
ov->xw_surface->x, ov->xw_surface->y);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Register the X11 fd watcher now that StartUp() has completed and
|
||
* WINGs is initialised. Doing this here (not in wl_display_open) ensures
|
||
* the pump thread never touches Xlib. */
|
||
if (wl_state.x_display && !wl_state.x11_event_source) {
|
||
wl_state.x11_event_source = wl_event_loop_add_fd(
|
||
wl_display_get_event_loop(wl_state.display),
|
||
ConnectionNumber(wl_state.x_display),
|
||
WL_EVENT_READABLE,
|
||
wl_handle_x11_events, NULL);
|
||
if (wl_state.x11_event_source)
|
||
wwarning("wl_backend: X11 event source registered for WINGs dispatch");
|
||
}
|
||
|
||
wwarning("wl_backend: DISPLAY=%s WAYLAND_DISPLAY=%s",
|
||
getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)",
|
||
getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(unset)");
|
||
|
||
/* register compositor dispatch callback so WINGs modal loops
|
||
* (WMRunModalLoop, color panels, etc.) can tick the event loop. */
|
||
{
|
||
WMScreen *wmscr = wl_get_wmscreen();
|
||
if (wmscr) {
|
||
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
|
||
int loop_fd = wl_event_loop_get_fd(loop);
|
||
W_WL_SetLoopDispatch(wmscr, loop_fd, wl_wings_dispatch, NULL);
|
||
wwarning("wl_backend: WINGs loop dispatch registered (fd=%d)", loop_fd);
|
||
}
|
||
}
|
||
|
||
wwarning("wl_backend: event loop running");
|
||
|
||
/* Custom event loop: pump wlroots, then drain WINGs events.
|
||
* We can't use wl_display_run() because WM event handlers (OpenRootMenu,
|
||
* etc.) call WMMaskEvent which re-enters the wlroots loop. By draining
|
||
* WINGs events BETWEEN dispatches (not inside a timer callback), modal
|
||
* loops in WMMaskEvent can safely call wl_event_loop_dispatch. */
|
||
{
|
||
struct wl_event_loop *loop = wl_display_get_event_loop(wl_state.display);
|
||
while (!wl_state.terminate) {
|
||
wl_display_flush_clients(wl_state.display);
|
||
extern void W_WLClientDispatchPending(void);
|
||
W_WLClientDispatchPending();
|
||
wl_event_loop_dispatch(loop, 16);
|
||
/* Schedule frame AFTER dispatch so the frame event on the next
|
||
* iteration renders content from commits processed above. */
|
||
if (wl_state.output)
|
||
wlr_output_schedule_frame(wl_state.output);
|
||
|
||
/* Check inotify for defaults database changes */
|
||
#ifdef HAVE_INOTIFY
|
||
if (w_global.inotify.fd_event_queue >= 0) {
|
||
fd_set rfds;
|
||
struct timeval tv = { 0, 0 };
|
||
FD_ZERO(&rfds);
|
||
FD_SET(w_global.inotify.fd_event_queue, &rfds);
|
||
if (select(w_global.inotify.fd_event_queue + 1, &rfds, NULL, NULL, &tv) > 0
|
||
&& FD_ISSET(w_global.inotify.fd_event_queue, &rfds)) {
|
||
wl_handle_inotify_events();
|
||
}
|
||
}
|
||
#endif
|
||
|
||
/* Fire any expired WINGs timers (workspace name fade, etc.) */
|
||
W_CheckTimerHandlers();
|
||
|
||
/* Drain WINGs event queue */
|
||
WMScreen *wmscr = wl_get_wmscreen();
|
||
if (wmscr) {
|
||
const W_EventBackend *be = wmscr->eventBackend;
|
||
void *ectx = wmscr->eventBackendCtx;
|
||
WMEvent wev;
|
||
int limit = 64;
|
||
int handled = 0;
|
||
while (be->pending(ectx) && limit-- > 0) {
|
||
be->fetch(ectx, &wev);
|
||
WMHandleEvent(&wev);
|
||
handled++;
|
||
}
|
||
/* If WINGs events were processed (which may trigger client
|
||
* surface commits via wl_redisplay), do another short dispatch
|
||
* to process those commits so the next frame renders them. */
|
||
if (handled)
|
||
wl_event_loop_dispatch(loop, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
wwarning("wl_backend: event loop exited");
|
||
|
||
/* Remove pointer listeners before wlroots destroys the pointer device */
|
||
if (wl_state.pointer_dev) {
|
||
wl_list_remove(&wl_state.real_pointer_motion_abs_listener.link);
|
||
wl_list_remove(&wl_state.real_pointer_button_listener.link);
|
||
wl_list_remove(&wl_state.real_pointer_frame_listener.link);
|
||
wl_list_remove(&wl_state.real_pointer_axis_listener.link);
|
||
}
|
||
}
|
||
|
||
void
|
||
wl_event_loop_terminate(void)
|
||
{
|
||
/* Write a byte to the self-pipe. write(2) is async-signal-safe, so this
|
||
* can safely be called from a signal handler. The event loop will read
|
||
* the pipe and call wl_display_terminate() from within loop context,
|
||
* avoiding the double-free that results from calling wl_display_destroy()
|
||
* while wl_event_loop_dispatch() is active. */
|
||
if (wl_state.shutdown_pipe[1] >= 0) {
|
||
char c = 'T';
|
||
(void)write(wl_state.shutdown_pipe[1], &c, 1);
|
||
} else if (wl_state.display) {
|
||
/* fallback if pipe creation failed at startup */
|
||
wl_display_terminate(wl_state.display);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Drag icon overlay for WINGs compositor-internal DND */
|
||
|
||
|
||
|
||
/*
|
||
* 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);
|
||
}
|
||
|
||
|
||
/*
|
||
* 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);
|
||
}
|
||
|
||
/*
|
||
* 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)");
|
||
}
|
||
|
||
/*
|
||
* 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.
|
||
*/
|
||
/*
|
||
* 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");
|
||
}
|
||
|
||
/*
|
||
* 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");
|
||
}
|
||
|
||
|
||
|
||
#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 : "");
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Keyboard grabs */
|
||
|
||
|
||
|
||
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Screen internals */
|
||
|
||
int wl_screen_select_input(WScreen *scr, long mask) { (void)scr; (void)mask; return 1; }
|
||
void wl_screen_set_root_cursor(WScreen *scr, int cursor_id) { (void)scr; (void)cursor_id; }
|
||
void wl_screen_set_icon_sizes(WScreen *scr, int min_size, int max_size) { (void)scr; (void)min_size; (void)max_size; }
|
||
void wl_screen_setup_noticeboard(WScreen *scr) { (void)scr; }
|
||
#if USE_XWAYLAND
|
||
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 USE_XWAYLAND
|
||
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);
|
||
}
|
||
#endif
|
||
|
||
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);
|
||
}
|
||
#else
|
||
void wl_screen_alloc_gcs(WScreen *scr) { (void)scr; }
|
||
#endif
|
||
|
||
void
|
||
wl_screen_create_internals(WScreen *scr)
|
||
{
|
||
wl_state.scr = scr;
|
||
{
|
||
FILE *fp = fopen("/tmp/wl_backend_trace.log", "a");
|
||
if (fp) { fprintf(fp, "wl_screen_create_internals: scr=%p\n", (void*)scr); fclose(fp); }
|
||
}
|
||
#if USE_XWAYLAND
|
||
/* Create off-screen InputOnly X window as focus sink */
|
||
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;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
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);
|
||
|
||
#if USE_XWAYLAND
|
||
/* XWayland bridge: create X11 helper window for EWMH */
|
||
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;
|
||
}
|
||
#endif
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
|
||
/* 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; */
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
}
|
||
void wl_screen_create_pixmaps(WScreen *scr) { (void)scr; }
|
||
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;
|
||
}
|
||
void wl_screen_show_mini_screenshot(WScreen *scr, RImage *img) { (void)scr; (void)img; }
|
||
RImage *wl_screen_capture_area(WScreen *scr) { (void)scr; return NULL; }
|
||
RImage *wl_screen_capture_window(WScreen *scr, WWindow *wwin) { (void)scr; (void)wwin; return NULL; }
|
||
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;
|
||
}
|
||
|
||
void wl_set_desktop_background(WScreen *scr, const char *texture_desc)
|
||
{
|
||
if (!scr || !texture_desc)
|
||
return;
|
||
if (!wl_state.output) {
|
||
/* Output not ready yet — cache the background for later */
|
||
wfree(wl_state.pending_background);
|
||
wl_state.pending_background = wstrdup(texture_desc);
|
||
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) {
|
||
/* Try the path as-is first, then search pixmap_path */
|
||
RImage *img = RLoadImage(rc, path, 0);
|
||
if (!img && path[0] != '/') {
|
||
/* Relative path — search pixmap_path */
|
||
char *search_paths[] = { wPreferences.pixmap_path, PKGDATADIR "/Backgrounds", NULL };
|
||
for (int _pi = 0; search_paths[_pi]; _pi++) {
|
||
char _full[PATH_MAX];
|
||
snprintf(_full, sizeof(_full), "%s/%s", search_paths[_pi], path);
|
||
img = RLoadImage(rc, _full, 0);
|
||
if (img) break;
|
||
}
|
||
}
|
||
if (!img) {
|
||
} else {
|
||
|
||
/* 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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void wl_monitors_select_events(WScreen *scr) { (void)scr; }
|
||
|
||
|
||
|
||
|