On split display/render platforms (vc4 display + v3d render) the GLES2 renderer resolves to the display node, which has no 3D engine -- and wlroots then advertises that node as the dmabuf main device, so Mesa clients try to render there and fall back to llvmpipe (mpv goes software, drops hundreds of frames). Discover a real render node under /dev/dri and override the advertised main device with it; rendering, allocation and scanout paths are untouched, and a missing node falls back to the previous renderer-derived behavior.
14422 lines
607 KiB
C
14422 lines
607 KiB
C
/* kick.c -- nested kick compositor built on wlroots 0.20.
|
|
*
|
|
* Renders a headless wlroots output (via wlr_scene) inside a host Wayland
|
|
* window (the "host" connection, e.g. labwc). Input from the host is
|
|
* forwarded into a wlr_seat, so wlroots handles all sub-surface hit-testing,
|
|
* enter/leave, input regions and coordinate translation. Clients are always
|
|
* maximized to the host content area; a client's fullscreen request is
|
|
* honored and mirrors onto the host window, and vice versa.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
/* Sanitizer-gate defaults: baked in so they survive the test harness's
|
|
env -i spawns (which would otherwise lose ASAN_OPTIONS). Compiled out
|
|
of normal builds entirely. */
|
|
#if defined(__SANITIZE_ADDRESS__)
|
|
const char *__asan_default_options(void) {
|
|
return "detect_leaks=0:halt_on_error=1";
|
|
}
|
|
#endif
|
|
#if defined(__SANITIZE_UNDEFINED__)
|
|
const char *__ubsan_default_options(void) {
|
|
return "halt_on_error=1:print_stacktrace=1";
|
|
}
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <execinfo.h>
|
|
#include <fcntl.h>
|
|
#include <linux/memfd.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/poll.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/sysmacros.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <xf86drm.h>
|
|
|
|
#include <drm_fourcc.h>
|
|
|
|
#include <wayland-client.h>
|
|
#include <wayland-egl.h>
|
|
#include <wayland-server.h>
|
|
|
|
#include <libdecor.h>
|
|
|
|
#include "xdg-shell-client.h"
|
|
#include "xdg-decoration-client.h"
|
|
#include "ext-data-control-v1-client.h"
|
|
|
|
#include <EGL/egl.h>
|
|
#include <EGL/eglext.h>
|
|
#include <GLES2/gl2.h>
|
|
#include <GLES2/gl2ext.h>
|
|
|
|
#include <xkbcommon/xkbcommon.h>
|
|
|
|
/* xkbcommon 1.6 renamed/removed the Mod4 macro: >= 1.7 defines only the
|
|
legacy XKB_MOD_NAME_LOGO (kept for compat), < 1.7 defines the modern
|
|
XKB_MOD_NAME_MOD4. Prefer the modern name where it exists. */
|
|
#ifdef XKB_MOD_NAME_MOD4
|
|
#define XKB_PREFIX_MOD4_NAME XKB_MOD_NAME_MOD4
|
|
#else
|
|
#define XKB_PREFIX_MOD4_NAME XKB_MOD_NAME_LOGO
|
|
#endif
|
|
|
|
#include <cairo.h>
|
|
#include <pango/pangocairo.h>
|
|
#include <librsvg/rsvg.h>
|
|
#include <png.h>
|
|
|
|
#include <wlr/backend.h>
|
|
#include <wlr/backend/headless.h>
|
|
#include <wlr/backend/session.h>
|
|
#include <wlr/interfaces/wlr_keyboard.h>
|
|
#include <wlr/render/allocator.h>
|
|
#include <wlr/render/drm_format_set.h>
|
|
#include <wlr/render/egl.h>
|
|
#include <wlr/render/gles2.h>
|
|
#include <wlr/render/pass.h>
|
|
#include <wlr/render/swapchain.h>
|
|
#include <wlr/render/wlr_renderer.h>
|
|
#include <wlr/render/wlr_texture.h>
|
|
#include <wlr/types/wlr_alpha_modifier_v1.h>
|
|
#include <wlr/types/wlr_buffer.h>
|
|
#include <wlr/types/wlr_color_management_v1.h>
|
|
#include <wlr/types/wlr_color_representation_v1.h>
|
|
#include <wlr/types/wlr_compositor.h>
|
|
#include <wlr/types/wlr_content_type_v1.h>
|
|
#include <wlr/types/wlr_cursor.h>
|
|
#include <wlr/types/wlr_cursor_shape_v1.h>
|
|
#include <wlr/xcursor.h>
|
|
#include <wlr/types/wlr_ext_data_control_v1.h>
|
|
#include <wlr/types/wlr_ext_image_capture_source_v1.h>
|
|
#include <wlr/types/wlr_ext_image_copy_capture_v1.h>
|
|
#include <wlr/types/wlr_data_device.h>
|
|
#include <wlr/types/wlr_ext_foreign_toplevel_list_v1.h>
|
|
#include <wlr/types/wlr_foreign_toplevel_management_v1.h>
|
|
#include <wlr/types/wlr_fixes.h>
|
|
#include <wlr/types/wlr_fractional_scale_v1.h>
|
|
#include <wlr/types/wlr_gamma_control_v1.h>
|
|
#include <wlr/types/wlr_idle_inhibit_v1.h>
|
|
#include <wlr/types/wlr_idle_notify_v1.h>
|
|
#include <wlr/types/wlr_input_device.h>
|
|
#include <wlr/types/wlr_layer_shell_v1.h>
|
|
#include <wlr/types/wlr_keyboard.h>
|
|
#include <wlr/types/wlr_keyboard_shortcuts_inhibit_v1.h>
|
|
#include <wlr/types/wlr_linux_dmabuf_v1.h>
|
|
#include <wlr/types/wlr_output.h>
|
|
#include <wlr/types/wlr_output_layer.h>
|
|
#include <wlr/types/wlr_output_layout.h>
|
|
#include <wlr/types/wlr_output_power_management_v1.h>
|
|
#include <wlr/types/wlr_output_management_v1.h>
|
|
#include <wlr/types/wlr_pointer.h>
|
|
#include <wlr/types/wlr_pointer_constraints_v1.h>
|
|
#include <wlr/types/wlr_pointer_gestures_v1.h>
|
|
#include <wlr/types/wlr_presentation_time.h>
|
|
#include <wlr/types/wlr_primary_selection.h>
|
|
#include <wlr/types/wlr_primary_selection_v1.h>
|
|
#include <wlr/types/wlr_relative_pointer_v1.h>
|
|
#include <wlr/types/wlr_scene.h>
|
|
#include <wlr/types/wlr_seat.h>
|
|
#include <wlr/types/wlr_session_lock_v1.h>
|
|
#include <wlr/types/wlr_single_pixel_buffer_v1.h>
|
|
#include <wlr/types/wlr_touch.h>
|
|
#include <wlr/types/wlr_shm.h>
|
|
#include <wlr/types/wlr_subcompositor.h>
|
|
#include <wlr/types/wlr_tearing_control_v1.h>
|
|
#include <wlr/types/wlr_text_input_v3.h>
|
|
#include <wlr/types/wlr_input_method_v2.h>
|
|
#include <wlr/types/wlr_viewporter.h>
|
|
#include <wlr/types/wlr_virtual_keyboard_v1.h>
|
|
#include <wlr/types/wlr_virtual_pointer_v1.h>
|
|
#include <wlr/types/wlr_xcursor_manager.h>
|
|
#include <wlr/types/wlr_xdg_activation_v1.h>
|
|
#include <wlr/types/wlr_xdg_decoration_v1.h>
|
|
#include <wlr/types/wlr_xdg_system_bell_v1.h>
|
|
#include <wlr/types/wlr_xdg_toplevel_icon_v1.h>
|
|
#include <wlr/types/wlr_xdg_toplevel_tag_v1.h>
|
|
#include <wlr/types/wlr_xdg_dialog_v1.h>
|
|
#include <wlr/types/wlr_xdg_output_v1.h>
|
|
#include <wlr/types/wlr_xdg_shell.h>
|
|
#include <wlr/util/log.h>
|
|
|
|
/* XWayland support is on when the linked wlroots was built with it
|
|
(WLR_HAS_XWAYLAND in the public <wlr/config.h>) and the xcb dev headers
|
|
are available (KICK_HAVE_XCB from meson.build). Anything missing and the
|
|
X11 branches below compile away. */
|
|
#include <wlr/config.h>
|
|
#if defined(WLR_HAS_XWAYLAND) && defined(KICK_HAVE_XCB)
|
|
#define HAVE_XWAYLAND 1
|
|
#else
|
|
#define HAVE_XWAYLAND 0
|
|
#endif
|
|
|
|
#if HAVE_XWAYLAND
|
|
#include <wlr/xwayland/xwayland.h>
|
|
#include <wlr/xwayland/server.h>
|
|
#endif
|
|
|
|
/* Host-side cursor-shape-v1 interface descriptors, used for the manual
|
|
wl_proxy_marshal_flags() forwarding below. wlroots 0.19 exposed these via
|
|
its wlr_cursor_shape_v1.h (which included the generated server header);
|
|
0.20 only includes the enum header, so declare them here instead. The
|
|
definitions come from kick's own generated cursor-shape protocol code
|
|
(not from libwlroots), which is why the generated client header must NOT
|
|
be included here too -- it would redefine the shape enum. */
|
|
extern const struct wl_interface wp_cursor_shape_manager_v1_interface;
|
|
extern const struct wl_interface wp_cursor_shape_device_v1_interface;
|
|
|
|
/* server.tabs is a plain list of tabs; there is no tab limit. */
|
|
|
|
/* ===================== Server-side (wlroots) state ===================== */
|
|
|
|
/* A tab is only an organizing container: a list head holding the windows
|
|
that share it. With --tab-groups off, every window gets its own tab
|
|
(one window per tab). With --tab-groups on, a second window from the
|
|
same client is linked into its owner's window list. A tab carries zero
|
|
business data -- every field lives on kick_window. */
|
|
struct kick_tab {
|
|
struct wl_list link; /* server.tabs */
|
|
struct wl_list windows; /* kick_window.link */
|
|
};
|
|
|
|
/* One window. Every surface kick shows -- a command's first toplevel (a
|
|
black pending tab is its still-unsurfaced window), a grouped second
|
|
window from the same client, an external client's window -- is one
|
|
identical kick_window linked into its tab's window list. All state
|
|
(launch identity, surface, scene tree, listeners, icon, focus and
|
|
lifecycle) lives here; the old kick_app/kick_tab_group_member split is
|
|
gone. */
|
|
struct kick_window {
|
|
struct kick_tab *tab;
|
|
struct wl_list link; /* tab->windows */
|
|
|
|
char *cmd;
|
|
pid_t pid;
|
|
/* Process group the *connected* client lives in. Command-line commands
|
|
are forked all at once at startup (see launch_all_cli), so a CLI tab's
|
|
launcher window is claimed by whichever command's client connects --
|
|
matched by process group at claim time, not by order; grouped and
|
|
external windows claim later. A close must signal the group recorded
|
|
at claim time rather than the launcher's pid. */
|
|
pid_t pgid;
|
|
/* PID of the *connected* client (from claim-time credentials). Used as
|
|
the kill target when the client belongs to a process group we do not
|
|
own (e.g. one started from the compositor's own shell/terminal), where
|
|
signaling the whole group would kill the launcher's session too. */
|
|
pid_t client_pid;
|
|
struct wlr_xdg_toplevel *toplevel;
|
|
/* Foreign toplevel management handles (taskbar/switcher). One
|
|
wlr-foreign handle plus one ext-foreign handle per window; taskbars
|
|
enumerate these and can request activation/close. Both are NULL until
|
|
the window is claimed by a surfaced toplevel (a pending black window
|
|
has none). */
|
|
struct wlr_foreign_toplevel_handle_v1 *ft_handle;
|
|
struct wlr_ext_foreign_toplevel_handle_v1 *ext_handle;
|
|
struct wl_listener ft_activate;
|
|
struct wl_listener ft_close;
|
|
struct wl_listener ft_maximize;
|
|
struct wl_listener ft_fullscreen;
|
|
struct wl_listener ft_handle_destroy;
|
|
struct wl_listener ext_handle_destroy;
|
|
#if HAVE_XWAYLAND
|
|
/* An X11 window: an Xwayland surface instead of an xdg toplevel
|
|
(toplevel is NULL, is_x11 set). Shared window logic goes through the
|
|
window_* view helpers; only claim/scene wiring differs per surface
|
|
kind. */
|
|
bool is_x11;
|
|
struct wlr_xwayland_surface *xsurface;
|
|
#endif
|
|
/* This window's scene tree, a direct child of server.scene->tree. kick
|
|
shows exactly one window at a time, so only the focused window of the
|
|
active tab's tree is enabled (see show_window). */
|
|
struct wlr_scene_tree *tree;
|
|
bool mapped;
|
|
/* Whether this window is the one currently shown within its tab -- the
|
|
tab's rotation cursor. Exactly one window of a tab has it at a time;
|
|
kept across tab switches so returning to a tab restores the window
|
|
that was focused when the user left. */
|
|
bool focused;
|
|
/* The user closed this window (Alt+Delete). While set, tab cycling
|
|
skips tabs whose windows are all closing, and the SIGKILL escalation
|
|
timer (close_timer) may still be armed. */
|
|
bool closing;
|
|
struct wl_event_source *close_timer;
|
|
/* Extra grouped window of an X11 tab (--tab-groups on): a transient --
|
|
menu, palette, dialog -- that floats at its client's geometry instead
|
|
of being configured to the output size. */
|
|
bool transient;
|
|
|
|
/* Fullscreen state is NOT stored here: it lives in the protocol objects
|
|
(xdg toplevel->scheduled.fullscreen, X11 xsurface->fullscreen) and is
|
|
read via window_is_fullscreen. server.clients_fullscreen remains the
|
|
aggregate (host chrome OR any live window) driving panel visibility
|
|
and the fullscreen layout. */
|
|
|
|
struct wl_listener destroy;
|
|
struct wl_listener map;
|
|
struct wl_listener unmap;
|
|
struct wl_listener commit;
|
|
struct wl_listener set_title;
|
|
struct wl_listener set_app_id;
|
|
struct wl_listener request_maximize;
|
|
struct wl_listener request_fullscreen;
|
|
struct wl_listener request_minimize;
|
|
struct wl_listener request_move;
|
|
struct wl_listener request_resize;
|
|
/* xdg-dialog wiring: set when the toplevel carries an xdg_dialog_v1
|
|
object (modal flag mirrored). dialog_wired guards removal: the
|
|
dialog object lives and dies on its own schedule, and a toplevel
|
|
may gain a second one after the first is destroyed. */
|
|
struct wl_listener dialog_set_modal;
|
|
struct wl_listener dialog_destroy;
|
|
bool dialog_wired;
|
|
bool modal;
|
|
/* The wired xdg_dialog_v1 object (wlroots emits set_modal with NULL
|
|
data, so the object is recovered here, not from the event). */
|
|
struct wlr_xdg_dialog_v1 *dialog;
|
|
#if HAVE_XWAYLAND
|
|
/* Extra X11-only listeners: surface association and the ICCCM requests
|
|
with no xdg counterpart. map/commit/destroy/request_maximize/
|
|
request_fullscreen/request_minimize are shared with the xdg path. */
|
|
struct wl_listener xs_associate;
|
|
struct wl_listener xs_dissociate;
|
|
struct wl_listener xs_request_configure;
|
|
struct wl_listener xs_request_activate;
|
|
struct wl_listener xs_request_close;
|
|
struct wl_listener xs_set_title;
|
|
struct wl_listener xs_set_class;
|
|
/* map/commit listen on the associated wlr_surface; tracked so destroy
|
|
never removes links that dissociate already removed. */
|
|
bool xs_surface_linked;
|
|
#endif
|
|
|
|
int icon_w, icon_h;
|
|
|
|
/* Resolved overlay icon (a wlroots texture). */
|
|
struct wlr_texture *icon_wlr;
|
|
bool icon_resolved; /* true once a load attempt has been made */
|
|
char icon_app_id[128];
|
|
/* Client-supplied icon (xdg-toplevel-icon-v1): while set, the overlay
|
|
shows its texture and never re-guesses from app_id. NULL means the
|
|
desktop-file guesser (or placeholder) is in charge. */
|
|
struct wlr_xdg_toplevel_icon_v1 *proto_icon;
|
|
bool icon_protocol;
|
|
/* Client-supplied tag + description (xdg-toplevel-tag-v1): e.g. "main
|
|
window" plus its translation. Shown in the overlay row ("title
|
|
[tag]") and used for --tab-groups grouping; NULL until the client
|
|
sets one (X11 windows never have one). */
|
|
char *tag;
|
|
char *tag_desc;
|
|
|
|
/* Last cursor-shape-v1 shape requested by this window's client (0 = none
|
|
requested yet). Reset on focus change so a switched-from window's
|
|
shape never bleeds onto the next one. */
|
|
int cursor_shape;
|
|
|
|
/* Slot awaiting its surface. True from creation until a client claims
|
|
the window in handle_new_toplevel (matched by launch process group),
|
|
so a CLI tab or a fresh Alt+Shift+T terminal is live-but-black while
|
|
its command starts: it shows in the overlay with its command line as
|
|
the title, and cycling can land on it (backdrop + title). Cleared on
|
|
claim, and again when a black tab is closed. */
|
|
bool pending_claim;
|
|
|
|
/* Where this window came from. ORIGIN_CLI windows hold the command-line
|
|
commands and are forked all at once at startup (see launch_all_cli),
|
|
so the tab order mirrors the command line. ORIGIN_ADD_TAB is an
|
|
Alt+Shift+T terminal. ORIGIN_GROUP is a window from an
|
|
already-connected client (a bare second toplevel when --tab-groups is
|
|
on) and is glued right of its parent tab. ORIGIN_EXTERNAL is a client
|
|
we did not launch (e.g. started from our own shell). */
|
|
enum { ORIGIN_CLI, ORIGIN_ADD_TAB, ORIGIN_GROUP, ORIGIN_EXTERNAL } origin;
|
|
/* For ORIGIN_GROUP windows that get their own tab: the tab owning the
|
|
connected client this window belongs to (the "parent" tab its new tab
|
|
is glued right of). */
|
|
struct kick_tab *parent_tab;
|
|
|
|
/* Last overlay chip title drawn for this window; the row shows whichever
|
|
window the tab is currently displaying, so this cache lets the overlay
|
|
log one line per actual change. */
|
|
char row_title[128];
|
|
|
|
/* Node in the global MRU window stack (server.mru, most-recent first).
|
|
Every window joins at the tail on allocation and bubbles to the front
|
|
when it becomes current -- except while the tab-management prefix is
|
|
held (overlay visible), when focus changes are only previews and the
|
|
commit happens on prefix release. mru_linked guards removal: a window
|
|
is linked from window_alloc until it is freed. */
|
|
struct wl_list mru_link;
|
|
bool mru_linked;
|
|
};
|
|
|
|
/* A toplevel tag/description that arrived before its toplevel was claimed
|
|
(the client is asked to tag in the initial commit, which can beat the
|
|
compositor's claim). Consumed by claim_xdg_window; dropped when the
|
|
toplevel dies unclaimed. */
|
|
struct kick_pending_tag {
|
|
struct wl_list link; /* server.pending_tags */
|
|
struct wlr_xdg_toplevel *toplevel;
|
|
char *tag;
|
|
char *desc;
|
|
struct wl_listener toplevel_destroy;
|
|
};
|
|
|
|
struct kick_server {
|
|
struct wl_display *wl_display;
|
|
struct wl_event_loop *loop;
|
|
struct wlr_backend *backend;
|
|
struct wlr_renderer *renderer;
|
|
struct wlr_allocator *allocator;
|
|
struct wlr_scene *scene;
|
|
struct wlr_output *output;
|
|
struct wlr_scene_output *scene_output;
|
|
struct wlr_scene_rect *bg_rect;
|
|
/* Standalone-overlay hardware-plane path: the overlay (tab list or
|
|
confirm-quit modal) is presented on a real DRM overlay plane via
|
|
wlr_output_layer when the backend accepts it (libliftoff assigns it a
|
|
plane), so the main buffer never carries the overlay and direct scanout
|
|
of the focused client stays live underneath. */
|
|
struct wlr_output_layer *overlay_layer; /* persistent, per output */
|
|
struct wlr_swapchain *overlay_swapchain;
|
|
/* Last committed state of the persistent layer. buffer is NULL while
|
|
the menu is hidden (the plane parked); the entry is described on
|
|
every commit so the backend's layer set never churns. */
|
|
struct wlr_output_layer_state overlay_layer_state;
|
|
/* Plane capability, probed once per output: the first overlay hold asks
|
|
the plane with a real commit and records whether libliftoff accepted
|
|
the layer. Fixed hardware gives a fixed answer, so the verdict sticks
|
|
for the session and every later frame costs nothing. */
|
|
enum kick_overlay_plane {
|
|
OVERLAY_PLANE_UNKNOWN = 0, /* not probed yet; probe on the next hold */
|
|
OVERLAY_PLANE_USED, /* a real commit confirmed the layer */
|
|
OVERLAY_PLANE_OFF, /* hardware/knob: composite always */
|
|
} overlay_plane;
|
|
bool overlay_render_lock; /* attach-render lock held for the fallback */
|
|
/* Software-cursor lock held while up (DRM): with no cursor layer in
|
|
the allocation there is nothing for the menu layer to trade planes
|
|
with, so placement stays deterministic. The cursor itself is
|
|
painted into the plane buffer (overlay_pass_cursor) and, on the
|
|
composite path, re-added after the menu -- visible and topmost
|
|
either way. Either half alone is broken (lock alone buries the
|
|
cursor under the plane; paint alone doubles it); together correct. */
|
|
bool overlay_cursor_lock;
|
|
/* Standalone-only cursor: a real wlr_cursor so the pointer is visible
|
|
(hardware cursor plane on DRM, scene-composited on headless). */
|
|
struct wlr_cursor *cursor;
|
|
struct wlr_xcursor_manager *xcursor_mgr;
|
|
struct wlr_output_layout *output_layout;
|
|
struct wlr_seat *seat;
|
|
struct wlr_xdg_shell *xdg_shell;
|
|
/* Kept for wlr_xwayland_create(), which needs the compositor global. */
|
|
struct wlr_compositor *compositor;
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland: wlroots' XWM plus a lazy Xwayland server (spawned on the
|
|
first X11 client connect). or_windows holds the override-redirect
|
|
windows (menus, tooltips) that float above the tab trees. */
|
|
struct wlr_xwayland *xwayland;
|
|
struct wl_listener xwayland_new_surface;
|
|
struct wl_listener xwayland_ready;
|
|
struct wl_listener xwayland_destroy;
|
|
/* The Xwayland Wayland client currently watched for death (see the
|
|
handler below); re-attached on every (re)start. */
|
|
struct wl_client *xwayland_client_watched;
|
|
struct wl_listener xwayland_client_destroy;
|
|
struct wl_list or_windows;
|
|
struct wl_list x11_pendings;
|
|
/* The tab whose surface held the keyboard before an override-redirect
|
|
window took it; restored when that window unmaps. */
|
|
struct kick_tab *or_focus_owner;
|
|
#endif
|
|
/* Client cursor-shape-v1 requests (wlr_cursor_shape_manager_v1). The
|
|
manager is passive: it emits request_set_shape and we either apply the
|
|
shape to our standalone cursor or forward it to the host pointer. */
|
|
struct wlr_cursor_shape_manager_v1 *shape_mgr;
|
|
struct wl_listener shape_listener;
|
|
/* Protocol managers that only need to outlive the display: presentation
|
|
(frame pacing), xdg-output (logical output geometry) and viewporter
|
|
(wl_viewport buffer scaling). Kept as fields so the globals are not
|
|
garbage collected while clients still hold resources. */
|
|
struct wlr_presentation *presentation;
|
|
struct wlr_xdg_output_manager_v1 *xdg_output_mgr;
|
|
struct wlr_viewporter *viewporter;
|
|
/* Content hints (photo/video/game) + single-pixel buffers (1x1 solid
|
|
color, no shm pool). Both are passive: wlroots tags/allocates
|
|
internally, we only query the type for the debug log. */
|
|
struct wlr_content_type_manager_v1 *content_type_mgr;
|
|
struct wlr_single_pixel_buffer_manager_v1 *single_pixel_mgr;
|
|
/* Alpha modifier (wp_alpha_modifier_v1): a surface multiplies its own
|
|
alpha (fades, translucent video). Fully passive: wlroots applies
|
|
the multiplier when rendering, so advertising is the whole job. */
|
|
struct wlr_alpha_modifier_v1 *alpha_modifier_mgr;
|
|
/* Primary selection (zwp_primary_selection_v1). Clients request the
|
|
selection on their seat device; the request handler below is what
|
|
actually installs it on the seat, mirroring the clipboard path. */
|
|
struct wlr_primary_selection_v1_device_manager *primary_selection_mgr;
|
|
struct wl_listener request_set_primary_selection;
|
|
/* Foreign toplevel management (taskbars / switchers). wlr-foreign is the
|
|
classic protocol most status bars (waybar taskbar) consume; ext-foreign
|
|
is the newer replacement. One handle per live kick window. */
|
|
struct wlr_foreign_toplevel_manager_v1 *foreign_mgr;
|
|
struct wlr_ext_foreign_toplevel_list_v1 *ext_foreign_list;
|
|
/* Idle notification (ext-idle-notify-v1) plus idle-inhibit-v1. Reports
|
|
user inactivity to bars/DPMS and lets apps suppress it. */
|
|
struct wlr_idle_notifier_v1 *idle_notifier;
|
|
bool idle_inhibited;
|
|
struct wlr_idle_inhibit_manager_v1 *idle_inhibit_mgr;
|
|
struct wl_listener idle_new_inhibitor;
|
|
struct wl_listener idle_inhibit_destroy;
|
|
struct wl_list idle_inhibitors;
|
|
/* Screen capture (ext-image-copy-capture-v1 + ext-image-capture-source-v1)
|
|
for tools like grim. wlroots drives the output commit path itself (it
|
|
locks attach-render and calls wlr_output_update_needs_frame), so beyond
|
|
advertising the two globals there is nothing to wire up: every output
|
|
commit that carries a rendered buffer becomes a copyable frame. */
|
|
struct wlr_ext_image_copy_capture_manager_v1 *copy_capture_mgr;
|
|
struct wlr_ext_output_image_capture_source_manager_v1 *output_capture_mgr;
|
|
/* Tearing (wp-tearing-control-v1) + fractional-scale (wp-fractional-scale):
|
|
the shown surface's hint drives the commit's async-flip flag (see
|
|
apply_tearing_hint), and each surface learns the output scale at
|
|
claim. Per-object tearing listeners live in kick_tearing. */
|
|
struct wlr_tearing_control_manager_v1 *tearing_mgr;
|
|
struct wl_listener tearing_new_object;
|
|
struct wlr_fractional_scale_manager_v1 *fractional_scale_mgr;
|
|
/* Output power management (zwlr_output_power_management_v1): a client
|
|
(e.g. wlopm) blanks or unblanks a specific output. kick's seatless
|
|
headless/standalone output maps to the single server output. */
|
|
struct wlr_output_power_manager_v1 *output_power_mgr;
|
|
struct wl_listener output_power_set_mode;
|
|
bool output_powered;
|
|
/* Output management (kanshi/wdisplays reconfigure the output): the
|
|
single server output is published as a head; client size requests
|
|
are honored standalone and refused nested (the host owns the size).
|
|
Scale/transform changes and disabling are unsupported. */
|
|
struct wlr_output_manager_v1 *output_mgr;
|
|
struct wl_listener output_mgr_apply;
|
|
struct wl_listener output_mgr_test;
|
|
/* Gamma control (night-light tools like wlsunset/gammastep): a client
|
|
sets a per-output ramp; wlroots rejects outputs with no LUT (e.g.
|
|
the headless output) itself, so the handler only commits ramps for
|
|
the single server output. Only takes visible effect on DRM. */
|
|
struct wlr_gamma_control_manager_v1 *gamma_mgr;
|
|
struct wl_listener gamma_set;
|
|
/* Color representation (wp_color_representation_manager_v1): surfaces
|
|
declare their alpha mode / matrix coefficients / range; wlroots
|
|
validates client values against the advertised SDR sRGB set, so
|
|
beyond creating the manager there is nothing to wire. */
|
|
struct wlr_color_representation_manager_v1 *color_repr_mgr;
|
|
/* Color management (wp_color_manager_v1): clients describe surface
|
|
colors via image descriptions; the scene forwards feedback.
|
|
kick is an SDR sRGB session, so advertise perceptual intent with
|
|
sRGB transfer characteristics and primaries (no ICC/parametric
|
|
features, matching wlroots' current scope). */
|
|
struct wlr_color_manager_v1 *color_manager;
|
|
/* System bell (xdg_system_bell_v1): clients ask the compositor to
|
|
ring the bell on their behalf. The protocol leaves the rendering
|
|
up to the compositor (sound, visual flash, or nothing); kick logs
|
|
the ring at info level so it is visible under KICK_LOG=1. */
|
|
struct wlr_xdg_system_bell_v1 *bell_mgr;
|
|
struct wl_listener bell_ring;
|
|
/* Keyboard shortcuts inhibit (zwp_keyboard_shortcuts_inhibit_v1): lets a
|
|
fullscreen game/VM grab the compositor's keyboard shortcuts. While the
|
|
focused surface's inhibitor is active, the grab chords in
|
|
process_key_event are swallowed (see process_key_event). */
|
|
struct wlr_keyboard_shortcuts_inhibit_manager_v1 *kb_inhibit_mgr;
|
|
struct wl_listener kb_inhibit_new_inhibitor;
|
|
/* Virtual keyboard/pointer (zwp_virtual_keyboard_v1 /
|
|
zwp_virtual_pointer_v1): synthesised input from clients like wtype, fed
|
|
into the seat just like real devices. */
|
|
struct wlr_virtual_keyboard_manager_v1 *virtual_kb_mgr;
|
|
struct wl_listener virtual_kb_new;
|
|
struct wl_list virtual_kbs;
|
|
struct wlr_virtual_pointer_manager_v1 *virtual_ptr_mgr;
|
|
struct wl_listener virtual_ptr_new;
|
|
struct wl_list virtual_ptrs;
|
|
/* Xdg-activation (xdg-activation-v1): lets a launcher focus a window by
|
|
token instead of requiring user input at exactly the right time. On a
|
|
request_activate we switch to the tab owning that surface. */
|
|
struct wlr_xdg_activation_v1 *xdg_activation;
|
|
struct wl_listener xdg_activation_request;
|
|
/* xdg-dialog (xdg-dialog-v1): modal toplevels demand attention -- on
|
|
map (or when set modal while mapped) their tab is shown and the
|
|
dialog takes window focus, in both --tab-groups modes. */
|
|
struct wlr_xdg_wm_dialog_v1 *dialog_mgr;
|
|
struct wl_listener dialog_new;
|
|
/* Window decorations (zxdg_decoration_manager_v1): every tab is
|
|
maximized and kick draws no frames, so new decorations are pinned
|
|
to client-side (see handle_decoration_new). */
|
|
struct wlr_xdg_decoration_manager_v1 *decoration_mgr;
|
|
struct wl_listener decoration_new;
|
|
/* Toplevel icons (xdg-toplevel-icon-v1): exact client artwork takes
|
|
precedence over the desktop-file guess in the overlay; the guesser
|
|
stays as the fallback (and the only source for X11 windows). */
|
|
struct wlr_xdg_toplevel_icon_manager_v1 *icon_mgr;
|
|
struct wl_listener icon_set;
|
|
/* Toplevel tags (xdg-toplevel-tag-v1): a client labels its toplevel
|
|
("settings", ...) with an untranslated tag plus a translated
|
|
description. Tags show in the overlay row and drive --tab-groups
|
|
grouping; a tag set before claim waits in pending_tags. */
|
|
struct wlr_xdg_toplevel_tag_manager_v1 *tag_mgr;
|
|
struct wl_listener tag_set;
|
|
struct wl_listener tag_set_desc;
|
|
struct wl_list pending_tags; /* kick_pending_tag.link */
|
|
/* wl_fixes (core protocol): lets clients destroy registries and
|
|
acknowledge global removal without tripping protocol errors. wlroots
|
|
handles the requests itself; advertising the global is the whole
|
|
job (version 1 is all wlroots 0.20 implements). */
|
|
struct wlr_fixes *fixes;
|
|
/* Pointer constraints + relative pointer (zwp_pointer_constraints_v1 /
|
|
zwp_relative_pointer_v1). Only the focused surface's constraint is
|
|
active; its region lives in the surface's local space, so the surface's
|
|
scene origin is cached when the constraint activates. */
|
|
struct wlr_pointer_constraints_v1 *pointer_constraints;
|
|
struct wlr_relative_pointer_manager_v1 *relative_pointer_mgr;
|
|
struct wlr_pointer_constraint_v1 *active_constraint;
|
|
struct wl_listener constraint_destroy;
|
|
struct wl_listener seat_pointer_focus;
|
|
/* Pointer gestures (touchpad swipe/pinch/hold): libinput devices and
|
|
the test feed drive the shared forwarders below, which fan out to
|
|
gesture clients holding seat pointer focus (see gesture section). */
|
|
struct wlr_pointer_gestures_v1 *gestures;
|
|
/* Text input + input method (OSK/IME, e.g. fcitx5): the focused tab's
|
|
text-input state is relayed to the in-session input method and the
|
|
IME's preedit/commit strings back to the focused field (see the
|
|
text-input section). Popups (candidate windows) render into
|
|
input_popup_tree, above the tab trees. */
|
|
struct wlr_text_input_manager_v3 *text_input_mgr;
|
|
struct wlr_input_method_manager_v2 *input_method_mgr;
|
|
struct wl_listener text_input_new;
|
|
struct wl_listener input_method_new;
|
|
struct wl_listener seat_keyboard_focus;
|
|
struct wlr_scene_tree *input_popup_tree;
|
|
struct wl_list input_popups; /* kick_input_popup.link */
|
|
struct wl_list input_methods; /* kick_input_method.link */
|
|
double constraint_ox, constraint_oy;
|
|
double lock_x, lock_y;
|
|
double last_ptr_x, last_ptr_y;
|
|
/* Last cursor shape applied (standalone) or forwarded (nested), as a
|
|
wp_cursor_shape_device_v1 shape value; 0 = nothing applied yet. */
|
|
int cursor_shape;
|
|
struct wlr_keyboard keyboard;
|
|
struct wlr_swapchain *swapchain;
|
|
/* Self-pipe used to wake the idle event loop from the SIGUSR1 dump
|
|
handler (async-signal-safe write); the drain callback runs in the main
|
|
loop and the render gate consumes needs_redraw. */
|
|
struct wl_event_source *sigpipe_src;
|
|
int sig_pipe[2];
|
|
|
|
struct wl_listener new_output;
|
|
struct wl_listener new_toplevel;
|
|
struct wl_listener new_popup;
|
|
struct wl_listener frame;
|
|
struct wl_listener output_destroy;
|
|
struct wl_event_source *ctl_source;
|
|
|
|
/* The active tab in server.tabs. Set to the last command-line command's
|
|
tab at startup, so that tab is active -- black, with its command line
|
|
as the overlay title -- from the very first frame and the user only
|
|
waits for it. A tab that is active while still pending (no surfaced
|
|
window yet) shows the black backdrop and clear seat focus until one
|
|
maps. */
|
|
struct kick_tab *active_tab;
|
|
/* Tabs, in overlay/cycle order: a list of kick_tab, each holding its
|
|
window list. */
|
|
struct wl_list tabs;
|
|
/* MRU window stack, most-recent first: a list of kick_window.mru_link
|
|
covering every live window (plus still-black pending placeholders, so
|
|
Alt+Tab/Alt+1 can land on them like Alt+Up/Down can). prefix+Tab/1 walk
|
|
down this order without mutating it; the shown window moves to the
|
|
front on prefix release (or immediately when focus changes with the
|
|
prefix up). */
|
|
struct wl_list mru;
|
|
/* The window that was current when the prefix went down (overlay shown),
|
|
and whether the user navigated anywhere since. On prefix release a
|
|
navigated session commits the then-current window to the MRU front. */
|
|
struct kick_window *mru_alt_anchor;
|
|
bool mru_alt_navigated;
|
|
|
|
/* wlr-layer-shell (waybar / panels). Layer surfaces render into their
|
|
own always-on-top scene tree (layer_tree) instead of being tabs, so a
|
|
bar stays visible no matter which tab is focused. The bar's exclusive
|
|
zone shrinks the usable area the maximized tabs are sized into. */
|
|
struct wlr_layer_shell_v1 *layer_shell;
|
|
struct wl_listener new_layer_surface;
|
|
struct wlr_scene_tree *layer_tree;
|
|
/* xdg_popups from foreign clients (a layer-shell bar such as wf-panel-pi)
|
|
that have no live kick-managed parent hang off this dedicated scene
|
|
tree, rooted at the output origin, so their positioner geometry maps to
|
|
real pixels instead of inheriting a tab tree's bar-offset. Kept on top
|
|
like layer_tree. */
|
|
struct wlr_scene_tree *popup_tree;
|
|
/* Layer surfaces, in reservation order (head = highest layer / lowest
|
|
overlap priority); see kick_layer. */
|
|
struct wl_list layer_surfaces;
|
|
|
|
/* ================= ext-session-lock-v1 =================
|
|
When a lock client (swaylock) grabs the session, the screen is blanked
|
|
and input is held. lock_surface_* mirror the lock client's surface for
|
|
the single output; lock_surface_tree is a scene tree that floats above
|
|
everything (layer_tree and popup_tree) while locked. Background blanking
|
|
uses an opaque black rect beneath the lock surface. The scene grows no
|
|
persistent overhead here: lock_surface_tree and lock_backdrop are only
|
|
created while a lock request is in flight. */
|
|
struct wlr_session_lock_manager_v1 *session_lock_manager;
|
|
struct wl_listener session_lock_new_lock;
|
|
struct wlr_session_lock_v1 *session_lock;
|
|
bool session_locked;
|
|
struct wlr_scene_tree *lock_surface_tree;
|
|
struct wlr_scene_rect *lock_backdrop;
|
|
struct wlr_scene_tree *lock_surface_ssd;
|
|
struct wlr_session_lock_surface_v1 *lock_surface;
|
|
struct wl_listener lock_surface_destroy;
|
|
struct wl_listener lock_surface_map;
|
|
struct wl_listener session_lock_new_surface;
|
|
struct wl_listener session_lock_unlock;
|
|
struct wl_listener session_lock_destroy;
|
|
|
|
/* The command launched via --panel (a layer-shell panel such as waybar);
|
|
NULL when no panel was requested. */
|
|
char *panel_cmd;
|
|
pid_t panel_pid;
|
|
/* Origin of the usable area the maximized tabs are sized into: the top-left
|
|
of the output shifted by any bar's reserved edge, so tabs start below a
|
|
top bar instead of being covered by it. (0,0) with no bar reserved. */
|
|
int layout_x, layout_y;
|
|
|
|
struct xkb_context *xkb_ctx;
|
|
struct xkb_keymap *xkb_keymap;
|
|
struct xkb_state *xkb_state;
|
|
/* Modifier index of the tab-management prefix (Alt or Super/Mod4,
|
|
per --modifier). */
|
|
xkb_mod_index_t xkb_prefix_mod;
|
|
|
|
/* Aggregate fullscreen state: host chrome fullscreen OR any live
|
|
window fullscreen (read from the protocol objects via
|
|
window_is_fullscreen, never stored). Kept for the
|
|
"clients/members fullscreen=" status logs only; panel visibility
|
|
and the fullscreen layout follow the *shown* entry instead (see
|
|
shown_fullscreen). */
|
|
bool clients_fullscreen;
|
|
/* Whether the currently shown overlay entry is fullscreened. Panels
|
|
are hidden and tabs take the full output only while this is true;
|
|
viewing a normal entry restores the panel-respecting usable layout
|
|
even when another entry holds fullscreen in the background. */
|
|
bool shown_fullscreen;
|
|
|
|
/* Standalone (real output) mode: the output is driven directly by a
|
|
native backend (DRM/headless) instead of being blitted into a host
|
|
window. Set from KICK_MODE=standalone or a forced WLR_BACKENDS. */
|
|
bool standalone;
|
|
bool standalone_headless;
|
|
struct wlr_session *session;
|
|
struct wl_listener new_input;
|
|
|
|
/* Standalone native keyboard devices (one entry per libinput keyboard;
|
|
see standalone_kbdev below). */
|
|
struct wl_list kbdevs;
|
|
/* Standalone native pointer devices (one entry per libinput pointer;
|
|
see standalone_ptrdev below). */
|
|
struct wl_list ptrdevs;
|
|
/* Standalone native touchscreen devices (see standalone_touchdev). */
|
|
struct wl_list touchdevs;
|
|
};
|
|
|
|
/* Requested output resolution from --screen-mode WxH[@Hz]; 0/0/0 means "let
|
|
the output pick its preferred mode". Refresh is stored in mHz. */
|
|
static int mode_req_w, mode_req_h;
|
|
static int mode_req_refresh;
|
|
|
|
/* The command-line command to start focused (--start-cmd-index), 0-based as
|
|
soon as the (1-based) user value is parsed. -1 (unset) means the last
|
|
command, keeping the startup contract. */
|
|
static int start_cmd_index = -1;
|
|
|
|
/* ===================== Host-side (nested) state ===================== */
|
|
|
|
/* One rasterized overlay text run: white coverage tile (grayscale
|
|
antialiasing, premultiplied) drawn through the existing tint path, so
|
|
row dimming and the translucent cursor highlight behave exactly as
|
|
with the old per-glyph quads. Tiles are keyed by (string, width
|
|
budget) and cached for the session; Pango shapes UTF-8 directly, so
|
|
every glyph the fonts provide renders -- no fixed character set. */
|
|
struct overlay_text_entry {
|
|
char *text; /* owned UTF-8 copy, sanitized */
|
|
int max_px; /* ellipsization budget, INT_MAX = natural */
|
|
struct wlr_texture *tex; /* NULL when nothing drawable came out */
|
|
int w, h; /* pixel size (ellipsized) */
|
|
int baseline; /* pixels from tile top */
|
|
bool rastered; /* a raster attempt already ran */
|
|
unsigned long last_used; /* LRU clock */
|
|
};
|
|
|
|
struct kick_host {
|
|
struct wl_display *display;
|
|
struct wl_registry *registry;
|
|
struct wl_compositor *compositor;
|
|
struct wl_surface *surface;
|
|
struct xdg_wm_base *wm_base;
|
|
/* The nested host window has two backends selected at startup:
|
|
- direct xdg-shell (default on compositors advertising
|
|
zxdg_decoration_manager_v1 -- labwc/sway/KDE): kick owns the
|
|
xdg_surface/xdg_toplevel, requests server-side decorations and
|
|
acks configure events immediately. No set_window_geometry hints,
|
|
so no stale-view anchoring on fullscreen (labwc 0.9.8 bug).
|
|
- libdecor (hosts forcing CSD, e.g. Mutter/GNOME): libdecor owns the
|
|
shell objects and draws the titlebar.
|
|
KICK_LIBDECOR=1 forces the libdecor path for testing. */
|
|
bool use_libdecor;
|
|
struct libdecor *libdecor;
|
|
struct libdecor_frame *frame;
|
|
struct xdg_surface *xdg_surface;
|
|
struct xdg_toplevel *toplevel;
|
|
struct zxdg_decoration_manager_v1 *decoration;
|
|
struct zxdg_toplevel_decoration_v1 *decoration_obj;
|
|
struct wl_seat *seat;
|
|
struct wl_keyboard *keyboard;
|
|
struct wl_pointer *pointer;
|
|
struct wl_touch *touch;
|
|
/* Host cursor-shape-v1: forward a nested client's requested cursor shape
|
|
to the host compositor (which draws the cursor over our window). The
|
|
device binds to the host pointer and goes inert with it, so it is
|
|
recreated whenever pointer capability is re-gained. */
|
|
struct wp_cursor_shape_manager_v1 *shape_mgr;
|
|
struct wp_cursor_shape_device_v1 *shape_dev;
|
|
uint32_t ptr_serial; /* latest host wl_pointer.enter serial */
|
|
uint32_t seat_name; /* registry name of the bound wl_seat global */
|
|
bool registry_synced; /* initial registry scan completed */
|
|
bool configured;
|
|
bool fullscreen;
|
|
int width, height;
|
|
/* EGL */
|
|
bool egl_ok;
|
|
EGLDisplay egl_display;
|
|
EGLConfig egl_config;
|
|
EGLContext egl_context;
|
|
EGLSurface egl_surface;
|
|
struct wl_egl_window *egl_window;
|
|
/* host modifiers (for tab-switch detection) */
|
|
uint32_t host_mods_depressed;
|
|
uint32_t axis_source;
|
|
int32_t axis_discrete[2];
|
|
/* GL program */
|
|
GLuint gl_program;
|
|
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc, gl_color_loc;
|
|
GLuint import_tex;
|
|
/* Alt-held tab-title overlay */
|
|
bool overlay_visible;
|
|
int overlay_font_h; /* ascent + descent, pixels */
|
|
int overlay_font_asc; /* ascender, pixels */
|
|
/* Present gating: at most one buffer aimed at the host compositor at a
|
|
time, paced by the host surface's wl_callback. */
|
|
struct wl_callback *frame_cb;
|
|
bool present_pending;
|
|
bool back_dirty;
|
|
/* Host-window fullscreen request waiting for the present pipeline to
|
|
drain (see host_apply_fullscreen). */
|
|
bool fs_queued;
|
|
bool fs_target;
|
|
/* Keybind repeat for the captured tab-switch binding. The host
|
|
compositor only sends one physical press, so we drive the repeat
|
|
ourselves with a timer (fixed delay/rate), like labwc does for its
|
|
own keybinds. */
|
|
struct wl_event_source *repeat_timer;
|
|
uint32_t repeat_key;
|
|
int repeat_dir;
|
|
/* What each repeat tick should do: REPEAT_CYCLE switches tabs,
|
|
REPEAT_REORDER moves the active tab within the list, REPEAT_SUBCYCLE
|
|
rotates the active tab's stacked windows, REPEAT_MRU steps down the
|
|
MRU window stack. */
|
|
enum { REPEAT_CYCLE, REPEAT_REORDER, REPEAT_SUBCYCLE,
|
|
REPEAT_MEMBER_MOVE, REPEAT_MRU } repeat_kind;
|
|
};
|
|
|
|
static struct kick_server server;
|
|
static struct kick_host host;
|
|
static char kick_socket[64];
|
|
static char socket_path[256];
|
|
static volatile sig_atomic_t needs_redraw;
|
|
static bool frame_pending;
|
|
static bool shutting_down;
|
|
|
|
/* Terminal launched for the Alt+Shift+T "new tab" binding. Defaults to
|
|
alacritty (sane no-config font size, unlike foot's 8pt); overridable via
|
|
the $TERMINAL environment variable. The value is used verbatim by sh -c,
|
|
so it may carry arguments. */
|
|
static const char *term_cmd = "alacritty";
|
|
static bool term_key_held; /* Alt+Shift+T currently down (repeat latch) */
|
|
static bool delete_key_held; /* prefix+Delete currently down (repeat latch) */
|
|
static bool f11_key_held; /* prefix+F11 currently down (repeat latch) */
|
|
static bool quit_key_held; /* Ctrl+Alt+Q currently down (repeat latch) */
|
|
|
|
/* Confirm-before-quit: set when a quit was requested (host window close or
|
|
Ctrl+Alt+Q) with more than one live window open. While set, a modal
|
|
panel over the composited frame asks Y/Enter (quit) or N/Escape (keep
|
|
running) and every key is swallowed so focused clients never see them. */
|
|
static bool confirm_quit_pending;
|
|
|
|
/* The modifier prefix for the tab-management chords: hold = show overlay,
|
|
Up/Down = cycle, Shift+Up/Down = reorder, Delete = close. Defaults to
|
|
Alt; --modifier super selects Super (Mod4), which is a good fit for
|
|
standalone/DRM and labwc-style hosts that do not reserve it the way
|
|
GNOME does. */
|
|
enum kick_prefix { PREFIX_ALT, PREFIX_SUPER };
|
|
static enum kick_prefix tab_prefix = PREFIX_ALT;
|
|
|
|
/* --tab-groups: how a related window from an already-connected client groups
|
|
with its tab. A window is "related" when its client process is a (direct or
|
|
indirect) child of that tab's client process (process ancestry, walked up
|
|
/proc). 'on' (default) stacks related windows above the parent tab's
|
|
surface -- they share the tab's visibility and take keyboard focus while
|
|
that tab is shown, and never appear in cycling or the overlay (a
|
|
browser-style tab group); 'off' gives each related window its own tab. */
|
|
static bool tab_groups = true;
|
|
|
|
/* --focus-priority: boost the active window's process group (nice 0, io BE/4)
|
|
above the rest (nice +10, io BE/7). Off by default. Only ever pushes
|
|
background groups *down*, so no CAP_SYS_NICE / sudo is needed. Works in
|
|
both modes; the effect is strongest standalone where no host scheduler
|
|
competes. */
|
|
static bool opt_focus_priority = false;
|
|
|
|
/* ===================== Clipboard bridge ===================== */
|
|
|
|
enum bridge_backend {
|
|
BRIDGE_BACKEND_NONE = 0,
|
|
BRIDGE_BACKEND_DATA_CONTROL,
|
|
BRIDGE_BACKEND_WL_DATA_DEVICE,
|
|
};
|
|
|
|
/* Bridges the nested seat's selection with the host compositor's. Two host
|
|
backends share one inner (nested-seat) implementation:
|
|
- data-control: ext_data_control_v1 (labwc/sway/etc.), full duplex,
|
|
independent of keyboard focus; its v2 set_primary_selection /
|
|
primary_selection pair carries the primary (middle-click) selection.
|
|
- wl_data_device: plain stable protocol (Mutter/GNOME fallback). Writing
|
|
the host selection always works (needs a captured keyboard serial);
|
|
reading the host selection only works while our window is focused.
|
|
It has no primary selection, so the primary bridge only exists on the
|
|
data-control backend.
|
|
Data flows by passing file descriptors straight through in both directions
|
|
(host offer -> nested source, nested source -> host source), so no
|
|
intermediate copy is needed. */
|
|
struct clipboard_bridge {
|
|
enum bridge_backend backend;
|
|
|
|
/* nested (wlroots) seat */
|
|
struct wl_listener request_set_selection;
|
|
struct wl_listener set_selection;
|
|
struct wl_listener set_primary_selection;
|
|
|
|
/* the nested selection source currently mirrored to the host; serves
|
|
host paste requests until it is destroyed */
|
|
struct wlr_data_source *inner_src;
|
|
struct wl_listener inner_src_destroy;
|
|
bool inner_clear_requested; /* nested client asked to clear the selection */
|
|
bool ignore_next_inner; /* one-shot echo guard for host->nested clears */
|
|
|
|
/* the nested primary-selection source currently mirrored to the host;
|
|
serves host primary-selection requests (middle click) until destroyed */
|
|
struct wlr_primary_selection_source *inner_primary_src;
|
|
struct wl_listener inner_primary_src_destroy;
|
|
bool inner_primary_clear_requested;
|
|
bool primary_ignore_next_inner; /* one-shot guard for host->nested echoes */
|
|
|
|
/* host: standard wl_data_device (always bound; the fallback backend) */
|
|
struct wl_data_device_manager *ddm;
|
|
struct wl_data_device *dd;
|
|
struct wl_data_source *dd_source; /* our host-side source (nested copy) */
|
|
struct wl_data_offer *dd_offer; /* current host selection offer */
|
|
struct wl_data_offer *dd_echo_offer; /* offer describing our own set */
|
|
uint32_t kbd_serial; /* last host keyboard/enter serial */
|
|
|
|
/* host: ext-data-control backend (if the host exposes it) */
|
|
struct ext_data_control_manager_v1 *dc_manager;
|
|
struct ext_data_control_device_v1 *dc_device;
|
|
struct ext_data_control_source_v1 *dc_source;
|
|
struct ext_data_control_offer_v1 *dc_offer;
|
|
struct ext_data_control_offer_v1 *dc_echo_offer;
|
|
/* host primary selection over the same device: our published source,
|
|
the current host offer, and the echo of our own set */
|
|
struct ext_data_control_source_v1 *dc_primary_source;
|
|
struct ext_data_control_offer_v1 *dc_primary_offer;
|
|
struct ext_data_control_offer_v1 *dc_primary_echo_offer;
|
|
|
|
/* MIME list currently being collected from a host offer */
|
|
char **mimes;
|
|
size_t mimes_len, mimes_cap;
|
|
bool dc_pending_echo; /* our own set via data-control: echo not yet consumed */
|
|
bool dd_pending_echo; /* our own set via wl_data_device: echo not yet consumed */
|
|
bool dc_primary_pending_echo; /* our own primary set: echo not yet consumed */
|
|
bool host_set_pending; /* nested selection not yet on the host; retry on focus */
|
|
|
|
/* Eager text prefetch: when a real host selection is mirrored, its primary
|
|
text MIME is pulled into this buffer at copy time so pastes are served
|
|
instantly instead of asking the host for each one. */
|
|
int prefetch_fd; /* pipe read end, -1 when idle */
|
|
struct wl_event_source *prefetch_src;
|
|
char *prefetch_buf;
|
|
size_t prefetch_len, prefetch_cap;
|
|
char prefetch_mime[64]; /* canonical text MIME, "" if none */
|
|
bool prefetch_ready; /* buffer complete (EOF received) */
|
|
|
|
/* Eager text prefetch for the mirrored primary selection. Kept separate
|
|
from the clipboard prefetch above: the host can change either selection
|
|
on its own, so their buffers must never collide. */
|
|
int primary_prefetch_fd;
|
|
struct wl_event_source *primary_prefetch_src;
|
|
char *primary_prefetch_buf;
|
|
size_t primary_prefetch_len, primary_prefetch_cap;
|
|
char primary_prefetch_mime[64];
|
|
bool primary_prefetch_ready;
|
|
};
|
|
|
|
static struct clipboard_bridge bridge = {
|
|
.prefetch_fd = -1,
|
|
.primary_prefetch_fd = -1,
|
|
};
|
|
|
|
static void bridge_cleanup(void);
|
|
static void bridge_init(void);
|
|
static void bridge_attach_seat(void);
|
|
|
|
/* Frame dumps: SIGUSR1 (or KICK_AUTODUMP) requests a scene-buffer save
|
|
(client content as wlroots drew it) and a host back-buffer save (what we
|
|
composited for the host) on the next frame. */
|
|
static volatile sig_atomic_t dump_scene_req, dump_host_req;
|
|
static int dump_seq;
|
|
static bool client_dumped = false;
|
|
/* Directory for dump .ppm files (KICK_DUMP_DIR, default /tmp): parallel
|
|
test workers point each compositor at its own directory so per-boot
|
|
dump_seq counters cannot collide on filenames. */
|
|
static char dump_dir[512] = "/tmp";
|
|
|
|
static void dump_path(char *buf, size_t sz, const char *kind, int seq) {
|
|
snprintf(buf, sz, "%s/%s_%d.ppm", dump_dir, kind, seq);
|
|
}
|
|
|
|
/* Set by sigchld_handler whenever a child exits; the main loop clears it and
|
|
runs sweep_dead_startup_groups() to drop black tabs whose command exited
|
|
without ever connecting. A plain flag (set + cleared in the loop) keeps
|
|
the reaping work out of the signal handler. */
|
|
static volatile sig_atomic_t sigchld_seen = 0;
|
|
|
|
/* The most recently reaped child, recorded by sigchld_handler so the startup
|
|
sweep can tell the user WHY a tab's command died (usage error vs crash vs
|
|
signal). Best-effort by design: only one exit is kept, so a burst of child
|
|
exits can overwrite the entry before the sweep reads it -- the message then
|
|
just omits the status. sig_atomic_t holds pids on Linux (pid_t is int). */
|
|
static volatile sig_atomic_t last_child_pid = -1;
|
|
static volatile sig_atomic_t last_child_status = 0;
|
|
|
|
/* A tab is "live" when it can own the show: it has a surfaced window, or it
|
|
is pending (black) -- an unclaimed CLI tab at startup or a fresh
|
|
Alt+Shift+T terminal. Empty tabs are otherwise removed outright, so a
|
|
live check is all that's needed to know whether a tab should be shown,
|
|
cycled to, or handed the active crown. */
|
|
static bool tab_live(struct kick_tab *tab);
|
|
/* The currently focused toplevel (NULL while the active tab is black). */
|
|
static struct wlr_xdg_toplevel *active_toplevel(void);
|
|
static void set_active_tab(struct kick_tab *tab);
|
|
/* Re-nice/re-ionice every window's process group around `active` (the
|
|
window just entered by window_enter). No-op unless --focus-priority. */
|
|
static void apply_window_priorities(struct kick_window *active);
|
|
static void render_and_present(void);
|
|
static void overlay_sync_render_lock(void);
|
|
static void overlay_sync_cursor_lock(void);
|
|
static void layer_apply_layout_box(const struct wlr_box *full);
|
|
static void layer_apply_layout(void);
|
|
static void layer_raise_above_windows(void);
|
|
static void layer_update_visibility(void);
|
|
static void clients_set_fullscreen(bool fs);
|
|
static bool window_is_fullscreen(struct kick_window *w);
|
|
static bool window_is_maximized(struct kick_window *w);
|
|
static bool any_window_fullscreen(void);
|
|
static bool shown_is_fullscreen(void);
|
|
static void fullscreen_sync_shown(void);
|
|
static void fullscreen_sync_aggregate(void);
|
|
static void window_apply_fullscreen(struct kick_window *w, bool fs);
|
|
static bool window_is_shown(struct kick_window *w);
|
|
static void client_enter_fullscreen(struct kick_window *w);
|
|
static void client_exit_fullscreen(struct kick_window *w);
|
|
|
|
/* Request a redraw from a non-client source (kick's own UI: tab switches,
|
|
reorder, resize, overlay, boot). Marks the frame dirty and wakes the
|
|
event loop via wlr_output_schedule_frame so an idle compositor still
|
|
renders promptly; the drain of that wake presents on the next frame-ready
|
|
point instead of racing an in-flight page flip. */
|
|
static void kick_request_redraw(void) {
|
|
needs_redraw = true;
|
|
if (server.output)
|
|
wlr_output_schedule_frame(server.output);
|
|
}
|
|
static struct kick_window *window_for_toplevel(struct wlr_xdg_toplevel *toplevel);
|
|
static void host_teardown_window(void);
|
|
static int tab_index(struct kick_tab *tab);
|
|
/* xdg-dialog bodies live with the seat handlers; called from claim/map. */
|
|
static void modal_activate(struct kick_window *w);
|
|
static void dialog_wire(struct kick_window *w,
|
|
struct wlr_xdg_dialog_v1 *dialog);
|
|
/* Decoration pinning lives with the dialog handlers; called from map and
|
|
from late decoration binds. */
|
|
static void decoration_pin_client_side(struct wlr_xdg_toplevel *toplevel);
|
|
/* Foreign-toplevel (taskbar) handle lifecycle; bodies live with the idle
|
|
handlers. Called from window claim/teardown and state sync points. */
|
|
static void foreign_handle_create(struct kick_window *w);
|
|
static void foreign_handle_destroy(struct kick_window *w);
|
|
static void foreign_sync_title(struct kick_window *w);
|
|
static void foreign_sync_app_id(struct kick_window *w);
|
|
static void foreign_sync_output(struct kick_window *w);
|
|
static const char *foreign_str(const char *s);
|
|
static struct kick_window *tab_current_window(struct kick_tab *tab);
|
|
/* Overlay touch selection: hit-test bodies live with the overlay draw code;
|
|
called from the shared touch delivery path. */
|
|
static bool overlay_hit_test(double ox, double oy,
|
|
struct kick_tab **tab_out,
|
|
struct kick_window **win_out);
|
|
/* True while pointer motion/buttons must not reach clients: the Alt overlay
|
|
is up (hover previews tabs, clicks are swallowed). Defined with the
|
|
touch-selection state; called from the pointer paths above it. */
|
|
static bool overlay_eats_pointer(void);
|
|
/* Vertical wheel while the overlay eats pointer input: step tabs like
|
|
Alt+Up/Down instead of scrolling the client. Returns true when eaten
|
|
(callers send frame and return). Defined with overlay_eats_pointer. */
|
|
static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete);
|
|
/* Toplevel-icon body lives with the overlay code; called from teardown. */
|
|
static void icon_proto_release(struct kick_window *w);
|
|
/* Toplevel-tag bodies live with the foreign-handle section; called from
|
|
the tag events, window claim and the tag-group join below. */
|
|
static void handle_tag_set(struct wl_listener *l, void *data);
|
|
static void handle_tag_set_desc(struct wl_listener *l, void *data);
|
|
static void tag_consume_pending(struct kick_window *w);
|
|
static const char *pending_tag_for(struct wlr_xdg_toplevel *toplevel);
|
|
/* Output-management publish lives with the modeset handlers; called from
|
|
output setup and every resize. */
|
|
static void output_mgmt_publish(void);
|
|
static struct kick_tab *pick_successor(struct kick_tab *tab,
|
|
bool prefer_right);
|
|
static void remove_tab(struct kick_tab *tab);
|
|
static int add_tab(const char *cmd);
|
|
/* set_title / set_app_id listener: refresh the overlay row and mirror the
|
|
new title/app_id onto the taskbar handles. */
|
|
static void handle_window_meta(struct wl_listener *l, void *data);
|
|
static int cmd_in_path(const char *cmd);
|
|
static void insert_tab_after(struct kick_tab *tab, struct kick_tab *anchor);
|
|
static void launch_client(struct kick_window *w);
|
|
static void launch_all_cli(void);
|
|
/* MRU window-stack bodies live with the focus helpers; called from
|
|
window_alloc/teardown, window_enter and the prefix+Tab/1 key handling. */
|
|
static void mru_move_to_front(struct kick_window *w);
|
|
static void mru_remove(struct kick_window *w);
|
|
static struct kick_window *mru_current(void);
|
|
static void mru_step(void);
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland surface handlers (defined with the other toplevel handlers). */
|
|
static void handle_xwayland_new_surface(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xwayland_ready(struct wl_listener *listener, void *data);
|
|
static void handle_xwayland_client_destroy(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xwayland_destroy(struct wl_listener *listener, void *data);
|
|
static void x11_initial_configure(struct wlr_xwayland_surface *xs);
|
|
#endif
|
|
|
|
/* Cursor-shape-v1: apply a wp_cursor_shape_device_v1 shape value -- setting
|
|
the standalone cursor image or forwarding it to the host pointer. */
|
|
static void apply_cursor_shape(int shape);
|
|
static void handle_shape_set(struct wl_listener *listener, void *data);
|
|
static void host_cursor_shape_get_pointer(void);
|
|
static void host_cursor_shape_set(int shape);
|
|
|
|
/* Logging levels: quiet by default so a plain launch writes nothing to the
|
|
launching terminal except fatal errors. KICK_LOG=1 shows status lines,
|
|
KICK_LOG=2 adds the per-frame [diag] trace. SIGUSR1 and KICK_AUTODUMP
|
|
dumps always write their .ppm files regardless of level. */
|
|
enum { KICK_LOG_QUIET = 0, KICK_LOG_INFO = 1, KICK_LOG_DEBUG = 2 };
|
|
|
|
static int kick_log_level(void) {
|
|
static volatile sig_atomic_t level = -1;
|
|
if (level < 0) {
|
|
const char *v = getenv("KICK_LOG");
|
|
int l = v ? atoi(v) : 0;
|
|
if (l < KICK_LOG_QUIET) l = KICK_LOG_QUIET;
|
|
if (l > KICK_LOG_DEBUG) l = KICK_LOG_DEBUG;
|
|
level = l;
|
|
}
|
|
return level;
|
|
}
|
|
|
|
static void kick_vlog(int level, const char *fmt, va_list args) {
|
|
if (level > kick_log_level())
|
|
return;
|
|
vfprintf(stderr, fmt, args);
|
|
}
|
|
|
|
static void kick_log(int level, const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
kick_vlog(level, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
/* Monotonic milliseconds for stall analysis in diagnostic lines (X11
|
|
association/classification under load). Mirrors bridge_dbg_ms. */
|
|
static double kick_monotonic_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
/* Route libxkbcommon's own log output (which otherwise writes
|
|
"xkbcommon: ERROR: ..." straight to stderr, bypassing KICK_LOG) through
|
|
our level system. Errors surface at info level; warnings and below are
|
|
debug-only, so a quiet launch no longer leaks parse failures to the
|
|
launching terminal. */
|
|
static void kick_xkb_log(struct xkb_context *ctx, enum xkb_log_level level,
|
|
const char *fmt, va_list args)
|
|
{
|
|
(void)ctx;
|
|
int l = (level <= XKB_LOG_LEVEL_ERROR) ? KICK_LOG_INFO : KICK_LOG_DEBUG;
|
|
if (l > kick_log_level())
|
|
return;
|
|
fputs("kick: xkbcommon: ", stderr);
|
|
vfprintf(stderr, fmt, args);
|
|
}
|
|
|
|
static void kick_log_level_init(void) {
|
|
(void)kick_log_level();
|
|
}
|
|
|
|
/* The process group to signal for a window: the group its claimed client
|
|
lives in (set at claim time), falling back to the launcher pid for windows
|
|
with no client yet. */
|
|
static pid_t window_signal_group(struct kick_window *w) {
|
|
return (w && w->pgid > 0) ? w->pgid : (w ? w->pid : -1);
|
|
}
|
|
|
|
/* Whether a window has a killable target: either a process group we own, or
|
|
a connected client pid (external clients sharing our own group). */
|
|
static bool window_killable(struct kick_window *w) {
|
|
return w && (window_signal_group(w) > 0 || w->client_pid > 0);
|
|
}
|
|
|
|
/* Signal a window's process target. A group we created (launch_client runs
|
|
the child in its own setpgid'd group) is signaled as a group so the whole
|
|
client tree dies. A client we did NOT create, or one sharing our own
|
|
process group (e.g. started from the same terminal as the compositor), is
|
|
signaled by pid alone: group-signaling it would kill the launcher's
|
|
session, ourselves included. */
|
|
static void kill_window(struct kick_window *w, int sig) {
|
|
pid_t g = window_signal_group(w);
|
|
if (g > 0 && g != getpgid(0))
|
|
kill(-g, sig);
|
|
else if (w && w->client_pid > 0)
|
|
kill(w->client_pid, sig);
|
|
}
|
|
|
|
/* ioprio_set/get numbers without depending on <linux/ioprio.h>. */
|
|
#ifndef IOPRIO_WHO_PROCESS
|
|
#define IOPRIO_WHO_PROCESS 1
|
|
#endif
|
|
#ifndef IOPRIO_WHO_PGRP
|
|
#define IOPRIO_WHO_PGRP 2
|
|
#endif
|
|
#ifndef IOPRIO_CLASS_BE
|
|
#define IOPRIO_CLASS_BE 2
|
|
#endif
|
|
#ifndef IOPRIO_PRIO_VALUE
|
|
#define IOPRIO_PRIO_VALUE(class, data) (((class) << 13) | (data))
|
|
#endif
|
|
|
|
#define KICK_PRIO_ACTIVE_NICE 0
|
|
#define KICK_PRIO_BG_NICE 10
|
|
#define KICK_PRIO_ACTIVE_IODATA 4
|
|
#define KICK_PRIO_BG_IODATA 7
|
|
|
|
/* Boost the just-entered window's process group to normal priority and push
|
|
every other window's group down (nice +10, io best-effort/7). Single call
|
|
site: the tail of window_enter(), so every active-window change -- tab
|
|
cycling, Alt+Tab previews while the overlay is held, intra-tab rotation,
|
|
map/modal activation, boot crown -- rebalances. No-op unless --focus-priority.
|
|
Only ever deprioritizes background groups, so unprivileged users can run
|
|
it: raising nice / lowering io priority on our own UID needs no
|
|
CAP_SYS_NICE. Best-effort: ESRCH (exited group) is silent, other errors
|
|
are debug-logged, never fatal. */
|
|
static void apply_window_priorities(struct kick_window *active) {
|
|
if (!opt_focus_priority)
|
|
return;
|
|
static pid_t last_active_pgid = -2;
|
|
pid_t self_pgid = getpgid(0);
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link) {
|
|
bool is_active = (w == active);
|
|
int nice = is_active ? KICK_PRIO_ACTIVE_NICE : KICK_PRIO_BG_NICE;
|
|
int iodata = is_active ? KICK_PRIO_ACTIVE_IODATA : KICK_PRIO_BG_IODATA;
|
|
int ioprio = IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, iodata);
|
|
pid_t g = window_signal_group(w);
|
|
if (g > 0 && g != self_pgid) {
|
|
if (setpriority(PRIO_PGRP, (id_t)g, nice) != 0 && errno != ESRCH)
|
|
kick_log(KICK_LOG_DEBUG, "[prio] setpriority pgid=%d nice=%d: %s\n",
|
|
g, nice, strerror(errno));
|
|
long rc = syscall(SYS_ioprio_set, IOPRIO_WHO_PGRP, g, ioprio);
|
|
if (rc != 0 && errno != ESRCH)
|
|
kick_log(KICK_LOG_DEBUG, "[prio] ioprio pgid=%d be/%d: %s\n",
|
|
g, iodata, strerror(errno));
|
|
if (is_active && g != last_active_pgid) {
|
|
last_active_pgid = g;
|
|
kick_log(KICK_LOG_INFO, "[prio] active app=%d pgid=%d\n",
|
|
tab_index(w->tab), g);
|
|
}
|
|
} else if (w && w->client_pid > 0) {
|
|
/* External client sharing our own group: nudge by pid only,
|
|
so we never renice kick's own session. */
|
|
if (setpriority(PRIO_PROCESS, (id_t)w->client_pid, nice) != 0 &&
|
|
errno != ESRCH)
|
|
kick_log(KICK_LOG_DEBUG, "[prio] setpriority pid=%d nice=%d: %s\n",
|
|
w->client_pid, nice, strerror(errno));
|
|
long rc = syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS,
|
|
w->client_pid, ioprio);
|
|
if (rc != 0 && errno != ESRCH)
|
|
kick_log(KICK_LOG_DEBUG, "[prio] ioprio pid=%d be/%d: %s\n",
|
|
w->client_pid, iodata, strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Terminate every launched client: SIGTERM first for a graceful shutdown,
|
|
then SIGKILL any stragglers after a short grace period. */
|
|
static void terminate_clients(void) {
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link) {
|
|
if (w->pgid > 0)
|
|
kill(-w->pgid, SIGTERM);
|
|
else if (w->client_pid > 0)
|
|
kill(w->client_pid, SIGTERM);
|
|
}
|
|
}
|
|
struct timespec wait = { .tv_sec = 0, .tv_nsec = 150000000 };
|
|
nanosleep(&wait, NULL);
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link) {
|
|
if (w->pgid > 0)
|
|
kill(-w->pgid, SIGKILL);
|
|
else if (w->client_pid > 0)
|
|
kill(w->client_pid, SIGKILL);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Remove the session's Wayland socket file (and its companion .lock file,
|
|
both created by wl_display_add_socket), if present. The full path is
|
|
precomputed at startup so this only ever calls unlink()/snprintf(), which
|
|
are async-signal-safe and therefore safe from the signal/crash handlers
|
|
too. A missing file is fine: ENOENT is ignored, i.e. nothing to clean. */
|
|
static void remove_socket_file(void) {
|
|
unlink(socket_path);
|
|
char lock_path[sizeof(socket_path) + 6];
|
|
snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path);
|
|
unlink(lock_path);
|
|
}
|
|
|
|
static void overlay_font_fini(void);
|
|
static void standalone_overlay_damage(void);
|
|
|
|
/* Standalone native keyboard: one struct per libinput keyboard device, so
|
|
modifier state is always read from the device that emitted the event (the
|
|
legacy single-slot approach broke Shift/Caps when a host exposed several
|
|
keyboards, e.g. a consumer-control node alongside the real one). */
|
|
struct standalone_kbdev {
|
|
struct wlr_keyboard *keyboard;
|
|
struct wl_listener key;
|
|
struct wl_listener modifiers;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
/* Standalone native pointer: one struct per libinput pointer device. Each
|
|
device owns its listeners so a device going away (libinput suspends every
|
|
device when the session loses its VT) can detach them before wlroots
|
|
asserts the event lists are empty, and so all pointers -- not just the
|
|
last-attached one -- reach the seat. */
|
|
struct standalone_ptrdev {
|
|
struct wlr_pointer *pointer;
|
|
struct wl_listener motion;
|
|
struct wl_listener motion_absolute;
|
|
struct wl_listener button;
|
|
struct wl_listener axis;
|
|
struct wl_listener frame;
|
|
struct wl_listener swipe_begin;
|
|
struct wl_listener swipe_update;
|
|
struct wl_listener swipe_end;
|
|
struct wl_listener pinch_begin;
|
|
struct wl_listener pinch_update;
|
|
struct wl_listener pinch_end;
|
|
struct wl_listener hold_begin;
|
|
struct wl_listener hold_end;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
/* One libinput touchscreen; events carry normalized absolute coordinates
|
|
plus a slot id (multi-touch). Delivery goes through
|
|
seat_touch_deliver(), shared with the nested forwarder and ctl feed. */
|
|
struct standalone_touchdev {
|
|
struct wlr_touch *touch;
|
|
struct wl_listener down;
|
|
struct wl_listener motion;
|
|
struct wl_listener up;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
/* ===================== View helpers (xdg / X11) ===================== */
|
|
/* A window is an xdg toplevel or, with XWayland, an Xwayland surface.
|
|
These helpers keep the shared window logic (liveness, focus, overlay
|
|
titles, sizing, fullscreen) identical for both; the X11 branches compile
|
|
away without HAVE_XWAYLAND. */
|
|
|
|
/* Whether a window has a live surface object: an xdg toplevel exists from
|
|
claim until destroy, an X11 surface likewise. This is the per-window
|
|
liveness predicate the old app_has_window() summarized for a whole tab. */
|
|
static bool window_live(struct kick_window *w) {
|
|
if (!w) return false;
|
|
if (w->toplevel) return true;
|
|
#if HAVE_XWAYLAND
|
|
if (w->xsurface) return true;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Live windows across every tab, for the confirm-before-quit count: a window
|
|
that holds (or is about to hold) a surface and is not already closing.
|
|
Still-black pending placeholders have no surface yet, so they do not count
|
|
-- there is nothing to lose by quitting for them. */
|
|
static int live_window_count(void) {
|
|
int n = 0;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link)
|
|
if (window_live(w) && !w->closing)
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* The first (oldest) window of a tab, or NULL. The tab's launcher identity
|
|
(cmd, pending_claim) lives on it. */
|
|
static struct kick_window *tab_first_window(struct kick_tab *tab) {
|
|
if (!tab || wl_list_empty(&tab->windows)) return NULL;
|
|
return wl_container_of(tab->windows.next, (struct kick_window *)NULL, link);
|
|
}
|
|
|
|
/* Whether the tab is currently shown: any of its windows' scene trees are
|
|
enabled. A black (pending) tab has no tree yet and so is not visible,
|
|
which is exactly what the map handlers check to decide no-steal
|
|
activation. */
|
|
static bool tab_visible(struct kick_tab *tab) {
|
|
if (!tab) return false;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->tree && w->tree->node.enabled) return true;
|
|
return false;
|
|
}
|
|
|
|
static struct wlr_surface *window_surface(struct kick_window *w) {
|
|
if (!w) return NULL;
|
|
if (w->toplevel) return w->toplevel->base->surface;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface ? w->xsurface->surface : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *window_title(struct kick_window *w) {
|
|
if (!w) return NULL;
|
|
if (w->toplevel) return w->toplevel->title;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface ? w->xsurface->title : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *window_app_id(struct kick_window *w) {
|
|
if (!w) return NULL;
|
|
if (w->toplevel) return w->toplevel->app_id;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface ? w->xsurface->class : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static void window_set_activated(struct kick_window *w, bool on) {
|
|
if (!w) return;
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_activated(w->ft_handle, on);
|
|
if (w->toplevel) {
|
|
wlr_xdg_toplevel_set_activated(w->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11 && w->xsurface)
|
|
wlr_xwayland_surface_activate(w->xsurface, on);
|
|
#endif
|
|
}
|
|
|
|
static void window_set_maximized(struct kick_window *w, bool on) {
|
|
if (!w) return;
|
|
if (w->toplevel) {
|
|
wlr_xdg_toplevel_set_maximized(w->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11 && w->xsurface)
|
|
wlr_xwayland_surface_set_maximized(w->xsurface, on, on);
|
|
#endif
|
|
}
|
|
|
|
static void window_set_fullscreen(struct kick_window *w, bool on) {
|
|
if (!w) return;
|
|
if (w->toplevel) {
|
|
if (!w->toplevel->base->initialized) return;
|
|
wlr_xdg_toplevel_set_fullscreen(w->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11 && w->xsurface)
|
|
wlr_xwayland_surface_set_fullscreen(w->xsurface, on);
|
|
#endif
|
|
}
|
|
|
|
/* Live fullscreen state, read from the protocol objects (never stored):
|
|
xdg uses scheduled (what we've told the client; current lags until the
|
|
client acks, requested is the client's wish). Uninitialized windows read
|
|
as not fullscreen. */
|
|
static bool window_is_fullscreen(struct kick_window *w) {
|
|
if (!w) return false;
|
|
if (w->toplevel)
|
|
return w->toplevel->base->initialized && w->toplevel->scheduled.fullscreen;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface && w->xsurface->fullscreen;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Live maximized state (client-acked), for telling a fresh maximize
|
|
request (honor as maximize -- e.g. a session-restored startup request
|
|
arriving before first ack) from a button toggle on an already-maximized
|
|
window, where only fullscreen is bigger. */
|
|
static bool window_is_maximized(struct kick_window *w) {
|
|
if (!w) return false;
|
|
if (w->toplevel)
|
|
return w->toplevel->base->initialized && w->toplevel->current.maximized;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface && w->xsurface->maximized_vert &&
|
|
w->xsurface->maximized_horz;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Keep a mapped window at the maximized output size. XDG clients are told
|
|
via set_size; X11 clients get a configure to the full output at 0,0. */
|
|
static void window_set_size(struct kick_window *w, int width, int height) {
|
|
if (!w) return;
|
|
if (w->toplevel) {
|
|
wlr_xdg_toplevel_set_size(w->toplevel, width, height);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11 && w->xsurface && w->xsurface->surface)
|
|
wlr_xwayland_surface_configure(w->xsurface, 0, 0,
|
|
(uint16_t)width, (uint16_t)height);
|
|
#endif
|
|
}
|
|
|
|
/* Whether the underlying protocol object accepts state: xdg surfaces only
|
|
after base->initialized; X11 surfaces once associated with a wlr_surface. */
|
|
static bool window_initialized(struct kick_window *w) {
|
|
if (!w) return false;
|
|
if (w->toplevel) return w->toplevel->base->initialized;
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
return w->xsurface && w->xsurface->surface != NULL;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Graceful close for the surface kind: XDG windows get an xdg-toplevel
|
|
close request, X11 windows WM_DELETE_WINDOW. This is what lets a
|
|
stacked member be closed without touching its shared process. */
|
|
static void window_graceful_close(struct kick_window *w) {
|
|
if (!w) return;
|
|
if (w->toplevel && w->toplevel->base->initialized)
|
|
wlr_xdg_toplevel_send_close(w->toplevel);
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11 && w->xsurface)
|
|
wlr_xwayland_surface_close(w->xsurface);
|
|
#endif
|
|
}
|
|
|
|
/* A tab is live when any of its windows is: a surface object exists, or it
|
|
is still pending (black). Empty tabs are removed outright, so a live
|
|
check is all that's needed to know whether the tab should be shown,
|
|
cycled to, or handed the active crown. */
|
|
static bool tab_live(struct kick_tab *tab) {
|
|
if (!tab) return false;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (window_live(w) || w->pending_claim) return true;
|
|
return false;
|
|
}
|
|
|
|
static struct wlr_xdg_toplevel *active_toplevel(void) {
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
return w ? w->toplevel : NULL;
|
|
}
|
|
|
|
/* Full shutdown of the nested session: take down the clients first, then
|
|
free our own allocations and exit. Guarded so the various close paths
|
|
(host window close, last tab closed) only run once. */
|
|
static void shutdown_cleanup(void) {
|
|
if (shutting_down) return;
|
|
shutting_down = true;
|
|
terminate_clients();
|
|
struct kick_tab *tab, *tab_tmp;
|
|
wl_list_for_each_safe(tab, tab_tmp, &server.tabs, link) {
|
|
struct kick_window *w, *w_tmp;
|
|
wl_list_for_each_safe(w, w_tmp, &tab->windows, link) {
|
|
if (w->icon_wlr) {
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
w->icon_wlr = NULL;
|
|
}
|
|
icon_proto_release(w);
|
|
free(w->cmd);
|
|
free(w->tag);
|
|
free(w->tag_desc);
|
|
free(w);
|
|
}
|
|
free(tab);
|
|
}
|
|
wl_list_init(&server.tabs);
|
|
wl_list_init(&server.mru);
|
|
server.mru_alt_anchor = NULL;
|
|
server.mru_alt_navigated = false;
|
|
|
|
/* Detach + free the standalone native keyboard devices. */
|
|
struct standalone_kbdev *kd, *kd_tmp;
|
|
wl_list_for_each_safe(kd, kd_tmp, &server.kbdevs, link) {
|
|
wl_list_remove(&kd->link);
|
|
wl_list_remove(&kd->key.link);
|
|
wl_list_remove(&kd->modifiers.link);
|
|
wl_list_remove(&kd->destroy.link);
|
|
free(kd);
|
|
}
|
|
wl_list_init(&server.kbdevs);
|
|
|
|
/* Detach + free the standalone native pointer devices. */
|
|
struct standalone_ptrdev *pd, *pd_tmp;
|
|
wl_list_for_each_safe(pd, pd_tmp, &server.ptrdevs, link) {
|
|
wl_list_remove(&pd->link);
|
|
wl_list_remove(&pd->motion.link);
|
|
wl_list_remove(&pd->motion_absolute.link);
|
|
wl_list_remove(&pd->button.link);
|
|
wl_list_remove(&pd->axis.link);
|
|
wl_list_remove(&pd->frame.link);
|
|
wl_list_remove(&pd->destroy.link);
|
|
free(pd);
|
|
}
|
|
wl_list_init(&server.ptrdevs);
|
|
|
|
/* Detach + free the standalone native touchscreen devices. */
|
|
struct standalone_touchdev *td, *td_tmp;
|
|
wl_list_for_each_safe(td, td_tmp, &server.touchdevs, link) {
|
|
wl_list_remove(&td->link);
|
|
wl_list_remove(&td->down.link);
|
|
wl_list_remove(&td->motion.link);
|
|
wl_list_remove(&td->up.link);
|
|
wl_list_remove(&td->destroy.link);
|
|
free(td);
|
|
}
|
|
wl_list_init(&server.touchdevs);
|
|
|
|
if (server.xkb_state) xkb_state_unref(server.xkb_state);
|
|
if (server.xkb_keymap) xkb_keymap_unref(server.xkb_keymap);
|
|
if (server.xkb_ctx) xkb_context_unref(server.xkb_ctx);
|
|
overlay_font_fini();
|
|
bridge_cleanup();
|
|
remove_socket_file();
|
|
}
|
|
|
|
/* ===================== EGL helpers ===================== */
|
|
|
|
static PFNEGLCREATEIMAGEKHRPROC pfn_eglCreateImageKHR;
|
|
static PFNEGLDESTROYIMAGEKHRPROC pfn_eglDestroyImageKHR;
|
|
static PFNEGLGETPLATFORMDISPLAYEXTPROC pfn_eglGetPlatformDisplayEXT;
|
|
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pfn_glEGLImageTargetTexture2DOES;
|
|
|
|
static void *egl_proc(const char *name) {
|
|
void *p = (void *)eglGetProcAddress(name);
|
|
if (!p)
|
|
fprintf(stderr, "kick: EGL proc %s unavailable\n", name);
|
|
return p;
|
|
}
|
|
|
|
static GLuint compile_shader(GLenum type, const char *src) {
|
|
GLuint sh = glCreateShader(type);
|
|
glShaderSource(sh, 1, &src, NULL);
|
|
glCompileShader(sh);
|
|
GLint ok;
|
|
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
|
if (!ok) {
|
|
char log[512];
|
|
glGetShaderInfoLog(sh, sizeof(log), NULL, log);
|
|
fprintf(stderr, "kick: shader error: %s\n", log);
|
|
glDeleteShader(sh);
|
|
return 0;
|
|
}
|
|
return sh;
|
|
}
|
|
|
|
static bool gl_setup(void) {
|
|
const char *vs =
|
|
"attribute vec2 a_pos;\n"
|
|
"attribute vec2 a_uv;\n"
|
|
"varying vec2 v_uv;\n"
|
|
"void main() {\n"
|
|
" v_uv = a_uv;\n"
|
|
" gl_Position = vec4(a_pos, 0.0, 1.0);\n"
|
|
"}\n";
|
|
const char *fs =
|
|
"precision mediump float;\n"
|
|
"varying vec2 v_uv;\n"
|
|
"uniform sampler2D u_tex;\n"
|
|
"uniform vec4 u_color;\n"
|
|
"void main() {\n"
|
|
" gl_FragColor = texture2D(u_tex, v_uv) * u_color;\n"
|
|
"}\n";
|
|
GLuint vsh = compile_shader(GL_VERTEX_SHADER, vs);
|
|
GLuint fsh = compile_shader(GL_FRAGMENT_SHADER, fs);
|
|
if (!vsh || !fsh) return false;
|
|
host.gl_program = glCreateProgram();
|
|
glAttachShader(host.gl_program, vsh);
|
|
glAttachShader(host.gl_program, fsh);
|
|
glBindAttribLocation(host.gl_program, 0, "a_pos");
|
|
glBindAttribLocation(host.gl_program, 1, "a_uv");
|
|
glLinkProgram(host.gl_program);
|
|
GLint ok;
|
|
glGetProgramiv(host.gl_program, GL_LINK_STATUS, &ok);
|
|
glDeleteShader(vsh);
|
|
glDeleteShader(fsh);
|
|
if (!ok) {
|
|
char log[512];
|
|
glGetProgramInfoLog(host.gl_program, sizeof(log), NULL, log);
|
|
fprintf(stderr, "kick: link error: %s\n", log);
|
|
return false;
|
|
}
|
|
host.gl_pos_loc = glGetAttribLocation(host.gl_program, "a_pos");
|
|
host.gl_uv_loc = glGetAttribLocation(host.gl_program, "a_uv");
|
|
host.gl_tex_loc = glGetUniformLocation(host.gl_program, "u_tex");
|
|
host.gl_color_loc = glGetUniformLocation(host.gl_program, "u_color");
|
|
|
|
glGenTextures(1, &host.import_tex);
|
|
glBindTexture(GL_TEXTURE_2D, host.import_tex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Blit a source rect of a texture into the window at the given destination
|
|
rect (dst_x, dst_y, dst_w, dst_h, window coords), clipped to the window.
|
|
The source is stretched uniformly to the destination (1:1 when the rects
|
|
match). The draw is tinted by `color` (RGBA), so solid rects are drawn
|
|
from a 1x1 white texture. */
|
|
static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
|
|
int src_x, int src_y, int src_w, int src_h,
|
|
int dst_x, int dst_y, int dst_w, int dst_h,
|
|
int win_w, int win_h, const GLfloat color[4])
|
|
{
|
|
if (win_w <= 0 || win_h <= 0) return;
|
|
if (src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0) return;
|
|
if (src_x >= tex_w || src_y >= tex_h) return;
|
|
if (src_w > tex_w - src_x) src_w = tex_w - src_x;
|
|
if (src_h > tex_h - src_y) src_h = tex_h - src_y;
|
|
int cx = dst_x < 0 ? -dst_x : 0;
|
|
int cy = dst_y < 0 ? -dst_y : 0;
|
|
if (cx >= dst_w || cy >= dst_h) return;
|
|
int wx0 = dst_x + cx;
|
|
int wy0 = dst_y + cy;
|
|
int cw = dst_w - cx;
|
|
int ch = dst_h - cy;
|
|
int maxw = win_w - wx0;
|
|
int maxh = win_h - wy0;
|
|
if (cw > maxw) cw = maxw;
|
|
if (ch > maxh) ch = maxh;
|
|
if (cw <= 0 || ch <= 0) return;
|
|
|
|
float u0 = ((float)src_x + (float)src_w * cx / dst_w) / (float)tex_w;
|
|
float u1 = ((float)src_x + (float)src_w * (cx + cw) / dst_w) / (float)tex_w;
|
|
float v0 = ((float)src_y + (float)src_h * cy / dst_h) / (float)tex_h;
|
|
float v1 = ((float)src_y + (float)src_h * (cy + ch) / dst_h) / (float)tex_h;
|
|
|
|
float nx0 = -1.0f + 2.0f * (float)wx0 / (float)win_w;
|
|
float ny0 = 1.0f - 2.0f * (float)wy0 / (float)win_h;
|
|
float nx1 = -1.0f + 2.0f * (float)(wx0 + cw) / (float)win_w;
|
|
float ny1 = 1.0f - 2.0f * (float)(wy0 + ch) / (float)win_h;
|
|
|
|
GLfloat verts[8] = {
|
|
nx0, ny0,
|
|
nx1, ny0,
|
|
nx1, ny1,
|
|
nx0, ny1,
|
|
};
|
|
GLfloat uvs[8] = {
|
|
u0, v0,
|
|
u1, v0,
|
|
u1, v1,
|
|
u0, v1,
|
|
};
|
|
GLushort idx[6] = { 0, 1, 2, 0, 2, 3 };
|
|
|
|
glUseProgram(host.gl_program);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glUniform1i(host.gl_tex_loc, 0);
|
|
glUniform4fv(host.gl_color_loc, 1, color);
|
|
|
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, verts);
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, uvs);
|
|
glEnableVertexAttribArray(1);
|
|
|
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, idx);
|
|
|
|
glDisableVertexAttribArray(0);
|
|
glDisableVertexAttribArray(1);
|
|
}
|
|
|
|
/* ============ Alt-held tab-title overlay ============
|
|
While the host Alt modifier is held, a passive list of tab titles is
|
|
drawn into the top-left of the host window with the active tab
|
|
highlighted. Up/Down already cycle tabs while Alt is held, so the
|
|
highlight follows server.active_tab automatically. Titles are read from
|
|
the wlr_xdg_toplevel at draw time (terminals retitle themselves); a
|
|
still-black tab shows its command line instead. No per-client listeners
|
|
are needed. */
|
|
|
|
static PangoLayout *overlay_layout;
|
|
static bool overlay_font_ok;
|
|
|
|
/* Fixed rasterized font size; 30px is twice the original 15px constant. */
|
|
#define OVERLAY_FONT_PX 30
|
|
|
|
/* Overlay text tile cache: session-persistent, LRU-capped. Titles repeat
|
|
across holds (same tabs, mostly same titles), so the first hold warms it
|
|
and later holds hit; changed titles are just new keys. Evicted textures
|
|
are destroyed inline like icon textures. */
|
|
#define OVERLAY_TEXT_CACHE 64
|
|
static struct overlay_text_entry overlay_text_cache[OVERLAY_TEXT_CACHE];
|
|
static unsigned long overlay_text_clock;
|
|
|
|
/* KICK_FONT names a Pango font family description ("DejaVu Sans",
|
|
"Sans Bold 30"); Pango + fontconfig resolve it and fall back for glyphs
|
|
the family lacks (CJK, emoji). */
|
|
static const char *overlay_pick_family(void) {
|
|
const char *env = getenv("KICK_FONT");
|
|
return (env && env[0]) ? env : "Sans";
|
|
}
|
|
|
|
/* Sanitize overlay text: ASCII controls (including newline, which Pango
|
|
would break lines on) become spaces, preserving the old atlas behavior
|
|
where bytes <32 drew blank. Multi-byte UTF-8 passes through untouched. */
|
|
static size_t overlay_sanitize(char *dst, const char *src, size_t cap) {
|
|
size_t n = 0;
|
|
for (const unsigned char *p = (const unsigned char *)src;
|
|
*p && n + 1 < cap; p++) {
|
|
dst[n++] = (*p < 32 || *p == 127) ? ' ' : (char)*p;
|
|
}
|
|
dst[n] = '\0';
|
|
return n;
|
|
}
|
|
|
|
/* Back an over-long synthesized title off to a UTF-8 character boundary
|
|
so buffers never end mid-sequence (Pango would show replacement glyphs
|
|
for the split tail). */
|
|
static void overlay_utf8_cut(char *s) {
|
|
size_t n = strlen(s);
|
|
while (n > 0 && (s[n - 1] & 0xC0) == 0x80)
|
|
n--;
|
|
s[n] = '\0';
|
|
}
|
|
|
|
/* Set up the shared Pango layout for overlay text. CPU-side only (no GL
|
|
context needed): rasterization happens per string on first draw. Like
|
|
labwc, shaping/fallback come from Pango + fontconfig, so any UTF-8 the
|
|
fonts provide renders. Non-fatal: on failure the overlay still renders
|
|
its boxes, just without text. */
|
|
static void overlay_font_init(void) {
|
|
static bool overlay_font_done;
|
|
if (overlay_font_done) return;
|
|
overlay_font_done = true;
|
|
|
|
const char *family = overlay_pick_family();
|
|
PangoFontMap *map = pango_cairo_font_map_get_default();
|
|
PangoContext *ctx = pango_font_map_create_context(map);
|
|
overlay_layout = pango_layout_new(ctx);
|
|
g_object_unref(ctx);
|
|
if (!overlay_layout) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] Pango layout failed\n");
|
|
return;
|
|
}
|
|
PangoFontDescription *desc = pango_font_description_from_string(family);
|
|
pango_font_description_set_absolute_size(desc,
|
|
(double)OVERLAY_FONT_PX * PANGO_SCALE);
|
|
pango_layout_set_font_description(overlay_layout, desc);
|
|
PangoContext *lctx = pango_layout_get_context(overlay_layout);
|
|
PangoFontMetrics *metrics =
|
|
pango_context_get_metrics(lctx, desc, NULL);
|
|
int asc = metrics ?
|
|
PANGO_PIXELS(pango_font_metrics_get_ascent(metrics)) : OVERLAY_FONT_PX;
|
|
int height = metrics ? PANGO_PIXELS(
|
|
pango_font_metrics_get_ascent(metrics) +
|
|
pango_font_metrics_get_descent(metrics)) : OVERLAY_FONT_PX;
|
|
if (metrics)
|
|
pango_font_metrics_unref(metrics);
|
|
pango_font_description_free(desc);
|
|
host.overlay_font_asc = asc > 0 ? asc : OVERLAY_FONT_PX;
|
|
host.overlay_font_h = height > 0 ? height : OVERLAY_FONT_PX;
|
|
overlay_font_ok = true;
|
|
kick_log(KICK_LOG_INFO, "[overlay] font %s ready\n", family);
|
|
}
|
|
|
|
static void overlay_text_entry_free(struct overlay_text_entry *e) {
|
|
free(e->text);
|
|
e->text = NULL;
|
|
if (e->tex) {
|
|
wlr_texture_destroy(e->tex);
|
|
e->tex = NULL;
|
|
}
|
|
e->w = e->h = e->baseline = 0;
|
|
e->rastered = false;
|
|
}
|
|
|
|
static void overlay_font_fini(void) {
|
|
for (size_t i = 0; i < OVERLAY_TEXT_CACHE; i++)
|
|
overlay_text_entry_free(&overlay_text_cache[i]);
|
|
if (overlay_layout) {
|
|
g_object_unref(overlay_layout);
|
|
overlay_layout = NULL;
|
|
}
|
|
/* labwc's workaround: release Pango's default font map at exit so its
|
|
dedicated thread is gone before our SIGTERM teardown path runs. */
|
|
pango_cairo_font_map_set_default(NULL);
|
|
overlay_font_ok = false;
|
|
}
|
|
|
|
/* Rasterize one overlay entry into a white coverage tile (grayscale
|
|
antialiasing: subpixel rendering over our translucent blending path
|
|
misbehaves, per labwc's findings) and upload it like the icon path.
|
|
Reconfigures the shared layout from the entry first: other shapes may
|
|
have run since this entry was measured. Only called for strings that
|
|
are actually drawn, so the raster log proves what reached the screen. */
|
|
static struct wlr_texture *overlay_text_raster(struct overlay_text_entry *e) {
|
|
char clean[512];
|
|
overlay_sanitize(clean, e->text ? e->text : "", sizeof(clean));
|
|
pango_layout_set_text(overlay_layout, clean, -1);
|
|
if (e->max_px == INT_MAX) {
|
|
pango_layout_set_width(overlay_layout, -1);
|
|
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_NONE);
|
|
} else {
|
|
int budget = e->max_px < 0 ? 0 : e->max_px;
|
|
pango_layout_set_width(overlay_layout, budget * PANGO_SCALE);
|
|
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_END);
|
|
}
|
|
int w = 0, h = 0;
|
|
pango_layout_get_pixel_size(overlay_layout, &w, &h);
|
|
e->w = w;
|
|
e->h = h;
|
|
e->baseline = PANGO_PIXELS(pango_layout_get_baseline(overlay_layout));
|
|
e->rastered = true;
|
|
if (w <= 0 || h <= 0)
|
|
return NULL;
|
|
cairo_surface_t *surf =
|
|
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
|
|
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
|
cairo_surface_destroy(surf);
|
|
return NULL;
|
|
}
|
|
cairo_t *cr = cairo_create(surf);
|
|
cairo_font_options_t *opts = cairo_font_options_create();
|
|
cairo_font_options_set_antialias(opts, CAIRO_ANTIALIAS_GRAY);
|
|
cairo_set_font_options(cr, opts);
|
|
cairo_font_options_destroy(opts);
|
|
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
|
|
pango_cairo_show_layout(cr, overlay_layout);
|
|
cairo_destroy(cr);
|
|
cairo_surface_flush(surf);
|
|
int stride = cairo_image_surface_get_stride(surf);
|
|
unsigned char *src = cairo_image_surface_get_data(surf);
|
|
/* Cairo ARGB32 is premultiplied; the source is white, so coverage
|
|
equals alpha and the tile uploads as premultiplied white (r=g=b=a)
|
|
exactly like the old atlas did. */
|
|
unsigned char *rgba = malloc((size_t)w * h * 4);
|
|
struct wlr_texture *tex = NULL;
|
|
if (rgba && src) {
|
|
for (int y = 0; y < h; y++) {
|
|
unsigned char *srow = src + (size_t)y * stride;
|
|
unsigned char *drow = rgba + (size_t)y * w * 4;
|
|
for (int x = 0; x < w; x++) {
|
|
unsigned char a = srow[x * 4 + 3];
|
|
drow[x * 4 + 0] = a;
|
|
drow[x * 4 + 1] = a;
|
|
drow[x * 4 + 2] = a;
|
|
drow[x * 4 + 3] = a;
|
|
}
|
|
}
|
|
tex = wlr_texture_from_pixels(server.renderer,
|
|
DRM_FORMAT_ARGB8888, (uint32_t)w * 4, (uint32_t)w, (uint32_t)h,
|
|
rgba);
|
|
}
|
|
free(rgba);
|
|
cairo_surface_destroy(surf);
|
|
if (tex)
|
|
kick_log(KICK_LOG_DEBUG, "[overlay] raster: '%s' %dx%d\n", clean, w, h);
|
|
return tex;
|
|
}
|
|
|
|
/* Cached overlay text run for (string, width budget). Lookup shapes only
|
|
(cheap, no GL); the texture rasterizes on first draw, so measuring for
|
|
layout never uploads textures nobody paints. Session-persistent LRU:
|
|
titles repeat across holds, so later holds hit. */
|
|
static void overlay_text_shape(struct overlay_text_entry *e) {
|
|
char clean[512];
|
|
overlay_sanitize(clean, e->text ? e->text : "", sizeof(clean));
|
|
pango_layout_set_text(overlay_layout, clean, -1);
|
|
if (e->max_px == INT_MAX) {
|
|
pango_layout_set_width(overlay_layout, -1);
|
|
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_NONE);
|
|
} else {
|
|
int budget = e->max_px < 0 ? 0 : e->max_px;
|
|
pango_layout_set_width(overlay_layout, budget * PANGO_SCALE);
|
|
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_END);
|
|
}
|
|
pango_layout_get_pixel_size(overlay_layout, &e->w, &e->h);
|
|
e->baseline = PANGO_PIXELS(pango_layout_get_baseline(overlay_layout));
|
|
if (pango_layout_is_ellipsized(overlay_layout))
|
|
kick_log(KICK_LOG_DEBUG, "[overlay] ellipsis: '%s' budget=%d\n",
|
|
e->text ? e->text : "", e->max_px);
|
|
}
|
|
|
|
static struct overlay_text_entry *overlay_text_entry(const char *s,
|
|
int max_px) {
|
|
if (!s) s = "";
|
|
struct overlay_text_entry *oldest = &overlay_text_cache[0];
|
|
for (size_t i = 0; i < OVERLAY_TEXT_CACHE; i++) {
|
|
struct overlay_text_entry *e = &overlay_text_cache[i];
|
|
if (e->text && e->max_px == max_px && strcmp(e->text, s) == 0) {
|
|
e->last_used = ++overlay_text_clock;
|
|
return e;
|
|
}
|
|
if (!e->text || e->last_used < oldest->last_used)
|
|
oldest = e;
|
|
}
|
|
overlay_text_entry_free(oldest);
|
|
oldest->text = strdup(s);
|
|
if (!oldest->text)
|
|
return NULL;
|
|
oldest->max_px = max_px;
|
|
oldest->last_used = ++overlay_text_clock;
|
|
overlay_text_shape(oldest);
|
|
return oldest;
|
|
}
|
|
|
|
/* ===================== Overlay app icons ===================== */
|
|
|
|
/* Drawn square of the icon slot, in host pixels. */
|
|
#define OVERLAY_ICON_SZ 28
|
|
#ifndef MIN
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
#endif
|
|
|
|
static char *overlay_strndup(const char *s, size_t n) {
|
|
char *r = malloc(n + 1);
|
|
if (r) {
|
|
memcpy(r, s, n);
|
|
r[n] = '\0';
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static bool overlay_endswith(const char *s, const char *suffix) {
|
|
size_t ls = strlen(s), lx = strlen(suffix);
|
|
return ls >= lx && strcmp(s + ls - lx, suffix) == 0;
|
|
}
|
|
|
|
/* Read the Icon= value (case-sensitive, not the localized Icon[lang]= form)
|
|
out of a .desktop file, malloc'd, or NULL. */
|
|
static char *desktop_icon_from_file(FILE *f) {
|
|
char line[1024];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strncmp(line, "Icon=", 5) != 0)
|
|
continue;
|
|
if (line[5] == '[') /* Icon[locale]= */
|
|
continue;
|
|
size_t n = strlen(line);
|
|
while (n > 5 && (line[n - 1] == '\n' || line[n - 1] == '\r' ||
|
|
line[n - 1] == ' ' || line[n - 1] == '\t'))
|
|
n--;
|
|
return overlay_strndup(line + 5, n - 5);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Find `<app_id>.desktop` under an XDG data dir and return its Icon= value,
|
|
or NULL. */
|
|
static char *desktop_icon_name(const char *app_id) {
|
|
const char *data_home = getenv("XDG_DATA_HOME");
|
|
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (!data_dirs || !data_dirs[0])
|
|
data_dirs = "/usr/local/share:/usr/share";
|
|
char buf[4096];
|
|
|
|
if (data_home && data_home[0]) {
|
|
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop",
|
|
data_home, app_id);
|
|
FILE *f = fopen(buf, "r");
|
|
if (f) {
|
|
char *icon = desktop_icon_from_file(f);
|
|
fclose(f);
|
|
if (icon) return icon;
|
|
}
|
|
}
|
|
char *copy = strdup(data_dirs);
|
|
if (!copy) return NULL;
|
|
for (char *p = copy, *dir; p; p = dir) {
|
|
dir = strchr(p, ':');
|
|
if (dir) *dir++ = '\0';
|
|
if (p[0]) {
|
|
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop", p, app_id);
|
|
FILE *f = fopen(buf, "r");
|
|
if (f) {
|
|
char *icon = desktop_icon_from_file(f);
|
|
fclose(f);
|
|
if (icon) {
|
|
free(copy);
|
|
return icon;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
free(copy);
|
|
return NULL;
|
|
}
|
|
|
|
/* Turn an Icon= value (theme name, bare filename or absolute path) into a
|
|
loadable .png/.svg path, or NULL. */
|
|
static char *overlay_find_icon_file(const char *name) {
|
|
static const char *const themes[] = { "Adwaita", "hicolor", "default" };
|
|
static const char *const sizes[] =
|
|
{ "scalable", "48x48", "32x32", "24x24" };
|
|
static const char *const exts[] = { "png", "svg" };
|
|
char roots[8][512];
|
|
int n_roots = 0;
|
|
const char *data_home = getenv("XDG_DATA_HOME");
|
|
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (data_home && data_home[0] && n_roots < 8)
|
|
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", data_home);
|
|
if (!data_dirs || !data_dirs[0])
|
|
data_dirs = "/usr/local/share:/usr/share";
|
|
char *copy = strdup(data_dirs);
|
|
for (char *p = copy, *dir; p && n_roots < 8; p = dir) {
|
|
dir = strchr(p, ':');
|
|
if (dir) *dir++ = '\0';
|
|
if (p[0])
|
|
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", p);
|
|
}
|
|
free(copy);
|
|
|
|
if (strchr(name, '/') && access(name, R_OK) == 0 &&
|
|
(overlay_endswith(name, ".png") ||
|
|
overlay_endswith(name, ".svg")))
|
|
return strdup(name);
|
|
|
|
char buf[4096];
|
|
for (int tr = 0; tr < n_roots; tr++) {
|
|
for (size_t t = 0; t < sizeof(themes) / sizeof(themes[0]); t++) {
|
|
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) {
|
|
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
|
snprintf(buf, sizeof(buf), "%s/%s/%s/apps/%s.%s",
|
|
roots[tr], themes[t], sizes[s], name, exts[e]);
|
|
if (access(buf, R_OK) == 0)
|
|
return strdup(buf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static const char *const pixmap_roots[] = {
|
|
"/usr/share/pixmaps", "/usr/local/share/pixmaps",
|
|
};
|
|
for (size_t pr = 0; pr < sizeof(pixmap_roots) / sizeof(pixmap_roots[0]); pr++) {
|
|
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
|
snprintf(buf, sizeof(buf), "%s/%s.%s", pixmap_roots[pr], name, exts[e]);
|
|
if (access(buf, R_OK) == 0)
|
|
return strdup(buf);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Decode a PNG into straight (non-premultiplied) RGBA8, malloc'd. */
|
|
static unsigned char *overlay_load_png(const char *path, int *w, int *h) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL,
|
|
NULL);
|
|
if (!png) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_infop info = png_create_info_struct(png);
|
|
if (!info) {
|
|
png_destroy_read_struct(&png, NULL, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
if (setjmp(png_jmpbuf(png))) {
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_init_io(png, f);
|
|
png_read_info(png, info);
|
|
int bw = (int)png_get_image_width(png, info);
|
|
int bh = (int)png_get_image_height(png, info);
|
|
png_byte ct = png_get_color_type(png, info);
|
|
png_byte bd = png_get_bit_depth(png, info);
|
|
if (ct == PNG_COLOR_TYPE_PALETTE)
|
|
png_set_palette_to_rgb(png);
|
|
if (ct == PNG_COLOR_TYPE_GRAY && bd < 8)
|
|
png_set_expand_gray_1_2_4_to_8(png);
|
|
if (png_get_valid(png, info, PNG_INFO_tRNS))
|
|
png_set_tRNS_to_alpha(png);
|
|
if (bd == 16)
|
|
png_set_strip_16(png);
|
|
if (ct == PNG_COLOR_TYPE_RGB || ct == PNG_COLOR_TYPE_GRAY)
|
|
png_set_filler(png, 0xff, PNG_FILLER_AFTER);
|
|
if (ct == PNG_COLOR_TYPE_GRAY || ct == PNG_COLOR_TYPE_GRAY_ALPHA)
|
|
png_set_gray_to_rgb(png);
|
|
png_read_update_info(png, info);
|
|
|
|
size_t rowbytes = png_get_rowbytes(png, info);
|
|
size_t size = (size_t)bh * rowbytes;
|
|
unsigned char *rgba = malloc(size);
|
|
if (!rgba) {
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_bytep *rows = malloc(sizeof(png_bytep) * (size_t)bh);
|
|
if (!rows) {
|
|
free(rgba);
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
for (int y = 0; y < bh; y++)
|
|
rows[y] = rgba + (size_t)y * rowbytes;
|
|
png_read_image(png, rows);
|
|
free(rows);
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
*w = bw;
|
|
*h = bh;
|
|
return rgba;
|
|
}
|
|
|
|
/* Decode an SVG into straight RGBA8 at its intrinsic size, malloc'd. */
|
|
static unsigned char *overlay_load_svg(const char *path, int *w, int *h) {
|
|
GError *err = NULL;
|
|
RsvgHandle *handle = rsvg_handle_new_from_file(path, &err);
|
|
if (!handle || err) {
|
|
if (err) g_error_free(err);
|
|
return NULL;
|
|
}
|
|
double iw = 0, ih = 0;
|
|
if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &iw, &ih))
|
|
iw = ih = 48.0; /* dimension-less SVG: assume an icon-sized square */
|
|
if (iw <= 0 || ih <= 0) {
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
int bw = (int)iw, bh = (int)ih;
|
|
cairo_surface_t *surf =
|
|
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bw, bh);
|
|
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
cairo_t *cr = cairo_create(surf);
|
|
RsvgRectangle viewport = { 0, 0, iw, ih };
|
|
if (!rsvg_handle_render_document(handle, cr, &viewport, &err)) {
|
|
if (err) g_error_free(err);
|
|
cairo_destroy(cr);
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
cairo_destroy(cr);
|
|
cairo_surface_flush(surf);
|
|
|
|
/* Cairo's ARGB32 is premultiplied, little-endian byte order B,G,R,A. */
|
|
int n = bw * bh;
|
|
unsigned char *rgba = malloc(sizeof(unsigned char) * 4 * (size_t)n);
|
|
if (rgba) {
|
|
unsigned char *src = cairo_image_surface_get_data(surf);
|
|
for (int i = 0; i < n; i++) {
|
|
unsigned char a = src[i * 4 + 3];
|
|
unsigned char r = src[i * 4 + 2];
|
|
unsigned char g = src[i * 4 + 1];
|
|
unsigned char b = src[i * 4 + 0];
|
|
if (a > 0) {
|
|
r = (unsigned char)MIN((r * 255 + a / 2) / a, 255);
|
|
g = (unsigned char)MIN((g * 255 + a / 2) / a, 255);
|
|
b = (unsigned char)MIN((b * 255 + a / 2) / a, 255);
|
|
}
|
|
rgba[i * 4 + 0] = r;
|
|
rgba[i * 4 + 1] = g;
|
|
rgba[i * 4 + 2] = b;
|
|
rgba[i * 4 + 3] = a;
|
|
}
|
|
}
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
*w = bw;
|
|
*h = bh;
|
|
return rgba;
|
|
}
|
|
|
|
/* Resolve and upload `app`'s icon as a GL_RGBA texture (host EGL context
|
|
must be current). Best-effort: on any failure the app keeps no icon and
|
|
the overlay draws the placeholder instead. */
|
|
/* Resolve and upload an overlay icon for `app_id` into the supplied outputs.
|
|
Shared by the per-tab and per-window loaders so each tab-group window can
|
|
show its own icon; sets *tex (nested GL path) or *wlr (standalone path),
|
|
leaving the unused one NULL. */
|
|
static void overlay_make_icon(const char *app_id,
|
|
int *ow, int *oh,
|
|
struct wlr_texture **wlr) {
|
|
*ow = 0; *oh = 0; *wlr = NULL;
|
|
if (!app_id || !app_id[0])
|
|
return;
|
|
|
|
char *icon = desktop_icon_name(app_id);
|
|
char *path = overlay_find_icon_file(icon ? icon : app_id);
|
|
free(icon);
|
|
if (!path)
|
|
return;
|
|
|
|
int w = 0, h = 0;
|
|
unsigned char *rgba = NULL;
|
|
if (overlay_endswith(path, ".svg"))
|
|
rgba = overlay_load_svg(path, &w, &h);
|
|
else if (overlay_endswith(path, ".png"))
|
|
rgba = overlay_load_png(path, &w, &h);
|
|
free(path);
|
|
if (!rgba || w <= 0 || h <= 0) {
|
|
free(rgba);
|
|
return;
|
|
}
|
|
|
|
/* Hand the icon to the wlroots renderer as a premultiplied ARGB
|
|
texture (B,G,R,A byte order). */
|
|
unsigned char *pre = malloc((size_t)w * h * 4);
|
|
if (pre) {
|
|
for (int i = 0; i < w * h; i++) {
|
|
unsigned char a = rgba[i * 4 + 3];
|
|
if (a == 0) {
|
|
pre[i * 4 + 0] = 0;
|
|
pre[i * 4 + 1] = 0;
|
|
pre[i * 4 + 2] = 0;
|
|
pre[i * 4 + 3] = 0;
|
|
} else {
|
|
pre[i * 4 + 0] = (unsigned char)(rgba[i * 4 + 2] * a / 255);
|
|
pre[i * 4 + 1] = (unsigned char)(rgba[i * 4 + 1] * a / 255);
|
|
pre[i * 4 + 2] = (unsigned char)(rgba[i * 4 + 0] * a / 255);
|
|
pre[i * 4 + 3] = a;
|
|
}
|
|
}
|
|
*wlr = wlr_texture_from_pixels(server.renderer,
|
|
DRM_FORMAT_ARGB8888, (uint32_t)w * 4, (uint32_t)w,
|
|
(uint32_t)h, pre);
|
|
free(pre);
|
|
}
|
|
free(rgba);
|
|
*ow = w; *oh = h;
|
|
}
|
|
|
|
static void overlay_icon_load_window(struct kick_window *w) {
|
|
w->icon_resolved = true;
|
|
const char *id = window_app_id(w);
|
|
snprintf(w->icon_app_id, sizeof(w->icon_app_id), "%s", id ? id : "");
|
|
/* Re-resolves happen on every map and app_id change: drop the previous
|
|
texture first, or one leaks per reload (overlay_make_icon only
|
|
overwrites the pointer). Protocol icons never reach here. */
|
|
if (w->icon_wlr) {
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
w->icon_wlr = NULL;
|
|
}
|
|
overlay_make_icon(w->icon_app_id,
|
|
&w->icon_w, &w->icon_h, &w->icon_wlr);
|
|
if (w->icon_wlr)
|
|
kick_log(KICK_LOG_INFO, "[overlay] icon %s %dx%d\n",
|
|
w->icon_app_id, w->icon_w, w->icon_h);
|
|
}
|
|
|
|
/* ===================== Toplevel icons =====================
|
|
Exact client artwork (xdg-toplevel-icon-v1) takes precedence over the
|
|
desktop-file guesser: the largest uploaded buffer becomes the overlay
|
|
texture (downscaled at draw). The guesser stays as the fallback -- and
|
|
the only source for X11 windows -- so every window still gets an icon:
|
|
a cleared/empty icon drops back to guessing on next draw. */
|
|
|
|
static void icon_proto_release(struct kick_window *w) {
|
|
if (!w)
|
|
return;
|
|
if (w->proto_icon) {
|
|
wlr_xdg_toplevel_icon_v1_unref(w->proto_icon);
|
|
w->proto_icon = NULL;
|
|
}
|
|
w->icon_protocol = false;
|
|
}
|
|
|
|
static void handle_icon_set(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_toplevel_icon_manager_v1_set_icon_event *ev = data;
|
|
struct kick_window *w = window_for_toplevel(ev->toplevel);
|
|
if (!w)
|
|
return;
|
|
/* Drop any previous protocol icon (and its texture) first. */
|
|
icon_proto_release(w);
|
|
if (w->icon_wlr) {
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
w->icon_wlr = NULL;
|
|
}
|
|
if (!ev->icon || wl_list_empty(&ev->icon->buffers)) {
|
|
/* Cleared (or empty): fall back to the app_id guesser on next
|
|
draw instead of keeping a stale texture. */
|
|
w->icon_resolved = false;
|
|
return;
|
|
}
|
|
/* Largest buffer wins; the overlay downscales with bilinear. */
|
|
struct wlr_xdg_toplevel_icon_v1_buffer *best = NULL;
|
|
struct wlr_xdg_toplevel_icon_v1_buffer *b;
|
|
wl_list_for_each(b, &ev->icon->buffers, link) {
|
|
if (!b->buffer || b->buffer->width <= 0 || b->buffer->height <= 0)
|
|
continue;
|
|
if (!best || b->buffer->width * b->buffer->height >
|
|
best->buffer->width * best->buffer->height)
|
|
best = b;
|
|
}
|
|
if (!best) {
|
|
w->icon_resolved = false;
|
|
return;
|
|
}
|
|
struct wlr_texture *tex =
|
|
wlr_texture_from_buffer(server.renderer, best->buffer);
|
|
if (!tex) {
|
|
kick_log(KICK_LOG_DEBUG, "[overlay] proto icon texture failed app=%d\n",
|
|
tab_index(w->tab));
|
|
w->icon_resolved = false;
|
|
return;
|
|
}
|
|
w->proto_icon = wlr_xdg_toplevel_icon_v1_ref(ev->icon);
|
|
w->icon_protocol = true;
|
|
w->icon_wlr = tex;
|
|
w->icon_w = best->buffer->width;
|
|
w->icon_h = best->buffer->height;
|
|
w->icon_resolved = true;
|
|
const char *id = window_app_id(w);
|
|
snprintf(w->icon_app_id, sizeof(w->icon_app_id), "%s", id ? id : "");
|
|
kick_log(KICK_LOG_INFO, "[overlay] icon %s proto %dx%d\n",
|
|
w->icon_app_id, w->icon_w, w->icon_h);
|
|
}
|
|
|
|
/* Cached overlay text run for (string, width budget): lookup shapes only
|
|
(cheap, no GL), for measuring layout. Rasterization happens exclusively
|
|
in overlay_text_draw_tile, so the raster log proves what reached the
|
|
screen and measuring never uploads textures nobody paints. */
|
|
static struct overlay_text_entry *overlay_text_tile(const char *s,
|
|
int max_px) {
|
|
if (!overlay_font_ok || !overlay_layout) return NULL;
|
|
return overlay_text_entry(s, max_px);
|
|
}
|
|
|
|
/* Draw-path lookup: measure, then rasterize on first draw. */
|
|
static struct overlay_text_entry *overlay_text_draw_tile(const char *s,
|
|
int max_px) {
|
|
struct overlay_text_entry *e = overlay_text_tile(s, max_px);
|
|
if (!e) return NULL;
|
|
if (!e->rastered && e->text && e->text[0])
|
|
e->tex = overlay_text_raster(e);
|
|
return e;
|
|
}
|
|
|
|
static int overlay_text_width(const char *s) {
|
|
struct overlay_text_entry *e = overlay_text_tile(s, INT_MAX);
|
|
return e ? e->w : 0;
|
|
}
|
|
|
|
/* Row title for an overlay tab: the window the tab is actually showing --
|
|
on the active tab that is its focused window; a background tab previews
|
|
its current window, what switching over would show -- or the command
|
|
line for a still-black (pending) tab. Logs one INFO line whenever a
|
|
row's resolved title changes, so a stacked window swapping the title
|
|
underneath an unchanged tab is observable in logs and tests. */
|
|
static const char *tab_row_title(struct kick_tab *tab) {
|
|
const char *t = NULL;
|
|
struct kick_window *pick = tab_current_window(tab);
|
|
const char *st = pick ? window_title(pick) : NULL;
|
|
if (pick && st && st[0])
|
|
t = st;
|
|
struct kick_window *first = tab_first_window(tab);
|
|
if (!t && first && first->cmd && first->cmd[0])
|
|
t = first->cmd;
|
|
const char *cur = t ? t : "";
|
|
if (pick && strcmp(cur, pick->row_title) != 0) {
|
|
kick_log(KICK_LOG_INFO, "[title] app=%d '%s'\n",
|
|
tab_index(tab), cur);
|
|
snprintf(pick->row_title, sizeof(pick->row_title), "%s", cur);
|
|
overlay_utf8_cut(pick->row_title);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
static bool overlay_window_icon_needs_load(struct kick_window *w) {
|
|
/* A live protocol icon never re-guesses, not even across app_id
|
|
changes: the client's artwork is authoritative. */
|
|
if (w->icon_protocol)
|
|
return false;
|
|
if (!w->icon_resolved)
|
|
return true;
|
|
const char *id = window_app_id(w);
|
|
return id && strcmp(id, w->icon_app_id) != 0;
|
|
}
|
|
|
|
/* Display title for one window: its own title, else its app_id, else a
|
|
generic placeholder. A client tag appends in brackets ("title [tag]").
|
|
Writes into buf whenever the shown string must be synthesized (so the
|
|
returned pointer always outlives the call). */
|
|
static const char *overlay_window_title(struct kick_window *w,
|
|
char *buf, size_t sz) {
|
|
const char *t = window_title(w);
|
|
if (t && t[0] && (!w->tag || !w->tag[0]))
|
|
return t;
|
|
const char *base = (t && t[0]) ? t : NULL;
|
|
if (!base) {
|
|
const char *id = window_app_id(w);
|
|
base = (id && id[0]) ? id : "window";
|
|
}
|
|
if (w->tag && w->tag[0])
|
|
snprintf(buf, sz, "%s [%s]", base, w->tag);
|
|
else
|
|
snprintf(buf, sz, "%s", base);
|
|
overlay_utf8_cut(buf);
|
|
return buf;
|
|
}
|
|
|
|
/* Whether every window of the tab is mid-close (so the row should vanish
|
|
from the overlay and the tab from cycling right away). */
|
|
static bool tab_closing(struct kick_tab *tab) {
|
|
if (!tab) return false;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if ((window_live(w) || w->pending_claim) && !w->closing)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/* A row in the overlay (both the host-GL and the standalone wlr paths):
|
|
skip tabs whose windows are dead or all mid-close; black pending tabs are
|
|
rows too. */
|
|
static bool tab_overlay_row(struct kick_tab *tab) {
|
|
return tab_live(tab) && !tab_closing(tab);
|
|
}
|
|
|
|
/* ---------- Alt overlay rendering (single wlr render pass) ----------
|
|
Both modes composite the scene into an output buffer with wlroots and
|
|
paint the overlay onto that buffer as a second render pass -- nested
|
|
before blitting the frame to its host window, standalone before commit.
|
|
One renderer, one layout, one set of assets: the text tiles and icons
|
|
exist only as wlr textures. */
|
|
|
|
static struct wlr_render_color overlay_premul(float r, float g, float b, float a) {
|
|
return (struct wlr_render_color){ r * a, g * a, b * a, a };
|
|
}
|
|
|
|
static void overlay_pass_rect(struct wlr_render_pass *pass,
|
|
int x, int y, int w, int h,
|
|
float r, float g, float b, float a)
|
|
{
|
|
if (w <= 0 || h <= 0) return;
|
|
struct wlr_render_rect_options o = {
|
|
.box = { .x = x, .y = y, .width = w, .height = h },
|
|
.color = overlay_premul(r, g, b, a),
|
|
};
|
|
wlr_render_pass_add_rect(pass, &o);
|
|
}
|
|
|
|
static void overlay_pass_text(struct wlr_render_pass *pass, const char *s,
|
|
int max_px, int x, int y, float alpha)
|
|
{
|
|
if (!overlay_font_ok) return;
|
|
struct overlay_text_entry *e = overlay_text_draw_tile(s, max_px);
|
|
if (!e || !e->tex) return;
|
|
/* Same baseline the old per-glyph quads used (ascender below y). */
|
|
struct wlr_render_texture_options o = {
|
|
.texture = e->tex,
|
|
.src_box = { .x = 0, .y = 0, .width = e->w, .height = e->h },
|
|
.dst_box = { .x = x, .y = y + host.overlay_font_asc - e->baseline,
|
|
.width = e->w, .height = e->h },
|
|
.alpha = &alpha,
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(pass, &o);
|
|
}
|
|
|
|
static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
|
|
struct wlr_texture *tex,
|
|
int ix, int iy, int size, float alpha)
|
|
{
|
|
if (!tex) return;
|
|
struct wlr_render_texture_options o = {
|
|
.texture = tex,
|
|
.dst_box = { .x = ix, .y = iy, .width = size, .height = size },
|
|
.alpha = &alpha,
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(pass, &o);
|
|
}
|
|
|
|
/* Cursor inside the overlay plane (Option 1): while the overlay is held on
|
|
standalone DRM the hardware cursor plane is parked, so the cursor image
|
|
is painted into the plane buffer after the menu -- cursor above overlay
|
|
above content, ordered by our own paint order on every backend and every
|
|
plane the layer lands on. The themed image is premultiplied on upload
|
|
exactly like the text tiles; only frame 0 is used (no animation). */
|
|
struct overlay_cursor_tex {
|
|
char name[64];
|
|
struct wlr_texture *tex;
|
|
int w, h, hotx, hoty;
|
|
};
|
|
static struct overlay_cursor_tex cursor_tex;
|
|
|
|
static void overlay_cursor_drop(void) {
|
|
if (cursor_tex.tex) {
|
|
wlr_texture_destroy(cursor_tex.tex);
|
|
cursor_tex.tex = NULL;
|
|
}
|
|
cursor_tex.name[0] = '\0';
|
|
}
|
|
|
|
static const struct overlay_cursor_tex *overlay_cursor_image(void) {
|
|
if (!server.cursor || !server.xcursor_mgr)
|
|
return NULL;
|
|
const char *name = "default";
|
|
if (server.cursor_shape > 0) {
|
|
const char *shape_name = wlr_cursor_shape_v1_name(
|
|
(enum wp_cursor_shape_device_v1_shape)server.cursor_shape);
|
|
if (shape_name && shape_name[0])
|
|
name = shape_name;
|
|
}
|
|
if (!cursor_tex.tex || strcmp(cursor_tex.name, name) != 0) {
|
|
overlay_cursor_drop();
|
|
struct wlr_xcursor *xc = wlr_xcursor_manager_get_xcursor(
|
|
server.xcursor_mgr, name, 1);
|
|
if ((!xc || xc->image_count < 1) && strcmp(name, "default") != 0) {
|
|
xc = wlr_xcursor_manager_get_xcursor(server.xcursor_mgr,
|
|
"default", 1);
|
|
name = "default";
|
|
}
|
|
if (!xc || xc->image_count < 1)
|
|
return NULL;
|
|
struct wlr_xcursor_image *im = xc->images[0];
|
|
if (!im || !im->buffer || im->width == 0 || im->height == 0)
|
|
return NULL;
|
|
int w = (int)im->width, h = (int)im->height;
|
|
uint32_t *px = malloc((size_t)w * h * 4);
|
|
if (!px)
|
|
return NULL;
|
|
uint32_t *src = (uint32_t *)im->buffer;
|
|
for (int i = 0; i < w * h; i++) {
|
|
uint32_t p = src[i];
|
|
unsigned a = (p >> 24) & 0xff, r = (p >> 16) & 0xff,
|
|
g = (p >> 8) & 0xff, b = p & 0xff;
|
|
r = r * a / 255;
|
|
g = g * a / 255;
|
|
b = b * a / 255;
|
|
px[i] = (a << 24) | (r << 16) | (g << 8) | b;
|
|
}
|
|
struct wlr_texture *tex = wlr_texture_from_pixels(
|
|
server.renderer, DRM_FORMAT_ARGB8888,
|
|
(uint32_t)w * 4, (uint32_t)w, (uint32_t)h, px);
|
|
free(px);
|
|
if (!tex)
|
|
return NULL;
|
|
snprintf(cursor_tex.name, sizeof(cursor_tex.name), "%s", name);
|
|
cursor_tex.tex = tex;
|
|
cursor_tex.w = w;
|
|
cursor_tex.h = h;
|
|
cursor_tex.hotx = (int)im->hotspot_x;
|
|
cursor_tex.hoty = (int)im->hotspot_y;
|
|
}
|
|
return &cursor_tex;
|
|
}
|
|
|
|
static void overlay_pass_cursor(struct wlr_render_pass *pass,
|
|
int origin_x, int origin_y) {
|
|
/* Only when a software cursor is in effect (locked): a hardware
|
|
cursor plane already scans out above every layer, so painting here
|
|
as well would double it. The plane path only runs on DRM, where
|
|
that means transient states like screen capture. */
|
|
if (!server.output || server.output->software_cursor_locks == 0)
|
|
return;
|
|
const struct overlay_cursor_tex *c = overlay_cursor_image();
|
|
if (!c || !c->tex || !server.cursor)
|
|
return;
|
|
/* Standalone layout holds the single output at (0,0), scale 1, so the
|
|
cursor's layout coordinates are output-buffer pixels; shift into the
|
|
box-sized plane buffer. Partially off-box cursors clip in hardware. */
|
|
int x = (int)server.cursor->x - origin_x - c->hotx;
|
|
int y = (int)server.cursor->y - origin_y - c->hoty;
|
|
struct wlr_render_texture_options o = {
|
|
.texture = c->tex,
|
|
.src_box = { .x = 0, .y = 0, .width = c->w, .height = c->h },
|
|
.dst_box = { .x = x, .y = y, .width = c->w, .height = c->h },
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(pass, &o);
|
|
}
|
|
|
|
/* Confirm-before-quit modal, drawn in place of the Alt tab list while a quit
|
|
is pending: a centered panel with the live-window count and the y/n keys.
|
|
Same styling language as the overlay -- same backdrop, rim strokes, and
|
|
text tiles. */
|
|
|
|
/* Centered box geometry shared by the tab overlay's draw pass and hit-test
|
|
(overlay_tab_box) and by the confirm-quit modal (overlay_confirm_box).
|
|
Both are measured in the visible canvas (host.width/height), which in
|
|
standalone is the output buffer size at scale 1, so the same numbers serve
|
|
as a plane's dst_box in output-buffer coordinates. Return values are the
|
|
absolute box position/size; the caller shifts by its paint origin. */
|
|
struct overlay_box {
|
|
int x, y, w, h;
|
|
};
|
|
|
|
/* Tab list: counts the visible rows and the widest row, then the centered
|
|
box. Returns the number of rows (0 = nothing to draw or hit). */
|
|
static int overlay_tab_box(struct overlay_box *box) {
|
|
if (wl_list_empty(&server.tabs)) return 0;
|
|
int margin = 8, boxpad = 6;
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8, member_gap = 12;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
int row_gap = 8;
|
|
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
|
int max_row_w = 0, n_rows = 0;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
if (!tab_overlay_row(tab)) continue;
|
|
int n = 0, row_w = 0;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (!w->mapped) continue;
|
|
char tb[256];
|
|
const char *t = overlay_window_title(w, tb, sizeof(tb));
|
|
overlay_utf8_cut(tb);
|
|
row_w += icon_sz + icon_gap + overlay_text_width(t);
|
|
n++;
|
|
}
|
|
if (n > 0) {
|
|
if (n > 1) row_w += (n - 1) * member_gap;
|
|
} else {
|
|
struct kick_window *first = tab_first_window(tab);
|
|
const char *ct = (first && first->cmd && first->cmd[0])
|
|
? first->cmd : "pending";
|
|
row_w = icon_sz + icon_gap + overlay_text_width(ct);
|
|
}
|
|
if (row_w > max_row_w) max_row_w = row_w;
|
|
n_rows++;
|
|
}
|
|
if (n_rows == 0) return 0;
|
|
|
|
int avail_w = host.width - 2 * margin - 2 * boxpad;
|
|
if (max_row_w > avail_w) max_row_w = avail_w;
|
|
|
|
int bw = max_row_w + 2 * boxpad;
|
|
int bh = n_rows * row_h + (n_rows - 1) * row_gap + 2 * boxpad;
|
|
if (bw > host.width - 2 * margin) bw = host.width - 2 * margin;
|
|
if (bh > host.height - 2 * margin) bh = host.height - 2 * margin;
|
|
box->x = (host.width - bw) / 2;
|
|
box->y = (host.height - bh) / 2;
|
|
box->w = bw;
|
|
box->h = bh;
|
|
return n_rows;
|
|
}
|
|
|
|
/* Confirm-quit panel box; always presentable when called. */
|
|
static int overlay_confirm_box(struct overlay_box *box) {
|
|
int n = live_window_count();
|
|
char l1[96], l2[96];
|
|
snprintf(l1, sizeof(l1), "Quit kick? %d window%s open.", n,
|
|
n == 1 ? "" : "s");
|
|
snprintf(l2, sizeof(l2), "[Y]es quit [N]o keep running");
|
|
|
|
int margin = 8, boxpad = 10;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
int row_gap = 6;
|
|
int w1 = overlay_text_width(l1), w2 = overlay_text_width(l2);
|
|
int bw = (w1 > w2 ? w1 : w2) + 2 * boxpad;
|
|
int bh = 2 * row_h + row_gap + 2 * boxpad;
|
|
if (bw > host.width - 2 * margin) bw = host.width - 2 * margin;
|
|
if (bh > host.height - 2 * margin) bh = host.height - 2 * margin;
|
|
box->x = (host.width - bw) / 2;
|
|
box->y = (host.height - bh) / 2;
|
|
box->w = bw;
|
|
box->h = bh;
|
|
return 1;
|
|
}
|
|
|
|
/* Draw a box shifted to a paint origin: the composite path paints into the
|
|
full canvas buffer at origin (0,0) (absolute coords), the plane path paints
|
|
into a box-sized buffer at origin (box.x, box.y) so the box lands at
|
|
0..box.w/0..box.h in that buffer. */
|
|
static void overlay_apply_origin(struct overlay_box *box, int origin_x,
|
|
int origin_y) {
|
|
box->x -= origin_x;
|
|
box->y -= origin_y;
|
|
}
|
|
|
|
static void draw_confirm_quit_pass(struct wlr_render_pass *pass,
|
|
int origin_x, int origin_y) {
|
|
struct overlay_box box;
|
|
if (!overlay_confirm_box(&box)) return;
|
|
overlay_apply_origin(&box, origin_x, origin_y);
|
|
int box_x = box.x, box_y = box.y, box_w = box.w, box_h = box.h;
|
|
|
|
int n = live_window_count();
|
|
char l1[96], l2[96];
|
|
snprintf(l1, sizeof(l1), "Quit kick? %d window%s open.", n,
|
|
n == 1 ? "" : "s");
|
|
snprintf(l2, sizeof(l2), "[Y]es quit [N]o keep running");
|
|
|
|
int boxpad = 10;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
int row_gap = 6;
|
|
|
|
int bw = 1;
|
|
overlay_pass_rect(pass, box_x + bw, box_y + bw,
|
|
box_w - 2 * bw, box_h - 2 * bw,
|
|
0.07f, 0.07f, 0.09f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y, box_w, bw, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y + box_h - bw, box_w, bw,
|
|
0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y, bw, box_h, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x + box_w - bw, box_y, bw, box_h,
|
|
0.45f, 0.45f, 0.48f, 1.0f);
|
|
|
|
int row_inner = box_w - 2 * boxpad;
|
|
for (int r = 0; r < 2; r++) {
|
|
const char *s = r == 0 ? l1 : l2;
|
|
int tw = overlay_text_width(s);
|
|
int x = box_x + boxpad + (row_inner - tw) / 2;
|
|
int y = box_y + boxpad + r * (row_h + row_gap);
|
|
overlay_pass_text(pass, s, INT_MAX,
|
|
x, y + (row_h - host.overlay_font_h) / 2, 0.95f);
|
|
}
|
|
}
|
|
|
|
static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
|
|
int origin_x, int origin_y) {
|
|
if (!host.overlay_visible && !confirm_quit_pending) return;
|
|
if (confirm_quit_pending) {
|
|
draw_confirm_quit_pass(pass, origin_x, origin_y);
|
|
return;
|
|
}
|
|
struct overlay_box box;
|
|
if (overlay_tab_box(&box) <= 0) return;
|
|
overlay_apply_origin(&box, origin_x, origin_y);
|
|
int box_x = box.x, box_y = box.y, box_w = box.w, box_h = box.h;
|
|
|
|
int boxpad = 6;
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8, member_gap = 12;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
int row_gap = 8;
|
|
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
|
struct kick_tab *tab;
|
|
|
|
int bw = 1;
|
|
/* Opaque backdrop filling the box, framed by four rim strokes -- keeps
|
|
the titles readable over any content. (Alpha below 1.0 here gives a
|
|
see-through panel; render_standalone() forces whole-scene repaints
|
|
while the overlay shows, so a translucent value stays true instead of
|
|
stacking toward opaque on partial repaints.) */
|
|
overlay_pass_rect(pass, box_x + bw, box_y + bw, box_w - 2 * bw, box_h - 2 * bw,
|
|
0.07f, 0.07f, 0.09f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y, box_w, bw, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y + box_h - bw, box_w, bw,
|
|
0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x, box_y, bw, box_h, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x + box_w - bw, box_y, bw, box_h,
|
|
0.45f, 0.45f, 0.48f, 1.0f);
|
|
|
|
int row = 0;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
if (!tab_overlay_row(tab)) continue;
|
|
(void)tab_row_title(tab);
|
|
int ry = box_y + boxpad + row * (row_h + row_gap);
|
|
bool active = tab == server.active_tab;
|
|
int row_inner = box_w - 2 * boxpad;
|
|
|
|
struct kick_window *wins[32];
|
|
char tbuf[32][256];
|
|
const char *strs[32];
|
|
int shares[32];
|
|
int widths[32];
|
|
int n = 0;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (!w->mapped) continue;
|
|
if (n >= 32) break;
|
|
if (overlay_window_icon_needs_load(w))
|
|
overlay_icon_load_window(w);
|
|
wins[n++] = w;
|
|
}
|
|
int share = 0;
|
|
if (n > 0) {
|
|
int avail = row_inner - n * (icon_sz + icon_gap);
|
|
if (n > 1) avail -= (n - 1) * member_gap;
|
|
share = avail / n;
|
|
if (share < 0) share = 0;
|
|
}
|
|
int total_w = 0;
|
|
for (int k = 0; k < n; k++) {
|
|
/* Titles are ellipsized at draw time by the text tile itself.
|
|
overlay_window_title returns the client string directly when
|
|
there is nothing to synthesize, so draw the RETURNED pointer:
|
|
the buffer below is only filled in the tag/fallback cases. */
|
|
strs[k] = overlay_window_title(wins[k], tbuf[k], sizeof(tbuf[k]));
|
|
shares[k] = share;
|
|
struct overlay_text_entry *te =
|
|
overlay_text_tile(strs[k], share);
|
|
widths[k] = te ? te->w : 0;
|
|
total_w += icon_sz + icon_gap + widths[k];
|
|
}
|
|
if (n > 1) total_w += (n - 1) * member_gap;
|
|
|
|
int pen = box_x + boxpad + (row_inner - total_w) / 2;
|
|
int iy = ry + (row_h - icon_sz) / 2;
|
|
if (n == 0) {
|
|
struct kick_window *first = tab_first_window(tab);
|
|
const char *ct = (first && first->cmd && first->cmd[0])
|
|
? first->cmd : "pending";
|
|
int cavail = row_inner - (icon_sz + icon_gap) - 4;
|
|
if (cavail < 0) cavail = 0;
|
|
struct overlay_text_entry *cte = overlay_text_tile(ct, cavail);
|
|
int ctw = cte ? cte->w : 0;
|
|
int cmw = icon_sz + icon_gap + ctw;
|
|
int cpen = box_x + boxpad + (row_inner - cmw) / 2;
|
|
overlay_pass_rect(pass, cpen - 2, ry - 1, cmw + 4, row_h,
|
|
0.22f, 0.22f, 0.25f, 1.0f);
|
|
overlay_pass_text(pass, ct, cavail, cpen + icon_sz + icon_gap,
|
|
ry + (row_h - host.overlay_font_h) / 2, 0.9f);
|
|
}
|
|
for (int k = 0; k < n; k++) {
|
|
struct kick_window *w = wins[k];
|
|
int tw = widths[k];
|
|
int mw = icon_sz + icon_gap + tw;
|
|
bool is_cursor = active && w->focused;
|
|
overlay_pass_rect(pass, pen - 2, ry - 1, mw + 4, row_h,
|
|
0.22f, 0.22f, 0.25f, 1.0f);
|
|
if (is_cursor)
|
|
overlay_pass_rect(pass, pen - 2, ry - 1, mw + 4, row_h,
|
|
0.24f, 0.36f, 0.60f, 0.9f);
|
|
int mx = pen, my = iy;
|
|
if (w->icon_wlr) {
|
|
overlay_pass_icon_tex(pass, w->icon_wlr, mx, my, icon_sz,
|
|
active ? 1.0f : 0.72f);
|
|
} else {
|
|
overlay_pass_rect(pass, mx, my, icon_sz, icon_sz,
|
|
0.45f, 0.45f, 0.48f, 0.85f);
|
|
overlay_pass_rect(pass, mx + 3, my + 3, icon_sz - 6,
|
|
icon_sz - 6, 0.07f, 0.07f, 0.09f, 1.0f);
|
|
}
|
|
overlay_pass_text(pass, strs[k], shares[k], mx + icon_sz + icon_gap,
|
|
ry + (row_h - host.overlay_font_h) / 2,
|
|
active ? 1.0f : 0.72f * 0.9f);
|
|
pen += mw + member_gap;
|
|
}
|
|
row++;
|
|
}
|
|
}
|
|
|
|
/* Hit-test a touch point (output-local pixels, same space as host.width/
|
|
/// height) against the overlay chips drawn by draw_tab_overlay_pass.
|
|
/// Shares the exact layout math via overlay_tab_box: same constants, same
|
|
/// measure-then-layout, same chip rects (pen-2, ry-1, mw+4 x row_h).
|
|
///
|
|
/// On a chip hit, returns that window. As a touch-friendly fallback, a
|
|
/// point inside a row's vertical band but between chips selects the row's
|
|
/// current window. Points outside every row (including the box padding
|
|
/// and меж-row gaps) miss: the caller swallows them without selecting.
|
|
/// Returns false unless the overlay is up and the point lands on a row. */
|
|
static bool overlay_hit_test(double ox, double oy,
|
|
struct kick_tab **tab_out,
|
|
struct kick_window **win_out) {
|
|
if (tab_out) *tab_out = NULL;
|
|
if (win_out) *win_out = NULL;
|
|
if (!host.overlay_visible || confirm_quit_pending) return false;
|
|
|
|
struct overlay_box box;
|
|
if (overlay_tab_box(&box) <= 0) return false;
|
|
int box_x = box.x, box_y = box.y, box_w = box.w;
|
|
|
|
int boxpad = 6;
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8, member_gap = 12;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
int row_gap = 8;
|
|
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
|
struct kick_tab *tab;
|
|
|
|
int row = 0;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
if (!tab_overlay_row(tab)) continue;
|
|
int ry = box_y + boxpad + row * (row_h + row_gap);
|
|
int row_inner = box_w - 2 * boxpad;
|
|
|
|
struct kick_window *wins[32];
|
|
int widths[32];
|
|
int n = 0;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (!w->mapped) continue;
|
|
if (n >= 32) break;
|
|
wins[n++] = w;
|
|
}
|
|
int share = 0;
|
|
if (n > 0) {
|
|
int avail = row_inner - n * (icon_sz + icon_gap);
|
|
if (n > 1) avail -= (n - 1) * member_gap;
|
|
share = avail / n;
|
|
if (share < 0) share = 0;
|
|
}
|
|
int total_w = 0;
|
|
for (int k = 0; k < n; k++) {
|
|
char tb[256];
|
|
const char *s = overlay_window_title(wins[k], tb, sizeof(tb));
|
|
struct overlay_text_entry *te = overlay_text_tile(s, share);
|
|
widths[k] = te ? te->w : 0;
|
|
total_w += icon_sz + icon_gap + widths[k];
|
|
}
|
|
if (n > 1) total_w += (n - 1) * member_gap;
|
|
|
|
int pen = box_x + boxpad + (row_inner - total_w) / 2;
|
|
if (n == 0) {
|
|
struct kick_window *first = tab_first_window(tab);
|
|
const char *ct = (first && first->cmd && first->cmd[0])
|
|
? first->cmd : "pending";
|
|
int cavail = row_inner - (icon_sz + icon_gap) - 4;
|
|
if (cavail < 0) cavail = 0;
|
|
struct overlay_text_entry *cte = overlay_text_tile(ct, cavail);
|
|
int ctw = cte ? cte->w : 0;
|
|
int cmw = icon_sz + icon_gap + ctw;
|
|
int cpen = box_x + boxpad + (row_inner - cmw) / 2;
|
|
if (ox >= cpen - 2 && ox < cpen - 2 + cmw + 4 &&
|
|
oy >= ry - 1 && oy < ry - 1 + row_h) {
|
|
struct kick_window *cur = tab_current_window(tab);
|
|
if (!cur) cur = first;
|
|
if (tab_out) *tab_out = tab;
|
|
if (win_out) *win_out = cur;
|
|
return cur != NULL;
|
|
}
|
|
/* Row-band fallback for pending rows too. */
|
|
if (ox >= box_x && ox < box_x + box_w &&
|
|
oy >= ry - 1 && oy < ry - 1 + row_h) {
|
|
struct kick_window *cur = tab_current_window(tab);
|
|
if (!cur) cur = first;
|
|
if (tab_out) *tab_out = tab;
|
|
if (win_out) *win_out = cur;
|
|
return cur != NULL;
|
|
}
|
|
} else {
|
|
for (int k = 0; k < n; k++) {
|
|
int mw = icon_sz + icon_gap + widths[k];
|
|
if (ox >= pen - 2 && ox < pen - 2 + mw + 4 &&
|
|
oy >= ry - 1 && oy < ry - 1 + row_h) {
|
|
if (tab_out) *tab_out = tab;
|
|
if (win_out) *win_out = wins[k];
|
|
return true;
|
|
}
|
|
pen += mw + member_gap;
|
|
}
|
|
/* Between chips but inside this row: pick the row's window. */
|
|
if (ox >= box_x && ox < box_x + box_w &&
|
|
oy >= ry - 1 && oy < ry - 1 + row_h) {
|
|
struct kick_window *cur = tab_current_window(tab);
|
|
if (tab_out) *tab_out = tab;
|
|
if (win_out) *win_out = cur;
|
|
return cur != NULL;
|
|
}
|
|
}
|
|
row++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ---------- Standalone overlay rendering (wlr render pass) ----------
|
|
The standalone output paints via the wlroots GLES2 renderer, so the overlay
|
|
is drawn as a second pass on the same buffer directly with wlr textures.
|
|
Mirrors the layout computed by draw_tab_overlay() for the host-GL path. */
|
|
|
|
|
|
/* ===================== Server-side wlroots setup ===================== */
|
|
|
|
static void swapchain_resize(int width, int height) {
|
|
if (width < 50) width = 50;
|
|
if (height < 50) height = 50;
|
|
if (server.swapchain && server.swapchain->width == width
|
|
&& server.swapchain->height == height)
|
|
return;
|
|
static uint64_t mods[] = { DRM_FORMAT_MOD_LINEAR };
|
|
struct wlr_drm_format fmt = {
|
|
.format = DRM_FORMAT_ARGB8888,
|
|
.len = 1,
|
|
.capacity = 1,
|
|
.modifiers = mods,
|
|
};
|
|
if (server.swapchain) {
|
|
wlr_swapchain_destroy(server.swapchain);
|
|
server.swapchain = NULL;
|
|
}
|
|
server.swapchain = wlr_swapchain_create(server.allocator, width, height, &fmt);
|
|
if (!server.swapchain)
|
|
fprintf(stderr, "kick: swapchain create failed at %dx%d\n",
|
|
width, height);
|
|
}
|
|
|
|
/* Canvas size for sizing clients: the host window in nested mode (updated
|
|
synchronously on every host configure) or the native output standalone.
|
|
server.output lags host resizes until the next render applies them
|
|
(apply_pending_output_resize), so sizing a newly mapped/initial window
|
|
from the output can hand it a stale size with no reconfigure until the
|
|
next arrange -- a window mapping in that gap keeps the wrong size. */
|
|
static int canvas_width(void) {
|
|
if (!server.standalone)
|
|
return host.width;
|
|
return server.output ? server.output->width : host.width;
|
|
}
|
|
static int canvas_height(void) {
|
|
if (!server.standalone)
|
|
return host.height;
|
|
return server.output ? server.output->height : host.height;
|
|
}
|
|
|
|
static void output_resize(int width, int height) {
|
|
if (width < 50) width = 50;
|
|
if (height < 50) height = 50;
|
|
if (!server.output || !server.allocator) return;
|
|
|
|
/* Standalone has no host window tracking the size: the canvas follows
|
|
the request so the lazy modeset in apply_pending_output_resize
|
|
picks it up at render (nested is a no-op: the args already are the
|
|
host window's size). */
|
|
if (server.standalone) {
|
|
host.width = width;
|
|
host.height = height;
|
|
}
|
|
|
|
/* The new size is applied to the output lazily at render time (see
|
|
apply_pending_output_resize); here only the scene geometry and the
|
|
clients are told, so configure-time stays free of modesets and
|
|
swapchain reallocation. */
|
|
|
|
if (server.bg_rect)
|
|
wlr_scene_rect_set_size(server.bg_rect, width, height);
|
|
|
|
/* Size every mapped client into the area left after any bar's exclusive
|
|
zone (the layout pass applies the new full area before the output's
|
|
geometry is committed here, so tabs and the panel both track the
|
|
resize). A live-fullscreen entry keeps its own output-sized
|
|
configure, so maximize/size skip it to avoid state churn
|
|
(resize_tabs_to_usable checks per-window state). */
|
|
struct wlr_box full = { 0, 0, width, height };
|
|
layer_apply_layout_box(&full);
|
|
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* A host configure may resize the canvas; apply that size to the output
|
|
lazily, at render time, instead of modesetting and reallocating the
|
|
swapchain on every configure. Folding the custom mode into the pending
|
|
state makes wlr_scene build the frame at the new resolution and commit
|
|
mode+buffer in one atomic operation, so a drag costs one GBM alloc and one
|
|
commit per resized frame (no empty-buffer modeset churn in between).
|
|
Standalone outputs never change size here, so this is a no-op for them. */
|
|
static void apply_pending_output_resize(struct wlr_output_state *state) {
|
|
if (!server.output) return;
|
|
int w = host.width, h = host.height;
|
|
if (server.output->width == w && server.output->height == h) return;
|
|
swapchain_resize(w, h);
|
|
wlr_output_state_set_custom_mode(state, w, h, 0);
|
|
}
|
|
|
|
static void handle_output_frame(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
frame_pending = true;
|
|
/* No scheduling from this handler: wlr_output_schedule_frame re-emits
|
|
this same event via idle, so scheduling here can wedge dispatch. */
|
|
}
|
|
|
|
/* How many standalone outputs have been seen since the last (re)connect.
|
|
DRM destroys and re-creates the output when the session loses/regains its
|
|
VT, so this is reset to keep accepting the re-added connector. */
|
|
static int standalone_output_count;
|
|
|
|
/* DRM destroys the output when the session (VT) is lost. Clear our
|
|
references so nothing touches freed memory, detach the frame listener
|
|
(wlr_output_finish asserts it's empty), and let the connector re-added on
|
|
resume be accepted again. */
|
|
static void handle_output_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
(void)data;
|
|
wl_list_remove(&server.output_destroy.link);
|
|
wl_list_remove(&server.frame.link);
|
|
server.output = NULL;
|
|
server.scene_output = NULL; /* wlroots frees it via its output addon */
|
|
/* The overlay layer dies with the output (wlroots frees output->layers,
|
|
so destroy our handle too); the box-sized swapchain is ours and must
|
|
go. Re-probe the plane on the next output. */
|
|
if (server.overlay_layer) {
|
|
wlr_output_layer_destroy(server.overlay_layer);
|
|
server.overlay_layer = NULL;
|
|
}
|
|
server.overlay_layer_state = (struct wlr_output_layer_state){0};
|
|
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
|
|
server.overlay_render_lock = false;
|
|
server.overlay_cursor_lock = false;
|
|
if (server.overlay_swapchain) {
|
|
wlr_swapchain_destroy(server.overlay_swapchain);
|
|
server.overlay_swapchain = NULL;
|
|
}
|
|
if (server.swapchain) {
|
|
wlr_swapchain_destroy(server.swapchain);
|
|
server.swapchain = NULL;
|
|
}
|
|
standalone_output_count = 0;
|
|
frame_pending = false;
|
|
needs_redraw = false;
|
|
}
|
|
|
|
static void handle_new_output(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_output *output = data;
|
|
|
|
/* Standalone (multi-monitor allowed at the wlroots layer): drive only
|
|
the first output with the kick canvas, like the headless nested
|
|
single output. Later outputs are left disabled. */
|
|
if (server.standalone && standalone_output_count++ > 0) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"standalone: extra output ignored (first output only)\n");
|
|
return;
|
|
}
|
|
|
|
if (!wlr_output_init_render(output, server.allocator, server.renderer)) {
|
|
fprintf(stderr, "kick: output init_render failed\n");
|
|
return;
|
|
}
|
|
wlr_output_create_global(output, server.wl_display);
|
|
|
|
server.output = output;
|
|
server.scene_output = wlr_scene_output_create(server.scene, output);
|
|
wlr_scene_output_set_position(server.scene_output, 0, 0);
|
|
|
|
/* Always render the pointer into our own frame buffer (software cursor)
|
|
instead of a hardware cursor plane: the headless backend's set_cursor
|
|
hook makes wlroots believe it has a plane (so the scene software pass
|
|
is skipped) while actually drawing nothing, and a hardware plane would
|
|
float the cursor outside the buffer we composite/dump. */
|
|
/* Force software cursors only on headless outputs: their set_cursor
|
|
hook pretends a hardware plane exists while drawing nothing (so the
|
|
scene software pass gets skipped and dumps lose the pointer), and a
|
|
hardware plane would float outside the buffer we composite/dump.
|
|
Real DRM outputs have working hardware planes -- and locking them to
|
|
software cursoring would make direct scanout permanently ineligible
|
|
(the cursor must be composited), killing the zero-copy fullscreen
|
|
path this mode exists for. */
|
|
if (server.standalone && wlr_output_is_headless(output))
|
|
wlr_output_lock_software_cursors(output, true);
|
|
|
|
/* Standalone DRM output: the overlay gets one persistent wlr_output_layer
|
|
for the output's lifetime. libliftoff assigns it a real hardware
|
|
plane when the driver has one spare; keeping the same layer across
|
|
frames lets the allocation settle instead of re-solving on every
|
|
frame. The headless backend would claim every layer accepted but
|
|
never draw it, so it gets no layer and stays on the composite path.
|
|
Hiding the menu commits the layer with a NULL buffer, which clears
|
|
the plane (FB_ID 0); the layer object itself stays put. */
|
|
if (server.standalone && !server.standalone_headless) {
|
|
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
|
|
server.overlay_layer = wlr_output_layer_create(output);
|
|
server.overlay_layer_state = (struct wlr_output_layer_state){
|
|
.layer = server.overlay_layer,
|
|
.buffer = NULL,
|
|
};
|
|
if (!server.overlay_layer) {
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[render] overlay layer create failed, compositing\n");
|
|
}
|
|
if (server.overlay_swapchain) {
|
|
wlr_swapchain_destroy(server.overlay_swapchain);
|
|
server.overlay_swapchain = NULL;
|
|
}
|
|
}
|
|
|
|
/* A recreated output lost the old output's attach-render lock counter,
|
|
so recompute it from the current overlay state. overlay_sync checks
|
|
plane capability (UNKNOWN on a fresh output tries the plane, so no
|
|
lock) and covers both the tab list and the confirm-quit modal. */
|
|
server.overlay_render_lock = false;
|
|
server.overlay_cursor_lock = false;
|
|
overlay_sync_render_lock();
|
|
overlay_sync_cursor_lock();
|
|
|
|
/* Give the standalone cursor a place to live. Idempotent across VT
|
|
round-trips (wlr_output_layout_add reuses the existing entry), and the
|
|
cursor attaches its output cursor via the layout's add signal. The
|
|
nested (host-window) output is added too, so xdg-output's
|
|
get_xdg_output finds the output in the layout instead of asserting. */
|
|
if (server.output_layout)
|
|
wlr_output_layout_add(server.output_layout, output, 0, 0);
|
|
|
|
/* Render whenever the output is ready for a frame (refresh-rate driven),
|
|
like tinywl; this is what drives client frame callbacks / surface
|
|
enter events even before any damage has accumulated. */
|
|
server.frame.notify = handle_output_frame;
|
|
wl_signal_add(&output->events.frame, &server.frame);
|
|
server.output_destroy.notify = handle_output_destroy;
|
|
wl_signal_add(&output->events.destroy, &server.output_destroy);
|
|
|
|
/* Enable the output up front so buffer commits succeed even before the
|
|
host window has been configured. A native output drives at its
|
|
preferred mode; everything else keeps the fixed headless size. */
|
|
int w = output->width > 0 ? output->width : host.width;
|
|
int h = output->height > 0 ? output->height : host.height;
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
wlr_output_state_set_enabled(&state, true);
|
|
struct wlr_output_mode *mode = NULL;
|
|
if (server.standalone && !server.standalone_headless) {
|
|
if (mode_req_w > 0) {
|
|
/* --screen-mode WxH[@Hz]: pick an exact-resolution mode, highest
|
|
refresh when none was requested. A request the output can't
|
|
satisfy is a startup error: print every reported mode and exit
|
|
(before any client launches or the socket is advertised). */
|
|
struct wlr_output_mode *m;
|
|
int best_refresh = 0;
|
|
wl_list_for_each(m, &output->modes, link) {
|
|
if (m->width != mode_req_w || m->height != mode_req_h)
|
|
continue;
|
|
if (mode_req_refresh > 0) {
|
|
if (m->refresh == mode_req_refresh) {
|
|
mode = m;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (m->refresh > best_refresh) {
|
|
best_refresh = m->refresh;
|
|
mode = m;
|
|
}
|
|
}
|
|
if (!mode) {
|
|
fprintf(stderr, "kick: no %dx%d",
|
|
mode_req_w, mode_req_h);
|
|
if (mode_req_refresh > 0)
|
|
fprintf(stderr, "@%u.%03u", mode_req_refresh / 1000,
|
|
mode_req_refresh % 1000);
|
|
if (wl_list_empty(&output->modes))
|
|
fprintf(stderr, " mode; output reports no modes\n");
|
|
else
|
|
fprintf(stderr, " mode; available:\n");
|
|
wl_list_for_each(m, &output->modes, link) {
|
|
fprintf(stderr, " %dx%d@%u.%03u%s%s\n",
|
|
m->width, m->height, m->refresh / 1000,
|
|
m->refresh % 1000,
|
|
m->preferred ? " (preferred)" : "",
|
|
(m->width == mode_req_w && m->height == mode_req_h)
|
|
? " (requested size)" : "");
|
|
}
|
|
exit(1);
|
|
}
|
|
} else {
|
|
mode = wlr_output_preferred_mode(output);
|
|
}
|
|
}
|
|
if (mode) {
|
|
w = mode->width;
|
|
h = mode->height;
|
|
wlr_output_state_set_mode(&state, mode);
|
|
} else {
|
|
wlr_output_state_set_custom_mode(&state, w, h, 0);
|
|
}
|
|
wlr_output_commit_state(output, &state);
|
|
wlr_output_state_finish(&state);
|
|
|
|
/* Overlay geometry and the swapchain are sized to the visible canvas;
|
|
in standalone there is no host window, so use the output size itself. */
|
|
if (server.standalone) {
|
|
host.width = w;
|
|
host.height = h;
|
|
}
|
|
swapchain_resize(w, h);
|
|
|
|
/* An opaque full-output background node gives the scene a deterministic
|
|
backdrop so the nested frame is never transparent. (Direct scanout is
|
|
disabled separately above; a rect alone cannot prevent it because
|
|
wlroots culls a rect that a covering client surface occludes, leaving
|
|
a single render-list entry which is exactly what triggers scanout.)
|
|
On a re-connected output (VT round-trip) the node already exists; just
|
|
resize it. */
|
|
if (server.bg_rect) {
|
|
wlr_scene_rect_set_size(server.bg_rect, w, h);
|
|
} else {
|
|
server.bg_rect = wlr_scene_rect_create(&server.scene->tree, w, h,
|
|
(float[]){ 0.0f, 0.0f, 0.0f, 1.0f });
|
|
if (server.bg_rect)
|
|
wlr_scene_node_lower_to_bottom(&server.bg_rect->node);
|
|
}
|
|
|
|
/* Publish the fresh output to management clients (kanshi/wdisplays
|
|
learn the head and its modes/size on bind). */
|
|
output_mgmt_publish();
|
|
}
|
|
|
|
/* Nearest live tab, scanning right first (prefer_right) or left first.
|
|
The scan stops at the list ends -- it never wraps -- so the reflow after
|
|
a tab is removed matches the old first/last-tab behavior. NULL when
|
|
`tab` is the only live slot. */
|
|
static struct kick_tab *pick_successor(struct kick_tab *tab,
|
|
bool prefer_right) {
|
|
if (!tab) return NULL;
|
|
struct kick_tab *t;
|
|
if (prefer_right) {
|
|
/* Tabs after `tab` first (immediate right neighbor first), then the
|
|
ones before it (immediate left neighbor first -- no wrap to the far
|
|
end). */
|
|
bool past = false;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == tab) { past = true; continue; }
|
|
if (!past) continue;
|
|
if (tab_live(t) && !tab_closing(t)) return t;
|
|
}
|
|
past = false;
|
|
wl_list_for_each_reverse(t, &server.tabs, link) {
|
|
if (t == tab) { past = true; continue; }
|
|
if (!past) continue;
|
|
if (tab_live(t) && !tab_closing(t)) return t;
|
|
}
|
|
} else {
|
|
/* Tabs before `tab` first (immediate left neighbor first), then the
|
|
ones after it (immediate right neighbor first -- no wrap). */
|
|
bool past = false;
|
|
wl_list_for_each_reverse(t, &server.tabs, link) {
|
|
if (t == tab) { past = true; continue; }
|
|
if (!past) continue;
|
|
if (tab_live(t) && !tab_closing(t)) return t;
|
|
}
|
|
past = false;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == tab) { past = true; continue; }
|
|
if (!past) continue;
|
|
if (tab_live(t) && !tab_closing(t)) return t;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Detach all xdg-shell signal listeners from a window without invoking any
|
|
destroy handler or refocus logic. Used by both the normal destroy path
|
|
and the forceful remove_tab() cleanup. */
|
|
static void window_detach_xdg_listeners(struct kick_window *w) {
|
|
wl_list_remove(&w->destroy.link);
|
|
wl_list_remove(&w->map.link);
|
|
wl_list_remove(&w->unmap.link);
|
|
wl_list_remove(&w->commit.link);
|
|
wl_list_remove(&w->set_title.link);
|
|
wl_list_remove(&w->set_app_id.link);
|
|
wl_list_remove(&w->request_maximize.link);
|
|
wl_list_remove(&w->request_fullscreen.link);
|
|
wl_list_remove(&w->request_minimize.link);
|
|
wl_list_remove(&w->request_move.link);
|
|
wl_list_remove(&w->request_resize.link);
|
|
if (w->dialog_wired) {
|
|
wl_list_remove(&w->dialog_set_modal.link);
|
|
wl_list_remove(&w->dialog_destroy.link);
|
|
w->dialog_wired = false;
|
|
}
|
|
}
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* Same for an XWayland window: the claim-time listener set (destroy,
|
|
associate/dissociate, ICCCM requests, set_title/set_class) plus the
|
|
association-wired surface map/unmap/commit. */
|
|
/* Surface-only detach for X-server death: the wlr_surfaces die with the
|
|
server's Wayland connection while their xsurfaces (and our
|
|
xsurface-level listeners, which the later xsurface destroy still needs
|
|
for tab teardown) survive. Idempotent. */
|
|
static void window_detach_x11_surface(struct kick_window *w) {
|
|
if (w->xs_surface_linked) {
|
|
wl_list_remove(&w->map.link);
|
|
wl_list_remove(&w->unmap.link);
|
|
wl_list_remove(&w->commit.link);
|
|
w->xs_surface_linked = false;
|
|
}
|
|
}
|
|
|
|
static void window_detach_x11_listeners(struct kick_window *w) {
|
|
wl_list_remove(&w->destroy.link);
|
|
wl_list_remove(&w->xs_associate.link);
|
|
wl_list_remove(&w->xs_dissociate.link);
|
|
wl_list_remove(&w->request_maximize.link);
|
|
wl_list_remove(&w->request_fullscreen.link);
|
|
wl_list_remove(&w->request_minimize.link);
|
|
wl_list_remove(&w->xs_request_configure.link);
|
|
wl_list_remove(&w->xs_request_activate.link);
|
|
wl_list_remove(&w->xs_request_close.link);
|
|
wl_list_remove(&w->xs_set_title.link);
|
|
wl_list_remove(&w->xs_set_class.link);
|
|
window_detach_x11_surface(w);
|
|
}
|
|
#endif
|
|
|
|
/* ============= Focus and show (unified tab/window model) ============= */
|
|
|
|
/* The tab's position in server.tabs, leftmost first (0-based). Used for
|
|
logs and diagnostics only; ordering decisions never index the list. */
|
|
static int tab_index(struct kick_tab *tab) {
|
|
int i = 0;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == tab) return i;
|
|
i++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* The window a tab's keyboard sits on: the focused window while it is still
|
|
mapped, else the newest mapped window, else the first window in the list
|
|
(a still-black pending placeholder, if any). NULL only for an empty tab. */
|
|
static struct kick_window *tab_current_window(struct kick_tab *tab) {
|
|
if (!tab) return NULL;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->focused && w->mapped)
|
|
return w;
|
|
struct kick_window *newest = NULL;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->mapped || window_live(w))
|
|
newest = w;
|
|
if (newest) return newest;
|
|
return tab_first_window(tab);
|
|
}
|
|
|
|
/* Make `w` (or none, when NULL) the only drawn window: disable every window's
|
|
scene tree across all tabs, then enable exactly `w`'s tree. Overlay
|
|
windows and popups are parented elsewhere and untouched. */
|
|
static void show_window(struct kick_window *w) {
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *it;
|
|
wl_list_for_each(it, &t->windows, link)
|
|
if (it->tree)
|
|
wlr_scene_node_set_enabled(&it->tree->node, it == w);
|
|
}
|
|
}
|
|
|
|
/* Give a window of the active tab the keyboard, demoting whatever held it
|
|
before (an earlier window of the same tab). The entered window becomes
|
|
the only drawn one. A window with no surface yet (a still-black pending
|
|
window) still becomes the tab's focused/current window; when it later
|
|
maps, window_enter runs again (its own tree is still disabled) to show
|
|
and activate it. */
|
|
static void window_enter(struct kick_window *w) {
|
|
/* While the session is locked nothing may take keyboard focus from the
|
|
lock surface (which is focused directly, outside the tab model); the
|
|
lock restore path clears session_locked before refocusing. */
|
|
if (!w || server.session_locked) return;
|
|
/* MRU bookkeeping: with the prefix up the entered window is the new most
|
|
recent; with the prefix held it is only a preview -- the stack must
|
|
not move until the prefix is released, but the session counts as
|
|
navigated when the preview leaves its anchor. */
|
|
if (host.overlay_visible) {
|
|
if (w != server.mru_alt_anchor)
|
|
server.mru_alt_navigated = true;
|
|
} else {
|
|
mru_move_to_front(w);
|
|
}
|
|
struct kick_tab *tab = w->tab;
|
|
/* A genuine no-op: already focused AND already drawn. The black ->
|
|
mapped promotion below is the one same-window case with something
|
|
still to do (focused but not yet shown). */
|
|
if (w->focused && w->tree && w->tree->node.enabled) return;
|
|
struct kick_window *it;
|
|
wl_list_for_each(it, &tab->windows, link)
|
|
if (it != w && it->focused) {
|
|
window_set_activated(it, false);
|
|
it->focused = false;
|
|
}
|
|
w->focused = true;
|
|
struct wlr_surface *surface = window_surface(w);
|
|
if (surface) {
|
|
window_set_activated(w, true);
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat, surface,
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
/* Seat keyboard focus is what routes keys to the client; map alone
|
|
does not guarantee it has arrived (enter can lag map under
|
|
load), so tests tapping keys wait on this line, not the map. */
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] kbd-enter app=%d\n",
|
|
tab_index(tab));
|
|
}
|
|
show_window(w->tree ? w : NULL);
|
|
/* The shown entry may have changed (e.g. onto/out of a fullscreen
|
|
window): bring panels + layout in line. Guarded no-op unless the
|
|
shown-fullscreen state actually flipped. */
|
|
fullscreen_sync_shown();
|
|
/* Single --focus-priority hook: the active window just changed (including
|
|
each Alt+Tab preview while the overlay is held). */
|
|
apply_window_priorities(w);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Make `tab` the active tab: hide the old tab's windows, then put the
|
|
keyboard on the new tab's current window (returning to a tab lands back on
|
|
the window that was focused when we left it). A black tab activates its
|
|
still-black pending window with no surface to enter; the keyboard focus is
|
|
dropped instead. Called with the same tab as already active only to
|
|
promote it black -> mapped (the one case with something still to do), so
|
|
callers that want a true no-op must guard first. */
|
|
static void set_active_tab(struct kick_tab *tab) {
|
|
if (!tab || server.session_locked) return;
|
|
struct kick_tab *old = server.active_tab;
|
|
if (old && old != tab) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &old->windows, link) {
|
|
if (w->focused)
|
|
window_set_activated(w, false);
|
|
if (w->tree)
|
|
wlr_scene_node_set_enabled(&w->tree->node, false);
|
|
}
|
|
}
|
|
server.active_tab = tab;
|
|
|
|
struct kick_window *enter = tab_current_window(tab);
|
|
if (enter) {
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
struct wlr_surface *s = window_surface(enter);
|
|
kick_log(KICK_LOG_DEBUG, "[diag] active app=%d surface=%dx%d "
|
|
"node on\n", tab_index(tab),
|
|
s ? s->current.width : 0, s ? s->current.height : 0);
|
|
}
|
|
window_enter(enter);
|
|
/* Landed on a stacked --tab-groups member (the tab's own window is
|
|
the member of a shown parent tab): handing the tab the keyboard
|
|
focuses the member, announced like the map path does. */
|
|
if (enter->origin == ORIGIN_GROUP && enter->tab == enter->parent_tab)
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(tab));
|
|
(void)tab_row_title(tab);
|
|
} else {
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] active slot=%d (black, awaiting "
|
|
"map)\n", tab_index(tab));
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
show_window(NULL);
|
|
}
|
|
|
|
/* The newly-active tab owns the cursor shape: apply its last requested
|
|
shape (or default when it never asked), so a switched-from tab's shape
|
|
never bleeds onto the next tab. */
|
|
apply_cursor_shape(enter && enter->cursor_shape > 0
|
|
? enter->cursor_shape
|
|
: WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
|
/* Landing on another tab may change the shown-fullscreen state (onto or
|
|
out of a fullscreen entry, or onto a black tab under a fullscreen
|
|
host): panels + layout follow. Guarded no-op otherwise. */
|
|
fullscreen_sync_shown();
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* After the focused window of the active tab unmaps or dies, hand the
|
|
keyboard back to the tab's current window. No-op unless the tab is the
|
|
active one. */
|
|
static void refocus_tab(struct kick_tab *tab) {
|
|
if (!tab || tab != server.active_tab) return;
|
|
struct kick_window *top = tab_current_window(tab);
|
|
if (top) {
|
|
kick_log(KICK_LOG_INFO, "[sub] refocus app=%d\n", tab_index(tab));
|
|
window_enter(top);
|
|
} else {
|
|
show_window(NULL);
|
|
kick_request_redraw();
|
|
}
|
|
}
|
|
|
|
/* Drop `tab` outright: cancel its close timers, sever any other tab's
|
|
parent_tab reference to it, free every remaining window (detaching
|
|
listeners so wlroots' destroy-time asserts and a re-claim into a reused
|
|
window never see stale links), and unlink the tab from server.tabs. The
|
|
caller must already have handed the active crown elsewhere; remove_tab
|
|
only defends active_tab against a tab that was somehow still on it. */
|
|
static void remove_tab(struct kick_tab *tab) {
|
|
if (!tab) return;
|
|
if (server.active_tab == tab)
|
|
server.active_tab = NULL;
|
|
/* An override-redirect menu may have stashed this tab as its focus
|
|
owner (or_handle_map); the menu outlives the tab when the tab is
|
|
closed first. Clear it so the later focus release drops keyboard
|
|
focus instead of activating freed memory. */
|
|
if (server.or_focus_owner == tab)
|
|
server.or_focus_owner = NULL;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == tab) continue;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link)
|
|
if (w->parent_tab == tab)
|
|
w->parent_tab = NULL;
|
|
}
|
|
struct kick_window *w, *wt;
|
|
wl_list_for_each_safe(w, wt, &tab->windows, link) {
|
|
if (w->close_timer) {
|
|
wl_event_source_remove(w->close_timer);
|
|
w->close_timer = NULL;
|
|
}
|
|
if (w->icon_wlr)
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
icon_proto_release(w);
|
|
if (w->toplevel)
|
|
window_detach_xdg_listeners(w);
|
|
#if HAVE_XWAYLAND
|
|
if (w->is_x11)
|
|
window_detach_x11_listeners(w);
|
|
#endif
|
|
mru_remove(w);
|
|
free(w->cmd);
|
|
free(w->tag);
|
|
free(w->tag_desc);
|
|
free(w);
|
|
}
|
|
wl_list_remove(&tab->link);
|
|
free(tab);
|
|
}
|
|
|
|
/* Remove a tab for good: if it is the active tab, hand the crown to a
|
|
neighbor (preferring left, so the tab that was second becomes the new
|
|
leftmost); when the last tab goes, the session tears down here, since
|
|
nothing else will. Then the tab itself is removed: closed tabs leave no
|
|
hole. */
|
|
static void drop_tab(struct kick_tab *tab) {
|
|
if (server.active_tab == tab) {
|
|
server.active_tab = NULL;
|
|
struct kick_tab *succ = pick_successor(tab, 0);
|
|
if (succ) {
|
|
set_active_tab(succ);
|
|
} else {
|
|
/* last tab closed: close the host window (nested only) and exit */
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
}
|
|
remove_tab(tab);
|
|
}
|
|
|
|
/* Shared tail of a window's destroy handling (xdg and X11 alike): the
|
|
per-kind handlers have already detached their own listeners; here we drop
|
|
the window from its tab, free it, hand the keyboard to the tab's remaining
|
|
current window if this one held it, and remove the tab when it empties
|
|
(which moves the active crown via drop_tab). */
|
|
static void window_destroy_common(struct kick_window *w) {
|
|
struct kick_tab *tab = w->tab;
|
|
wl_list_remove(&w->link);
|
|
kick_log(KICK_LOG_INFO, "[sub] unstack app=%d\n", tab_index(tab));
|
|
if (w->close_timer) {
|
|
wl_event_source_remove(w->close_timer);
|
|
w->close_timer = NULL;
|
|
}
|
|
bool had_focus = w->focused;
|
|
bool was_fs = window_is_fullscreen(w);
|
|
/* Tear down the taskbar handles so taskbars drop the entry now. */
|
|
foreign_handle_destroy(w);
|
|
free(w->tag);
|
|
free(w->tag_desc);
|
|
if (w->icon_wlr)
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
icon_proto_release(w);
|
|
mru_remove(w);
|
|
free(w);
|
|
if (wl_list_empty(&tab->windows)) {
|
|
drop_tab(tab);
|
|
/* Host chrome still fullscreen but its client is gone: hand
|
|
fullscreen to the successor so host and client stay in sync. */
|
|
if (host.fullscreen && server.active_tab) {
|
|
struct kick_window *nw = tab_current_window(server.active_tab);
|
|
if (nw && !window_is_fullscreen(nw) && window_initialized(nw))
|
|
window_set_fullscreen(nw, true);
|
|
}
|
|
(void)was_fs;
|
|
fullscreen_sync_aggregate();
|
|
return;
|
|
}
|
|
if (had_focus)
|
|
refocus_tab(tab);
|
|
if (was_fs) {
|
|
/* Fullscreen window closed itself: if the host chrome is still up,
|
|
keep a client fullscreen on this tab; otherwise just restore. */
|
|
if (host.fullscreen && tab == server.active_tab) {
|
|
struct kick_window *nw = tab_current_window(tab);
|
|
if (nw && !window_is_fullscreen(nw) && window_initialized(nw))
|
|
window_set_fullscreen(nw, true);
|
|
}
|
|
fullscreen_sync_aggregate();
|
|
}
|
|
/* The topmost window changed (a stacked member left): re-resolve the row
|
|
title now that focus has settled, so the fallback is log-observable
|
|
without an overlay draw. */
|
|
(void)tab_row_title(tab);
|
|
}
|
|
|
|
/* A window's xdg toplevel surface died. The scene node dies with the xdg
|
|
surface; drop the pointers, detach every listener we attached (wlroots
|
|
asserts the event lists are empty afterwards), and run the shared teardown
|
|
tail. This is the unified handler for what used to be the "main surface"
|
|
and the stacked tab-group members alike. */
|
|
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, destroy);
|
|
w->toplevel = NULL;
|
|
w->tree = NULL;
|
|
|
|
/* The overlay's row for this tab disappears the moment the toplevel
|
|
does, shrinking the box mid-menu; the destroy's own damage schedules
|
|
the repaint that shrinks it. */
|
|
window_detach_xdg_listeners(w);
|
|
window_destroy_common(w);
|
|
}
|
|
|
|
/* Grace period after the Alt+Delete SIGTERM before SIGKILL escalation. */
|
|
#define KICK_CLOSE_ESCALATE_MS 150
|
|
|
|
/* Escalate an Alt+Delete close to SIGKILL if the client ignored the SIGTERM
|
|
and its toplevel is still up. Live windows are removed at toplevel
|
|
destroy (which also cancels this timer), so for a mapped window the callback
|
|
never fires against a reaped or reused pid. For a window with no live
|
|
surface (a still-black pending window) there is no toplevel whose destroy
|
|
would reap it, so its removal happens here, right after the SIGKILL. */
|
|
static int close_window_escalate(void *data) {
|
|
struct kick_window *w = data;
|
|
if (w->closing && window_killable(w))
|
|
kill_window(w, SIGKILL);
|
|
if (w->close_timer) {
|
|
wl_event_source_remove(w->close_timer);
|
|
w->close_timer = NULL;
|
|
}
|
|
if (w->closing && !window_live(w))
|
|
window_destroy_common(w);
|
|
return 0;
|
|
}
|
|
|
|
/* Window close (Alt+Delete on the active window, or an X11 window's close
|
|
request). A window that still has siblings closes on its own: the tab
|
|
stays up and the destroy path refocuses the tab. The last window of a tab
|
|
closes the whole tab: the active crown moves to the right neighbor (or the
|
|
left neighbor if the closed tab was the rightmost) in this same call. The
|
|
client's whole process group gets SIGTERM, then SIGKILL after a short
|
|
grace if it ignores the TERM (see close_window_escalate); X11 windows also
|
|
get WM_DELETE_WINDOW first. A closing black window (still pending) drops
|
|
out of the overlay and cycle right away and is removed by the escalation; a
|
|
closing last tab tears the session down here, since nothing else will. */
|
|
static void close_window(struct kick_window *w) {
|
|
if (!w || !window_killable(w)) return;
|
|
pid_t g = window_signal_group(w);
|
|
pid_t shown = (g > 0 && g != getpgid(0)) ? g : w->client_pid;
|
|
|
|
struct kick_tab *tab = w->tab;
|
|
w->closing = true;
|
|
int died = tab_index(tab);
|
|
|
|
/* A black window never fires a toplevel destroy, so drop it from the
|
|
overlay/cycle immediately; only the escalation timer and the
|
|
success-removal paths touch it afterwards. Clearing pending_claim
|
|
also retires the slot for good: a client connecting inside the grace
|
|
period can no longer claim this window (claim matching requires a
|
|
pending slot), so it lands on a group/external tab instead and no
|
|
graceful close is owed here -- the escalation reaps this window. */
|
|
if (!window_live(w))
|
|
w->pending_claim = false;
|
|
|
|
/* Whether this window is the tab's only one: closing it is closing the
|
|
whole tab, which moves the crown right now. A window with siblings
|
|
closes on its own; the tab and the crown stay put. */
|
|
bool sole = wl_list_length(&tab->windows) == 1;
|
|
|
|
if (sole) {
|
|
struct kick_tab *succ = pick_successor(tab, true);
|
|
if (succ) {
|
|
set_active_tab(succ);
|
|
} else {
|
|
/* Last live tab: tear the session down now. */
|
|
kick_log(KICK_LOG_INFO, "kick: closing tab %d (pid %d)\n",
|
|
died, (int)shown);
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
/* A stacked --tab-groups member shares its client (and process group)
|
|
with its parent tab's own window: closing it must only ask the
|
|
client to close that one surface -- never signal the shared process
|
|
group, or the tab's owner dies with it. The member's destroy path
|
|
(window_destroy_common) unstacks it and refocuses the tab. */
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab) {
|
|
window_graceful_close(w);
|
|
return;
|
|
}
|
|
|
|
/* X11 windows get WM_DELETE_WINDOW first; the SIGTERM below (and the
|
|
SIGKILL escalation) remains the universal fallback for both kinds. */
|
|
window_graceful_close(w);
|
|
kill_window(w, SIGTERM);
|
|
kick_log(KICK_LOG_INFO, "kick: closing tab %d (pid %d)\n",
|
|
died, (int)shown);
|
|
w->close_timer = wl_event_loop_add_timer(server.loop, close_window_escalate,
|
|
w);
|
|
if (w->close_timer)
|
|
wl_event_source_timer_update(w->close_timer, KICK_CLOSE_ESCALATE_MS);
|
|
}
|
|
|
|
/* Alt+Delete on the active tab closes the active WINDOW, not the whole tab:
|
|
the focused window is asked to close on its own and the tab stays up; only
|
|
when the tab holds a single window does Delete take the whole tab down.
|
|
The window's own destroy path (window_destroy_common) refocuses the tab,
|
|
just as if the client had closed it directly. */
|
|
static void close_active_window(void) {
|
|
struct kick_tab *tab = server.active_tab;
|
|
if (!tab) return;
|
|
struct kick_window *w = tab_current_window(tab);
|
|
if (!w) return;
|
|
close_window(w);
|
|
}
|
|
|
|
/* Maximize/minimize requests reinterpreted for a compositor where every
|
|
tab lives maximized: the middle button can only toggle fullscreen.
|
|
Client buttons are state-dependent -- on a maximized window the button
|
|
shows restore (unset), not maximize -- so both directions on an
|
|
already-maximized window mean fullscreen, and both directions on a
|
|
fullscreen window step back down. A request arriving before the client
|
|
ever acked maximized (e.g. a session-restored startup request) stays
|
|
plain maximize, so fresh windows never open fullscreened. Minimize
|
|
anywhere else is ignored: there is nothing smaller. */
|
|
static void handle_request_maximize(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_maximize);
|
|
struct wlr_xdg_toplevel *toplevel = w->toplevel;
|
|
kick_log(KICK_LOG_DEBUG, "[xdg] maximize request app=%d\n",
|
|
tab_index(w->tab));
|
|
if (!toplevel->base->initialized) return;
|
|
if (window_is_fullscreen(w)) {
|
|
client_exit_fullscreen(w);
|
|
return;
|
|
}
|
|
if (!window_is_maximized(w)) {
|
|
wlr_xdg_toplevel_set_maximized(toplevel, true);
|
|
return;
|
|
}
|
|
client_enter_fullscreen(w);
|
|
}
|
|
|
|
/* Queue a host-window fullscreen change until the present pipeline has
|
|
drained. labwc applies an acked configure at the next wl_surface commit;
|
|
while tabs animate, an eglSwapBuffers that started before the resize can
|
|
complete after our ack and marshal a stale-size frame post-ack -- the new
|
|
layout is then anchored to the old geometry ("fullscreen rooted where the
|
|
window was", margins top/left, cropped bottom/right). Waiting for the
|
|
frame callback proves the last swap reached the screen, so nothing stale
|
|
can follow the ack. Rapid requests collapse to the latest target. */
|
|
/* Apply a fullscreen change to the kick window on the active backend:
|
|
direct path talks xdg-shell immediately (its ack precedes every commit
|
|
deterministically, so labwc's deadline race cannot anchor stale state);
|
|
libdecor path queues until the present pipeline drains, for the same
|
|
reason. */
|
|
static void host_apply_fullscreen(bool fs) {
|
|
if (!host.use_libdecor) {
|
|
if (!host.toplevel) return; /* standalone has no host window */
|
|
if (fs)
|
|
xdg_toplevel_set_fullscreen(host.toplevel, NULL);
|
|
else
|
|
xdg_toplevel_unset_fullscreen(host.toplevel);
|
|
return;
|
|
}
|
|
if (!host.frame) return; /* standalone has no host window */
|
|
if (host.present_pending) {
|
|
host.fs_queued = true;
|
|
host.fs_target = fs;
|
|
return;
|
|
}
|
|
if (fs)
|
|
libdecor_frame_set_fullscreen(host.frame, NULL);
|
|
else
|
|
libdecor_frame_unset_fullscreen(host.frame);
|
|
}
|
|
|
|
/* A client-side fullscreen request for one entry: a real fullscreen
|
|
request or a reinterpreted maximize (maximized is the baseline, so a
|
|
maximize toggle is only meaningful as fullscreen). Only the requesting
|
|
overlay entry is told fullscreen; a shown entry also takes the host
|
|
chrome with it (nested), a hidden entry is just marked and stays hidden.
|
|
Exiting always drops the host chrome again (simple mirror). Shared by
|
|
the xdg, X11 and taskbar request paths. */
|
|
static void client_enter_fullscreen(struct kick_window *w) {
|
|
window_apply_fullscreen(w, true);
|
|
if (server.standalone) return;
|
|
if (window_is_shown(w))
|
|
host_apply_fullscreen(true);
|
|
}
|
|
|
|
static void client_exit_fullscreen(struct kick_window *w) {
|
|
window_apply_fullscreen(w, false);
|
|
if (server.standalone) return;
|
|
host_apply_fullscreen(false);
|
|
}
|
|
|
|
static void handle_request_fullscreen(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_fullscreen);
|
|
struct wlr_xdg_toplevel *toplevel = w->toplevel;
|
|
if (!toplevel->base->initialized) return;
|
|
/* wlroots 0.19 emits request_fullscreen with NULL data; the direction
|
|
lives in toplevel->requested.fullscreen. */
|
|
if (toplevel->requested.fullscreen)
|
|
client_enter_fullscreen(w);
|
|
else
|
|
client_exit_fullscreen(w);
|
|
}
|
|
|
|
/* Whether any live window holds fullscreen state (visible or hidden). */
|
|
static bool any_window_fullscreen(void) {
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link) {
|
|
if (window_is_fullscreen(w)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Whether the currently shown overlay entry is fullscreened: the active
|
|
tab's current window holding live fullscreen state -- or, when the
|
|
active tab is still black (no initialized window to judge), the host
|
|
chrome being fullscreen (Alt+F11 landed before the client mapped;
|
|
standalone has no host chrome, so a black tab there never counts). */
|
|
static bool shown_is_fullscreen(void) {
|
|
struct kick_tab *at = server.active_tab;
|
|
struct kick_window *w = at ? tab_current_window(at) : NULL;
|
|
if (w && window_initialized(w))
|
|
return window_is_fullscreen(w);
|
|
return host.fullscreen;
|
|
}
|
|
|
|
/* Recompute panel visibility + layout from the shown entry. Acts only
|
|
when the shown-fullscreen state changed, so tab switches among
|
|
same-state tabs (and overlay previews that return) send no extra
|
|
configures. */
|
|
static void fullscreen_sync_shown(void) {
|
|
bool shown = shown_is_fullscreen();
|
|
if (shown == server.shown_fullscreen)
|
|
return;
|
|
server.shown_fullscreen = shown;
|
|
kick_log(KICK_LOG_INFO, "[layout] shown fullscreen=%d\n", shown);
|
|
layer_update_visibility();
|
|
layer_apply_layout();
|
|
}
|
|
|
|
/* Recompute the aggregate fullscreen status logs, then bring visibility +
|
|
layout in line with the shown entry. */
|
|
static void fullscreen_sync_aggregate(void) {
|
|
bool agg = host.fullscreen || any_window_fullscreen();
|
|
if (agg != server.clients_fullscreen) {
|
|
server.clients_fullscreen = agg;
|
|
kick_log(KICK_LOG_INFO, "clients fullscreen=%d\n", agg);
|
|
kick_log(KICK_LOG_INFO, "members fullscreen=%d\n", agg);
|
|
}
|
|
fullscreen_sync_shown();
|
|
}
|
|
|
|
/* Whether a window is the currently shown overlay entry. */
|
|
static bool window_is_shown(struct kick_window *w) {
|
|
return w && w->tab == server.active_tab &&
|
|
w == tab_current_window(server.active_tab);
|
|
}
|
|
|
|
/* Apply a per-window fullscreen change and sync the aggregate. Hidden
|
|
windows are still told (protocol ack) so the state is live on return. */
|
|
static void window_apply_fullscreen(struct kick_window *w, bool fs) {
|
|
if (!w) {
|
|
fullscreen_sync_aggregate();
|
|
return;
|
|
}
|
|
if (!window_initialized(w)) {
|
|
fullscreen_sync_aggregate();
|
|
return;
|
|
}
|
|
bool cur = window_is_fullscreen(w);
|
|
window_set_fullscreen(w, fs);
|
|
if (cur != fs)
|
|
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs);
|
|
fullscreen_sync_aggregate();
|
|
}
|
|
|
|
/* Propagate a host-window fullscreen transition onto clients per entry.
|
|
Enter: fullscreen the focused overlay entry only (unless one already
|
|
holds fullscreen -- e.g. the client-mirrored echo arriving after the
|
|
request already flagged it, where re-flagging could hit the wrong
|
|
entry if focus moved mid-flight). A black (pending) active tab keeps
|
|
no protocol state to set; it picks fullscreen up at map via
|
|
host.fullscreen (see the initial-commit and map paths). Exit: clear
|
|
every live window and re-maximize. Visibility + layout then follow
|
|
the shown entry via the sync. */
|
|
static void clients_set_fullscreen(bool fs) {
|
|
if (fs) {
|
|
if (!any_window_fullscreen()) {
|
|
struct kick_tab *at = server.active_tab;
|
|
struct kick_window *w = at ? tab_current_window(at) : NULL;
|
|
if (w && window_initialized(w)) {
|
|
window_set_fullscreen(w, true);
|
|
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", true);
|
|
}
|
|
}
|
|
fullscreen_sync_aggregate();
|
|
return;
|
|
}
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link) {
|
|
if (window_is_fullscreen(w) && window_initialized(w))
|
|
window_set_fullscreen(w, false);
|
|
if (server.output &&
|
|
(w->toplevel || w->origin != ORIGIN_GROUP ||
|
|
w->tab != w->parent_tab)) {
|
|
window_set_maximized(w, true);
|
|
window_set_size(w, canvas_width(), canvas_height());
|
|
}
|
|
}
|
|
}
|
|
fullscreen_sync_aggregate();
|
|
}
|
|
|
|
/* Toggle fullscreen for the shown overlay entry. Nested flips the host
|
|
window chrome only when the chrome disagrees with the entry: entering
|
|
flags the focused entry directly and raises the chrome if it isn't up
|
|
(the configure echo then finds an entry already flagged and does not
|
|
re-flag); exiting drops the chrome and the configure-exit clears all.
|
|
Keyed off the entry, not the chrome, so Alt+F11 on a normal entry
|
|
fullscreen it (keeping an already-fullscreen chrome) instead of
|
|
shrinking the host back. Standalone has no host window, so flip the
|
|
focused entry directly. */
|
|
static void host_toggle_fullscreen(void) {
|
|
if (server.standalone) {
|
|
struct kick_tab *at = server.active_tab;
|
|
struct kick_window *w = at ? tab_current_window(at) : NULL;
|
|
if (w)
|
|
window_apply_fullscreen(w, !window_is_fullscreen(w));
|
|
else
|
|
fullscreen_sync_aggregate();
|
|
return;
|
|
}
|
|
struct kick_tab *at = server.active_tab;
|
|
struct kick_window *w = at ? tab_current_window(at) : NULL;
|
|
if (w && window_is_fullscreen(w)) {
|
|
host_apply_fullscreen(false);
|
|
return;
|
|
}
|
|
if (w)
|
|
window_apply_fullscreen(w, true);
|
|
if (!host.fullscreen)
|
|
host_apply_fullscreen(true);
|
|
}
|
|
|
|
static void handle_request_minimize(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_minimize);
|
|
/* Minimize steps a fullscreen entry back down to maximized; anywhere
|
|
else there is nothing smaller, so it stays ignored. */
|
|
if (w->toplevel && window_is_fullscreen(w)) {
|
|
client_exit_fullscreen(w);
|
|
return;
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[xdg] minimize request ignored\n");
|
|
}
|
|
|
|
static void handle_request_move(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[xdg] move request ignored\n");
|
|
}
|
|
|
|
static void handle_request_resize(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[xdg] resize request ignored\n");
|
|
}
|
|
|
|
/* Place a scene tree so the client's content sits at 0,0. Shared by tabs
|
|
and stacked tab-group members. */
|
|
static void position_tree(struct wlr_xdg_toplevel *toplevel,
|
|
struct wlr_scene_tree *tree) {
|
|
if (!tree) return;
|
|
/* CSD clients (foot/kitty) reserve a titlebar gutter above their
|
|
content, which we don't paint, so it shows as a dark bar and their
|
|
content sits too low (bottom row/cursor cropped). Shift them up by
|
|
that gutter so the dark bar is cropped instead of the terminal's
|
|
bottom. Clients with no gutter (geometry.y >= 0, e.g. wltest) are
|
|
left untouched. */
|
|
struct wlr_box geom = toplevel->base->geometry;
|
|
int y = -geom.y;
|
|
if (geom.y < 0)
|
|
y = 0;
|
|
/* Offset the tab content down (right) past any bar's reserved edge, so a
|
|
top bar does not cover the window's titlebar area. With no bar the
|
|
layout origin is (0,0) and the tab stays full-screen as before. */
|
|
wlr_scene_node_set_position(&tree->node, -geom.x + server.layout_x,
|
|
y + server.layout_y);
|
|
}
|
|
|
|
static void position_window(struct kick_window *w) {
|
|
if (!w || !w->tree || !w->toplevel) return;
|
|
position_tree(w->toplevel, w->tree);
|
|
}
|
|
|
|
static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, commit);
|
|
struct wlr_xdg_toplevel *toplevel = w->toplevel;
|
|
if (toplevel->base->initial_commit) {
|
|
wlr_xdg_toplevel_set_maximized(toplevel, true);
|
|
/* No output while standalone lost its VT (destroyed until the
|
|
connector is re-added): size then, not now, or this deref faults. */
|
|
if (server.output)
|
|
wlr_xdg_toplevel_set_size(toplevel, canvas_width(),
|
|
canvas_height());
|
|
/* Per-tab: new windows start maximized-bigger, never fullscreen --
|
|
except a black tab revealed while the host chrome is fullscreen
|
|
(Alt+F11 landed before its client mapped): the focused entry
|
|
picks fullscreen up here. */
|
|
if (host.fullscreen && w->tab == server.active_tab &&
|
|
w == tab_current_window(server.active_tab))
|
|
wlr_xdg_toplevel_set_fullscreen(toplevel, true);
|
|
}
|
|
/* Geometry can settle after the first frames (kitty/foot report a
|
|
negative y once they decide on their titlebar), so keep the scene
|
|
position in sync with every commit instead of only at activation. */
|
|
position_window(w);
|
|
|
|
/* [diag] What does this client commit, and is its scene node visible? */
|
|
struct wlr_surface *s = toplevel->base->surface;
|
|
struct wlr_buffer *cbuf = s ? s->current.buffer : NULL;
|
|
struct wlr_dmabuf_attributes dma;
|
|
static struct wlr_buffer *last_logged = NULL;
|
|
if (cbuf && cbuf != last_logged) {
|
|
last_logged = cbuf;
|
|
bool is_dma = wlr_buffer_get_dmabuf(cbuf, &dma);
|
|
bool node_on = tab_visible(w->tab);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
kick_log(KICK_LOG_DEBUG, "[diag] commit app=%d geom=%dx%d+%d+%d "
|
|
"buf=%dx%d dma=%d%s node_enabled=%d active=%d\n",
|
|
tab_index(w->tab),
|
|
toplevel->base->geometry.width,
|
|
toplevel->base->geometry.height,
|
|
toplevel->base->geometry.x,
|
|
toplevel->base->geometry.y,
|
|
s->current.buffer_width,
|
|
s->current.buffer_height,
|
|
is_dma,
|
|
is_dma ? (dma.n_planes ? "" : " planes=0") : "",
|
|
node_on,
|
|
w->focused);
|
|
if (is_dma) {
|
|
kick_log(KICK_LOG_DEBUG, "[diag] dmabuf planes=%d "
|
|
"format=0x%x mod=0x%llx\n",
|
|
dma.n_planes, dma.format,
|
|
(unsigned long long)dma.modifier);
|
|
}
|
|
}
|
|
if (!is_dma) {
|
|
/* shm buffer: flag for KICK_AUTODUMP; at debug level also count
|
|
non-black pixels and save the client's own buffer so we can tell
|
|
"client painted nothing" from "we failed to import". */
|
|
void *data;
|
|
uint32_t fmt;
|
|
size_t stride;
|
|
if (wlr_buffer_begin_data_ptr_access(
|
|
cbuf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
|
|
&data, &fmt, &stride)) {
|
|
client_dumped = true;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
int bw = cbuf->width, bh = cbuf->height;
|
|
unsigned long nonblack = 0, lum = 0;
|
|
unsigned char *p = data;
|
|
for (int yy = 0; yy < bh; yy++) {
|
|
unsigned char *row = p + (size_t)yy * stride;
|
|
for (int xx = 0; xx < bw; xx++) {
|
|
unsigned long v = row[xx*4+0] + row[xx*4+1]
|
|
+ row[xx*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[diag] shm fmt=0x%x "
|
|
"nonblack=%lu/%lu mean=%lu\n", fmt, nonblack,
|
|
(unsigned long)bw * bh,
|
|
lum / ((unsigned long)bw * bh + 1));
|
|
char cpath[512];
|
|
dump_path(cpath, sizeof(cpath), "client", dump_seq);
|
|
FILE *cf = fopen(cpath, "wb");
|
|
if (cf) {
|
|
fprintf(cf, "P6\n%d %d\n255\n", bw, bh);
|
|
unsigned char *cp = data;
|
|
for (int yy = 0; yy < bh; yy++) {
|
|
unsigned char *crow = cp + (size_t)yy * stride;
|
|
for (int xx = 0; xx < bw; xx++) {
|
|
fputc(crow[xx*4+0], cf);
|
|
fputc(crow[xx*4+1], cf);
|
|
fputc(crow[xx*4+2], cf);
|
|
}
|
|
}
|
|
fclose(cf);
|
|
kick_log(KICK_LOG_DEBUG, "[diag] client buffer "
|
|
"-> %s\n", cpath);
|
|
}
|
|
}
|
|
wlr_buffer_end_data_ptr_access(cbuf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Insert `tab` in server.tabs immediately after `anchor`; append at the far
|
|
right if anchor is NULL or no longer present. */
|
|
static void insert_tab_after(struct kick_tab *tab, struct kick_tab *anchor) {
|
|
if (anchor)
|
|
wl_list_insert(&anchor->link, &tab->link);
|
|
else
|
|
wl_list_insert(server.tabs.prev, &tab->link);
|
|
}
|
|
|
|
/* A new toplevel joins an existing tab when its client was forked -- directly
|
|
or transitively -- from an already-connected client: the shell running
|
|
inside a terminal tab forks gnome-calculator, so the calculator's pid chain
|
|
runs back through the shell to the terminal client. Exact pid equality (a
|
|
client opening a second toplevel) is the depth-0 case. Walk /proc/<pid>/stat
|
|
ppid links, capped, to find the owning tab; NULL if the client is not
|
|
descended from any tab. */
|
|
static struct kick_tab *tab_group_owner(pid_t cpid) {
|
|
if (cpid <= 1)
|
|
return NULL;
|
|
pid_t p = cpid;
|
|
for (int depth = 0; depth < 8; depth++) {
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &t->windows, link)
|
|
if (window_live(w) && w->client_pid == p)
|
|
return t;
|
|
}
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/proc/%d/stat", (int)p);
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return NULL;
|
|
char buf[4096];
|
|
ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0)
|
|
return NULL;
|
|
buf[n] = '\0';
|
|
char *last = strrchr(buf, ')');
|
|
if (!last)
|
|
return NULL;
|
|
char state;
|
|
pid_t ppid = -1;
|
|
if (sscanf(last + 1, " %c %d", &state, &ppid) != 2 || ppid <= 1)
|
|
return NULL;
|
|
p = ppid;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* =============== Window list operations (unified model) =============== */
|
|
|
|
/* Move a window `dir` slots within its tab's window list (+1 = toward
|
|
newer/right, -1 = toward older/left), wrapping at the ends. Reorders the
|
|
overlay row order live; focus and visibility are unchanged (the moved
|
|
window keeps the keyboard if it held it). */
|
|
static void reorder_window(struct kick_window *w, int dir) {
|
|
struct kick_tab *tab = w ? w->tab : NULL;
|
|
if (!tab || wl_list_length(&tab->windows) < 2) return;
|
|
|
|
/* Collect to an array (the established 16-cap pattern) so the wrap
|
|
arithmetic reads like the tab reorder: index math first, one splice
|
|
after. Node pointers are stable across wl_list_remove. */
|
|
struct kick_window *members[16];
|
|
int n = 0;
|
|
struct kick_window *it;
|
|
wl_list_for_each(it, &tab->windows, link)
|
|
if (n < 16) members[n++] = it;
|
|
|
|
int from = -1;
|
|
for (int i = 0; i < n; i++)
|
|
if (members[i] == w) { from = i; break; }
|
|
if (from < 0) return;
|
|
|
|
int to = (from + dir + n) % n;
|
|
if (to == from) return;
|
|
|
|
wl_list_remove(&w->link);
|
|
/* Wrap inserts: right-wrap lands at the head (after the sentinel),
|
|
left-wrap at the tail (after the current last node). */
|
|
if (dir > 0 && to == 0)
|
|
wl_list_insert(&tab->windows, &w->link);
|
|
else if (dir < 0 && to == n - 1)
|
|
wl_list_insert(tab->windows.prev, &w->link);
|
|
else if (dir > 0)
|
|
wl_list_insert(&members[to]->link, &w->link); /* after neighbor */
|
|
else
|
|
wl_list_insert(members[to]->link.prev, &w->link); /* before */
|
|
|
|
kick_log(KICK_LOG_INFO, "[member] moved app=%d slot %d -> %d\n",
|
|
tab_index(tab), from, to);
|
|
/* Input binding changed the visible order: repaint like any other
|
|
keybinding (whole damage keeps the composite coat clean). */
|
|
if (host.overlay_visible) {
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
else if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
kick_request_redraw();
|
|
}
|
|
}
|
|
|
|
/* Rotate the active tab's windows one step in `dir` (-1 = backward): the
|
|
plain list of windows (oldest first) steps to the neighbor of whatever is
|
|
showing, wrapping at both ends. The list itself is never mutated;
|
|
w->focused alone tracks where the rotation sits, so repeated steps walk
|
|
the windows deterministically. Returns false when nothing is mapped
|
|
(nothing to rotate), leaving the key free to pass through to the client. */
|
|
static bool cycle_tab_windows(struct kick_tab *tab, int dir) {
|
|
struct kick_window *members[16];
|
|
struct kick_window *w;
|
|
int n = 0;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->mapped && window_surface(w) && n < 16) members[n++] = w;
|
|
|
|
if (n == 0)
|
|
return false;
|
|
|
|
int cur = 0;
|
|
while (cur < n && members[cur] != tab_current_window(tab)) cur++;
|
|
/* An active tab without a focused mapped window counts as the first
|
|
(oldest) window, so backward wraps to the newest window and forward to
|
|
the oldest -- exactly where cycling picks up from a freshly shown
|
|
tab. */
|
|
window_enter(members[(cur + dir + n) % n]);
|
|
kick_log(KICK_LOG_INFO, "[sub] cycle app=%d\n", tab_index(tab));
|
|
/* Cycling changes which stacked member is shown: re-resolve the row
|
|
title at this data source so it is log-observable without an
|
|
overlay draw. */
|
|
(void)tab_row_title(tab);
|
|
kick_request_redraw();
|
|
return true;
|
|
}
|
|
|
|
/* Whether a newly-mapped window should grab keyboard focus. Shared by the
|
|
xdg and X11 map handlers. */
|
|
static bool window_map_should_activate(struct kick_window *w) {
|
|
if (w->origin == ORIGIN_ADD_TAB)
|
|
return true;
|
|
if (w->origin == ORIGIN_GROUP && w->parent_tab) {
|
|
if (w->is_x11)
|
|
return w->parent_tab == server.active_tab;
|
|
struct kick_window *first = tab_first_window(w->parent_tab);
|
|
return first && first->toplevel &&
|
|
first->toplevel == active_toplevel();
|
|
}
|
|
return w->tab == server.active_tab && !tab_visible(w->tab);
|
|
}
|
|
|
|
/* Content-type name for the debug log. Queried on demand from the
|
|
manager (the protocol has no change event); "none" covers untagged
|
|
surfaces and a missing manager alike. */
|
|
static const char *surface_content_type_name(struct wlr_surface *s) {
|
|
if (!s || !server.content_type_mgr)
|
|
return "none";
|
|
switch (wlr_surface_get_content_type_v1(server.content_type_mgr, s)) {
|
|
case WP_CONTENT_TYPE_V1_TYPE_PHOTO:
|
|
return "photo";
|
|
case WP_CONTENT_TYPE_V1_TYPE_VIDEO:
|
|
return "video";
|
|
case WP_CONTENT_TYPE_V1_TYPE_GAME:
|
|
return "game";
|
|
default:
|
|
return "none";
|
|
}
|
|
}
|
|
|
|
static void handle_window_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, map);
|
|
w->mapped = true;
|
|
/* Pin client-side decorations now that the surface can take a
|
|
configure (see decoration_pin_client_side); X11 windows never
|
|
carry xdg decorations. */
|
|
if (w->toplevel)
|
|
decoration_pin_client_side(w->toplevel);
|
|
#if HAVE_XWAYLAND
|
|
/* X11 windows that joined a --tab-groups group are transients -- menus,
|
|
dialogs, palettes -- not workspaces: they keep their own geometry and
|
|
float at the absolute position their client chose. Never configure
|
|
them to the output size (that reallocates their backing store to
|
|
garbage and stretches menus across the screen); just place the node.
|
|
X11 windows that own a tab (CLI/Add-tab/external, and a --tab-groups
|
|
off related window on its own tab) get the maximized treatment like
|
|
any tab. */
|
|
if (w->is_x11 && w->xsurface && w->tree) {
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab) {
|
|
wlr_scene_node_set_position(&w->tree->node,
|
|
w->xsurface->x, w->xsurface->y);
|
|
} else {
|
|
x11_initial_configure(w->xsurface);
|
|
}
|
|
}
|
|
#endif
|
|
/* Black-tab fix: a tab revealed while the host chrome is fullscreen
|
|
(Alt+F11 landed before its client mapped) picks fullscreen up here.
|
|
Live fullscreen states otherwise survive mapping on their own. */
|
|
if (host.fullscreen && w->tab == server.active_tab &&
|
|
w == tab_current_window(server.active_tab) && window_initialized(w))
|
|
window_set_fullscreen(w, true);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] map app=%d content=%s\n",
|
|
tab_index(w->tab),
|
|
surface_content_type_name(window_surface(w)));
|
|
|
|
/* A new tab never steals focus on its own. The exceptions: an
|
|
Alt+Shift+T terminal (add_tab), a tab-group window glued after a shown
|
|
parent tab, and the active tab's own first map (a still-black tab
|
|
revealed the moment its first frame arrives). Every other map stays
|
|
hidden -- the overlay and cycling reach it, but it does not yank the
|
|
active surface. A window's toplevel/xsurface is set at claim time
|
|
(before its map), so "is it already shown?" is whether any window tree
|
|
in the tab is enabled, not whether active_toplevel() is set. */
|
|
bool activate = window_map_should_activate(w);
|
|
|
|
/* Modal dialogs demand attention: a late dialog object (created between
|
|
claim and map) is mirrored here, then the modal takes its tab. */
|
|
if (!w->dialog_wired && w->toplevel)
|
|
dialog_wire(w, wlr_xdg_dialog_v1_try_from_wlr_xdg_toplevel(w->toplevel));
|
|
|
|
/* A window claimed while its tab was still black may have been drawn with
|
|
a placeholder icon; once a real client maps it, re-resolve the icon so
|
|
the overlay picks up its app_id. */
|
|
w->icon_resolved = false;
|
|
|
|
if (w->modal) {
|
|
modal_activate(w);
|
|
} else if (activate) {
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab) {
|
|
/* A grouped window of a shown parent tab takes the keyboard
|
|
within it; whether it wins the seat depends on the parent tab
|
|
being the one shown, which is exactly the activate test. It
|
|
does NOT promote the tab or emit an active transition. */
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else if (w->origin == ORIGIN_GROUP) {
|
|
/* --tab-groups off: the related window OWNS the showing tab
|
|
(tab != parent_tab), so its map promotes that tab like any
|
|
fresh tab -- the crown follows what the screen shows. */
|
|
set_active_tab(w->tab);
|
|
} else if (w->tab == server.active_tab) {
|
|
/* The active tab's own black crown revealed: the tab was crowned
|
|
while still pending (no surface, so no seat enter there), and
|
|
its first map promotes it without announcing a fresh active
|
|
transition -- the boot crown already owns the tab. */
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else {
|
|
set_active_tab(w->tab);
|
|
}
|
|
} else if (server.active_tab == w->tab) {
|
|
/* A window joining an already-shown active tab takes the keyboard
|
|
within it. For the active tab's own window a re-map stays quiet
|
|
(activate is false for a visible tab), so only genuine arrivals
|
|
grab the keyboard -- same rule the old member_enter applied. */
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "[sub] map app=%d\n", tab_index(w->tab));
|
|
}
|
|
/* A newly mapped window of the shown tab joins outside the normal
|
|
commit-damage flow (X11 windows can even pair and commit before
|
|
classification attaches them): the enable/position surgery here
|
|
doesn't reliably produce output damage on its own, so the first frame
|
|
shows black until an unrelated repaint. Force a full repaint for
|
|
every window arrival on the shown tab, any backend. */
|
|
if (server.active_tab == w->tab && server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
/* The first commit that maps the toplevel carries its final app_id/title
|
|
(the client sends them in the same commit as the first buffer). The
|
|
wlroots set_title/set_app_id signals already fired before claim, so
|
|
re-sync the taskbar handles here to the post-map values. The activated
|
|
flag is set downstream by window_enter/set_active_tab via
|
|
window_set_activated, but reflect whatever focus we just settled on. */
|
|
foreign_sync_output(w);
|
|
foreign_sync_title(w);
|
|
foreign_sync_app_id(w);
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_activated(w->ft_handle, w->focused);
|
|
/* The first commit carries the final title with the tab assigned and
|
|
focus settled: re-resolve the row title here (not at claim time, when
|
|
the title may not have arrived yet) so [title] is observable in logs
|
|
without an overlay draw. */
|
|
if (w->tab)
|
|
(void)tab_row_title(w->tab);
|
|
}
|
|
|
|
static void handle_window_unmap(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, unmap);
|
|
w->mapped = false;
|
|
if (w->focused)
|
|
refocus_tab(w->tab);
|
|
}
|
|
|
|
/* Allocate a window struct bound to `tab` and append it to the tab's window
|
|
list (newest at the tail). The caller sets kind/protocol pointers and
|
|
listeners right after. */
|
|
static struct kick_window *window_alloc(struct kick_tab *tab) {
|
|
struct kick_window *w = calloc(1, sizeof(*w));
|
|
if (!w) return NULL;
|
|
w->tab = tab;
|
|
wl_list_insert(tab->windows.prev, &w->link);
|
|
/* New windows start least-recent: background arrivals must not hijack
|
|
Alt+Tab. The window bubbles to the front when it becomes current
|
|
(window_enter with the prefix up). */
|
|
wl_list_insert(server.mru.prev, &w->mru_link);
|
|
w->mru_linked = true;
|
|
return w;
|
|
}
|
|
|
|
/* Claim an xdg toplevel by hanging it on `w` (a pending/black window or a
|
|
freshly allocated one): back the window struct with the toplevel and its
|
|
scene tree, record the client's pid/process group, and attach the full
|
|
listener set. */
|
|
static void claim_xdg_window(struct kick_tab *tab, struct kick_window *w,
|
|
struct wlr_xdg_toplevel *toplevel, pid_t cpid,
|
|
pid_t pgid) {
|
|
w->toplevel = toplevel;
|
|
w->pending_claim = false;
|
|
w->client_pid = cpid;
|
|
w->pgid = (pgid > 0 && pgid != getpgid(0)) ? pgid : -1;
|
|
/* A tag set in the initial commit may have beaten the claim; pick up
|
|
any pending tag/description now (see tag_consume_pending). */
|
|
tag_consume_pending(w);
|
|
w->tree = wlr_scene_xdg_surface_create(&server.scene->tree, toplevel->base);
|
|
layer_raise_above_windows();
|
|
/* First commit keeps the window on the tab bar; the scene tree shows
|
|
nothing until a map makes the tab (or its client) current. Without
|
|
this the node starts enabled and a still-black pending window of the
|
|
active tab reads as "already drawn", so its first map skips the seat
|
|
enter. */
|
|
wlr_scene_node_set_enabled(&w->tree->node, false);
|
|
|
|
/* [claim] ties the window to the *connected client's* PID, so tests can
|
|
kill the tab that is actually active. A grouped window of a shown
|
|
parent tab is announced as [sub] stack instead: it never claims a slot
|
|
of its own. A --tab-groups off related window on its OWN tab claims
|
|
a real slot, so it logs [claim]. */
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab)
|
|
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", tab_index(tab));
|
|
else
|
|
kick_log(KICK_LOG_INFO, "[claim] app=%d pid=%d\n",
|
|
tab_index(tab), (int)cpid);
|
|
|
|
w->destroy.notify = handle_toplevel_destroy;
|
|
wl_signal_add(&toplevel->events.destroy, &w->destroy);
|
|
|
|
w->map.notify = handle_window_map;
|
|
wl_signal_add(&toplevel->base->surface->events.map, &w->map);
|
|
w->unmap.notify = handle_window_unmap;
|
|
wl_signal_add(&toplevel->base->surface->events.unmap, &w->unmap);
|
|
|
|
w->commit.notify = handle_toplevel_commit;
|
|
wl_signal_add(&toplevel->base->surface->events.commit, &w->commit);
|
|
|
|
w->set_title.notify = handle_window_meta;
|
|
wl_signal_add(&toplevel->events.set_title, &w->set_title);
|
|
w->set_app_id.notify = handle_window_meta;
|
|
wl_signal_add(&toplevel->events.set_app_id, &w->set_app_id);
|
|
|
|
/* Expose the window to taskbars once it has a toplevel (title/app_id may
|
|
still be blank until the client commits them; later set_title/
|
|
set_app_id events re-sync the handles). */
|
|
foreign_handle_create(w);
|
|
|
|
/* Tell the surface the output scale up front: clients that wait for
|
|
preferred_scale before sizing assets would otherwise guess. The
|
|
scale is fixed per session, so one notification covers the surface. */
|
|
if (server.fractional_scale_mgr && toplevel->base->surface) {
|
|
double scale = server.output ? (double)server.output->scale : 1.0;
|
|
wlr_fractional_scale_v1_notify_scale(toplevel->base->surface, scale);
|
|
}
|
|
|
|
w->request_maximize.notify = handle_request_maximize;
|
|
wl_signal_add(&toplevel->events.request_maximize, &w->request_maximize);
|
|
w->request_fullscreen.notify = handle_request_fullscreen;
|
|
wl_signal_add(&toplevel->events.request_fullscreen, &w->request_fullscreen);
|
|
w->request_minimize.notify = handle_request_minimize;
|
|
wl_signal_add(&toplevel->events.request_minimize, &w->request_minimize);
|
|
w->request_move.notify = handle_request_move;
|
|
wl_signal_add(&toplevel->events.request_move, &w->request_move);
|
|
w->request_resize.notify = handle_request_resize;
|
|
wl_signal_add(&toplevel->events.request_resize, &w->request_resize);
|
|
}
|
|
|
|
static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_toplevel *toplevel = data;
|
|
|
|
pid_t cpid = -1;
|
|
if (toplevel->base->client)
|
|
wl_client_get_credentials(toplevel->base->client->client,
|
|
&cpid, NULL, NULL);
|
|
|
|
/* A window awaiting its toplevel -- an unclaimed command-line command or
|
|
a fresh Alt+Shift+T terminal -- belongs to exactly ONE client: the one
|
|
WE launched for it. launch_client() runs each command in its own
|
|
process group (setpgid(0,0)), so the connected client's group id
|
|
equals the launcher pid w->pid, including through a shell wrapper
|
|
(`sh -c "foot -e htop"`). Matching by group -- not exact pid -- is
|
|
what keeps a wrapped command on its own command-line slot. A client
|
|
from a group we never created (an external one) never matches, so it
|
|
can't steal a still-loading tab's position. */
|
|
pid_t pgid = cpid > 0 ? getpgid(cpid) : -1;
|
|
struct kick_window *w = NULL;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *cand;
|
|
wl_list_for_each(cand, &tab->windows, link) {
|
|
if (cand->toplevel || !cand->pending_claim) continue;
|
|
if (cand->pid > 0 && cand->pid == pgid) {
|
|
w = cand;
|
|
break;
|
|
}
|
|
}
|
|
if (w) break;
|
|
}
|
|
/* A second toplevel from a client that already owns a tab: with
|
|
--tab-groups on it joins its owner's window list; otherwise it gets
|
|
its own tab glued right after its parent (the protocol parent, else
|
|
the owning tab), never touching a reserved pending window.
|
|
Command-line commands are all forked at startup (see launch_all_cli);
|
|
their clients match the pending loop above, and only then. */
|
|
if (!w && cpid > 0) {
|
|
struct kick_tab *owner = tab_group_owner(cpid);
|
|
if (owner) {
|
|
if (tab_groups) {
|
|
w = window_alloc(owner);
|
|
} else {
|
|
struct kick_tab *nt = calloc(1, sizeof(*nt));
|
|
if (!nt) return;
|
|
wl_list_init(&nt->windows);
|
|
w = window_alloc(nt);
|
|
if (!w) { free(nt); return; }
|
|
struct kick_window *pa = toplevel->parent
|
|
? window_for_toplevel(toplevel->parent) : NULL;
|
|
insert_tab_after(nt, pa ? pa->tab : owner);
|
|
}
|
|
if (!w) return;
|
|
w->origin = ORIGIN_GROUP;
|
|
w->parent_tab = owner;
|
|
}
|
|
}
|
|
/* Defense in depth: a tag that beat the claim (see tag_consume_pending)
|
|
joins its tag group here; normally tags arrive after claim and group
|
|
via tag_try_group while the window is still unmapped. */
|
|
if (!w && tab_groups) {
|
|
const char *ptag = pending_tag_for(toplevel);
|
|
if (ptag) {
|
|
struct kick_tab *gt;
|
|
wl_list_for_each(gt, &server.tabs, link) {
|
|
struct kick_window *cur = tab_current_window(gt);
|
|
if (cur && cur->toplevel && cur->tag &&
|
|
strcmp(cur->tag, ptag) == 0) {
|
|
w = window_alloc(gt);
|
|
if (!w) return;
|
|
w->origin = ORIGIN_GROUP;
|
|
w->parent_tab = gt;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* No pending window matched and no owner tab: an external client. Give
|
|
it its own tab so it appears on top and stays reachable via tab
|
|
cycling, right after the tab that created it (protocol parent, else
|
|
the active tab); far right if unidentified. */
|
|
if (!w) {
|
|
struct kick_tab *nt = calloc(1, sizeof(*nt));
|
|
if (!nt) return;
|
|
wl_list_init(&nt->windows);
|
|
w = window_alloc(nt);
|
|
if (!w) { free(nt); return; }
|
|
struct kick_window *pa = toplevel->parent
|
|
? window_for_toplevel(toplevel->parent) : NULL;
|
|
if (!pa)
|
|
pa = tab_current_window(server.active_tab);
|
|
insert_tab_after(nt, pa ? pa->tab : NULL);
|
|
w->origin = ORIGIN_EXTERNAL;
|
|
}
|
|
|
|
claim_xdg_window(w->tab, w, toplevel, cpid, pgid);
|
|
/* A dialog object created in the same commit as the toplevel already
|
|
exists: mirror it now (later ones arrive via handle_dialog_new). */
|
|
dialog_wire(w, wlr_xdg_dialog_v1_try_from_wlr_xdg_toplevel(toplevel));
|
|
}
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* ===================== XWayland (X11 tabs) ===================== */
|
|
/* An X11 window becomes a window of a tab exactly like an xdg toplevel:
|
|
claimed against a pending command-line window by process group, linked
|
|
into its ancestor's tab, or given its own tab as an external. Two
|
|
structural differences drive the code below:
|
|
- Every X11 window belongs to ONE Wayland client (the Xwayland process),
|
|
so client identity comes from xsurface->pid -- the real X client's PID
|
|
from _NET_WM_PID -- never from wl_client credentials.
|
|
- The wlr_surface only exists between the associate/dissociate events,
|
|
so the surface map/commit listeners are attached there, not at claim
|
|
time (dissociate always precedes destroy; a flag guards both orders).
|
|
Override-redirect windows (menus, tooltips) are handled further below:
|
|
they float above the tabs and never become windows of a tab. */
|
|
|
|
static void handle_xs_surface_map(struct wl_listener *listener, void *data);
|
|
static void handle_xs_commit(struct wl_listener *listener, void *data);
|
|
static void handle_xs_associate(struct wl_listener *listener, void *data);
|
|
static void or_handle_map(struct wl_listener *listener, void *data);
|
|
static void or_handle_unmap(struct wl_listener *listener, void *data);
|
|
static void or_handle_commit(struct wl_listener *listener, void *data);
|
|
|
|
static struct kick_window *window_for_xsurface(struct wlr_xwayland_surface *xs) {
|
|
if (!xs) return NULL;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->xsurface == xs)
|
|
return w;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Keep a managed X11 window's content at the tab origin: kick configures
|
|
every tab to the full output at 0,0, so a client that placed itself
|
|
elsewhere before obeying gets its offset cropped (mirrors position_tree's
|
|
CSD-gutter handling). Offsets beyond the output are ignored rather than
|
|
blanking the whole tab. */
|
|
static void x11_sync_position(struct kick_window *w) {
|
|
struct wlr_xwayland_surface *xs = w->xsurface;
|
|
if (!xs || !w->tree || !server.output) return;
|
|
int nx = (xs->x > 0 && xs->x < server.output->width) ? -xs->x : 0;
|
|
int ny = (xs->y > 0 && xs->y < server.output->height) ? -xs->y : 0;
|
|
wlr_scene_node_set_position(&w->tree->node, nx + server.layout_x,
|
|
ny + server.layout_y);
|
|
}
|
|
|
|
/* Maximize-and-size treatment shared by new X11 tabs (X11 has no
|
|
initial_commit to hook, and reconfiguring on every map is idempotent). */
|
|
static void x11_initial_configure(struct wlr_xwayland_surface *xs) {
|
|
if (!xs || !xs->surface || !server.output) return;
|
|
wlr_xwayland_surface_configure(xs, 0, 0,
|
|
(uint16_t)canvas_width(),
|
|
(uint16_t)canvas_height());
|
|
wlr_xwayland_surface_set_maximized(xs, true, true);
|
|
}
|
|
|
|
/* Claim an X11 toplevel by backing `w` with the xwayland surface: a scene
|
|
tree, the client's pid/process group, and the full X11 listener set. The
|
|
surface map/commit listeners attach later, at associate (the surface only
|
|
exists between associate/dissociate). */
|
|
static void handle_xs_destroy(struct wl_listener *listener, void *data);
|
|
static void handle_xs_associate(struct wl_listener *listener, void *data);
|
|
static void handle_xs_dissociate(struct wl_listener *listener, void *data);
|
|
static void handle_xs_commit(struct wl_listener *listener, void *data);
|
|
static void handle_xs_surface_map(struct wl_listener *listener, void *data);
|
|
static void handle_xs_request_maximize(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xs_request_minimize(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xs_request_fullscreen(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xs_request_configure(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xs_request_activate(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xs_request_close(struct wl_listener *listener, void *data);
|
|
static void handle_xs_set_title(struct wl_listener *listener, void *data);
|
|
static void handle_xs_set_class(struct wl_listener *listener, void *data);
|
|
static void claim_x11_window(struct kick_tab *tab, struct kick_window *w,
|
|
struct wlr_xwayland_surface *xsurface,
|
|
pid_t xpid, pid_t pgid) {
|
|
w->is_x11 = true;
|
|
w->xsurface = xsurface;
|
|
w->pending_claim = false;
|
|
w->client_pid = xpid;
|
|
w->pgid = (pgid > 0 && pgid != getpgid(0)) ? pgid : -1;
|
|
w->tree = wlr_scene_tree_create(&server.scene->tree);
|
|
if (!w->tree) {
|
|
w->is_x11 = false;
|
|
w->xsurface = NULL;
|
|
return;
|
|
}
|
|
layer_raise_above_windows();
|
|
/* hidden until it becomes the active tab */
|
|
wlr_scene_node_set_enabled(&w->tree->node, false);
|
|
|
|
/* A grouped window (a transient of its owner tab) never claims a slot
|
|
of its own; [claim] ties a tab-owning window to the connected client's
|
|
PID. A --tab-groups off related window on its own tab claims a real
|
|
slot, so it logs [claim]. */
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab)
|
|
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", tab_index(tab));
|
|
else
|
|
kick_log(KICK_LOG_INFO, "[claim] app=%d pid=%d\n",
|
|
tab_index(tab), (int)xpid);
|
|
|
|
w->destroy.notify = handle_xs_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &w->destroy);
|
|
w->xs_associate.notify = handle_xs_associate;
|
|
wl_signal_add(&xsurface->events.associate, &w->xs_associate);
|
|
w->xs_dissociate.notify = handle_xs_dissociate;
|
|
wl_signal_add(&xsurface->events.dissociate, &w->xs_dissociate);
|
|
|
|
w->request_maximize.notify = handle_xs_request_maximize;
|
|
wl_signal_add(&xsurface->events.request_maximize, &w->request_maximize);
|
|
w->request_fullscreen.notify = handle_xs_request_fullscreen;
|
|
wl_signal_add(&xsurface->events.request_fullscreen,
|
|
&w->request_fullscreen);
|
|
w->request_minimize.notify = handle_xs_request_minimize;
|
|
wl_signal_add(&xsurface->events.request_minimize, &w->request_minimize);
|
|
w->xs_request_configure.notify = handle_xs_request_configure;
|
|
wl_signal_add(&xsurface->events.request_configure,
|
|
&w->xs_request_configure);
|
|
w->xs_request_activate.notify = handle_xs_request_activate;
|
|
wl_signal_add(&xsurface->events.request_activate,
|
|
&w->xs_request_activate);
|
|
w->xs_request_close.notify = handle_xs_request_close;
|
|
wl_signal_add(&xsurface->events.request_close, &w->xs_request_close);
|
|
w->xs_set_title.notify = handle_xs_set_title;
|
|
wl_signal_add(&xsurface->events.set_title, &w->xs_set_title);
|
|
w->xs_set_class.notify = handle_xs_set_class;
|
|
wl_signal_add(&xsurface->events.set_class, &w->xs_set_class);
|
|
|
|
/* Membership now begins at first map, which can land after the xwm has
|
|
already paired the window's wl_surface (associate fired before we
|
|
existed to hear it). Attach the scene surface immediately in that
|
|
case, and run the map treatment when the surface is already live. */
|
|
if (xsurface->surface) {
|
|
handle_xs_associate(&w->xs_associate, NULL);
|
|
if (xsurface->surface->mapped)
|
|
handle_xs_surface_map(&w->map, NULL);
|
|
}
|
|
|
|
/* Expose the X11 window to taskbars (title/class sync via the
|
|
set_title/set_class handlers below, which call into foreign_sync_*). */
|
|
foreign_handle_create(w);
|
|
}
|
|
|
|
/* --- Managed X11 windows --- */
|
|
|
|
static void handle_xs_associate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_associate);
|
|
if (!w->xsurface || !w->xsurface->surface || !w->tree) return;
|
|
/* The scene node rendering the window's buffers; recreated per
|
|
association (the wlr_surface dies with it). */
|
|
wlr_scene_surface_create(w->tree, w->xsurface->surface);
|
|
w->map.notify = handle_xs_surface_map;
|
|
wl_signal_add(&w->xsurface->surface->events.map, &w->map);
|
|
w->unmap.notify = handle_window_unmap;
|
|
wl_signal_add(&w->xsurface->surface->events.unmap, &w->unmap);
|
|
w->commit.notify = handle_xs_commit;
|
|
wl_signal_add(&w->xsurface->surface->events.commit, &w->commit);
|
|
w->xs_surface_linked = true;
|
|
/* Membership starts at first map, so association can arrive after the
|
|
window already mapped and committed (an instantly-shown menu popup):
|
|
the map event fired before this listener existed. Replay it (the
|
|
caller does), or the grouped window stays an invisible phantom with
|
|
live content behind it. */
|
|
if (w->origin == ORIGIN_GROUP) {
|
|
bool mapped = w->xsurface->surface->mapped;
|
|
kick_log(KICK_LOG_DEBUG, "[x11] member associate app=%d%s\n",
|
|
tab_index(w->tab), mapped ? " (late: already mapped)" : "");
|
|
}
|
|
}
|
|
|
|
static void handle_xs_dissociate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_dissociate);
|
|
if (!w->xs_surface_linked) return;
|
|
wl_list_remove(&w->map.link);
|
|
wl_list_remove(&w->unmap.link);
|
|
wl_list_remove(&w->commit.link);
|
|
w->xs_surface_linked = false;
|
|
}
|
|
|
|
static void handle_xs_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, commit);
|
|
x11_sync_position(w);
|
|
}
|
|
|
|
static void handle_xs_surface_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, map);
|
|
w->mapped = true;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] map app=%d\n", tab_index(w->tab));
|
|
|
|
/* Same geometry rule as handle_window_map: X11 windows that grouped onto a
|
|
--tab-groups parent tab are transients -- menus, dialogs, palettes --
|
|
they keep their own geometry and float at the absolute position their
|
|
client chose; never configure them to the output size (that reallocates
|
|
their backing store to garbage and stretches menus across the screen),
|
|
just place the node. X11 windows that own a tab -- including a
|
|
--tab-groups off related window on its own tab -- get the maximized
|
|
treatment like any tab. */
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab) {
|
|
wlr_scene_node_set_position(&w->tree->node,
|
|
w->xsurface->x, w->xsurface->y);
|
|
} else {
|
|
x11_initial_configure(w->xsurface);
|
|
}
|
|
|
|
/* Same no-steal activation policy as handle_window_map. */
|
|
bool activate = window_map_should_activate(w);
|
|
/* A slot claimed while black may show a placeholder icon; re-resolve
|
|
against the now-known WM_CLASS. */
|
|
w->icon_resolved = false;
|
|
|
|
if (activate) {
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab) {
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else if (w->origin == ORIGIN_GROUP) {
|
|
/* --tab-groups off: the related window owns its own tab, so the
|
|
map promotes it exactly like a fresh external tab. */
|
|
set_active_tab(w->tab);
|
|
} else if (w->tab == server.active_tab) {
|
|
/* Same-tab promotion of the black crown, without a fresh active
|
|
transition (mirrors handle_window_map). */
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else {
|
|
set_active_tab(w->tab);
|
|
}
|
|
} else if (server.active_tab == w->tab) {
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", tab_index(w->tab));
|
|
window_enter(w);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "[sub] map app=%d\n", tab_index(w->tab));
|
|
}
|
|
/* A newly mapped window of the shown tab joins outside the normal
|
|
commit-damage flow; force a full repaint, any backend. The held
|
|
overlay's new chip rides the same frame. */
|
|
if (server.active_tab == w->tab && server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
}
|
|
|
|
static void handle_xs_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, destroy);
|
|
w->xsurface = NULL;
|
|
/* Plain trees (unlike xdg-surface trees) have no owner to reap them:
|
|
destroy explicitly or one node leaks per closed X11 window. */
|
|
if (w->tree) {
|
|
wlr_scene_node_destroy(&w->tree->node);
|
|
w->tree = NULL;
|
|
}
|
|
|
|
/* Same overlay-shrink cleanup as handle_toplevel_destroy. */
|
|
window_detach_x11_listeners(w);
|
|
window_destroy_common(w);
|
|
}
|
|
|
|
static void handle_xs_request_maximize(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_maximize);
|
|
if (!w->xsurface) return;
|
|
/* wlroots pre-applies the wish and signals only on change, so the live
|
|
flags ARE the request: cleared means restore, set means maximize.
|
|
Restore on a fullscreen entry steps back down; anywhere else it is
|
|
left alone (deliberately not re-asserted: re-asserting would swallow
|
|
the next maximize ADD, and the client now offers its maximize button
|
|
instead). Maximize on a mapped tab-owning window is a genuine
|
|
toggle -- maximized is the baseline, so it means fullscreen; anywhere
|
|
earlier (or on a floating transient) it stays plain maximize. */
|
|
if (!w->xsurface->maximized_vert || !w->xsurface->maximized_horz) {
|
|
if (window_is_fullscreen(w))
|
|
client_exit_fullscreen(w);
|
|
return;
|
|
}
|
|
if (window_is_fullscreen(w)) {
|
|
client_exit_fullscreen(w);
|
|
return;
|
|
}
|
|
if (w->mapped && (w->origin != ORIGIN_GROUP || w->tab != w->parent_tab))
|
|
client_enter_fullscreen(w);
|
|
else
|
|
wlr_xwayland_surface_set_maximized(w->xsurface, true, true);
|
|
}
|
|
|
|
static void handle_xs_request_minimize(struct wl_listener *listener,
|
|
void *data) {
|
|
struct wlr_xwayland_minimize_event *event = data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_minimize);
|
|
/* Minimize steps a fullscreen entry back down to maximized; anywhere
|
|
else kick has no minimized state: tabs are shown or hidden. */
|
|
if (event && event->minimize && w->xsurface && window_is_fullscreen(w))
|
|
client_exit_fullscreen(w);
|
|
}
|
|
|
|
static void handle_xs_request_fullscreen(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, request_fullscreen);
|
|
struct wlr_xwayland_surface *xs = w->xsurface;
|
|
if (!xs) return;
|
|
/* (wlroots applies the client's wish to xs->fullscreen before
|
|
emitting, so the change is already live here: log unconditionally,
|
|
since window_apply's change detection cannot see it.) */
|
|
bool fs = xs->fullscreen;
|
|
if (fs)
|
|
client_enter_fullscreen(w);
|
|
else
|
|
client_exit_fullscreen(w);
|
|
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs);
|
|
}
|
|
|
|
static void handle_xs_request_configure(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w,
|
|
xs_request_configure);
|
|
/* The WM answer to any geometry request: maximized at the output size. */
|
|
x11_initial_configure(w->xsurface);
|
|
}
|
|
|
|
static void handle_xs_request_activate(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_request_activate);
|
|
/* An explicit activation request (the app's own window list, a pager)
|
|
may steal focus, exactly like Alt-cycling onto the tab. */
|
|
set_active_tab(w->tab);
|
|
}
|
|
|
|
static void handle_xs_request_close(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_request_close);
|
|
/* A tab-group X11 window (a transient grouped onto the owner's tab)
|
|
closing on its own only drops out of the tab -- its destroy path
|
|
handles it -- so leave it be. A --tab-groups off related window owns
|
|
its tab outright, so its close tears the tab down, matching Alt+Delete
|
|
and the Wayland behavior. */
|
|
if (w->origin == ORIGIN_GROUP && w->tab == w->parent_tab)
|
|
return;
|
|
close_window(w);
|
|
}
|
|
|
|
static void handle_xs_set_title(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_set_title);
|
|
/* Picked up by the overlay row on its next draw (row_title cache);
|
|
the next repaint. */
|
|
(void)w;
|
|
if (w && w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_title(w->ft_handle, foreign_str(window_title(w)));
|
|
if (w && w->ext_handle) {
|
|
struct wlr_ext_foreign_toplevel_handle_v1_state st = {
|
|
.title = foreign_str(window_title(w)),
|
|
.app_id = foreign_str(window_app_id(w)),
|
|
};
|
|
wlr_ext_foreign_toplevel_handle_v1_update_state(w->ext_handle, &st);
|
|
}
|
|
}
|
|
|
|
static void handle_xs_set_class(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, xs_set_class);
|
|
/* Re-resolve the overlay icon against the new WM_CLASS; the
|
|
held menus repaint it on their next frame. */
|
|
w->icon_resolved = false;
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_app_id(w->ft_handle,
|
|
foreign_str(window_app_id(w)));
|
|
if (w->ext_handle) {
|
|
struct wlr_ext_foreign_toplevel_handle_v1_state st = {
|
|
.title = foreign_str(window_title(w)),
|
|
.app_id = foreign_str(window_app_id(w)),
|
|
};
|
|
wlr_ext_foreign_toplevel_handle_v1_update_state(w->ext_handle, &st);
|
|
}
|
|
}
|
|
|
|
/* --- Override-redirect windows (menus, tooltips, popups) ---
|
|
Never tabs: they float above every tab tree at their absolute X position
|
|
while mapped, and take the keyboard only when their window type says they
|
|
want it (rofi-style launchers). */
|
|
|
|
struct kick_or_window {
|
|
struct wlr_xwayland_surface *xsurface;
|
|
struct wlr_scene_tree *tree;
|
|
bool surface_linked;
|
|
bool took_focus;
|
|
struct wl_listener destroy;
|
|
struct wl_listener associate;
|
|
struct wl_listener dissociate;
|
|
struct wl_listener map;
|
|
struct wl_listener unmap;
|
|
struct wl_listener commit;
|
|
struct wl_listener set_geometry;
|
|
struct wl_list link; /* server.or_windows */
|
|
};
|
|
|
|
static void or_window_position(struct kick_or_window *orw) {
|
|
struct wlr_xwayland_surface *xs = orw->xsurface;
|
|
if (!xs || !orw->tree) return;
|
|
/* Absolute layout coordinates: menus open where the app put them. */
|
|
wlr_scene_node_set_position(&orw->tree->node, xs->x, xs->y);
|
|
}
|
|
|
|
/* Hand the keyboard back to the tab that held it before this window took
|
|
it (or clear focus when nothing did). A second focus-taking menu may
|
|
still be open: keep the keyboard on it instead of yanking focus back to
|
|
the tab while a menu is up (the stashed owner stays put for the final
|
|
release). */
|
|
static void or_window_focus_release(struct kick_or_window *orw) {
|
|
if (!orw->took_focus) return;
|
|
orw->took_focus = false;
|
|
struct kick_tab *owner = server.or_focus_owner;
|
|
struct kick_or_window *it;
|
|
wl_list_for_each(it, &server.or_windows, link) {
|
|
if (it == orw || !it->took_focus || !it->xsurface ||
|
|
!it->xsurface->surface)
|
|
continue;
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
it->xsurface->surface,
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
server.or_focus_owner = owner;
|
|
return;
|
|
}
|
|
server.or_focus_owner = NULL;
|
|
if (owner)
|
|
set_active_tab(owner);
|
|
else
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
}
|
|
|
|
static void or_handle_associate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, associate);
|
|
if (!orw->xsurface || !orw->xsurface->surface || !orw->tree) return;
|
|
wlr_scene_surface_create(orw->tree, orw->xsurface->surface);
|
|
orw->map.notify = or_handle_map;
|
|
wl_signal_add(&orw->xsurface->surface->events.map, &orw->map);
|
|
orw->unmap.notify = or_handle_unmap;
|
|
wl_signal_add(&orw->xsurface->surface->events.unmap, &orw->unmap);
|
|
orw->commit.notify = or_handle_commit;
|
|
wl_signal_add(&orw->xsurface->surface->events.commit, &orw->commit);
|
|
orw->surface_linked = true;
|
|
or_window_position(orw);
|
|
/* Same late-map replay as the member path: an override-redirect popup
|
|
can map and commit before its surface pairing reaches us. */
|
|
bool mapped = orw->xsurface->surface->mapped;
|
|
kick_log(KICK_LOG_DEBUG, "[x11] or associate class=%s%s\n",
|
|
orw->xsurface->class ? orw->xsurface->class : "?",
|
|
mapped ? " (late: already mapped)" : "");
|
|
if (mapped)
|
|
or_handle_map(&orw->map, NULL);
|
|
}
|
|
|
|
static void or_handle_dissociate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, dissociate);
|
|
if (!orw->surface_linked) return;
|
|
wl_list_remove(&orw->map.link);
|
|
wl_list_remove(&orw->unmap.link);
|
|
wl_list_remove(&orw->commit.link);
|
|
orw->surface_linked = false;
|
|
}
|
|
|
|
static void or_handle_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, map);
|
|
wlr_scene_node_set_enabled(&orw->tree->node, true);
|
|
or_window_position(orw);
|
|
/* Late-associated OR popups join already-mapped, outside the commit
|
|
damage flow -- same first-frame-black hazard as X11 members. */
|
|
if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
if (wlr_xwayland_surface_override_redirect_wants_focus(orw->xsurface)) {
|
|
if (!server.or_focus_owner && server.active_tab)
|
|
server.or_focus_owner = server.active_tab;
|
|
orw->took_focus = true;
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
orw->xsurface->surface,
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
}
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_handle_unmap(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, unmap);
|
|
wlr_scene_node_set_enabled(&orw->tree->node, false);
|
|
or_window_focus_release(orw);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_handle_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, commit);
|
|
or_window_position(orw);
|
|
}
|
|
|
|
static void or_handle_set_geometry(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, set_geometry);
|
|
kick_log(KICK_LOG_DEBUG, "[or] move wid=%u\n",
|
|
orw->xsurface ? (unsigned)orw->xsurface->window_id : 0);
|
|
or_window_position(orw);
|
|
}
|
|
|
|
static void or_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, destroy);
|
|
wl_list_remove(&orw->destroy.link);
|
|
wl_list_remove(&orw->associate.link);
|
|
wl_list_remove(&orw->dissociate.link);
|
|
wl_list_remove(&orw->set_geometry.link);
|
|
if (orw->surface_linked) {
|
|
wl_list_remove(&orw->map.link);
|
|
wl_list_remove(&orw->unmap.link);
|
|
wl_list_remove(&orw->commit.link);
|
|
orw->surface_linked = false;
|
|
}
|
|
or_window_focus_release(orw);
|
|
/* Same plain-tree ownership as managed X11 windows: destroy explicitly. */
|
|
if (orw->tree)
|
|
wlr_scene_node_destroy(&orw->tree->node);
|
|
wl_list_remove(&orw->link);
|
|
free(orw);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_window_create(struct wlr_xwayland_surface *xsurface) {
|
|
struct kick_or_window *orw = calloc(1, sizeof(*orw));
|
|
if (!orw) return;
|
|
orw->xsurface = xsurface;
|
|
orw->tree = wlr_scene_tree_create(&server.scene->tree);
|
|
if (!orw->tree) {
|
|
free(orw);
|
|
return;
|
|
}
|
|
layer_raise_above_windows();
|
|
wlr_scene_node_set_enabled(&orw->tree->node, false);
|
|
orw->associate.notify = or_handle_associate;
|
|
wl_signal_add(&xsurface->events.associate, &orw->associate);
|
|
orw->dissociate.notify = or_handle_dissociate;
|
|
wl_signal_add(&xsurface->events.dissociate, &orw->dissociate);
|
|
orw->set_geometry.notify = or_handle_set_geometry;
|
|
wl_signal_add(&xsurface->events.set_geometry, &orw->set_geometry);
|
|
orw->destroy.notify = or_handle_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &orw->destroy);
|
|
if (xsurface->surface) {
|
|
/* Defensive: association normally happens after new_surface. */
|
|
or_handle_associate(&orw->associate, NULL);
|
|
}
|
|
wl_list_insert(server.or_windows.prev, &orw->link);
|
|
kick_log(KICK_LOG_INFO, "[x11] or window class=%s\n",
|
|
xsurface->class ? xsurface->class : "?");
|
|
kick_log(KICK_LOG_DEBUG, "[x11] or create wid=%u %dx%d@%d,%d\n",
|
|
(unsigned)xsurface->window_id,
|
|
(int)xsurface->width, (int)xsurface->height,
|
|
(int)xsurface->x, (int)xsurface->y);
|
|
}
|
|
|
|
static void classify_xwayland_window(struct wlr_xwayland_surface *xsurface);
|
|
|
|
/* An X11 window that has been seen but not yet mapped. GTK toolkits open
|
|
an invisible "leader" window before any real frame; it never maps and
|
|
never gains a Wayland surface, so membership decisions -- tab claim,
|
|
tab-group attach, external append -- wait for the window's first map.
|
|
A window that never shows itself never joins the tab set, and is freed
|
|
silently when the client destroys it. */
|
|
struct kick_x11_pending {
|
|
struct wlr_xwayland_surface *xsurface;
|
|
struct wl_listener destroy;
|
|
struct wl_listener associate; /* bridge to the wlr_surface's map */
|
|
struct wl_listener dissociate; /* surface died pre-map: unhook map */
|
|
struct wl_listener map;
|
|
bool surface_hooked;
|
|
struct wl_list link; /* server.x11_pendings */
|
|
};
|
|
|
|
static void x11_pending_teardown(struct kick_x11_pending *p) {
|
|
wl_list_remove(&p->link);
|
|
wl_list_remove(&p->destroy.link);
|
|
/* The associate listener is attached unconditionally at new_surface, so
|
|
it must be removed unconditionally here too: a window destroyed
|
|
before ever associating a Wayland surface (VLC's short-lived
|
|
probe windows) leaves it linked otherwise, and wlroots asserts on
|
|
the non-empty associate list at destroy. */
|
|
wl_list_remove(&p->associate.link);
|
|
wl_list_remove(&p->dissociate.link);
|
|
if (p->surface_hooked)
|
|
wl_list_remove(&p->map.link);
|
|
free(p);
|
|
}
|
|
|
|
static void x11_pending_handle_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_x11_pending *p = wl_container_of(listener, p, map);
|
|
struct wlr_xwayland_surface *xsurface = p->xsurface;
|
|
kick_log(KICK_LOG_DEBUG, "[x11] t=%.0f pending wid=%u mapped\n",
|
|
kick_monotonic_ms(), (unsigned)xsurface->window_id);
|
|
x11_pending_teardown(p);
|
|
classify_xwayland_window(xsurface);
|
|
}
|
|
|
|
static void x11_pending_handle_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_x11_pending *p = wl_container_of(listener, p, destroy);
|
|
kick_log(KICK_LOG_DEBUG, "[x11] pending wid=%u destroyed unmapped\n",
|
|
(unsigned)p->xsurface->window_id);
|
|
x11_pending_teardown(p); /* never mapped: no tab, no member, no trace */
|
|
}
|
|
|
|
static void x11_pending_handle_associate(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_x11_pending *p = wl_container_of(listener, p, associate);
|
|
struct wlr_surface *surface = p->xsurface->surface;
|
|
/* Stall analysis: log every associate event (not just the hooked one)
|
|
with the surface state, so a stuck pairing shows exactly where the
|
|
chain stops (no surface yet vs surfaceless spin). */
|
|
kick_log(KICK_LOG_DEBUG, "[x11] t=%.0f pending wid=%u associate "
|
|
"surface=%p mapped=%d hooked=%d\n", kick_monotonic_ms(),
|
|
(unsigned)p->xsurface->window_id, (void *)surface,
|
|
surface ? (int)surface->mapped : -1, (int)p->surface_hooked);
|
|
if (!surface || p->surface_hooked)
|
|
return;
|
|
p->surface_hooked = true;
|
|
p->map.notify = x11_pending_handle_map;
|
|
wl_signal_add(&surface->events.map, &p->map);
|
|
if (surface->mapped) /* defensive: mapped before we could hook */
|
|
x11_pending_handle_map(&p->map, NULL);
|
|
}
|
|
|
|
/* The paired surface died before any map (client vanished mid-startup):
|
|
unhook the map listener now, or its node dangles into the freed surface's
|
|
signal list and the later xsurface-destroy teardown reads freed memory. */
|
|
static void x11_pending_handle_dissociate(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_x11_pending *p = wl_container_of(listener, p, dissociate);
|
|
if (!p->surface_hooked)
|
|
return;
|
|
wl_list_remove(&p->map.link);
|
|
p->surface_hooked = false;
|
|
}
|
|
|
|
static void handle_xwayland_new_surface(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_xwayland_surface *xsurface = data;
|
|
|
|
if (xsurface->override_redirect) {
|
|
or_window_create(xsurface);
|
|
return;
|
|
}
|
|
|
|
struct kick_x11_pending *p = calloc(1, sizeof(*p));
|
|
if (!p)
|
|
return;
|
|
p->xsurface = xsurface;
|
|
p->destroy.notify = x11_pending_handle_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &p->destroy);
|
|
p->associate.notify = x11_pending_handle_associate;
|
|
kick_log(KICK_LOG_DEBUG, "[x11] pending wid=%u pid=%d\n",
|
|
(unsigned)xsurface->window_id, (int)xsurface->pid);
|
|
wl_signal_add(&xsurface->events.associate, &p->associate);
|
|
p->dissociate.notify = x11_pending_handle_dissociate;
|
|
wl_signal_add(&xsurface->events.dissociate, &p->dissociate);
|
|
wl_list_insert(server.x11_pendings.prev, &p->link);
|
|
}
|
|
|
|
/* Decide where a newly *mapped* X11 window belongs: claim a pending window
|
|
by launcher process group, link it into an ancestor's tab, or append as
|
|
an external tab. Mirrors handle_new_toplevel. */
|
|
static void classify_xwayland_window(struct wlr_xwayland_surface *xsurface) {
|
|
|
|
/* The real X client's PID (_NET_WM_PID); 0 when the client didn't set
|
|
it, which lands the window in the external-append path below. */
|
|
pid_t xpid = xsurface->pid;
|
|
pid_t pgid = xpid > 0 ? getpgid(xpid) : -1;
|
|
/* Stall analysis: entry log (the outcome logs [claim]/[sub] stack
|
|
downstream in claim_x11_window). */
|
|
kick_log(KICK_LOG_DEBUG, "[x11] t=%.0f classify wid=%u xpid=%d pgid=%d\n",
|
|
kick_monotonic_ms(), (unsigned)xsurface->window_id,
|
|
(int)xpid, (int)pgid);
|
|
|
|
/* Claim a pending window by launcher process group: an X11 command
|
|
forked by launch_client inherits its group exactly like a Wayland
|
|
client. */
|
|
struct kick_window *w = NULL;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *cand;
|
|
wl_list_for_each(cand, &tab->windows, link) {
|
|
if (cand->xsurface || !cand->pending_claim) continue;
|
|
if (cand->pid > 0 && cand->pid == pgid) {
|
|
w = cand;
|
|
break;
|
|
}
|
|
}
|
|
if (w) break;
|
|
}
|
|
/* A window from a process descended from a connected tab follows the
|
|
same group rules as a second xdg toplevel. */
|
|
if (!w && xpid > 0) {
|
|
struct kick_tab *owner = tab_group_owner(xpid);
|
|
if (owner) {
|
|
if (tab_groups) {
|
|
w = window_alloc(owner);
|
|
} else {
|
|
struct kick_tab *nt = calloc(1, sizeof(*nt));
|
|
if (!nt) return;
|
|
wl_list_init(&nt->windows);
|
|
w = window_alloc(nt);
|
|
if (!w) { free(nt); return; }
|
|
struct kick_window *pa = window_for_xsurface(xsurface->parent);
|
|
insert_tab_after(nt, pa ? pa->tab : owner);
|
|
}
|
|
if (!w) return;
|
|
w->origin = ORIGIN_GROUP;
|
|
w->parent_tab = owner;
|
|
}
|
|
}
|
|
/* No pending window matched and no owner tab: an external client. Give
|
|
it its own tab, right after the tab that created it (the surface's
|
|
parent, else the active tab). */
|
|
if (!w) {
|
|
struct kick_tab *nt = calloc(1, sizeof(*nt));
|
|
if (!nt) return;
|
|
wl_list_init(&nt->windows);
|
|
w = window_alloc(nt);
|
|
if (!w) { free(nt); return; }
|
|
struct kick_window *pa = window_for_xsurface(xsurface->parent);
|
|
if (!pa)
|
|
pa = tab_current_window(server.active_tab);
|
|
insert_tab_after(nt, pa ? pa->tab : NULL);
|
|
w->origin = ORIGIN_EXTERNAL;
|
|
}
|
|
|
|
claim_x11_window(w->tab, w, xsurface, xpid, pgid);
|
|
}
|
|
|
|
static void handle_xwayland_ready(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
kick_log(KICK_LOG_INFO, "[xwayland] ready display=%s\n",
|
|
server.xwayland && server.xwayland->display_name
|
|
? server.xwayland->display_name : "?");
|
|
/* (Re)attach the client-death watch: each (re)start brings a fresh
|
|
Wayland client object. The destroy signal fires before that
|
|
client's resources are purged, which is exactly the window the
|
|
detach above needs. */
|
|
if (server.xwayland && server.xwayland->server &&
|
|
server.xwayland->server->client &&
|
|
server.xwayland->server->client != server.xwayland_client_watched) {
|
|
if (server.xwayland_client_watched)
|
|
wl_list_remove(&server.xwayland_client_destroy.link);
|
|
server.xwayland_client_destroy.notify = handle_xwayland_client_destroy;
|
|
wl_client_add_destroy_listener(server.xwayland->server->client,
|
|
&server.xwayland_client_destroy);
|
|
server.xwayland_client_watched = server.xwayland->server->client;
|
|
}
|
|
}
|
|
|
|
static void handle_xwayland_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
wl_list_remove(&server.xwayland_new_surface.link);
|
|
wl_list_remove(&server.xwayland_ready.link);
|
|
wl_list_remove(&server.xwayland_destroy.link);
|
|
if (server.xwayland_client_watched) {
|
|
wl_list_remove(&server.xwayland_client_destroy.link);
|
|
server.xwayland_client_watched = NULL;
|
|
}
|
|
server.xwayland = NULL;
|
|
kick_log(KICK_LOG_INFO, "[xwayland] gone\n");
|
|
}
|
|
|
|
/* The Xwayland Wayland client died (SIGKILLed server, crash, ...); wlroots
|
|
restarts the X server lazily, but the purge of THIS client's resources
|
|
destroys wlr_surfaces out from under our map/unmap/commit listeners
|
|
(wlroots asserts on non-empty listener lists at surface destroy:
|
|
surface_handle_resource_destroy). This destroy signal fires BEFORE the
|
|
purge, so detach proactively here. The xsurface-level listeners must
|
|
SURVIVE (the later xsurface destroy drives tab teardown), so detach
|
|
surface listeners only, across claimed windows, override-redirect
|
|
windows and unclaimed pendings. Idempotent; a later dissociate /
|
|
xsurface-destroy finds clean flags. */
|
|
static void handle_xwayland_client_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener; (void)data;
|
|
wl_list_remove(&server.xwayland_client_destroy.link);
|
|
server.xwayland_client_watched = NULL;
|
|
int n = 0;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (w->is_x11 && w->xs_surface_linked) {
|
|
window_detach_x11_surface(w);
|
|
n++;
|
|
}
|
|
}
|
|
}
|
|
struct kick_or_window *orw;
|
|
wl_list_for_each(orw, &server.or_windows, link) {
|
|
if (orw->surface_linked) {
|
|
wl_list_remove(&orw->map.link);
|
|
wl_list_remove(&orw->unmap.link);
|
|
wl_list_remove(&orw->commit.link);
|
|
orw->surface_linked = false;
|
|
n++;
|
|
}
|
|
}
|
|
struct kick_x11_pending *p;
|
|
wl_list_for_each(p, &server.x11_pendings, link) {
|
|
if (p->surface_hooked) {
|
|
wl_list_remove(&p->map.link);
|
|
p->surface_hooked = false;
|
|
n++;
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[xwayland] client died, detached %d surface "
|
|
"listeners\n", n);
|
|
}
|
|
#endif /* HAVE_XWAYLAND */
|
|
|
|
/* ===================== Tab management ===================== */
|
|
|
|
static struct kick_window *window_for_toplevel(struct wlr_xdg_toplevel *toplevel) {
|
|
if (!toplevel) return NULL;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->toplevel == toplevel)
|
|
return w;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* The window whose client (by its wl_client) owns the given toplevel
|
|
surface, or NULL. Used to attribute a cursor-shape request to the
|
|
requesting window so shapes are cached per window and reset on focus
|
|
changes. */
|
|
static struct kick_window *window_for_client(struct wl_client *client) {
|
|
if (!client) return NULL;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->toplevel &&
|
|
wl_resource_get_client(w->toplevel->base->surface->resource)
|
|
== client)
|
|
return w;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
struct kick_popup {
|
|
struct wlr_xdg_surface *xdg_surface;
|
|
struct wlr_scene_tree *scene;
|
|
/* Live-popup registry: every popup kick has created is on this list so
|
|
that a later popup can find its parent popup's scene tree by *address
|
|
comparison* alone (never dereferencing a possibly-freed pointer). */
|
|
struct wl_list link;
|
|
struct wl_listener commit;
|
|
struct wl_listener destroy;
|
|
struct wl_listener reposition;
|
|
};
|
|
|
|
/* Live popups created by kick (see kick_popup.link). Keyed-by-address lookup
|
|
only: resolve a popup's parent by comparing the parent wl_surface pointer
|
|
against each entry's surface, which never dereferences a stale pointer. */
|
|
static struct wl_list live_popups;
|
|
|
|
/* Lazily sealed (set to true once) right before the first popup is tracked. */
|
|
static bool live_popups_ready;
|
|
|
|
static void live_popups_init(void) {
|
|
if (!live_popups_ready) {
|
|
wl_list_init(&live_popups);
|
|
live_popups_ready = true;
|
|
}
|
|
}
|
|
|
|
/* Enough of the host window for popups to be positioned within; open menus
|
|
may exceed the toplevel but we keep them inside the visible area. */
|
|
static void popup_usable_box(struct wlr_box *box) {
|
|
box->x = 0;
|
|
box->y = 0;
|
|
if (server.output) {
|
|
box->width = server.output->width;
|
|
box->height = server.output->height;
|
|
} else {
|
|
box->width = host.width;
|
|
box->height = host.height;
|
|
}
|
|
}
|
|
|
|
/* Popups (menus, tooltips) are regular wlr_scene_xdg_surface nodes under
|
|
their parent's tree, so wlroots' scene renderer composites them into the
|
|
output buffer like any other surface. We only need to make sure the client
|
|
gets its initial configure (unconstrain from the output box) and to place
|
|
each popup under its parent's tree. */
|
|
static void popup_handle_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, commit);
|
|
struct wlr_xdg_surface *xdg_surface = tp->xdg_surface;
|
|
/* The client cannot map/render the popup until it receives its initial
|
|
configure, which wlroots only sends via wlr_xdg_popup_unconstrain_from
|
|
(which also adjusts geometry within the usable area). initial_commit is
|
|
true on the first commit; later commits are ignored. */
|
|
if (xdg_surface->initial_commit && xdg_surface->popup) {
|
|
struct wlr_box box;
|
|
popup_usable_box(&box);
|
|
wlr_xdg_popup_unconstrain_from_box(xdg_surface->popup, &box);
|
|
}
|
|
/* Once the popup maps, report its output-space top-left and size. Tests
|
|
key on this line to assert a bar's menu hung flush under the bar; the
|
|
coordinates are scene-exact (no wall-clock guesswork). */
|
|
if (xdg_surface->surface->mapped && tp->scene) {
|
|
int lx = 0, ly = 0;
|
|
wlr_scene_node_coords(&tp->scene->node, &lx, &ly);
|
|
pid_t pcpid = -1;
|
|
if (xdg_surface->client && xdg_surface->client->client)
|
|
wl_client_get_credentials(xdg_surface->client->client,
|
|
&pcpid, NULL, NULL);
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[popup] out=%d,%d geo=%dx%d pcpid=%d\n", lx, ly,
|
|
xdg_surface->geometry.width, xdg_surface->geometry.height,
|
|
pcpid);
|
|
}
|
|
}
|
|
|
|
static void popup_handle_reposition(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, reposition);
|
|
if (tp->xdg_surface->popup) {
|
|
struct wlr_box box;
|
|
popup_usable_box(&box);
|
|
wlr_xdg_popup_unconstrain_from_box(tp->xdg_surface->popup, &box);
|
|
}
|
|
}
|
|
|
|
static void popup_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, destroy);
|
|
wl_list_remove(&tp->commit.link);
|
|
wl_list_remove(&tp->destroy.link);
|
|
wl_list_remove(&tp->reposition.link);
|
|
wl_list_remove(&tp->link);
|
|
/* The popup's xdg_surface outlives this kick_popup (it is freed later by
|
|
wlroots' destroy_xdg_surface); clear the data pointer we stashed there
|
|
so a later nest-under-this-surface popup never reads freed memory. */
|
|
if (tp->xdg_surface)
|
|
tp->xdg_surface->data = NULL;
|
|
free(tp);
|
|
}
|
|
|
|
static void create_popup(struct wlr_xdg_popup *popup,
|
|
struct wlr_scene_tree *parent_tree) {
|
|
struct wlr_xdg_surface *xdg_surface = popup->base;
|
|
struct kick_popup *tp = calloc(1, sizeof(*tp));
|
|
if (!tp) return;
|
|
tp->xdg_surface = xdg_surface;
|
|
tp->scene = wlr_scene_xdg_surface_create(parent_tree, xdg_surface);
|
|
/* wlr_scene_xdg_surface_create destroys the node when the surface is
|
|
destroyed, so we only track positioning/reposition listeners here. */
|
|
tp->commit.notify = popup_handle_commit;
|
|
wl_signal_add(&xdg_surface->surface->events.commit, &tp->commit);
|
|
tp->destroy.notify = popup_handle_destroy;
|
|
/* Listen on the popup's own destroy: wlroots emits it (then asserts its
|
|
event lists are empty) before the popup is freed. */
|
|
wl_signal_add(&popup->events.destroy, &tp->destroy);
|
|
tp->reposition.notify = popup_handle_reposition;
|
|
wl_signal_add(&popup->events.reposition, &tp->reposition);
|
|
/* remember the scene tree so a submenu popup can find its parent's tree */
|
|
xdg_surface->data = tp;
|
|
live_popups_init();
|
|
wl_list_insert(live_popups.prev, &tp->link);
|
|
}
|
|
|
|
/* The scene tree a window's popups must hang off: each window's popups ride
|
|
on that window's own tree (so they show only while it is the current
|
|
window and position within it). */
|
|
static struct wlr_scene_tree *popup_tree_for_window(struct kick_window *w) {
|
|
return w ? w->tree : NULL;
|
|
}
|
|
|
|
static void handle_new_popup(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_popup *popup = data;
|
|
|
|
/* Popups position themselves relative to their immediate parent surface,
|
|
so place each popup under its parent's scene tree.
|
|
|
|
The parent is ONLY ever located by *address comparison* against kick's
|
|
live surfaces -- never by dereferencing popup->parent. wlroots can
|
|
hand us a popup whose parent wl_surface was already freed (outside the
|
|
xdg teardown cascade), and wlr_xdg_surface_try_from_wlr_surface() then
|
|
dereferences that freed pointer and crashes. Comparing the raw address
|
|
against live surfaces is safe: a match means the parent is alive and we
|
|
use its tree, no match means the parent is gone (or not kick-managed)
|
|
and we skip the popup instead of faulting. */
|
|
struct wlr_scene_tree *parent_tree = NULL;
|
|
struct wlr_surface *parent_ws = popup->parent;
|
|
|
|
live_popups_init();
|
|
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (w->toplevel &&
|
|
w->toplevel->base->surface == parent_ws) {
|
|
parent_tree = popup_tree_for_window(w);
|
|
goto found;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct kick_popup *tp;
|
|
wl_list_for_each(tp, &live_popups, link) {
|
|
if (tp->xdg_surface->surface == parent_ws) {
|
|
parent_tree = tp->scene;
|
|
goto found;
|
|
}
|
|
}
|
|
|
|
found:
|
|
/* A popup's parent may be gone or not kick-managed (e.g. it belonged to
|
|
a window that closed, or a non-tab surface such as a layer-shell bar).
|
|
Rather than drop the popup -- which would leave the client waiting
|
|
forever for its initial configure ("Timed out waiting for initial
|
|
.configure") -- fall back so the popup still gets rendered and
|
|
configured, choosing the mount by which client made it:
|
|
* a parentless popup from the active tab's own client still hangs
|
|
under that window's (bar-offset) tree, keeping it aligned with the
|
|
app's content just as before; and
|
|
* a popup from any other client (a layer-shell bar such as
|
|
wf-panel-pi, or a gone/foreign surface) maps at the output origin
|
|
so its positioner geometry lands on real pixels instead of being
|
|
shifted down/right past the bar's reserved edge. */
|
|
if (!parent_tree) {
|
|
pid_t pcpid = -1;
|
|
if (popup->base->client)
|
|
wl_client_get_credentials(popup->base->client->client,
|
|
&pcpid, NULL, NULL);
|
|
struct kick_window *cur = tab_current_window(server.active_tab);
|
|
if (cur && cur->client_pid > 0 && cur->client_pid == pcpid) {
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[popup] parent %p not live; reusing active window tree "
|
|
"for same-client popup\n",
|
|
(void *)parent_ws);
|
|
parent_tree = popup_tree_for_window(cur);
|
|
} else {
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[popup] parent %p not live; hanging popup under popup "
|
|
"tree at output origin\n",
|
|
(void *)parent_ws);
|
|
parent_tree = server.popup_tree;
|
|
}
|
|
}
|
|
if (!parent_tree) return;
|
|
create_popup(popup, parent_tree);
|
|
}
|
|
|
|
/* ===================== wlr-layer-shell (waybar / panels) =================
|
|
Layer-shell surfaces are never tabs: each renders into server.layer_tree, a
|
|
dedicated child of server.scene->tree that sits above every window tree and
|
|
is never toggled by show_window()/set_active_tab(), so a bar stays visible
|
|
across all tabs. Only a layer surface's own exclusive_zone shrinks the
|
|
usable area the maximized tabs are sized into (delegated to wlroots' scene
|
|
helper), mirroring sway and labwc: a transient zero-exclusive-zone surface
|
|
(e.g. a panel notification) is positioned but never resizes the tabs.
|
|
|
|
wlroots' own scene helper (wlr_scene_layer_surface_v1_*) does the heavy
|
|
lifting for us: it computes the surface's anchored/centered geometry from
|
|
its pending state and, for mapped surfaces with a positive exclusive zone,
|
|
shrinks a caller-supplied usable area for the next layer surface. We drive
|
|
that in two passes -- exclusive-zone surfaces first (highest layer first)
|
|
so they always get preferred placement regardless of launch order, then the
|
|
zero-exclusive-zone remainder within the residue (lowest layer first) --
|
|
and size every maximized tab to the residual usable box. */
|
|
|
|
struct kick_layer {
|
|
struct wl_list link;
|
|
struct wlr_layer_surface_v1 *surface;
|
|
struct wlr_scene_layer_surface_v1 *scene;
|
|
struct wl_listener commit;
|
|
struct wl_listener destroy;
|
|
bool mapped;
|
|
/* Whether the surface's first commit (which sets wlroots' initialized
|
|
flag) has been seen; guards the first configure. */
|
|
bool seen_init;
|
|
};
|
|
|
|
/* The full output box, used as the starting full_area for the layer layout
|
|
pass and the "which area may tabs ever grow into" bounds. Tracks the
|
|
canvas (see canvas_width), not the lazily-committed output object. */
|
|
static void layer_full_area(struct wlr_box *box) {
|
|
box->x = 0;
|
|
box->y = 0;
|
|
box->width = canvas_width();
|
|
box->height = canvas_height();
|
|
}
|
|
|
|
/* Re-size clients into the layout boxes, mirroring output_resize's loop. A
|
|
live-fullscreen entry tracks the *output* size, never the usable box:
|
|
sizing it into usable would shrink a hidden entry wrongly, and skipping
|
|
it entirely would leave a newly fullscreened entry painting small at the
|
|
top-left with blank space (nested grows the output on fullscreen, so the
|
|
entry must learn the new size here). Every other window is maximized
|
|
into the usable box. Scene positions re-sync for all. */
|
|
static void resize_tabs_to_usable(const struct wlr_box *usable,
|
|
const struct wlr_box *full) {
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (!window_live(w)) continue;
|
|
if (window_is_fullscreen(w)) {
|
|
window_set_size(w, full->width, full->height);
|
|
} else {
|
|
window_set_maximized(w, true);
|
|
window_set_size(w, usable->width, usable->height);
|
|
}
|
|
/* Move the tab content down/right past a bar's reserved edge now,
|
|
not only on the client's next commit. These are idempotent:
|
|
position_window does nothing for X11 windows and
|
|
x11_sync_position does nothing for XDG windows. */
|
|
position_window(w);
|
|
x11_sync_position(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Keep the bar's scene tree above every window tree. layer_tree is created
|
|
at scene setup before any windows attach (so they'd otherwise stack on top),
|
|
and each new window subtree is appended above any siblings -- so after every
|
|
window claim we re-raise the layer tree to stay on top. */
|
|
static void layer_raise_above_windows(void) {
|
|
if (server.layer_tree)
|
|
wlr_scene_node_raise_to_top(&server.layer_tree->node);
|
|
if (server.popup_tree)
|
|
wlr_scene_node_raise_to_top(&server.popup_tree->node);
|
|
if (server.input_popup_tree)
|
|
wlr_scene_node_raise_to_top(&server.input_popup_tree->node);
|
|
}
|
|
|
|
/* Fullscreen takes the whole output: hide the panel/layer trees (and the
|
|
parentless panel-popup tree hanging off them) so a bar such as waybar does
|
|
not paint over the shown fullscreen entry. Panels stay visible while a
|
|
normal entry is shown, even with another entry fullscreened in the
|
|
background. The IME popup tree stays up so text input keeps working
|
|
while fullscreen. */
|
|
static void layer_update_visibility(void) {
|
|
bool show = !shown_is_fullscreen();
|
|
if (server.layer_tree)
|
|
wlr_scene_node_set_enabled(&server.layer_tree->node, show);
|
|
if (server.popup_tree)
|
|
wlr_scene_node_set_enabled(&server.popup_tree->node, show);
|
|
}
|
|
|
|
/* Layer orders for the two configure passes, mirroring labwc's
|
|
layers_arrange(): exclusive-zone surfaces run from the highest layer down
|
|
so they always get preferred placement, the zero-exclusive-zone remainder
|
|
runs from the lowest layer up inside the residue. */
|
|
static const enum zwlr_layer_shell_v1_layer layer_order_exclusive[] = {
|
|
ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_TOP,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND,
|
|
};
|
|
static const enum zwlr_layer_shell_v1_layer layer_order_rest[] = {
|
|
ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_TOP,
|
|
ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
|
|
};
|
|
|
|
/* Configure every layer surface on `want` whose exclusive-zone class matches:
|
|
exclusive=true means a non-zero exclusive_zone (including -1, the full-area
|
|
request), exclusive=false the zero-exclusive-zone remainder. wlroots' scene
|
|
helper shrinks the shared usable box inside the call for mapped surfaces
|
|
with a positive exclusive zone, and positions everything else within the
|
|
current residue without shrinking it. */
|
|
static void layer_configure_class(enum zwlr_layer_shell_v1_layer want,
|
|
bool exclusive, const struct wlr_box *full,
|
|
struct wlr_box *usable) {
|
|
struct kick_layer *sl;
|
|
wl_list_for_each(sl, &server.layer_surfaces, link) {
|
|
if (!sl->scene || !sl->seen_init) continue;
|
|
if (sl->surface->current.layer != want) continue;
|
|
if (!!sl->surface->current.exclusive_zone != exclusive) continue;
|
|
wlr_scene_layer_surface_v1_configure(sl->scene, full, usable);
|
|
}
|
|
}
|
|
|
|
/* Run the layer layout pass: configure every layer surface against the given
|
|
full area -- exclusive-zone surfaces first so they reserve, then the
|
|
zero-exclusive-zone remainder within the residue -- and size all maximized
|
|
tabs into the final usable box. While the *shown* entry is fullscreen it
|
|
takes the whole output: layer surfaces keep their geometry (so they return
|
|
intact when a normal entry is shown again) but their exclusive-zone
|
|
reservations are discarded and the tabs are sized to the full area.
|
|
A hidden fullscreen entry is sized to the full box (not the usable
|
|
one) by the resize loop's per-window rule. */
|
|
static void layer_apply_layout_box(const struct wlr_box *full) {
|
|
struct wlr_box usable = *full;
|
|
|
|
if (shown_is_fullscreen()) {
|
|
struct wlr_box discard = *full;
|
|
for (size_t i = 0;
|
|
i < sizeof(layer_order_exclusive) / sizeof(layer_order_exclusive[0]);
|
|
i++)
|
|
layer_configure_class(layer_order_exclusive[i], true, full, &discard);
|
|
|
|
for (size_t i = 0;
|
|
i < sizeof(layer_order_rest) / sizeof(layer_order_rest[0]); i++)
|
|
layer_configure_class(layer_order_rest[i], false, full, &discard);
|
|
|
|
server.layout_x = full->x;
|
|
server.layout_y = full->y;
|
|
resize_tabs_to_usable(full, full);
|
|
kick_request_redraw();
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0;
|
|
i < sizeof(layer_order_exclusive) / sizeof(layer_order_exclusive[0]);
|
|
i++)
|
|
layer_configure_class(layer_order_exclusive[i], true, full, &usable);
|
|
|
|
for (size_t i = 0;
|
|
i < sizeof(layer_order_rest) / sizeof(layer_order_rest[0]); i++)
|
|
layer_configure_class(layer_order_rest[i], false, full, &usable);
|
|
|
|
server.layout_x = usable.x;
|
|
server.layout_y = usable.y;
|
|
resize_tabs_to_usable(&usable, full);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Layout pass for the current (already-applied) output size. */
|
|
static void layer_apply_layout(void) {
|
|
struct wlr_box full;
|
|
layer_full_area(&full);
|
|
layer_apply_layout_box(&full);
|
|
}
|
|
|
|
static void layer_destroy(struct kick_layer *sl) {
|
|
wl_list_remove(&sl->link);
|
|
/* The surface's own destroy path (which raised this listener) is what
|
|
frees the wlr_layer_surface_v1 and, via wlroots' scene helper, the
|
|
scene node; here we only drop our bookkeeping. */
|
|
free(sl);
|
|
}
|
|
|
|
static void layer_handle_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_layer *sl = wl_container_of(listener, sl, commit);
|
|
bool mapped = sl->surface->surface->mapped;
|
|
/* Deterministic "the bar is on screen" signal for tests: fires on the
|
|
commit that maps a layer surface, including a first-commit map. */
|
|
if (mapped && !sl->mapped) {
|
|
pid_t pid = -1;
|
|
if (sl->surface->resource)
|
|
wl_client_get_credentials(
|
|
wl_resource_get_client(sl->surface->resource),
|
|
&pid, NULL, NULL);
|
|
kick_log(KICK_LOG_DEBUG, "[layer] map pid=%d\n", pid);
|
|
}
|
|
if (!sl->seen_init) {
|
|
/* First commit. wlroots' layer_surface_role_commit runs first and
|
|
sets surface->initialized, so this is the earliest point we may call
|
|
wlr_layer_surface_v1_configure (which asserts initialized). Deliver
|
|
the first geometry even though the surface isn't mapped yet. */
|
|
sl->seen_init = true;
|
|
sl->mapped = mapped;
|
|
layer_apply_layout();
|
|
} else if (mapped != sl->mapped) {
|
|
sl->mapped = mapped;
|
|
/* exclusive-zone reservation applies only once the surface is mapped
|
|
(wlr_scene_layer_surface_v1_configure checks surface->mapped), so
|
|
the bar's map is what actually shrinks the tabs. */
|
|
layer_apply_layout();
|
|
} else if (mapped) {
|
|
/* Already mapped: the commit may have changed size/anchor/exclusive
|
|
zone/desired size, so re-run layout to keep the bar and tabs in
|
|
step. */
|
|
layer_apply_layout();
|
|
}
|
|
}
|
|
|
|
static void layer_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_layer *sl = wl_container_of(listener, sl, destroy);
|
|
/* Drop both listeners we registered before releasing bookkeeping: the
|
|
destroy listener (safe to remove here, inside its own emit) and the
|
|
commit listener on the wl_surface, which the layer-surface destroy
|
|
path does not touch and would otherwise fire on freed memory once a
|
|
late commit arrives during teardown. */
|
|
wl_list_remove(&sl->commit.link);
|
|
wl_list_remove(&sl->destroy.link);
|
|
layer_destroy(sl);
|
|
layer_apply_layout();
|
|
}
|
|
|
|
static void handle_new_layer_surface(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_layer_surface_v1 *layer_surface = data;
|
|
|
|
/* The protocol lets the client bind a layer surface without naming an
|
|
output; kick has exactly one output, so always anchor to it. */
|
|
if (layer_surface->output == NULL && server.output)
|
|
layer_surface->output = server.output;
|
|
|
|
struct kick_layer *sl = calloc(1, sizeof(*sl));
|
|
if (!sl) {
|
|
wlr_layer_surface_v1_destroy(layer_surface);
|
|
return;
|
|
}
|
|
sl->surface = layer_surface;
|
|
sl->scene = wlr_scene_layer_surface_v1_create(server.layer_tree,
|
|
layer_surface);
|
|
layer_raise_above_windows();
|
|
wl_list_insert(server.layer_surfaces.prev, &sl->link);
|
|
|
|
sl->commit.notify = layer_handle_commit;
|
|
wl_signal_add(&layer_surface->surface->events.commit, &sl->commit);
|
|
sl->destroy.notify = layer_handle_destroy;
|
|
wl_signal_add(&layer_surface->events.destroy, &sl->destroy);
|
|
}
|
|
|
|
/* Fork the --panel command (only ever one, hence no slot bookkeeping like
|
|
tabs need). Runs in its own process group but is never claimed by
|
|
handle_new_toplevel: a layer-shell client never creates a toplevel. */
|
|
static void launch_panel(void) {
|
|
if (!server.panel_cmd) return;
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
setpgid(0, 0);
|
|
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!xdg_runtime) xdg_runtime = "/tmp";
|
|
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
|
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
|
|
unsetenv("DISPLAY");
|
|
/* Same mode-var scrub as CLI clients (see launch_client): the panel
|
|
must not inherit standalone either. Explicit per-command values
|
|
still work: the command's own assignments happen after this. */
|
|
unsetenv("KICK_MODE");
|
|
unsetenv("WLR_BACKENDS");
|
|
/* A panel is a background process; its [info]/[warning] chatter must
|
|
not spill onto the launching terminal. As with CLI clients, only
|
|
debug logging keeps the bar's own output visible. */
|
|
if (kick_log_level() < KICK_LOG_DEBUG) {
|
|
int nullfd = open("/dev/null", O_WRONLY);
|
|
if (nullfd >= 0) {
|
|
dup2(nullfd, STDOUT_FILENO);
|
|
dup2(nullfd, STDERR_FILENO);
|
|
if (nullfd > STDERR_FILENO) close(nullfd);
|
|
}
|
|
}
|
|
/* The panel shares the login session's D-Bus bus like every other
|
|
compositor's panels do (labwc/sway exec theirs plainly too), so
|
|
single-instance panels (wf-panel-pi >= 1.31 owns
|
|
com.raspberrypi.wfpanelpi) defer to an already-running host copy
|
|
and exit without mapping -- see the --panel README note. */
|
|
execlp("/bin/sh", "sh", "-c", server.panel_cmd, NULL);
|
|
_exit(127);
|
|
}
|
|
if (pid > 0) {
|
|
server.panel_pid = pid;
|
|
kick_log(KICK_LOG_INFO, "[panel] pid=%d cmd=%s\n", pid, server.panel_cmd);
|
|
}
|
|
}
|
|
|
|
/* Apply a cursor-shape-v1 shape value end to end: in standalone mode set the
|
|
composited cursor image from the theme; in nested mode forward the shape to
|
|
the host pointer so the host draws the right cursor over our window. */
|
|
static void apply_cursor_shape(int shape) {
|
|
if (shape == 0 || shape == server.cursor_shape)
|
|
return;
|
|
server.cursor_shape = shape;
|
|
|
|
const char *name = wlr_cursor_shape_v1_name(
|
|
(enum wp_cursor_shape_device_v1_shape)shape);
|
|
if (server.standalone) {
|
|
if (server.cursor && server.xcursor_mgr)
|
|
wlr_cursor_set_xcursor(server.cursor, server.xcursor_mgr, name);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] shape %s\n", name);
|
|
} else {
|
|
host_cursor_shape_set(shape);
|
|
}
|
|
}
|
|
|
|
/* A nested client switched its cursor shape via cursor-shape-v1. Cache the
|
|
shape on the requesting window (so switching windows resets to that
|
|
window's own last shape, or default) and apply it. */
|
|
static void handle_shape_set(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_cursor_shape_manager_v1_request_set_shape_event *ev = data;
|
|
if (ev->device_type != WLR_CURSOR_SHAPE_MANAGER_V1_DEVICE_TYPE_POINTER)
|
|
return;
|
|
struct kick_window *w = window_for_client(ev->seat_client->client);
|
|
if (w)
|
|
w->cursor_shape = ev->shape;
|
|
apply_cursor_shape(ev->shape);
|
|
}
|
|
|
|
/* ===================== MRU window stack =====================
|
|
Alt+Tab / Alt+1 walk the windows most-recently-used-first: each press
|
|
previews the next window after the shown one in server.mru order, wrapping
|
|
at the bottom, without mutating the stack. The shown window moves to the
|
|
front when the prefix is released (see update_overlay_visible), or
|
|
immediately when focus changes with the prefix up (window_enter). */
|
|
|
|
static void mru_move_to_front(struct kick_window *w) {
|
|
if (!w || !w->mru_linked) return;
|
|
if (server.mru.next == &w->mru_link) return; /* already most recent */
|
|
wl_list_remove(&w->mru_link);
|
|
wl_list_insert(&server.mru, &w->mru_link);
|
|
}
|
|
|
|
static void mru_remove(struct kick_window *w) {
|
|
if (!w || !w->mru_linked) return;
|
|
wl_list_remove(&w->mru_link);
|
|
w->mru_linked = false;
|
|
/* The anchor is only compared by pointer on prefix release; never
|
|
dereferenced, but clear it so a destroyed anchor can't masquerade as
|
|
"no navigation happened" logic elsewhere. */
|
|
if (server.mru_alt_anchor == w)
|
|
server.mru_alt_anchor = NULL;
|
|
}
|
|
|
|
/* The window the user is currently looking at: the focused window of the
|
|
active tab (a still-black pending placeholder when the tab awaits its
|
|
client). */
|
|
static struct kick_window *mru_current(void) {
|
|
return tab_current_window(server.active_tab);
|
|
}
|
|
|
|
/* Whether a window is a reachable Alt+Tab stop: it holds (or awaits) a
|
|
surface and is not being closed. Mirrors tab_overlay_row/cycle_tab so
|
|
Alt+Tab lands everywhere Alt+Up/Down can. */
|
|
static bool mru_reachable(struct kick_window *w) {
|
|
if (!w || w->closing) return false;
|
|
return window_live(w) || w->pending_claim;
|
|
}
|
|
|
|
/* Preview the next window after the shown one in MRU order, wrapping to the
|
|
most recent. With a single reachable window this re-shows it (a no-op
|
|
that still counts as navigation, so release commits it to the front). */
|
|
static void mru_step(void) {
|
|
if (wl_list_empty(&server.mru)) return;
|
|
struct kick_window *cur = mru_current();
|
|
struct kick_window *order[512];
|
|
int n = 0;
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &server.mru, mru_link) {
|
|
if (n < 512 && mru_reachable(w))
|
|
order[n++] = w;
|
|
}
|
|
if (n == 0) return;
|
|
int idx = -1;
|
|
for (int i = 0; i < n; i++) {
|
|
if (order[i] == cur) { idx = i; break; }
|
|
}
|
|
/* idx < 0 (shown window not in the stack) falls through to order[0]:
|
|
the most recent window. Otherwise the first press from the top lands
|
|
on order[1], the second-from-top, as specified. */
|
|
struct kick_window *target = order[(idx + 1) % n];
|
|
if (target->tab != server.active_tab)
|
|
set_active_tab(target->tab);
|
|
/* set_active_tab enters the tab's current window, which may be a
|
|
different stacked member; pin the exact MRU target. Harmless when
|
|
target is already shown (window_enter no-ops on focused+enabled). */
|
|
window_enter(target);
|
|
server.mru_alt_navigated = true;
|
|
kick_log(KICK_LOG_INFO, "[mru] preview app=%d\n", tab_index(target->tab));
|
|
}
|
|
|
|
/* Switch to the next live tab in `dir` direction (Alt+Down: +1), wrapping
|
|
around the end of the tab list, skipping tabs that are mid-close. With
|
|
only the current tab live, does nothing. */
|
|
static void cycle_tab(int dir) {
|
|
if (wl_list_empty(&server.tabs)) return;
|
|
struct kick_tab *cur = server.active_tab;
|
|
struct kick_tab *t;
|
|
if (dir > 0) {
|
|
/* First pass: tabs after the current one; second: the ones before
|
|
it (so the ring wraps exactly once). With no current tab the
|
|
first pass alone covers the whole list. */
|
|
bool past = (cur == NULL);
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == cur) { past = true; continue; }
|
|
if (past && tab_live(t) && !tab_closing(t)) {
|
|
set_active_tab(t);
|
|
return;
|
|
}
|
|
}
|
|
wl_list_for_each(t, &server.tabs, link) {
|
|
if (t == cur) break;
|
|
if (tab_live(t) && !tab_closing(t)) {
|
|
set_active_tab(t);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
bool past = (cur == NULL);
|
|
wl_list_for_each_reverse(t, &server.tabs, link) {
|
|
if (t == cur) { past = true; continue; }
|
|
if (past && tab_live(t) && !tab_closing(t)) {
|
|
set_active_tab(t);
|
|
return;
|
|
}
|
|
}
|
|
wl_list_for_each_reverse(t, &server.tabs, link) {
|
|
if (t == cur) break;
|
|
if (tab_live(t) && !tab_closing(t)) {
|
|
set_active_tab(t);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Move the active tab one position up (dir>0: later in the list) or down
|
|
(dir<0) within the tab order, wrapping at the ends. The active
|
|
surface is unchanged; only the tab's position in the list moves, which
|
|
the overlay and cycling both iterate. Only live tabs (mapped or pending
|
|
black, mirroring cycle_tab) that aren't mid-close count as positions;
|
|
with fewer than two of them there is nowhere to move. */
|
|
static void reorder_active_tab(int dir) {
|
|
if (wl_list_empty(&server.tabs)) return;
|
|
struct kick_tab *cur = server.active_tab;
|
|
if (!cur) return;
|
|
|
|
/* Collect the live, non-closing tabs as an indexable list. */
|
|
struct kick_tab *tabs[256];
|
|
int n = 0;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link)
|
|
if (n < 256 && tab_live(t) && !tab_closing(t))
|
|
tabs[n++] = t;
|
|
if (n < 2) return;
|
|
|
|
int from = -1;
|
|
for (int i = 0; i < n; i++)
|
|
if (tabs[i] == cur) {
|
|
from = i;
|
|
break;
|
|
}
|
|
if (from < 0) return;
|
|
|
|
int to = (from + dir + n) % n;
|
|
if (to == from) return;
|
|
|
|
wl_list_remove(&cur->link);
|
|
/* wl_list_insert() inserts after its anchor. Moving down (to > from) the
|
|
moved tab lands directly ahead of the tab that was at `to`, so it must
|
|
be spliced in AFTER tabs[to]; moving up (to < from) it lands directly
|
|
behind tabs[to - 1]. Anchoring on tabs[to - 1] for a rightward move
|
|
resolves to cur itself (already removed) and splices it out of the
|
|
list entirely. */
|
|
if (to == 0)
|
|
wl_list_insert(&server.tabs, &cur->link);
|
|
else if (to > from)
|
|
wl_list_insert(&tabs[to]->link, &cur->link);
|
|
else
|
|
wl_list_insert(&tabs[to - 1]->link, &cur->link);
|
|
|
|
kick_log(KICK_LOG_INFO, "[reorder] app=%d dir=%d\n", to, dir);
|
|
/* Input binding changed the visible order: repaint like any other
|
|
keybinding (whole damage keeps the composite coat clean). */
|
|
if (host.overlay_visible) {
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
else if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
kick_request_redraw();
|
|
}
|
|
}
|
|
|
|
/* Fixed keybind repeat: first repeat after 600ms, then every 40ms (25Hz).
|
|
The initial delay honours KICK_REPEAT_DELAY_MS so tests can stretch it
|
|
past any load-stalled press/release gap: a wall-clock repeat firing
|
|
between a tap's queued press and its release would otherwise displace
|
|
MRU/cycle/reorder state that discrete-chord tests assert exactly. */
|
|
#define KICK_REPEAT_DELAY_MS 600
|
|
#define KICK_REPEAT_INTERVAL_MS 40
|
|
|
|
static long kick_repeat_delay_ms(void) {
|
|
const char *e = getenv("KICK_REPEAT_DELAY_MS");
|
|
if (e && *e) {
|
|
long v = atol(e);
|
|
if (v > 0)
|
|
return v;
|
|
}
|
|
return KICK_REPEAT_DELAY_MS;
|
|
}
|
|
|
|
static void kbd_repeat_cancel(void) {
|
|
if (host.repeat_timer) {
|
|
kick_log(KICK_LOG_DEBUG, "[repeat] stop key=%u\n", host.repeat_key);
|
|
wl_event_source_remove(host.repeat_timer);
|
|
host.repeat_timer = NULL;
|
|
}
|
|
host.repeat_key = 0;
|
|
}
|
|
|
|
static int kbd_repeat_cb(void *data) {
|
|
(void)data;
|
|
if (host.repeat_key == 0)
|
|
return 0;
|
|
if (host.repeat_kind == REPEAT_REORDER)
|
|
reorder_active_tab(host.repeat_dir);
|
|
else if (host.repeat_kind == REPEAT_SUBCYCLE)
|
|
cycle_tab_windows(server.active_tab, host.repeat_dir);
|
|
else if (host.repeat_kind == REPEAT_MEMBER_MOVE) {
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
if (w)
|
|
reorder_window(w, host.repeat_dir);
|
|
} else if (host.repeat_kind == REPEAT_MRU)
|
|
mru_step();
|
|
else
|
|
cycle_tab(host.repeat_dir);
|
|
if (host.repeat_timer)
|
|
wl_event_source_timer_update(host.repeat_timer,
|
|
KICK_REPEAT_INTERVAL_MS);
|
|
return 0;
|
|
}
|
|
|
|
static void kbd_repeat_start(int key, int dir, int kind) {
|
|
kbd_repeat_cancel();
|
|
host.repeat_key = key;
|
|
host.repeat_dir = dir;
|
|
host.repeat_kind = kind;
|
|
host.repeat_timer = wl_event_loop_add_timer(server.loop, kbd_repeat_cb, NULL);
|
|
if (host.repeat_timer) {
|
|
static const char *const kinds[] = {
|
|
"cycle", "reorder", "subcycle", "member-move", "mru",
|
|
};
|
|
const char *name = (kind >= 0 && kind < 5) ? kinds[kind] : "?";
|
|
kick_log(KICK_LOG_DEBUG, "[repeat] start key=%d kind=%s\n", key, name);
|
|
wl_event_source_timer_update(host.repeat_timer,
|
|
(int)kick_repeat_delay_ms());
|
|
}
|
|
}
|
|
|
|
/* ===================== Clipboard bridge implementation ===================== */
|
|
|
|
static void bridge_mimes_reset(void) {
|
|
for (size_t i = 0; i < bridge.mimes_len; i++)
|
|
free(bridge.mimes[i]);
|
|
bridge.mimes_len = 0;
|
|
}
|
|
|
|
static void bridge_mimes_append(const char *mime) {
|
|
if (bridge.mimes_len == bridge.mimes_cap) {
|
|
size_t ncap = bridge.mimes_cap ? bridge.mimes_cap * 2 : 8;
|
|
char **nm = realloc(bridge.mimes, ncap * sizeof(*nm));
|
|
if (!nm)
|
|
return;
|
|
bridge.mimes = nm;
|
|
bridge.mimes_cap = ncap;
|
|
}
|
|
bridge.mimes[bridge.mimes_len++] = strdup(mime);
|
|
}
|
|
|
|
static double bridge_dbg_ms(void);
|
|
static void bridge_inner_src_destroy(struct wl_listener *l, void *data);
|
|
static bool bridge_prefetch_mime_compatible(const char *mime);
|
|
static void bridge_prefetch_serve(const char *mime, int32_t fd);
|
|
static bool bridge_primary_prefetch_mime_compatible(const char *mime);
|
|
static void bridge_primary_prefetch_serve(const char *mime, int32_t fd);
|
|
|
|
/* Compositor-side nested source: when a nested client pastes, wlroots calls
|
|
our impl->send() and we pull the data from the host offer straight into the
|
|
fd wlroots gave us (zero copy, no relay). */
|
|
static void bridge_inner_source_send(struct wlr_data_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-src] t=%.0f inner_send mime=%s inner_src=%p "
|
|
"prefetch_ready=%d dc_offer=%p dd_offer=%p\n", bridge_dbg_ms(),
|
|
mime, (void *)bridge.inner_src, bridge.prefetch_ready,
|
|
(void *)bridge.dc_offer, (void *)bridge.dd_offer);
|
|
if (bridge_prefetch_mime_compatible(mime)) {
|
|
bridge_prefetch_serve(mime, fd);
|
|
return;
|
|
}
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_offer) {
|
|
ext_data_control_offer_v1_receive(bridge.dc_offer, mime, fd);
|
|
close(fd);
|
|
} else if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE &&
|
|
bridge.dd_offer) {
|
|
wl_data_offer_receive(bridge.dd_offer, mime, fd);
|
|
close(fd);
|
|
} else {
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_source_destroy(struct wlr_data_source *src) {
|
|
free(src);
|
|
}
|
|
|
|
static const struct wlr_data_source_impl bridge_inner_source_impl = {
|
|
.send = bridge_inner_source_send,
|
|
.destroy = bridge_inner_source_destroy,
|
|
};
|
|
|
|
static void bridge_set_inner_src(struct wlr_data_source *src) {
|
|
if (bridge.inner_src == src)
|
|
return;
|
|
if (bridge.inner_src) {
|
|
wl_list_remove(&bridge.inner_src_destroy.link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
bridge.inner_src = src;
|
|
if (src) {
|
|
bridge.inner_src_destroy.notify = bridge_inner_src_destroy;
|
|
wl_signal_add(&src->events.destroy, &bridge.inner_src_destroy);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_src_destroy(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f inner_src destroyed\n",
|
|
bridge_dbg_ms());
|
|
wl_list_remove(&l->link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
|
|
/* Put a host selection into the nested seat. The source stays valid for the
|
|
whole lifetime of the host offer, so pastes can be served asynchronously. */
|
|
static void bridge_mirror_to_nested(void) {
|
|
struct wlr_data_source *src = calloc(1, sizeof(*src));
|
|
if (!src) {
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
wlr_data_source_init(src, &bridge_inner_source_impl);
|
|
for (size_t i = 0; i < bridge.mimes_len; i++) {
|
|
char **p = wl_array_add(&src->mime_types, sizeof(char *));
|
|
if (!p) {
|
|
for (size_t j = i; j < bridge.mimes_len; j++)
|
|
free(bridge.mimes[j]);
|
|
bridge.mimes_len = 0;
|
|
break;
|
|
}
|
|
*p = bridge.mimes[i];
|
|
bridge.mimes[i] = NULL; /* ownership moves into the wl_array */
|
|
}
|
|
bridge.mimes_len = 0;
|
|
bridge_set_inner_src(src);
|
|
wlr_seat_set_selection(server.seat, src,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
static void bridge_clear_nested(void) {
|
|
if (!server.seat->selection_source)
|
|
return;
|
|
bridge.ignore_next_inner = true;
|
|
wlr_seat_set_selection(server.seat, NULL,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
/* ---------------- primary selection mirror (host <-> nested seat) -------- */
|
|
|
|
/* The nested primary-selection source we install when a host offer is
|
|
mirrored in. A nested client asking for the data has the request pulled
|
|
straight from the host offer over the data-control pipe. */
|
|
static void bridge_inner_primary_source_send(struct wlr_primary_selection_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)src;
|
|
if (bridge_primary_prefetch_mime_compatible(mime)) {
|
|
bridge_primary_prefetch_serve(mime, fd);
|
|
return;
|
|
}
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_primary_offer) {
|
|
ext_data_control_offer_v1_receive(bridge.dc_primary_offer, mime, fd);
|
|
close(fd);
|
|
} else {
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_primary_source_destroy(struct wlr_primary_selection_source *src) {
|
|
free(src);
|
|
}
|
|
|
|
static const struct wlr_primary_selection_source_impl bridge_inner_primary_source_impl = {
|
|
.send = bridge_inner_primary_source_send,
|
|
.destroy = bridge_inner_primary_source_destroy,
|
|
};
|
|
|
|
static void bridge_inner_primary_src_destroy(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
wl_list_remove(&l->link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
|
|
static void bridge_set_inner_primary_src(struct wlr_primary_selection_source *src) {
|
|
if (bridge.inner_primary_src == src)
|
|
return;
|
|
if (bridge.inner_primary_src) {
|
|
wl_list_remove(&bridge.inner_primary_src_destroy.link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
bridge.inner_primary_src = src;
|
|
if (src) {
|
|
bridge.inner_primary_src_destroy.notify = bridge_inner_primary_src_destroy;
|
|
wl_signal_add(&src->events.destroy, &bridge.inner_primary_src_destroy);
|
|
}
|
|
}
|
|
|
|
/* Put the collected host primary offer (bridge.mimes) into the nested seat. */
|
|
static void bridge_mirror_primary_to_nested(void) {
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] mirror host primary into nested "
|
|
"(mimes=%zu)\n", bridge.mimes_len);
|
|
struct wlr_primary_selection_source *src = calloc(1, sizeof(*src));
|
|
if (!src) {
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
wlr_primary_selection_source_init(src, &bridge_inner_primary_source_impl);
|
|
for (size_t i = 0; i < bridge.mimes_len; i++) {
|
|
char **p = wl_array_add(&src->mime_types, sizeof(char *));
|
|
if (!p) {
|
|
for (size_t j = i; j < bridge.mimes_len; j++)
|
|
free(bridge.mimes[j]);
|
|
bridge.mimes_len = 0;
|
|
break;
|
|
}
|
|
*p = bridge.mimes[i];
|
|
bridge.mimes[i] = NULL; /* ownership moves into the wl_array */
|
|
}
|
|
bridge.mimes_len = 0;
|
|
wlr_seat_set_primary_selection(server.seat, src,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
static void bridge_primary_clear_nested(void) {
|
|
if (!server.seat->primary_selection_source)
|
|
return;
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] clear nested primary\n");
|
|
bridge.primary_ignore_next_inner = true;
|
|
wlr_seat_set_primary_selection(server.seat, NULL,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
/* ---------------- text prefetch: serve pastes from a buffered copy -------- */
|
|
|
|
#define KICK_PREFETCH_MAX (16 * 1024 * 1024)
|
|
|
|
static void bridge_prefetch_cancel(void) {
|
|
if (bridge.prefetch_src) {
|
|
wl_event_source_remove(bridge.prefetch_src);
|
|
bridge.prefetch_src = NULL;
|
|
}
|
|
if (bridge.prefetch_fd >= 0) {
|
|
close(bridge.prefetch_fd);
|
|
bridge.prefetch_fd = -1;
|
|
}
|
|
free(bridge.prefetch_buf);
|
|
bridge.prefetch_buf = NULL;
|
|
bridge.prefetch_len = bridge.prefetch_cap = 0;
|
|
bridge.prefetch_mime[0] = '\0';
|
|
bridge.prefetch_ready = false;
|
|
}
|
|
|
|
/* Drain the prefetch pipe into the buffer. Returns true when the stream is
|
|
finished (EOF/error) and the buffer should be kept as the paste payload. */
|
|
static bool bridge_prefetch_drain(int fd) {
|
|
for (;;) {
|
|
size_t room = bridge.prefetch_cap - bridge.prefetch_len;
|
|
if (bridge.prefetch_len + 4096 > bridge.prefetch_cap &&
|
|
bridge.prefetch_cap < KICK_PREFETCH_MAX) {
|
|
size_t ncap = bridge.prefetch_cap ? bridge.prefetch_cap * 2 : 8192;
|
|
if (ncap > KICK_PREFETCH_MAX)
|
|
ncap = KICK_PREFETCH_MAX;
|
|
char *nb = realloc(bridge.prefetch_buf, ncap);
|
|
if (nb) {
|
|
bridge.prefetch_buf = nb;
|
|
bridge.prefetch_cap = ncap;
|
|
room = bridge.prefetch_cap - bridge.prefetch_len;
|
|
}
|
|
}
|
|
if (room == 0) {
|
|
/* cap reached: drain without storing to keep the writer unblocked */
|
|
char tmp;
|
|
ssize_t n = read(fd, &tmp, 1);
|
|
if (n > 0)
|
|
continue;
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
ssize_t n = read(fd, bridge.prefetch_buf + bridge.prefetch_len, room);
|
|
if (n > 0) {
|
|
bridge.prefetch_len += (size_t)n;
|
|
continue;
|
|
}
|
|
if (n == 0)
|
|
return true; /* EOF */
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false; /* more data pending */
|
|
return true; /* read error: keep whatever we got */
|
|
}
|
|
}
|
|
|
|
static int bridge_prefetch_cb(int fd, uint32_t mask, void *data) {
|
|
(void)data; (void)mask;
|
|
if (!bridge_prefetch_drain(fd))
|
|
return 0;
|
|
if (bridge.prefetch_src) {
|
|
wl_event_source_remove(bridge.prefetch_src);
|
|
bridge.prefetch_src = NULL;
|
|
}
|
|
close(fd);
|
|
bridge.prefetch_fd = -1;
|
|
bridge.prefetch_ready = true;
|
|
return 0;
|
|
}
|
|
|
|
/* Pick the best text MIME offered for prefetching. */
|
|
static const char *bridge_pick_text_mime(void) {
|
|
static const char *pref[] = {
|
|
"text/plain;charset=utf-8", "text/plain", "UTF8_STRING", NULL,
|
|
};
|
|
for (int i = 0; pref[i]; i++)
|
|
for (size_t j = 0; j < bridge.mimes_len; j++)
|
|
if (strcmp(bridge.mimes[j], pref[i]) == 0)
|
|
return bridge.mimes[j];
|
|
for (size_t j = 0; j < bridge.mimes_len; j++)
|
|
if (strncmp(bridge.mimes[j], "text/", 5) == 0)
|
|
return bridge.mimes[j];
|
|
return NULL;
|
|
}
|
|
|
|
/* Start pulling the current host offer's primary text MIME into our buffer.
|
|
Must be called while bridge.mimes holds the freshly collected list. */
|
|
static void bridge_prefetch_start(void) {
|
|
bridge_prefetch_cancel();
|
|
if (!bridge.mimes_len)
|
|
return;
|
|
const char *mime = bridge_pick_text_mime();
|
|
if (!mime)
|
|
return;
|
|
int p[2];
|
|
if (pipe2(p, O_CLOEXEC | O_NONBLOCK) != 0)
|
|
return;
|
|
bridge.prefetch_fd = p[0];
|
|
bridge.prefetch_src = wl_event_loop_add_fd(server.loop, p[0],
|
|
WL_EVENT_READABLE, bridge_prefetch_cb, NULL);
|
|
if (!bridge.prefetch_src) {
|
|
close(p[0]);
|
|
close(p[1]);
|
|
bridge.prefetch_fd = -1;
|
|
return;
|
|
}
|
|
snprintf(bridge.prefetch_mime, sizeof(bridge.prefetch_mime), "%s", mime);
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_offer) {
|
|
ext_data_control_offer_v1_receive(bridge.dc_offer, mime, p[1]);
|
|
close(p[1]);
|
|
} else if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE &&
|
|
bridge.dd_offer) {
|
|
wl_data_offer_receive(bridge.dd_offer, mime, p[1]);
|
|
close(p[1]);
|
|
} else {
|
|
bridge_prefetch_cancel();
|
|
}
|
|
}
|
|
|
|
/* The requested MIME can be served from the prefetched plain-text buffer. */
|
|
static bool bridge_prefetch_mime_compatible(const char *mime) {
|
|
if (!bridge.prefetch_ready || bridge.prefetch_mime[0] == '\0')
|
|
return false;
|
|
if (strcmp(mime, bridge.prefetch_mime) == 0)
|
|
return true;
|
|
bool a = strncmp(mime, "text/plain", 10) == 0 ||
|
|
strcmp(mime, "UTF8_STRING") == 0;
|
|
bool b = strncmp(bridge.prefetch_mime, "text/plain", 10) == 0 ||
|
|
strcmp(bridge.prefetch_mime, "UTF8_STRING") == 0;
|
|
return a && b;
|
|
}
|
|
|
|
static void bridge_prefetch_serve(const char *mime, int32_t fd) {
|
|
if (!bridge_prefetch_mime_compatible(mime)) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
size_t off = 0;
|
|
while (off < bridge.prefetch_len) {
|
|
ssize_t n = write(fd, bridge.prefetch_buf + off,
|
|
bridge.prefetch_len - off);
|
|
if (n > 0) {
|
|
off += (size_t)n;
|
|
} else if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
/* ---------------- primary-selection text prefetch ----------------------- */
|
|
|
|
#define KICK_PRIMARY_PREFETCH_MAX KICK_PREFETCH_MAX
|
|
|
|
static void bridge_primary_prefetch_cancel(void) {
|
|
if (bridge.primary_prefetch_src) {
|
|
wl_event_source_remove(bridge.primary_prefetch_src);
|
|
bridge.primary_prefetch_src = NULL;
|
|
}
|
|
if (bridge.primary_prefetch_fd >= 0) {
|
|
close(bridge.primary_prefetch_fd);
|
|
bridge.primary_prefetch_fd = -1;
|
|
}
|
|
free(bridge.primary_prefetch_buf);
|
|
bridge.primary_prefetch_buf = NULL;
|
|
bridge.primary_prefetch_len = bridge.primary_prefetch_cap = 0;
|
|
bridge.primary_prefetch_mime[0] = '\0';
|
|
bridge.primary_prefetch_ready = false;
|
|
}
|
|
|
|
static bool bridge_primary_prefetch_drain(int fd) {
|
|
for (;;) {
|
|
size_t room = bridge.primary_prefetch_cap - bridge.primary_prefetch_len;
|
|
if (bridge.primary_prefetch_len + 4096 > bridge.primary_prefetch_cap &&
|
|
bridge.primary_prefetch_cap < KICK_PRIMARY_PREFETCH_MAX) {
|
|
size_t ncap = bridge.primary_prefetch_cap
|
|
? bridge.primary_prefetch_cap * 2 : 8192;
|
|
if (ncap > KICK_PRIMARY_PREFETCH_MAX)
|
|
ncap = KICK_PRIMARY_PREFETCH_MAX;
|
|
char *nb = realloc(bridge.primary_prefetch_buf, ncap);
|
|
if (nb) {
|
|
bridge.primary_prefetch_buf = nb;
|
|
bridge.primary_prefetch_cap = ncap;
|
|
room = bridge.primary_prefetch_cap - bridge.primary_prefetch_len;
|
|
}
|
|
}
|
|
if (room == 0) {
|
|
char tmp;
|
|
ssize_t n = read(fd, &tmp, 1);
|
|
if (n > 0)
|
|
continue;
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
ssize_t n = read(fd, bridge.primary_prefetch_buf + bridge.primary_prefetch_len,
|
|
room);
|
|
if (n > 0) {
|
|
bridge.primary_prefetch_len += (size_t)n;
|
|
continue;
|
|
}
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static int bridge_primary_prefetch_cb(int fd, uint32_t mask, void *data) {
|
|
(void)data; (void)mask;
|
|
if (!bridge_primary_prefetch_drain(fd))
|
|
return 0;
|
|
if (bridge.primary_prefetch_src) {
|
|
wl_event_source_remove(bridge.primary_prefetch_src);
|
|
bridge.primary_prefetch_src = NULL;
|
|
}
|
|
close(fd);
|
|
bridge.primary_prefetch_fd = -1;
|
|
bridge.primary_prefetch_ready = true;
|
|
return 0;
|
|
}
|
|
|
|
/* Start pulling the current host primary offer's text MIME into our buffer.
|
|
Must be called while bridge.mimes holds the freshly collected list. */
|
|
static void bridge_primary_prefetch_start(void) {
|
|
bridge_primary_prefetch_cancel();
|
|
if (bridge.backend != BRIDGE_BACKEND_DATA_CONTROL || !bridge.dc_primary_offer)
|
|
return;
|
|
if (!bridge.mimes_len)
|
|
return;
|
|
const char *mime = bridge_pick_text_mime();
|
|
if (!mime)
|
|
return;
|
|
int p[2];
|
|
if (pipe2(p, O_CLOEXEC | O_NONBLOCK) != 0)
|
|
return;
|
|
bridge.primary_prefetch_fd = p[0];
|
|
bridge.primary_prefetch_src = wl_event_loop_add_fd(server.loop, p[0],
|
|
WL_EVENT_READABLE, bridge_primary_prefetch_cb, NULL);
|
|
if (!bridge.primary_prefetch_src) {
|
|
close(p[0]);
|
|
close(p[1]);
|
|
bridge.primary_prefetch_fd = -1;
|
|
return;
|
|
}
|
|
snprintf(bridge.primary_prefetch_mime, sizeof(bridge.primary_prefetch_mime),
|
|
"%s", mime);
|
|
ext_data_control_offer_v1_receive(bridge.dc_primary_offer, mime, p[1]);
|
|
close(p[1]);
|
|
}
|
|
|
|
/* The requested MIME can be served from the prefetched primary-text buffer. */
|
|
static bool bridge_primary_prefetch_mime_compatible(const char *mime) {
|
|
if (!bridge.primary_prefetch_ready || bridge.primary_prefetch_mime[0] == '\0')
|
|
return false;
|
|
if (strcmp(mime, bridge.primary_prefetch_mime) == 0)
|
|
return true;
|
|
bool a = strncmp(mime, "text/plain", 10) == 0 ||
|
|
strcmp(mime, "UTF8_STRING") == 0;
|
|
bool b = strncmp(bridge.primary_prefetch_mime, "text/plain", 10) == 0 ||
|
|
strcmp(bridge.primary_prefetch_mime, "UTF8_STRING") == 0;
|
|
return a && b;
|
|
}
|
|
|
|
static void bridge_primary_prefetch_serve(const char *mime, int32_t fd) {
|
|
if (!bridge_primary_prefetch_mime_compatible(mime)) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
size_t off = 0;
|
|
while (off < bridge.primary_prefetch_len) {
|
|
ssize_t n = write(fd, bridge.primary_prefetch_buf + off,
|
|
bridge.primary_prefetch_len - off);
|
|
if (n > 0) {
|
|
off += (size_t)n;
|
|
} else if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
/* ---------------- host backend: ext-data-control ---------------- */
|
|
|
|
static void bridge_dc_source_send(void *data,
|
|
struct ext_data_control_source_v1 *src, const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc_source_send mime=%s "
|
|
"fd=%d inner_src=%p\n", bridge_dbg_ms(), mime, fd,
|
|
(void *)bridge.inner_src);
|
|
if (bridge.inner_src) {
|
|
wlr_data_source_send(bridge.inner_src, mime, fd);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc_source_send "
|
|
"forwarded mime=%s fd=%d\n", bridge_dbg_ms(), mime, fd);
|
|
} else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dc_source_cancelled(void *data,
|
|
struct ext_data_control_source_v1 *src) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] host cancelled our data-control source\n");
|
|
ext_data_control_source_v1_destroy(src);
|
|
bridge.dc_source = NULL;
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
}
|
|
|
|
static const struct ext_data_control_source_v1_listener bridge_dc_source_listener = {
|
|
.send = bridge_dc_source_send,
|
|
.cancelled = bridge_dc_source_cancelled,
|
|
};
|
|
|
|
/* Our published host primary-selection source: host middle-click requests
|
|
are served by asking the originating nested client for the data. */
|
|
static void bridge_dc_primary_source_send(void *data,
|
|
struct ext_data_control_source_v1 *src, const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
if (bridge.inner_primary_src)
|
|
wlr_primary_selection_source_send(bridge.inner_primary_src, mime, fd);
|
|
else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dc_primary_source_cancelled(void *data,
|
|
struct ext_data_control_source_v1 *src) {
|
|
(void)data;
|
|
ext_data_control_source_v1_destroy(src);
|
|
bridge.dc_primary_source = NULL;
|
|
bridge.dc_primary_pending_echo = false;
|
|
}
|
|
|
|
static const struct ext_data_control_source_v1_listener bridge_dc_primary_source_listener = {
|
|
.send = bridge_dc_primary_source_send,
|
|
.cancelled = bridge_dc_primary_source_cancelled,
|
|
};
|
|
|
|
static void bridge_dc_offer_mime(void *data,
|
|
struct ext_data_control_offer_v1 *offer, const char *mime) {
|
|
(void)data; (void)offer;
|
|
bridge_mimes_append(mime);
|
|
}
|
|
|
|
static const struct ext_data_control_offer_v1_listener bridge_dc_offer_listener = {
|
|
.offer = bridge_dc_offer_mime,
|
|
};
|
|
|
|
static void bridge_dc_device_finished(void *data,
|
|
struct ext_data_control_device_v1 *dev) {
|
|
(void)data; (void)dev;
|
|
bridge.backend = bridge.dd ? BRIDGE_BACKEND_WL_DATA_DEVICE
|
|
: BRIDGE_BACKEND_NONE;
|
|
if (bridge.dc_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
/* Our published clipboard source and its echo die with the device: take
|
|
them down so nothing later compares against or publishes through a
|
|
stale proxy. Echo guards from the dead device can never resolve, so
|
|
clear them -- otherwise the next real offer is swallowed as our own
|
|
echo. The nested selection survives in the seat; defer its
|
|
re-publish to the fallback backend (same as a cancelled dd source). */
|
|
if (bridge.dc_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_source);
|
|
bridge.dc_source = NULL;
|
|
}
|
|
if (bridge.dc_echo_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_echo_offer);
|
|
bridge.dc_echo_offer = NULL;
|
|
}
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
bridge.dc_primary_pending_echo = false;
|
|
if (server.seat && server.seat->selection_source &&
|
|
server.seat->selection_source->impl != &bridge_inner_source_impl)
|
|
bridge.host_set_pending = true;
|
|
bridge_primary_prefetch_cancel();
|
|
if (bridge.dc_device) {
|
|
ext_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: host data-control "
|
|
"finished; falling back to %s\n",
|
|
bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE ?
|
|
"focus-based wl_data_device" : "none");
|
|
}
|
|
|
|
static void bridge_dc_device_data_offer(void *data,
|
|
struct ext_data_control_device_v1 *dev,
|
|
struct ext_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc data_offer %p pending_echo=%d "
|
|
"primary_pending=%d\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dc_pending_echo, bridge.dc_primary_pending_echo);
|
|
if (bridge.dc_pending_echo) {
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dc_echo_offer = offer;
|
|
} else if (bridge.dc_primary_pending_echo) {
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge.dc_primary_echo_offer = offer;
|
|
}
|
|
bridge_mimes_reset();
|
|
ext_data_control_offer_v1_add_listener(offer, &bridge_dc_offer_listener, NULL);
|
|
}
|
|
|
|
static void bridge_dc_device_selection(void *data,
|
|
struct ext_data_control_device_v1 *dev,
|
|
struct ext_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc selection offer=%p pending_echo=%d "
|
|
"echo_offer=%p\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dc_pending_echo, (void *)bridge.dc_echo_offer);
|
|
bridge_prefetch_cancel();
|
|
/* Destroying our own previous source in bridge_host_set_selection makes the
|
|
compositor briefly emit selection(NULL) before the echo of the new source;
|
|
it is not a real clear, so ignore it while our echo is still pending. */
|
|
if (!offer && bridge.dc_pending_echo)
|
|
return;
|
|
if (offer && offer == bridge.dc_echo_offer) {
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge_mimes_reset();
|
|
ext_data_control_offer_v1_destroy(offer);
|
|
bridge.dc_pending_echo = false;
|
|
return;
|
|
}
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
if (bridge.dc_offer)
|
|
ext_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = offer;
|
|
if (!offer) {
|
|
bridge_mimes_reset();
|
|
bridge_clear_nested();
|
|
return;
|
|
}
|
|
bridge_prefetch_start();
|
|
bridge_mirror_to_nested();
|
|
}
|
|
|
|
/* Primary selection bridged over the same data-control device (v2): the
|
|
host's primary_selection event mirrors the host's middle-click selection
|
|
into the nested seat (and the echo of our own set is swallowed). */
|
|
static void bridge_dc_device_primary_selection(void *data,
|
|
struct ext_data_control_device_v1 *dev,
|
|
struct ext_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
if (offer && offer == bridge.dc_primary_echo_offer) {
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_mimes_reset();
|
|
ext_data_control_offer_v1_destroy(offer);
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] primary echo swallowed\n");
|
|
return;
|
|
}
|
|
if (offer == NULL) {
|
|
/* Like the clipboard's selection(NULL): destroying our previous source
|
|
in bridge_host_set_primary_selection makes the compositor briefly
|
|
emit primary_selection(NULL) before the echo of the new one, which is
|
|
not a real clear -- let pending stay armed for the echo's data_offer. */
|
|
if (bridge.dc_primary_pending_echo)
|
|
return;
|
|
/* Offers are client-owned until destroyed: drop the superseded one
|
|
instead of leaking its proxy. */
|
|
if (bridge.dc_primary_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
bridge_mimes_reset();
|
|
bridge_primary_prefetch_cancel();
|
|
bridge_primary_clear_nested();
|
|
return;
|
|
}
|
|
bridge.dc_primary_pending_echo = false;
|
|
if (bridge.dc_primary_offer)
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = offer;
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] host primary_selection offer\n");
|
|
/* The offer's mimes were collected by the preceding data_offer/offer
|
|
events; the data_offer handler has already reset bridge.mimes. */
|
|
bridge_primary_prefetch_start();
|
|
bridge_mirror_primary_to_nested();
|
|
}
|
|
|
|
static const struct ext_data_control_device_v1_listener bridge_dc_listener = {
|
|
.data_offer = bridge_dc_device_data_offer,
|
|
.selection = bridge_dc_device_selection,
|
|
.primary_selection = bridge_dc_device_primary_selection,
|
|
.finished = bridge_dc_device_finished,
|
|
};
|
|
|
|
/* ---------------- host backend: wl_data_device (focus-based) ---------------- */
|
|
|
|
static void bridge_dd_source_send(void *data, struct wl_data_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd_source_send mime=%s fd=%d "
|
|
"inner_src=%p\n", bridge_dbg_ms(), mime, fd,
|
|
(void *)bridge.inner_src);
|
|
if (bridge.inner_src) {
|
|
wlr_data_source_send(bridge.inner_src, mime, fd);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd_source_send "
|
|
"forwarded mime=%s fd=%d\n", bridge_dbg_ms(), mime, fd);
|
|
} else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dd_source_cancelled(void *data, struct wl_data_source *src) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] host cancelled our data-device source\n");
|
|
wl_data_source_destroy(src);
|
|
bridge.dd_source = NULL;
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
bridge.host_set_pending = true;
|
|
}
|
|
|
|
static const struct wl_data_source_listener bridge_dd_source_listener = {
|
|
.send = bridge_dd_source_send,
|
|
.cancelled = bridge_dd_source_cancelled,
|
|
};
|
|
|
|
static void bridge_dd_offer_mime(void *data, struct wl_data_offer *offer,
|
|
const char *mime) {
|
|
(void)data; (void)offer;
|
|
bridge_mimes_append(mime);
|
|
}
|
|
|
|
static const struct wl_data_offer_listener bridge_dd_offer_listener = {
|
|
.offer = bridge_dd_offer_mime,
|
|
/* source_actions / action left unset: drag & drop is not bridged */
|
|
};
|
|
|
|
static void bridge_dd_data_offer(void *data, struct wl_data_device *dev,
|
|
struct wl_data_offer *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd data_offer %p pending_echo=%d\n",
|
|
bridge_dbg_ms(), (void *)offer, bridge.dd_pending_echo);
|
|
if (bridge.dd_pending_echo) {
|
|
bridge.dd_pending_echo = false;
|
|
bridge.dd_echo_offer = offer;
|
|
}
|
|
bridge_mimes_reset();
|
|
wl_data_offer_add_listener(offer, &bridge_dd_offer_listener, NULL);
|
|
}
|
|
|
|
static void bridge_dd_selection(void *data, struct wl_data_device *dev,
|
|
struct wl_data_offer *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd selection offer=%p pending_echo=%d "
|
|
"echo_offer=%p\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dd_pending_echo, (void *)bridge.dd_echo_offer);
|
|
if (!offer) {
|
|
/* Focus blip: the host selection is momentarily gone (or we lost
|
|
keyboard focus). Keep the last-known nested selection and, above
|
|
all, leave the pending-echo flags intact so an in-flight dc/dd
|
|
set_selection reply is still recognized as our own echo. */
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
bridge_prefetch_cancel();
|
|
if (offer == bridge.dd_echo_offer) {
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge_mimes_reset();
|
|
wl_data_offer_destroy(offer);
|
|
bridge.dd_pending_echo = false;
|
|
return;
|
|
}
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
if (bridge.dd_offer)
|
|
wl_data_offer_destroy(bridge.dd_offer);
|
|
bridge.dd_offer = offer;
|
|
bridge_prefetch_start();
|
|
bridge_mirror_to_nested();
|
|
}
|
|
|
|
static const struct wl_data_device_listener bridge_dd_listener = {
|
|
.data_offer = bridge_dd_data_offer,
|
|
.selection = bridge_dd_selection,
|
|
/* enter/leave/motion/drop left unset: drag & drop is not bridged */
|
|
};
|
|
|
|
/* ---------------- nested seat side ---------------- */
|
|
|
|
static void bridge_dbg_src_destroyed(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-src] t=%.0f nested selection source destroyed\n",
|
|
bridge_dbg_ms());
|
|
wl_list_remove(&l->link);
|
|
free(l);
|
|
}
|
|
|
|
static double bridge_dbg_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
static void bridge_seat_request_set_selection(struct wl_listener *l, void *data) {
|
|
(void)l;
|
|
struct wlr_seat_request_set_selection_event *event = data;
|
|
bridge.inner_clear_requested = (event->source == NULL);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f request_set_selection "
|
|
"src=%p impl=%p serial=%u\n", bridge_dbg_ms(), (void *)event->source,
|
|
(void *)(event->source ? event->source->impl : NULL),
|
|
event->serial);
|
|
if (event->source) {
|
|
struct wl_listener *dl = calloc(1, sizeof(*dl));
|
|
if (dl) {
|
|
dl->notify = bridge_dbg_src_destroyed;
|
|
wl_signal_add(&event->source->events.destroy, dl);
|
|
}
|
|
}
|
|
wlr_seat_set_selection(server.seat, event->source, event->serial);
|
|
}
|
|
|
|
static void seat_request_set_primary_selection(struct wl_listener *l,
|
|
void *data) {
|
|
(void)l;
|
|
struct wlr_seat_request_set_primary_selection_event *event = data;
|
|
bridge.inner_primary_clear_requested = (event->source == NULL);
|
|
wlr_seat_set_primary_selection(server.seat, event->source, event->serial);
|
|
}
|
|
|
|
/* An idle-inhibit-v1 inhibitor was destroyed. Recompute the inhibited flag
|
|
from the surviving inhibitors. */
|
|
/* One held idle inhibitor: the destroy listener (wired to the wlroots
|
|
destroy signal, which asserts every listener detaches during emission)
|
|
and the idle_inhibitors link must be SEPARATE nodes -- one wl_list node
|
|
can never sit in two lists at once. */
|
|
struct kick_inhibitor {
|
|
struct wl_listener destroy;
|
|
struct wl_list link; /* server.idle_inhibitors */
|
|
};
|
|
|
|
static void handle_idle_inhibitor_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_inhibitor *inh =
|
|
wl_container_of(listener, inh, destroy);
|
|
wl_list_remove(&inh->destroy.link);
|
|
wl_list_remove(&inh->link);
|
|
server.idle_inhibited = !wl_list_empty(&server.idle_inhibitors);
|
|
wlr_idle_notifier_v1_set_inhibited(server.idle_notifier, server.idle_inhibited);
|
|
free(inh);
|
|
}
|
|
|
|
/* A client bound idle-inhibit-v1 and added an inhibitor: keep the notifier
|
|
inhibited until every inhibitor is gone (labwc's idle.c behaviour). */
|
|
static void handle_idle_new_inhibitor(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_idle_inhibitor_v1 *inhibitor = data;
|
|
/* Track each inhibitor with its own heap struct so the destroy signal
|
|
can remove exactly that inhibitor from the shared list. */
|
|
struct kick_inhibitor *k = calloc(1, sizeof(*k));
|
|
if (!k)
|
|
return;
|
|
k->destroy.notify = handle_idle_inhibitor_destroy;
|
|
wl_signal_add(&inhibitor->events.destroy, &k->destroy);
|
|
wl_list_insert(&server.idle_inhibitors, &k->link);
|
|
server.idle_inhibited = !wl_list_empty(&server.idle_inhibitors);
|
|
wlr_idle_notifier_v1_set_inhibited(server.idle_notifier, server.idle_inhibited);
|
|
}
|
|
|
|
/* ===================== Tearing + fractional-scale =====================
|
|
- wp-tearing-control-v1 lets a surface request unthrottled presentation.
|
|
The shown tab's focused surface hint drives the output commit's
|
|
async-flip flag: on DRM this becomes a real tearing page flip (game
|
|
tabs pair it with content-type=game); headless/nested outputs ignore
|
|
the flag, correctly, since a tear cannot pass through the host.
|
|
- wp-fractional-scale-v1 tells a surface the output scale. kick's
|
|
output scale is fixed per session, so each surface is notified once
|
|
at claim (from the live output scale, covering HiDPI DRM too). */
|
|
struct kick_tearing {
|
|
struct wlr_tearing_control_v1 *tc;
|
|
struct wl_listener set_hint;
|
|
struct wl_listener destroy;
|
|
};
|
|
|
|
static void handle_tearing_hint(struct wl_listener *listener, void *data) {
|
|
struct kick_tearing *kt = wl_container_of(listener, kt, set_hint);
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[tearing] surface %p hint=%d\n",
|
|
(void *)kt->tc->surface, (int)kt->tc->current);
|
|
/* The commit path re-reads the hint every frame, but a hint change
|
|
alone produces no frame: request one so async takes effect (or
|
|
stops) promptly. */
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_tearing_destroy(struct wl_listener *listener, void *data) {
|
|
struct kick_tearing *kt = wl_container_of(listener, kt, destroy);
|
|
(void)data;
|
|
wl_list_remove(&kt->set_hint.link);
|
|
wl_list_remove(&kt->destroy.link);
|
|
free(kt);
|
|
}
|
|
|
|
static void handle_tearing_new_object(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_tearing_control_v1 *tc = data;
|
|
struct kick_tearing *kt = calloc(1, sizeof(*kt));
|
|
if (!kt)
|
|
return;
|
|
kt->tc = tc;
|
|
kt->set_hint.notify = handle_tearing_hint;
|
|
wl_signal_add(&tc->events.set_hint, &kt->set_hint);
|
|
kt->destroy.notify = handle_tearing_destroy;
|
|
wl_signal_add(&tc->events.destroy, &kt->destroy);
|
|
kick_log(KICK_LOG_DEBUG, "[tearing] surface %p hint=%d\n",
|
|
(void *)tc->surface, (int)tc->current);
|
|
}
|
|
|
|
/* Whether the shown tab's focused surface asked for async presentation. */
|
|
static bool shown_surface_wants_tearing(void) {
|
|
if (!server.tearing_mgr)
|
|
return false;
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s)
|
|
return false;
|
|
return wlr_tearing_control_manager_v1_surface_hint_from_surface(
|
|
server.tearing_mgr, s) ==
|
|
WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC;
|
|
}
|
|
|
|
/* Apply the shown surface's hint to an output commit. Logs transitions
|
|
only, so steady vsync (the common case) stays quiet at debug level. */
|
|
static void apply_tearing_hint(struct wlr_output_state *state) {
|
|
bool async = shown_surface_wants_tearing();
|
|
state->tearing_page_flip = async;
|
|
static bool last_async = false;
|
|
if (async != last_async) {
|
|
last_async = async;
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
kick_log(KICK_LOG_DEBUG, "[tearing] %s app=%d\n", async ? "async" : "vsync",
|
|
w ? tab_index(w->tab) : -1);
|
|
}
|
|
}
|
|
|
|
/* ===================== Keyboard shortcuts inhibit =====================
|
|
A fullscreen game/VM asks that the compositor stop processing its keyboard
|
|
shortcuts so they reach the client instead. Honored only while the
|
|
inhibitor's surface actually has keyboard focus: new inhibitors for the
|
|
focused surface activate immediately, and a lazy sync at grab-chord time
|
|
(de)activates across focus changes. While honored, the tab/window grab
|
|
chords are swallowed (see process_key_event); quit and VT switch stay
|
|
available so a stuck client can never trap the session. */
|
|
static void handle_kb_inhibit_new_inhibitor(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_keyboard_shortcuts_inhibitor_v1 *inhib = data;
|
|
struct wlr_surface *focused = server.seat ?
|
|
server.seat->keyboard_state.focused_surface : NULL;
|
|
if (inhib->surface == focused)
|
|
wlr_keyboard_shortcuts_inhibitor_v1_activate(inhib);
|
|
}
|
|
|
|
/* Sync (in)active across focus changes and report whether the FOCUSED
|
|
surface's shortcuts are currently inhibited. Evaluated lazily at grab-
|
|
chord time so every focus path (tab switch, lock, xwayland OR, nested)
|
|
is covered without hooking each one. */
|
|
static bool kb_inhibit_focused(void) {
|
|
if (!server.kb_inhibit_mgr || !server.seat) return false;
|
|
struct wlr_surface *focused =
|
|
server.seat->keyboard_state.focused_surface;
|
|
bool inhibited = false;
|
|
struct wlr_keyboard_shortcuts_inhibitor_v1 *inhib;
|
|
wl_list_for_each(inhib, &server.kb_inhibit_mgr->inhibitors, link) {
|
|
bool mine = (inhib->surface == focused);
|
|
if (mine && !inhib->active)
|
|
wlr_keyboard_shortcuts_inhibitor_v1_activate(inhib);
|
|
else if (!mine && inhib->active)
|
|
wlr_keyboard_shortcuts_inhibitor_v1_deactivate(inhib);
|
|
if (mine)
|
|
inhibited = inhib->active;
|
|
}
|
|
return inhibited;
|
|
}
|
|
|
|
/* ===================== Output power management =====================
|
|
A client (wlopm) blanks or unblanks an output. kick has a single output,
|
|
so any set_mode for it toggles the whole session: OFF makes the render
|
|
paths overwrite the composited scene with black before commit (see
|
|
render_standalone / render_and_present), so the committed frame is truly
|
|
dark rather than a stale scene frame. Starts powered (see server init). */
|
|
static void handle_output_power_set_mode(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_output_power_v1_set_mode_event *ev = data;
|
|
if (ev->output != server.output) return;
|
|
bool on = (ev->mode == ZWLR_OUTPUT_POWER_V1_MODE_ON);
|
|
if (on == server.output_powered) return;
|
|
server.output_powered = on;
|
|
kick_log(KICK_LOG_INFO, "[power] output %s\n", on ? "on" : "off");
|
|
if (on && server.scene_output) {
|
|
/* The swapchain recycles buffers, so the black power-off frame is
|
|
still sitting in it with no scene damage outstanding. Force a
|
|
full repaint so the scene is re-composited instead of re-committing
|
|
the stale black buffer. */
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
}
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* ===================== Output management =====================
|
|
kanshi/wdisplays-style clients list the single head and request sizes.
|
|
Standalone honors custom sizes through the normal resize path (tabs
|
|
reflow, swapchain follows lazily at render); nested refuses anything
|
|
but the live size because the host window owns the geometry. Anything
|
|
beyond size (disabling, scale, transform) is refused everywhere: one
|
|
fixed output per session. Successful applies republish so listening
|
|
clients converge. */
|
|
|
|
/* Broadcast the live output as the manager's current configuration;
|
|
wlroots creates/updates/destroys heads from it and only notifies on
|
|
change. No-op until both manager and output exist. */
|
|
static void output_mgmt_publish(void) {
|
|
if (!server.output_mgr || !server.output)
|
|
return;
|
|
struct wlr_output_configuration_v1 *config =
|
|
wlr_output_configuration_v1_create();
|
|
if (!config)
|
|
return;
|
|
if (!wlr_output_configuration_head_v1_create(config, server.output)) {
|
|
wlr_output_configuration_v1_destroy(config);
|
|
return;
|
|
}
|
|
wlr_output_manager_v1_set_configuration(server.output_mgr, config);
|
|
}
|
|
|
|
/* Re-publish after a commit changed the output size. Resizes land
|
|
lazily (host configures, client applies), so the commit -- not the
|
|
request -- is where the new size becomes observable. */
|
|
static void output_mgmt_sync(void) {
|
|
if (!server.output_mgr || !server.output)
|
|
return;
|
|
static int last_w = -1, last_h = -1;
|
|
if (server.output->width == last_w && server.output->height == last_h)
|
|
return;
|
|
last_w = server.output->width;
|
|
last_h = server.output->height;
|
|
output_mgmt_publish();
|
|
}
|
|
|
|
/* The config head addressing our output, if the client included it. */
|
|
static struct wlr_output_configuration_head_v1 *mgmt_head_for_output(
|
|
struct wlr_output_configuration_v1 *config) {
|
|
if (!server.output)
|
|
return NULL;
|
|
struct wlr_output_configuration_head_v1 *h;
|
|
wl_list_for_each(h, &config->heads, link) {
|
|
if (h->state.output == server.output)
|
|
return h;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Requested size, preferring an explicit mode over a custom one. */
|
|
static void mgmt_head_size(struct wlr_output_configuration_head_v1 *h,
|
|
int *w, int *hh) {
|
|
if (h->state.mode) {
|
|
*w = h->state.mode->width;
|
|
*hh = h->state.mode->height;
|
|
} else {
|
|
*w = h->state.custom_mode.width;
|
|
*hh = h->state.custom_mode.height;
|
|
}
|
|
}
|
|
|
|
/* Whether a client config is one we can honor: our output, still
|
|
enabled, same scale/transform, sane size. Nested additionally
|
|
requires the live size (the host owns it). */
|
|
static bool mgmt_config_ok(struct wlr_output_configuration_v1 *config,
|
|
int *w, int *hh) {
|
|
struct wlr_output_configuration_head_v1 *h =
|
|
mgmt_head_for_output(config);
|
|
if (!h || !h->state.enabled || !server.output)
|
|
return false;
|
|
mgmt_head_size(h, w, hh);
|
|
if (*w < 50 || *hh < 50 || *w > 16384 || *hh > 16384)
|
|
return false;
|
|
if (h->state.scale != (float)server.output->scale ||
|
|
h->state.transform != server.output->transform)
|
|
return false;
|
|
if (!server.standalone &&
|
|
(*w != server.output->width || *hh != server.output->height))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static void handle_output_mgr_test(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_output_configuration_v1 *config = data;
|
|
int w = 0, hh = 0;
|
|
bool ok = mgmt_config_ok(config, &w, &hh);
|
|
if (ok && server.standalone) {
|
|
/* Honest modeset probe on top of the sanity check. */
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
wlr_output_state_set_custom_mode(&state, w, hh, 0);
|
|
ok = wlr_output_test_state(server.output, &state);
|
|
wlr_output_state_finish(&state);
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[output] test %dx%d %s\n", w, hh,
|
|
ok ? "ok" : "failed");
|
|
if (ok)
|
|
wlr_output_configuration_v1_send_succeeded(config);
|
|
else
|
|
wlr_output_configuration_v1_send_failed(config);
|
|
wlr_output_configuration_v1_destroy(config);
|
|
}
|
|
|
|
static void handle_output_mgr_apply(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_output_configuration_v1 *config = data;
|
|
int w = 0, hh = 0;
|
|
bool ok = mgmt_config_ok(config, &w, &hh);
|
|
if (ok && server.standalone) {
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
wlr_output_state_set_custom_mode(&state, w, hh, 0);
|
|
ok = wlr_output_test_state(server.output, &state);
|
|
wlr_output_state_finish(&state);
|
|
}
|
|
if (ok) {
|
|
if (server.standalone)
|
|
output_resize(w, hh);
|
|
output_mgmt_publish();
|
|
kick_log(KICK_LOG_INFO, "[output] apply %dx%d\n", w, hh);
|
|
wlr_output_configuration_v1_send_succeeded(config);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "[output] apply %dx%d refused\n", w, hh);
|
|
wlr_output_configuration_v1_send_failed(config);
|
|
}
|
|
wlr_output_configuration_v1_destroy(config);
|
|
}
|
|
|
|
/* Gamma control: apply the client's ramp to the single output (or reset it
|
|
when the control went away: wlroots emits set_gamma with NULL then). A
|
|
failed commit fails the control so the client (wlsunset) retries later
|
|
instead of waiting on a ramp that never landed. */
|
|
static void handle_gamma_set(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_gamma_control_manager_v1_set_gamma_event *ev = data;
|
|
if (ev->output != server.output) {
|
|
if (ev->control)
|
|
wlr_gamma_control_v1_send_failed_and_destroy(ev->control);
|
|
return;
|
|
}
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
bool ok = wlr_gamma_control_v1_apply(ev->control, &state);
|
|
if (ok)
|
|
ok = wlr_output_commit_state(ev->output, &state);
|
|
wlr_output_state_finish(&state);
|
|
if (!ok) {
|
|
if (ev->control)
|
|
wlr_gamma_control_v1_send_failed_and_destroy(ev->control);
|
|
return;
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[gamma] ramp %s size=%zu\n",
|
|
ev->control ? "applied" : "reset",
|
|
ev->control ? ev->control->ramp_size : 0);
|
|
}
|
|
|
|
/* ===================== Virtual keyboard/pointer =====================
|
|
Synthesised input (wtype & co.) feeds the seat through the exact same
|
|
paths as real devices: virtual keys go through process_key_event so grab
|
|
chords work, virtual pointer motion/buttons drive native_ptr_* and the
|
|
seat. The client may omit the keymap (wtype sends keys only); then the
|
|
server's keymap is installed so modifiers resolve. Cleanup uses a
|
|
resource-destroy listener, which libwayland runs before wlroots'
|
|
destructor (wlr_keyboard/pointer_finish asserts empty listener lists). */
|
|
/* Forward declarations: the real-device input paths these reuse live below. */
|
|
static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
|
uint32_t mods_depressed);
|
|
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group);
|
|
/* Text-input section (defined with the seat handlers): forward a key or
|
|
modifier state to the IME's keyboard grab instead of the seat. Returns
|
|
true when a grab consumed the event. */
|
|
static bool im_grab_forward_key(uint32_t time, uint32_t key, uint32_t state);
|
|
static bool im_grab_forward_modifiers(struct wlr_keyboard *kb);
|
|
static void native_ptr_motion(uint32_t time, struct wlr_input_device *dev,
|
|
double dx, double dy);
|
|
static void native_ptr_abs(uint32_t time, struct wlr_input_device *dev,
|
|
double x, double y);
|
|
static void standalone_ptr_button_event(struct wl_listener *listener,
|
|
void *data);
|
|
static void standalone_ptr_axis_event(struct wl_listener *listener,
|
|
void *data);
|
|
static void standalone_ptr_frame_event(struct wl_listener *listener,
|
|
void *data);
|
|
|
|
struct virtual_kb {
|
|
struct wlr_virtual_keyboard_v1 *vkb;
|
|
struct wl_listener key;
|
|
struct wl_listener modifiers;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
struct virtual_ptr {
|
|
struct wlr_virtual_pointer_v1 *vptr;
|
|
struct wl_listener motion;
|
|
struct wl_listener motion_absolute;
|
|
struct wl_listener button;
|
|
struct wl_listener axis;
|
|
struct wl_listener frame;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
static void virtual_kbd_key(struct wl_listener *listener, void *data) {
|
|
struct virtual_kb *vk = wl_container_of(listener, vk, key);
|
|
struct wlr_keyboard_key_event *ev = data;
|
|
struct wlr_keyboard *kb = &vk->vkb->keyboard;
|
|
uint32_t mods = wlr_keyboard_get_modifiers(kb);
|
|
if (!process_key_event(ev->time_msec, ev->keycode, ev->state, mods)) {
|
|
if (im_grab_forward_key(ev->time_msec, ev->keycode, ev->state))
|
|
return;
|
|
wlr_seat_set_keyboard(server.seat, kb);
|
|
wlr_seat_keyboard_notify_key(server.seat, ev->time_msec, ev->keycode,
|
|
ev->state);
|
|
}
|
|
}
|
|
|
|
static void virtual_kbd_modifiers(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct virtual_kb *vk = wl_container_of(listener, vk, modifiers);
|
|
struct wlr_keyboard *kb = &vk->vkb->keyboard;
|
|
process_modifiers(kb->modifiers.depressed, kb->modifiers.latched,
|
|
kb->modifiers.locked, kb->modifiers.group);
|
|
if (im_grab_forward_modifiers(kb))
|
|
return;
|
|
wlr_seat_set_keyboard(server.seat, kb);
|
|
wlr_seat_keyboard_notify_modifiers(server.seat, &kb->modifiers);
|
|
}
|
|
|
|
static void virtual_kbd_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct virtual_kb *vk = wl_container_of(listener, vk, destroy);
|
|
wl_list_remove(&vk->key.link);
|
|
wl_list_remove(&vk->modifiers.link);
|
|
wl_list_remove(&vk->link);
|
|
free(vk);
|
|
}
|
|
|
|
static void handle_virtual_kb_new(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_virtual_keyboard_v1 *vkb = data;
|
|
/* Without a client keymap the device never emits modifiers; give it
|
|
ours so hotkeys and the seat work (a later client keymap replaces it
|
|
inside wlroots). */
|
|
if (!vkb->has_keymap && server.xkb_keymap)
|
|
wlr_keyboard_set_keymap(&vkb->keyboard, server.xkb_keymap);
|
|
struct virtual_kb *vk = calloc(1, sizeof(*vk));
|
|
if (!vk) return;
|
|
vk->vkb = vkb;
|
|
vk->key.notify = virtual_kbd_key;
|
|
wl_signal_add(&vkb->keyboard.events.key, &vk->key);
|
|
vk->modifiers.notify = virtual_kbd_modifiers;
|
|
wl_signal_add(&vkb->keyboard.events.modifiers, &vk->modifiers);
|
|
vk->destroy.notify = virtual_kbd_destroy;
|
|
wl_resource_add_destroy_listener(vkb->resource, &vk->destroy);
|
|
wl_list_insert(&server.virtual_kbs, &vk->link);
|
|
kick_log(KICK_LOG_INFO, "[input] virtual keyboard\n");
|
|
}
|
|
|
|
static void virtual_ptr_motion(struct wl_listener *listener, void *data) {
|
|
struct virtual_ptr *vp = wl_container_of(listener, vp, motion);
|
|
struct wlr_pointer_motion_event *ev = data;
|
|
native_ptr_motion(ev->time_msec, &vp->vptr->pointer.base,
|
|
ev->delta_x, ev->delta_y);
|
|
}
|
|
|
|
static void virtual_ptr_abs(struct wl_listener *listener, void *data) {
|
|
struct virtual_ptr *vp = wl_container_of(listener, vp, motion_absolute);
|
|
struct wlr_pointer_motion_absolute_event *ev = data;
|
|
native_ptr_abs(ev->time_msec, &vp->vptr->pointer.base, ev->x, ev->y);
|
|
}
|
|
|
|
static void virtual_ptr_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct virtual_ptr *vp = wl_container_of(listener, vp, destroy);
|
|
wl_list_remove(&vp->motion.link);
|
|
wl_list_remove(&vp->motion_absolute.link);
|
|
wl_list_remove(&vp->button.link);
|
|
wl_list_remove(&vp->axis.link);
|
|
wl_list_remove(&vp->frame.link);
|
|
wl_list_remove(&vp->link);
|
|
free(vp);
|
|
}
|
|
|
|
static void handle_virtual_ptr_new(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_virtual_pointer_v1_new_pointer_event *ev = data;
|
|
struct wlr_virtual_pointer_v1 *vptr = ev->new_pointer;
|
|
struct virtual_ptr *vp = calloc(1, sizeof(*vp));
|
|
if (!vp) return;
|
|
vp->vptr = vptr;
|
|
vp->motion.notify = virtual_ptr_motion;
|
|
wl_signal_add(&vptr->pointer.events.motion, &vp->motion);
|
|
vp->motion_absolute.notify = virtual_ptr_abs;
|
|
wl_signal_add(&vptr->pointer.events.motion_absolute, &vp->motion_absolute);
|
|
/* Button/axis/frame ignore their listener container, so reuse the real
|
|
device handlers directly. */
|
|
vp->button.notify = standalone_ptr_button_event;
|
|
wl_signal_add(&vptr->pointer.events.button, &vp->button);
|
|
vp->axis.notify = standalone_ptr_axis_event;
|
|
wl_signal_add(&vptr->pointer.events.axis, &vp->axis);
|
|
vp->frame.notify = standalone_ptr_frame_event;
|
|
wl_signal_add(&vptr->pointer.events.frame, &vp->frame);
|
|
vp->destroy.notify = virtual_ptr_destroy;
|
|
wl_resource_add_destroy_listener(vptr->resource, &vp->destroy);
|
|
wl_list_insert(&server.virtual_ptrs, &vp->link);
|
|
kick_log(KICK_LOG_INFO, "[input] virtual pointer\n");
|
|
}
|
|
|
|
/* ===================== Xdg-activation =====================
|
|
Launchers focus windows by token instead of racing map order. On
|
|
request_activate, find the kick window owning the surface and switch to
|
|
its tab (set_active_tab lands on the tab's current window; window_enter
|
|
then focuses the exact surface). Both are gated on session_locked, so
|
|
activation can never bypass the lock screen. */
|
|
static void handle_xdg_activation_request(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_activation_v1_request_activate_event *ev = data;
|
|
if (!ev->surface) return;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link) {
|
|
if (window_surface(w) == ev->surface) {
|
|
kick_log(KICK_LOG_INFO, "[activation] focus tab=%d\n",
|
|
tab_index(tab));
|
|
set_active_tab(tab);
|
|
window_enter(w);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* System bell: a client rang the bell. Log it at info level; the
|
|
protocol explicitly allows any rendering including nothing, and
|
|
quiet-by-default means no unconditional audible/visual output. */
|
|
static void handle_bell_ring(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
(void)data;
|
|
kick_log(KICK_LOG_INFO, "[bell] ring\n");
|
|
}
|
|
|
|
/* ===================== Foreign toplevel management =====================
|
|
Taskbars/switchers (waybar taskbar, disorder, etc.) enumerate kick's
|
|
windows through wlr-foreign-toplevel-management-v1 and
|
|
ext-foreign-toplevel-list-v1. Each surfaced kick_window owns one of each
|
|
handle; the state sync helpers below mirror kick's own window state onto
|
|
the handles, and the request handlers translate taskbar requests into the
|
|
existing window_* control calls. */
|
|
|
|
/* wlroots' set_app_id / set_title strdup their argument, so a NULL (an xdg
|
|
toplevel that has not yet been given an app_id / title) would crash. Use
|
|
"" instead; the client corrects us via set_app_id/set_title once it maps. */
|
|
static const char *foreign_str(const char *s) {
|
|
return s ? s : "";
|
|
}
|
|
|
|
static void foreign_sync_title(struct kick_window *w) {
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_title(w->ft_handle, foreign_str(window_title(w)));
|
|
if (w->ext_handle) {
|
|
struct wlr_ext_foreign_toplevel_handle_v1_state st = {
|
|
.title = foreign_str(window_title(w)),
|
|
.app_id = foreign_str(window_app_id(w)),
|
|
};
|
|
wlr_ext_foreign_toplevel_handle_v1_update_state(w->ext_handle, &st);
|
|
}
|
|
}
|
|
|
|
static void foreign_sync_app_id(struct kick_window *w) {
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_set_app_id(w->ft_handle, foreign_str(window_app_id(w)));
|
|
if (w->ext_handle) {
|
|
struct wlr_ext_foreign_toplevel_handle_v1_state st = {
|
|
.title = foreign_str(window_title(w)),
|
|
.app_id = foreign_str(window_app_id(w)),
|
|
};
|
|
wlr_ext_foreign_toplevel_handle_v1_update_state(w->ext_handle, &st);
|
|
}
|
|
}
|
|
|
|
static void foreign_sync_output(struct kick_window *w) {
|
|
if (!w->ft_handle || !server.output) return;
|
|
wlr_foreign_toplevel_handle_v1_output_enter(w->ft_handle, server.output);
|
|
}
|
|
|
|
static void handle_foreign_request_activate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, ft_activate);
|
|
set_active_tab(w->tab);
|
|
window_enter(w);
|
|
}
|
|
|
|
static void handle_foreign_request_close(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, ft_close);
|
|
close_window(w);
|
|
}
|
|
|
|
static void handle_foreign_request_maximize(struct wl_listener *listener, void *data) {
|
|
struct wlr_foreign_toplevel_handle_v1_maximized_event *event = data;
|
|
struct kick_window *w = wl_container_of(listener, w, ft_maximize);
|
|
kick_log(KICK_LOG_DEBUG, "[foreign] maximize request app=%d\n",
|
|
tab_index(w->tab));
|
|
/* Uniform with client chrome: the middle button toggles fullscreen --
|
|
maximized is the baseline, so either direction on an already-maximized
|
|
window means fullscreen, and either direction on a fullscreen window
|
|
steps back down. A fresh request stays plain maximize. */
|
|
if (window_is_fullscreen(w)) {
|
|
client_exit_fullscreen(w);
|
|
return;
|
|
}
|
|
if (event->maximized && window_is_maximized(w))
|
|
client_enter_fullscreen(w);
|
|
else
|
|
window_set_maximized(w, true);
|
|
}
|
|
|
|
static void handle_foreign_request_fullscreen(struct wl_listener *listener, void *data) {
|
|
struct wlr_foreign_toplevel_handle_v1_fullscreen_event *event = data;
|
|
struct kick_window *w = wl_container_of(listener, w, ft_fullscreen);
|
|
if (event->fullscreen)
|
|
client_enter_fullscreen(w);
|
|
else
|
|
client_exit_fullscreen(w);
|
|
}
|
|
|
|
static void handle_foreign_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, ft_handle_destroy);
|
|
/* wlroots asserts every listener list is empty before it destroys the
|
|
handle, so tear down the request listeners (and our own destroy link)
|
|
here -- this runs from the wlr_foreign_toplevel_handle_v1_destroy's
|
|
events.destroy emission, before those assertions. */
|
|
wl_list_remove(&w->ft_activate.link);
|
|
wl_list_remove(&w->ft_close.link);
|
|
wl_list_remove(&w->ft_maximize.link);
|
|
wl_list_remove(&w->ft_fullscreen.link);
|
|
wl_list_remove(&w->ft_handle_destroy.link);
|
|
w->ft_handle = NULL;
|
|
}
|
|
|
|
static void handle_ext_foreign_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_window *w = wl_container_of(listener, w, ext_handle_destroy);
|
|
wl_list_remove(&w->ext_handle_destroy.link);
|
|
w->ext_handle = NULL;
|
|
}
|
|
|
|
/* ======================= ext-session-lock-v1 =======================
|
|
The session is locked by an external lock client (e.g. swaylock) that
|
|
grabbed ext_session_lock_manager_v1. While locked the screen is blanked
|
|
with an opaque black rect and the lock client's per-output surface floats
|
|
above it; every grab chord is swallowed (see process_key_event) and no
|
|
window may take keyboard focus (window_enter / set_active_tab guard on
|
|
server.session_locked). kick manages a single output, so a single lock
|
|
surface is tracked. If the lock client dies without unlocking, the screen
|
|
stays blanked (server.session_locked remains true) and only a fresh lock
|
|
request may take over. */
|
|
|
|
/* Create the lock scene tree and blank the output with an opaque black rect
|
|
rooted at the output origin. Idempotent. */
|
|
static void session_lock_blank(void) {
|
|
if (server.lock_surface_tree)
|
|
return;
|
|
server.lock_surface_tree = wlr_scene_tree_create(&server.scene->tree);
|
|
if (server.output)
|
|
server.lock_backdrop = wlr_scene_rect_create(server.lock_surface_tree,
|
|
server.output->width, server.output->height,
|
|
(float[4]){ 0, 0, 0, 1 });
|
|
wlr_scene_node_raise_to_top(&server.lock_surface_tree->node);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Destroy the lock scene tree (removing the black rect and the lock surface
|
|
subtree with it). Idempotent. */
|
|
static void session_lock_unblank(void) {
|
|
if (server.lock_surface_tree)
|
|
wlr_scene_node_destroy(&server.lock_surface_tree->node);
|
|
server.lock_surface_tree = NULL;
|
|
server.lock_backdrop = NULL;
|
|
server.lock_surface_ssd = NULL;
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Put the lock surface's scene subtree in place, above the black backdrop,
|
|
and configure it to the output's current dimensions. */
|
|
static void session_lock_surface_place(struct wlr_session_lock_surface_v1 *ls) {
|
|
if (!ls || !ls->surface)
|
|
return;
|
|
session_lock_blank();
|
|
if (!server.lock_surface_ssd) {
|
|
server.lock_surface_ssd = wlr_scene_subsurface_tree_create(
|
|
server.lock_surface_tree, ls->surface);
|
|
wlr_scene_node_raise_to_top(&server.lock_surface_ssd->node);
|
|
}
|
|
if (server.output)
|
|
wlr_session_lock_surface_v1_configure(ls, server.output->width,
|
|
server.output->height);
|
|
}
|
|
|
|
/* The lock surface rendered and the whole output is blanked: give it the
|
|
keyboard and pointer so the lock client (swaylock's prompt) receives input.
|
|
Delegated to an idle callback because at map time wlroots has only processed
|
|
the role commit and the surface/buffer isn't settable-focusable yet. */
|
|
static void session_lock_focus_surface(void *data) {
|
|
(void)data;
|
|
struct wlr_session_lock_surface_v1 *ls = server.lock_surface;
|
|
if (!ls || !ls->surface)
|
|
return;
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat, ls->surface,
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
wlr_seat_pointer_notify_enter(server.seat, ls->surface, 0, 0);
|
|
wlr_seat_pointer_notify_motion(server.seat, 0, 0, 0);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_session_lock_surface_map(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
wl_event_loop_add_idle(server.loop, session_lock_focus_surface, NULL);
|
|
}
|
|
|
|
static void handle_session_lock_surface_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
wlr_seat_pointer_notify_clear_focus(server.seat);
|
|
wl_list_remove(&server.lock_surface_destroy.link);
|
|
wl_list_remove(&server.lock_surface_map.link);
|
|
server.lock_surface = NULL;
|
|
server.lock_surface_ssd = NULL;
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_session_lock_new_surface(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_session_lock_surface_v1 *ls = data;
|
|
/* Single output, single surface: a second new_surface from a misbehaving
|
|
client must not orphan the first surface's listener links (later
|
|
destroy would fire into reused nodes and corrupt the lists). */
|
|
if (server.lock_surface) {
|
|
wl_list_remove(&server.lock_surface_destroy.link);
|
|
wl_list_remove(&server.lock_surface_map.link);
|
|
}
|
|
server.lock_surface = ls;
|
|
server.lock_surface_destroy.notify = handle_session_lock_surface_destroy;
|
|
wl_signal_add(&ls->events.destroy, &server.lock_surface_destroy);
|
|
server.lock_surface_map.notify = handle_session_lock_surface_map;
|
|
wl_signal_add(&ls->surface->events.map, &server.lock_surface_map);
|
|
session_lock_surface_place(ls);
|
|
}
|
|
|
|
/* Hand the lock to the next lock in line: replay the focus onto the tab model
|
|
now that session_locked is cleared, and show the previously active window. */
|
|
static void session_lock_restore_focus(void) {
|
|
server.session_locked = false;
|
|
struct kick_tab *tab = server.active_tab;
|
|
if (tab) {
|
|
struct kick_window *enter = tab_current_window(tab);
|
|
if (enter)
|
|
window_enter(enter);
|
|
else
|
|
show_window(NULL);
|
|
}
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_session_lock_unlock(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
if (!server.session_lock) return;
|
|
if (server.lock_surface) {
|
|
wl_list_remove(&server.lock_surface_destroy.link);
|
|
wl_list_remove(&server.lock_surface_map.link);
|
|
server.lock_surface = NULL;
|
|
server.lock_surface_ssd = NULL;
|
|
}
|
|
session_lock_unblank();
|
|
session_lock_restore_focus();
|
|
/* The locked session is over: drop the lock so a fresh lock request
|
|
can take over afterwards. The wlr lock object itself is intentionally
|
|
left alone: unlock_and_destroy already destroyed its resource (so no
|
|
finished event could reach the client anyway), and destroying it here
|
|
would trip wlroots' empty-listener asserts -- we run inside the
|
|
unlock emission, where wayland-server's own markers still sit on the
|
|
unlock list. Detaching our own listeners is safe mid-emission. */
|
|
server.session_lock = NULL;
|
|
wl_list_remove(&server.session_lock_unlock.link);
|
|
wl_list_remove(&server.session_lock_new_surface.link);
|
|
wl_list_remove(&server.session_lock_destroy.link);
|
|
}
|
|
|
|
static void handle_session_lock_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
/* The lock client died without unlocking: the screen stays locked and
|
|
blanked (session_locked remains true) as a deliberate security choice.
|
|
Only a fresh lock request may take over. */
|
|
if (server.session_lock) {
|
|
wl_list_remove(&server.session_lock_unlock.link);
|
|
wl_list_remove(&server.session_lock_new_surface.link);
|
|
wl_list_remove(&server.session_lock_destroy.link);
|
|
}
|
|
server.session_lock = NULL;
|
|
if (server.lock_surface) {
|
|
wl_list_remove(&server.lock_surface_destroy.link);
|
|
wl_list_remove(&server.lock_surface_map.link);
|
|
server.lock_surface = NULL;
|
|
server.lock_surface_ssd = NULL;
|
|
}
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_session_lock_new_lock(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_session_lock_v1 *lock = data;
|
|
/* A second lock while already locked is refused outright. If an earlier
|
|
lock client died (session_locked true, session_lock NULL), replace the
|
|
blanked state: tear down the old lock surfaces and proceed. */
|
|
if (server.session_locked && server.session_lock) {
|
|
wlr_session_lock_v1_destroy(lock);
|
|
return;
|
|
}
|
|
if (server.session_locked) {
|
|
session_lock_unblank();
|
|
if (server.lock_surface) {
|
|
wl_list_remove(&server.lock_surface_destroy.link);
|
|
wl_list_remove(&server.lock_surface_map.link);
|
|
server.lock_surface = NULL;
|
|
server.lock_surface_ssd = NULL;
|
|
}
|
|
}
|
|
|
|
server.session_lock = lock;
|
|
server.session_locked = true;
|
|
server.session_lock_new_surface.notify = handle_session_lock_new_surface;
|
|
wl_signal_add(&lock->events.new_surface, &server.session_lock_new_surface);
|
|
server.session_lock_unlock.notify = handle_session_lock_unlock;
|
|
wl_signal_add(&lock->events.unlock, &server.session_lock_unlock);
|
|
server.session_lock_destroy.notify = handle_session_lock_destroy;
|
|
wl_signal_add(&lock->events.destroy, &server.session_lock_destroy);
|
|
|
|
/* Draw nothing but the black blank while locked; no window may grab
|
|
focus (window_enter / set_active_tab are gated on session_locked). */
|
|
show_window(NULL);
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (kbd) wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
wlr_seat_pointer_notify_clear_focus(server.seat);
|
|
session_lock_blank();
|
|
wlr_session_lock_v1_send_locked(lock);
|
|
}
|
|
|
|
|
|
/* Create the taskbar-visible handles for a freshly-claimed window and seed
|
|
their initial state. Called once a window has the client's app_id/title
|
|
(i.e. at claim time). */
|
|
static void foreign_handle_create(struct kick_window *w) {
|
|
if (!server.foreign_mgr || !server.ext_foreign_list || w->ft_handle) return;
|
|
|
|
w->ft_handle = wlr_foreign_toplevel_handle_v1_create(server.foreign_mgr);
|
|
if (!w->ft_handle) return;
|
|
wlr_foreign_toplevel_handle_v1_set_app_id(w->ft_handle, foreign_str(window_app_id(w)));
|
|
wlr_foreign_toplevel_handle_v1_set_title(w->ft_handle, foreign_str(window_title(w)));
|
|
wlr_foreign_toplevel_handle_v1_set_maximized(w->ft_handle, true);
|
|
wlr_foreign_toplevel_handle_v1_set_activated(w->ft_handle, w->focused);
|
|
foreign_sync_output(w);
|
|
|
|
struct wlr_ext_foreign_toplevel_handle_v1_state st = {
|
|
.title = foreign_str(window_title(w)),
|
|
.app_id = foreign_str(window_app_id(w)),
|
|
};
|
|
w->ext_handle = wlr_ext_foreign_toplevel_handle_v1_create(server.ext_foreign_list, &st);
|
|
|
|
w->ft_activate.notify = handle_foreign_request_activate;
|
|
wl_signal_add(&w->ft_handle->events.request_activate, &w->ft_activate);
|
|
w->ft_close.notify = handle_foreign_request_close;
|
|
wl_signal_add(&w->ft_handle->events.request_close, &w->ft_close);
|
|
w->ft_maximize.notify = handle_foreign_request_maximize;
|
|
wl_signal_add(&w->ft_handle->events.request_maximize, &w->ft_maximize);
|
|
w->ft_fullscreen.notify = handle_foreign_request_fullscreen;
|
|
wl_signal_add(&w->ft_handle->events.request_fullscreen, &w->ft_fullscreen);
|
|
w->ft_handle_destroy.notify = handle_foreign_handle_destroy;
|
|
wl_signal_add(&w->ft_handle->events.destroy, &w->ft_handle_destroy);
|
|
if (w->ext_handle) {
|
|
w->ext_handle_destroy.notify = handle_ext_foreign_handle_destroy;
|
|
wl_signal_add(&w->ext_handle->events.destroy, &w->ext_handle_destroy);
|
|
}
|
|
}
|
|
|
|
static void foreign_handle_destroy(struct kick_window *w) {
|
|
if (w->ft_handle)
|
|
wlr_foreign_toplevel_handle_v1_destroy(w->ft_handle);
|
|
if (w->ext_handle)
|
|
wlr_ext_foreign_toplevel_handle_v1_destroy(w->ext_handle);
|
|
w->ft_handle = NULL;
|
|
w->ext_handle = NULL;
|
|
}
|
|
|
|
/* ===================== Toplevel tags (xdg-toplevel-tag-v1) =====================
|
|
A client labels a toplevel with an untranslated tag ("settings", ...) plus
|
|
a translated description. The tag shows in the overlay row ("title [tag]")
|
|
and, with --tab-groups on, windows carrying the same tag as an existing
|
|
tab's window stack onto that tab instead of opening their own.
|
|
|
|
Ordering matters: wlroots emits new_toplevel when the client creates the
|
|
toplevel object, so the tag request always arrives after claim. Grouping
|
|
therefore happens when the tag lands, but only while the window is still
|
|
unmapped (invisible) -- regrouping a mapped window would yank it out from
|
|
under the user. Tags that somehow arrive before claim wait in
|
|
server.pending_tags and are consumed at claim. */
|
|
|
|
static struct kick_pending_tag *pending_tag_find(
|
|
struct wlr_xdg_toplevel *toplevel) {
|
|
struct kick_pending_tag *p;
|
|
wl_list_for_each(p, &server.pending_tags, link)
|
|
if (p->toplevel == toplevel)
|
|
return p;
|
|
return NULL;
|
|
}
|
|
|
|
static const char *pending_tag_for(struct wlr_xdg_toplevel *toplevel) {
|
|
struct kick_pending_tag *p = toplevel ? pending_tag_find(toplevel) : NULL;
|
|
return (p && p->tag && p->tag[0]) ? p->tag : NULL;
|
|
}
|
|
|
|
/* Dropped when the toplevel dies unclaimed. Unreachable by design: claim
|
|
is synchronous in handle_new_toplevel, so a pending tag only exists on
|
|
the calloc-failure path below -- excluded from line coverage. */
|
|
/* GCOVR_EXCL_START */
|
|
static void handle_pending_tag_toplevel_destroy(struct wl_listener *l,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_pending_tag *p = wl_container_of(l, p, toplevel_destroy);
|
|
wl_list_remove(&p->link);
|
|
wl_list_remove(&p->toplevel_destroy.link);
|
|
free(p->tag);
|
|
free(p->desc);
|
|
free(p);
|
|
}
|
|
/* GCOVR_EXCL_STOP */
|
|
|
|
/* Log one line per actual change, like the overlay's row_title cache. */
|
|
static void tag_log(struct kick_window *w) {
|
|
kick_log(KICK_LOG_INFO, "[tag] app=%d tag=%s desc=%s\n",
|
|
tab_index(w->tab), w->tag ? w->tag : "-",
|
|
w->tag_desc ? w->tag_desc : "-");
|
|
}
|
|
|
|
/* Stack an unmapped external window onto the tab whose window carries the
|
|
same tag (browser-style tag groups). Called when a tag lands; the mover
|
|
is invisible so the join is silent, and its later map proceeds exactly
|
|
like a claim-time grouping ([sub] focus, shared visibility). */
|
|
static void tag_try_group(struct kick_window *w) {
|
|
if (!tab_groups || !w || !w->toplevel || w->mapped)
|
|
return;
|
|
if (!w->tag || !w->tag[0] || w->origin != ORIGIN_EXTERNAL)
|
|
return;
|
|
struct kick_tab *gt;
|
|
wl_list_for_each(gt, &server.tabs, link) {
|
|
if (gt == w->tab)
|
|
continue;
|
|
struct kick_window *cur = tab_current_window(gt);
|
|
if (cur && cur->toplevel && cur->tag && strcmp(cur->tag, w->tag) == 0) {
|
|
struct kick_tab *old = w->tab;
|
|
wl_list_remove(&w->link);
|
|
wl_list_insert(gt->windows.prev, &w->link);
|
|
w->tab = gt;
|
|
w->origin = ORIGIN_GROUP;
|
|
w->parent_tab = gt;
|
|
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", tab_index(gt));
|
|
/* Grouping can change the tab's topmost window: re-resolve the
|
|
row title at this data source. */
|
|
(void)tab_row_title(gt);
|
|
/* The abandoned tab held only this window; dropping it cannot
|
|
end the session (the joined tab still lives). */
|
|
if (wl_list_empty(&old->windows))
|
|
drop_tab(old);
|
|
/* The regroup paints on the next frame while held. */
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Apply one side of a tag update (the other side passes NULL and is left
|
|
alone). An empty string clears. */
|
|
static void tag_apply(struct wlr_xdg_toplevel *toplevel, const char *tag,
|
|
const char *desc) {
|
|
if (!toplevel || (!tag && !desc))
|
|
return;
|
|
struct kick_window *w = window_for_toplevel(toplevel);
|
|
if (w) {
|
|
if (tag) {
|
|
free(w->tag);
|
|
w->tag = (tag[0] != '\0') ? strdup(tag) : NULL;
|
|
}
|
|
if (desc) {
|
|
free(w->tag_desc);
|
|
w->tag_desc = (desc[0] != '\0') ? strdup(desc) : NULL;
|
|
}
|
|
tag_log(w);
|
|
/* A freshly set tag may complete a group while the window is still
|
|
unmapped; a description-only update never regroups. */
|
|
if (tag && w->tag)
|
|
tag_try_group(w);
|
|
/* The new tag paints on the next frame while held. */
|
|
return;
|
|
}
|
|
/* Pre-claim: stash until claim_xdg_window picks it up. */
|
|
struct kick_pending_tag *p = pending_tag_find(toplevel);
|
|
if (!p) {
|
|
p = calloc(1, sizeof(*p));
|
|
if (!p)
|
|
return;
|
|
p->toplevel = toplevel;
|
|
p->toplevel_destroy.notify = handle_pending_tag_toplevel_destroy;
|
|
wl_signal_add(&toplevel->events.destroy, &p->toplevel_destroy);
|
|
wl_list_insert(&server.pending_tags, &p->link);
|
|
}
|
|
if (tag) {
|
|
free(p->tag);
|
|
p->tag = (tag[0] != '\0') ? strdup(tag) : NULL;
|
|
}
|
|
if (desc) {
|
|
free(p->desc);
|
|
p->desc = (desc[0] != '\0') ? strdup(desc) : NULL;
|
|
}
|
|
}
|
|
|
|
static void handle_tag_set(struct wl_listener *l, void *data) {
|
|
(void)l;
|
|
struct wlr_xdg_toplevel_tag_manager_v1_set_tag_event *ev = data;
|
|
tag_apply(ev->toplevel, ev->tag, NULL);
|
|
}
|
|
|
|
static void handle_tag_set_desc(struct wl_listener *l, void *data) {
|
|
(void)l;
|
|
struct wlr_xdg_toplevel_tag_manager_v1_set_description_event *ev = data;
|
|
tag_apply(ev->toplevel, NULL, ev->description);
|
|
}
|
|
|
|
/* Move a pre-claim pending tag onto the freshly claimed window. */
|
|
static void tag_consume_pending(struct kick_window *w) {
|
|
if (!w || !w->toplevel)
|
|
return;
|
|
struct kick_pending_tag *p = pending_tag_find(w->toplevel);
|
|
if (!p)
|
|
return;
|
|
wl_list_remove(&p->link);
|
|
wl_list_remove(&p->toplevel_destroy.link);
|
|
free(w->tag);
|
|
w->tag = p->tag;
|
|
free(w->tag_desc);
|
|
w->tag_desc = p->desc;
|
|
free(p);
|
|
if (w->tag || w->tag_desc)
|
|
tag_log(w);
|
|
}
|
|
|
|
static void bridge_host_set_selection(struct wlr_data_source *src) {
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f host_set_selection src=%p impl=%p "
|
|
"backend=%d dc_device=%p mimes=%zu\n", bridge_dbg_ms(),
|
|
(void *)src, (void *)(src ? src->impl : NULL), bridge.backend,
|
|
(void *)bridge.dc_device,
|
|
src ? src->mime_types.size / sizeof(char *) : 0);
|
|
bridge_set_inner_src(src);
|
|
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device) {
|
|
if (bridge.dc_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_source);
|
|
bridge.dc_source = NULL;
|
|
}
|
|
struct ext_data_control_source_v1 *s =
|
|
ext_data_control_manager_v1_create_data_source(bridge.dc_manager);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
ext_data_control_source_v1_offer(s, *p);
|
|
ext_data_control_source_v1_add_listener(s, &bridge_dc_source_listener, NULL);
|
|
bridge.dc_source = s;
|
|
bridge.dc_pending_echo = true;
|
|
bridge.dd_pending_echo = true;
|
|
ext_data_control_device_v1_set_selection(bridge.dc_device, s);
|
|
bridge.host_set_pending = false;
|
|
return;
|
|
}
|
|
|
|
if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE && bridge.dd) {
|
|
if (bridge.dd_source) {
|
|
wl_data_source_destroy(bridge.dd_source);
|
|
bridge.dd_source = NULL;
|
|
}
|
|
if (!bridge.kbd_serial) {
|
|
bridge.host_set_pending = true;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host keyboard "
|
|
"serial yet; deferring host set_selection until focus\n");
|
|
return;
|
|
}
|
|
struct wl_data_source *s =
|
|
wl_data_device_manager_create_data_source(bridge.ddm);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
wl_data_source_offer(s, *p);
|
|
wl_data_source_add_listener(s, &bridge_dd_source_listener, NULL);
|
|
bridge.dd_source = s;
|
|
bridge.dc_pending_echo = true;
|
|
bridge.dd_pending_echo = true;
|
|
wl_data_device_set_selection(bridge.dd, s, bridge.kbd_serial);
|
|
bridge.host_set_pending = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void bridge_host_clear(void) {
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device)
|
|
ext_data_control_device_v1_set_selection(bridge.dc_device, NULL);
|
|
/* focus-based backend: nothing reliable to clear without focus/serial */
|
|
}
|
|
|
|
static void bridge_host_set_primary_selection(struct wlr_primary_selection_source *src) {
|
|
bridge_set_inner_primary_src(src);
|
|
/* A nested client is overriding whatever host primary was mirrored in, so
|
|
the mirror's prefetch is stale. */
|
|
bridge_primary_prefetch_cancel();
|
|
|
|
if (bridge.backend != BRIDGE_BACKEND_DATA_CONTROL || !bridge.dc_device)
|
|
return; /* wl_data_device backend has no primary channel */
|
|
if (bridge.dc_primary_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (!src)
|
|
return;
|
|
struct ext_data_control_source_v1 *s =
|
|
ext_data_control_manager_v1_create_data_source(bridge.dc_manager);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
ext_data_control_source_v1_offer(s, *p);
|
|
ext_data_control_source_v1_add_listener(s,
|
|
&bridge_dc_primary_source_listener, NULL);
|
|
bridge.dc_primary_source = s;
|
|
bridge.dc_primary_pending_echo = true;
|
|
ext_data_control_device_v1_set_primary_selection(bridge.dc_device, s);
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: primary selection -> host\n");
|
|
}
|
|
|
|
static void bridge_host_primary_clear(void) {
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device)
|
|
ext_data_control_device_v1_set_primary_selection(bridge.dc_device, NULL);
|
|
}
|
|
|
|
static void bridge_seat_set_primary_selection(struct wl_listener *l, void *data) {
|
|
(void)l; (void)data;
|
|
struct wlr_primary_selection_source *src = server.seat->primary_selection_source;
|
|
if (src && src->impl == &bridge_inner_primary_source_impl)
|
|
return; /* our own host->nested mirror */
|
|
if (bridge.primary_ignore_next_inner) {
|
|
bridge.primary_ignore_next_inner = false;
|
|
return;
|
|
}
|
|
if (!src) {
|
|
if (bridge.inner_primary_clear_requested) {
|
|
bridge.inner_primary_clear_requested = false;
|
|
bridge_host_primary_clear();
|
|
}
|
|
return;
|
|
}
|
|
bridge.inner_primary_clear_requested = false;
|
|
bridge_host_set_primary_selection(src);
|
|
}
|
|
|
|
static void bridge_seat_set_selection(struct wl_listener *l, void *data) {
|
|
(void)l; (void)data;
|
|
struct wlr_data_source *src = server.seat->selection_source;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f seat set_selection src=%p "
|
|
"impl=%p inner_mirror=%d ignore_next=%d inner_clear_requested=%d\n",
|
|
bridge_dbg_ms(), (void *)src, (void *)(src ? src->impl : NULL),
|
|
src && src->impl == &bridge_inner_source_impl,
|
|
bridge.ignore_next_inner, bridge.inner_clear_requested);
|
|
if (src && src->impl == &bridge_inner_source_impl)
|
|
return; /* our own host->nested mirror */
|
|
if (bridge.ignore_next_inner) {
|
|
bridge.ignore_next_inner = false;
|
|
return;
|
|
}
|
|
if (!src) {
|
|
if (bridge.inner_clear_requested) {
|
|
bridge.inner_clear_requested = false;
|
|
bridge_host_clear();
|
|
}
|
|
return;
|
|
}
|
|
bridge.inner_clear_requested = false;
|
|
bridge_host_set_selection(src);
|
|
}
|
|
|
|
/* ---------------- lifecycle ---------------- */
|
|
|
|
static void bridge_attach_seat(void) {
|
|
bridge.request_set_selection.notify = bridge_seat_request_set_selection;
|
|
wl_signal_add(&server.seat->events.request_set_selection,
|
|
&bridge.request_set_selection);
|
|
bridge.set_selection.notify = bridge_seat_set_selection;
|
|
wl_signal_add(&server.seat->events.set_selection, &bridge.set_selection);
|
|
bridge.set_primary_selection.notify = bridge_seat_set_primary_selection;
|
|
wl_signal_add(&server.seat->events.set_primary_selection,
|
|
&bridge.set_primary_selection);
|
|
}
|
|
|
|
static void bridge_init(void) {
|
|
if (!host.seat) {
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host seat; disabled\n");
|
|
return;
|
|
}
|
|
/* Re-entrant: drop any objects bound to a previous host seat so a runtime
|
|
seat re-add (registry churn, e.g. VT round-trip) re-binds cleanly. */
|
|
if (bridge.dd)
|
|
wl_data_device_release(bridge.dd);
|
|
bridge.dd = NULL;
|
|
if (bridge.dc_device)
|
|
ext_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
if (bridge.dc_primary_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.dd_offer = NULL;
|
|
bridge.dc_offer = NULL;
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
if (bridge.ddm) {
|
|
bridge.dd = wl_data_device_manager_get_data_device(bridge.ddm, host.seat);
|
|
wl_data_device_add_listener(bridge.dd, &bridge_dd_listener, NULL);
|
|
}
|
|
if (bridge.dc_manager) {
|
|
bridge.dc_device = ext_data_control_manager_v1_get_data_device(
|
|
bridge.dc_manager, host.seat);
|
|
ext_data_control_device_v1_add_listener(bridge.dc_device,
|
|
&bridge_dc_listener, NULL);
|
|
bridge.backend = BRIDGE_BACKEND_DATA_CONTROL;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: host data-control backend "
|
|
"(clipboard and primary selection bridged)\n");
|
|
} else if (bridge.dd) {
|
|
bridge.backend = BRIDGE_BACKEND_WL_DATA_DEVICE;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: focus-based wl_data_device "
|
|
"backend (host lacks ext-data-control; host->nested sync needs "
|
|
"focus; primary selection not bridged)\n");
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host clipboard path\n");
|
|
}
|
|
|
|
/* A rebind drops the host-side selection (surviving sources are selected
|
|
on no device): re-publish the nested clipboard and primary so seat
|
|
churn, e.g. a VT round-trip, doesn't silently lose them. Mirrors are
|
|
host-originated and need no republish (impl check); at startup there
|
|
is no nested selection yet and both are no-ops. */
|
|
if (server.seat && server.seat->selection_source &&
|
|
server.seat->selection_source->impl != &bridge_inner_source_impl)
|
|
bridge_host_set_selection(server.seat->selection_source);
|
|
if (server.seat && server.seat->primary_selection_source &&
|
|
server.seat->primary_selection_source->impl !=
|
|
&bridge_inner_primary_source_impl)
|
|
bridge_host_set_primary_selection(server.seat->primary_selection_source);
|
|
}
|
|
|
|
static void bridge_cleanup(void) {
|
|
if (bridge.inner_src) {
|
|
wl_list_remove(&bridge.inner_src_destroy.link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
if (bridge.inner_primary_src) {
|
|
wl_list_remove(&bridge.inner_primary_src_destroy.link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
if (bridge.dc_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_source);
|
|
bridge.dc_source = NULL;
|
|
}
|
|
if (bridge.dc_primary_source) {
|
|
ext_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (bridge.dd_source) {
|
|
wl_data_source_destroy(bridge.dd_source);
|
|
bridge.dd_source = NULL;
|
|
}
|
|
if (bridge.dc_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
ext_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
if (bridge.dd_offer) {
|
|
wl_data_offer_destroy(bridge.dd_offer);
|
|
bridge.dd_offer = NULL;
|
|
}
|
|
if (bridge.dc_device) {
|
|
ext_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
}
|
|
bridge_mimes_reset();
|
|
free(bridge.mimes);
|
|
bridge.mimes = NULL;
|
|
bridge.mimes_cap = 0;
|
|
bridge_prefetch_cancel();
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
}
|
|
|
|
/* ===================== Host-side listeners ===================== */
|
|
|
|
/* The host compositor draws the nested cursor over our window, so a nested
|
|
client's cursor-shape-v1 request is forwarded to the host's
|
|
wp_cursor_shape_device_v1 (bound to our host wl_pointer). The host only
|
|
honors the shape while the pointer sits over our surface and ignores
|
|
requests with a stale serial, hence host.ptr_serial. When the host lacks
|
|
cursor-shape-v1 this is a no-op and the host keeps its default cursor. */
|
|
static void host_cursor_shape_get_pointer(void) {
|
|
if (!host.shape_mgr || !host.pointer) {
|
|
host.shape_dev = NULL;
|
|
return;
|
|
}
|
|
/* get_pointer = the sole new_id request (opcode 1); the interface symbol
|
|
is declared above (defined by our generated cursor-shape protocol
|
|
code, not by libwlr). */
|
|
host.shape_dev = (struct wp_cursor_shape_device_v1 *)wl_proxy_marshal_flags(
|
|
(struct wl_proxy *)host.shape_mgr, 1 /* WP_CURSOR_SHAPE_MANAGER_V1_GET_POINTER */,
|
|
&wp_cursor_shape_device_v1_interface,
|
|
wl_proxy_get_version((struct wl_proxy *)host.shape_mgr),
|
|
0, NULL, (struct wl_proxy *)host.pointer);
|
|
}
|
|
|
|
static void host_cursor_shape_set(int shape) {
|
|
if (!host.shape_dev) {
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host shape n/a\n");
|
|
return;
|
|
}
|
|
wl_proxy_marshal_flags((struct wl_proxy *)host.shape_dev,
|
|
1 /* WP_CURSOR_SHAPE_DEVICE_V1_SET_SHAPE */, NULL,
|
|
wl_proxy_get_version((struct wl_proxy *)host.shape_dev),
|
|
0, host.ptr_serial, (uint32_t)shape);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host shape %s\n",
|
|
wlr_cursor_shape_v1_name((enum wp_cursor_shape_device_v1_shape)shape));
|
|
}
|
|
|
|
/* wl_seat capabilities handler: see host_seat_capabilities() below (defined
|
|
after the host keyboard/pointer listeners it installs). */
|
|
static void host_seat_capabilities(void *data, struct wl_seat *seat,
|
|
uint32_t capabilities);
|
|
static void host_seat_name(void *data, struct wl_seat *seat, const char *name) {
|
|
(void)data; (void)seat; (void)name;
|
|
}
|
|
static const struct wl_seat_listener host_seat_listener = {
|
|
.capabilities = host_seat_capabilities,
|
|
.name = host_seat_name,
|
|
};
|
|
|
|
static void host_registry_global(void *data, struct wl_registry *registry,
|
|
uint32_t name, const char *interface,
|
|
uint32_t version)
|
|
{
|
|
(void)data;
|
|
if (strcmp(interface, wl_compositor_interface.name) == 0) {
|
|
host.compositor = wl_registry_bind(registry, name,
|
|
&wl_compositor_interface, version < 4 ? version : 4);
|
|
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
|
host.wm_base = wl_registry_bind(registry, name,
|
|
&xdg_wm_base_interface, version < 1 ? version : 1);
|
|
} else if (strcmp(interface,
|
|
zxdg_decoration_manager_v1_interface.name) == 0) {
|
|
/* Advertised by compositors that honor server-side decorations
|
|
(labwc/sway/KDE): marks the direct-xdg host-window path. */
|
|
host.decoration = wl_registry_bind(registry, name,
|
|
&zxdg_decoration_manager_v1_interface, 1);
|
|
|
|
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
|
/* Bind the seat and rely on the capabilities event (which the
|
|
compositor always sends after a bind) to (re)create the keyboard
|
|
and pointer objects -- including after a VT round-trip, where the
|
|
compositor invalidates seat objects and a re-bind or capability
|
|
re-gain must be observed to keep receiving input. */
|
|
host.seat = wl_registry_bind(registry, name,
|
|
&wl_seat_interface, version < 5 ? version : 5);
|
|
host.seat_name = name;
|
|
wl_seat_add_listener(host.seat, &host_seat_listener, NULL);
|
|
if (host.registry_synced)
|
|
bridge_init(); /* runtime seat re-add: re-establish the bridge */
|
|
} else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) {
|
|
host.shape_mgr = wl_registry_bind(registry, name,
|
|
&wp_cursor_shape_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host has cursor-shape-v1\n");
|
|
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
|
|
bridge.ddm = wl_registry_bind(registry, name,
|
|
&wl_data_device_manager_interface, version < 3 ? version : 3);
|
|
} else if (strcmp(interface,
|
|
ext_data_control_manager_v1_interface.name) == 0) {
|
|
bridge.dc_manager = wl_registry_bind(registry, name,
|
|
&ext_data_control_manager_v1_interface, 1);
|
|
kick_log(KICK_LOG_DEBUG, "clipboard bridge: host ext-data-control\n");
|
|
}
|
|
}
|
|
|
|
static void host_registry_global_remove(void *data, struct wl_registry *registry,
|
|
uint32_t name)
|
|
{
|
|
(void)data; (void)registry;
|
|
/* Only the wl_seat global's removal matters to us: its children (keyboard,
|
|
pointer, clipboard data devices) are invalidated with it and must be
|
|
re-created when the compositor advertises a new seat. Other globals we
|
|
bind (compositor, xdg_wm_base, ...) are process-lifetime. */
|
|
if (host.seat_name && name == host.seat_name) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] global %u removed\n", name);
|
|
host.seat_name = 0;
|
|
host.seat = NULL;
|
|
host.keyboard = NULL;
|
|
host.pointer = NULL;
|
|
host.shape_dev = NULL; /* inert with the removed seat's pointer */
|
|
/* Seat-owned bridge objects are invalidated too; null them so the
|
|
next bridge_init() on a fresh seat re-binds cleanly. */
|
|
bridge.dd = NULL;
|
|
bridge.dc_device = NULL;
|
|
bridge.dd_offer = NULL;
|
|
bridge.dc_offer = NULL;
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge.dc_primary_source = NULL;
|
|
bridge.dc_primary_offer = NULL;
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
/* Echo guards from the dead seat can never resolve: clear them so
|
|
the first real offer on the next seat isn't swallowed as our own
|
|
echo. (Manager-child sources survive, but are selected on no
|
|
device; bridge_init re-publishes the nested selection below.) */
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
}
|
|
}
|
|
|
|
static const struct wl_registry_listener host_registry_listener = {
|
|
.global = host_registry_global,
|
|
.global_remove = host_registry_global_remove,
|
|
};
|
|
|
|
static void host_xdg_wm_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) {
|
|
(void)data;
|
|
xdg_wm_base_pong(wm, serial);
|
|
}
|
|
|
|
static const struct xdg_wm_base_listener host_xdg_wm_listener = {
|
|
.ping = host_xdg_wm_ping,
|
|
};
|
|
|
|
static void host_resize(int width, int height) {
|
|
if (width < 50) width = 50;
|
|
if (height < 50) height = 50;
|
|
if (width == host.width && height == host.height) return;
|
|
host.width = width;
|
|
host.height = height;
|
|
if (host.egl_window)
|
|
wl_egl_window_resize(host.egl_window, width, height, 0, 0);
|
|
if (host.egl_ok) {
|
|
eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
|
|
host.egl_context);
|
|
}
|
|
/* Allocate the new swapchain here rather than lazily at render time:
|
|
the GBM allocation can take longer than a compositor's configure
|
|
response deadline, and the first frame at this size must be on the
|
|
wire before that deadline expires. */
|
|
swapchain_resize(width, height);
|
|
output_resize(width, height);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Tear down the nested window: drop the libdecor frame (and its decoration
|
|
subsurfaces) and the content surface. Only called from the exit paths, so
|
|
no ack/commit dance is needed. */
|
|
static void host_teardown_window(void) {
|
|
if (host.use_libdecor) {
|
|
if (host.frame) {
|
|
libdecor_frame_unref(host.frame);
|
|
host.frame = NULL;
|
|
}
|
|
} else {
|
|
if (host.toplevel) {
|
|
xdg_toplevel_destroy(host.toplevel);
|
|
host.toplevel = NULL;
|
|
}
|
|
if (host.xdg_surface) {
|
|
xdg_surface_destroy(host.xdg_surface);
|
|
host.xdg_surface = NULL;
|
|
}
|
|
}
|
|
if (host.surface) {
|
|
wl_surface_destroy(host.surface);
|
|
host.surface = NULL;
|
|
}
|
|
}
|
|
|
|
/* ---- Direct-xdg host-window path (SSD compositors: labwc/sway/KDE) ----
|
|
Kick owns the shell objects and acks configure events immediately -- the
|
|
arrangement that predates libdecor, where fullscreen state application
|
|
cannot be anchored to stale geometry because no set_window_geometry hint
|
|
is ever sent and the ack precedes every commit. */
|
|
|
|
static void host_xdg_surface_configure(void *data, struct xdg_surface *s,
|
|
uint32_t serial)
|
|
{
|
|
(void)data;
|
|
xdg_surface_ack_configure(s, serial);
|
|
}
|
|
|
|
static const struct xdg_surface_listener host_xdg_surface_listener = {
|
|
.configure = host_xdg_surface_configure,
|
|
};
|
|
|
|
static void host_xdg_toplevel_configure(void *data, struct xdg_toplevel *t,
|
|
int32_t width, int32_t height,
|
|
struct wl_array *states)
|
|
{
|
|
(void)data; (void)t;
|
|
/* Mirror the host window's state per entry: entering fullscreen
|
|
fullscreens the focused overlay entry only (others grow via the
|
|
resize below); leaving clears every live window. */
|
|
bool prev_fs = host.fullscreen;
|
|
host.fullscreen = false;
|
|
uint32_t *state;
|
|
wl_array_for_each(state, states)
|
|
if (*state == XDG_TOPLEVEL_STATE_FULLSCREEN)
|
|
host.fullscreen = true;
|
|
if (host.fullscreen != prev_fs) {
|
|
kick_log(KICK_LOG_INFO, "host fullscreen=%d\n", host.fullscreen);
|
|
clients_set_fullscreen(host.fullscreen);
|
|
}
|
|
if (width > 0 && height > 0)
|
|
host_resize(width, height);
|
|
host.configured = true;
|
|
}
|
|
|
|
/* Common quit tail for the user-requested exits: drop the host window
|
|
(nested; guarded no-op standalone), tear every client and window down,
|
|
and exit cleanly. */
|
|
static void do_quit(void) {
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
|
|
/* Show or dismiss the confirm-before-quit modal. The panel is drawn by the
|
|
same second pass as the Alt overlay, so the same dance applies: while it
|
|
is up, direct scanout must yield to the composite path (attach-render
|
|
lock) and the swapchain buffers must be forced to repaint, or the modal
|
|
would bake into a reused buffer like the overlay does. */
|
|
static void set_confirm_quit(bool on) {
|
|
if (on == confirm_quit_pending) return;
|
|
confirm_quit_pending = on;
|
|
kick_log(KICK_LOG_DEBUG, "[render] confirm-quit %s\n",
|
|
on ? "show" : "hide");
|
|
/* On a DRM output the modal can ride the overlay plane (probed like the
|
|
tab list); on the composite path it must repaint into our own buffer,
|
|
so the scanout lock follows whether the plane is usable. */
|
|
overlay_sync_render_lock();
|
|
overlay_sync_cursor_lock();
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
else if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* A quit was requested (host window close, or Ctrl+Alt+Q). With more than
|
|
one live window quitting would lose work, so raise the confirm modal and
|
|
wait for Y/N; a single window (or none) has nothing to lose and quits
|
|
straight away. */
|
|
static void quit_requested(void) {
|
|
int n = live_window_count();
|
|
kick_log(KICK_LOG_INFO, "kick: quit requested (%d window%s)\n",
|
|
n, n == 1 ? "" : "s");
|
|
if (n <= 1) {
|
|
do_quit();
|
|
return;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[confirm] quit: %d windows open "
|
|
"(Y=quit, N=cancel)\n", n);
|
|
set_confirm_quit(true);
|
|
}
|
|
|
|
static void host_xdg_toplevel_close(void *data, struct xdg_toplevel *t) {
|
|
(void)data; (void)t;
|
|
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
|
quit_requested();
|
|
}
|
|
|
|
static const struct xdg_toplevel_listener host_xdg_toplevel_listener = {
|
|
.configure = host_xdg_toplevel_configure,
|
|
.close = host_xdg_toplevel_close,
|
|
};
|
|
|
|
static void host_decoration_configure(void *data,
|
|
struct zxdg_toplevel_decoration_v1 *dec,
|
|
uint32_t mode)
|
|
{
|
|
(void)data; (void)dec;
|
|
kick_log(KICK_LOG_INFO, "kick: host decoration mode=%u (%s)\n",
|
|
mode,
|
|
mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE
|
|
? "server-side" : "client-side");
|
|
}
|
|
|
|
static const struct zxdg_toplevel_decoration_v1_listener
|
|
host_decoration_listener = {
|
|
.configure = host_decoration_configure,
|
|
};
|
|
|
|
static void host_frame_configure(struct libdecor_frame *frame,
|
|
struct libdecor_configuration *config,
|
|
void *data)
|
|
{
|
|
(void)data;
|
|
int width = 0, height = 0;
|
|
bool size_ok =
|
|
libdecor_configuration_get_content_size(config, frame, &width, &height);
|
|
if (!size_ok) {
|
|
/* The initial configure commonly offers no size (the compositor
|
|
lets a CSD client pick one). The old xdg_toplevel configure
|
|
handler also treated 0x0 as "keep the current size": commit the
|
|
current layout anyway so libdecor gets a real sized configure
|
|
once we present a buffer. */
|
|
width = host.width;
|
|
height = host.height;
|
|
}
|
|
enum libdecor_window_state state = 0;
|
|
bool have_state = libdecor_configuration_get_window_state(config, &state);
|
|
if (have_state) {
|
|
bool fs = (state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0;
|
|
if (fs != host.fullscreen) {
|
|
kick_log(KICK_LOG_INFO, "host fullscreen=%d\n", fs);
|
|
host.fullscreen = fs;
|
|
clients_set_fullscreen(fs);
|
|
}
|
|
}
|
|
/* libdecor reports the *content* size; the host window wraps that content
|
|
with its decorations. The content is exactly our wlr output. */
|
|
if (width > 0 && height > 0)
|
|
host_resize(width, height);
|
|
host.configured = true;
|
|
struct libdecor_state *st = libdecor_state_new(width, height);
|
|
libdecor_frame_commit(frame, st, config);
|
|
libdecor_state_free(st);
|
|
/* Do not follow the ack with a bare commit when the configure carried a
|
|
real size: that commit would apply the acked state (e.g. fullscreen)
|
|
using the still-attached previous frame, and labwc anchors the new
|
|
layout to that stale geometry -- the enlarged image lands at the old
|
|
origin with black margins top/left, cropped bottom/right. Skipping
|
|
it lets the next rendered frame apply the state at the correct size,
|
|
like every ordinary EGL client. The empty commit stays for the
|
|
size-less initial configure, where there is no state transition to
|
|
corrupt and libdecor needs the commit to get mapped. */
|
|
if (!size_ok && host.surface)
|
|
wl_surface_commit(host.surface);
|
|
/* labwc arms a 100 ms "did not respond" timer when it sends a configure;
|
|
on expiry it applies the move but keeps the old size -- the misplaced
|
|
fullscreen. Two measures keep us inside that deadline:
|
|
1. push the ack out immediately instead of waiting for the
|
|
end-of-loop flush;
|
|
2. render and present one frame at the new size right here, so a
|
|
correctly sized commit reaches labwc long before the timer fires
|
|
(commits sent before the ack are harmless -- they cannot apply
|
|
the new state -- and the first commit after the ack is the next
|
|
paced frame, already fresh). */
|
|
/* Push the ack out immediately instead of waiting for the
|
|
end-of-loop flush: labwc arms a 100 ms response deadline when it
|
|
sends a configure, and on expiry it applies the move while keeping
|
|
the old size -- the misplaced-fullscreen symptom. */
|
|
wl_display_flush(host.display);
|
|
}
|
|
|
|
static void host_frame_close(struct libdecor_frame *frame, void *data) {
|
|
(void)frame; (void)data;
|
|
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
|
quit_requested();
|
|
}
|
|
|
|
static void host_frame_commit(struct libdecor_frame *frame, void *data) {
|
|
/* Decoration via (synchronous) subsurfaces: libdecor asks us to commit the
|
|
content surface so the decoration appears atomically with the content. */
|
|
(void)frame; (void)data;
|
|
if (host.surface)
|
|
wl_surface_commit(host.surface);
|
|
}
|
|
|
|
static struct libdecor_frame_interface host_frame_iface = {
|
|
.configure = host_frame_configure,
|
|
.close = host_frame_close,
|
|
.commit = host_frame_commit,
|
|
};
|
|
|
|
static void host_libdecor_error(struct libdecor *ctx, enum libdecor_error error,
|
|
const char *message)
|
|
{
|
|
(void)ctx;
|
|
fprintf(stderr, "kick: libdecor error %d: %s\n", (int)error,
|
|
message ? message : "(null)");
|
|
}
|
|
|
|
static struct libdecor_interface host_libdecor_iface = {
|
|
.error = host_libdecor_error,
|
|
};
|
|
|
|
/* ---------- host keyboard ---------- */
|
|
|
|
/* (Re)build the xkb_state used to translate raw evdev keycodes into
|
|
keysyms for host hotkey matching. The state is derived from whatever
|
|
keymap is current (host-provided or fallback), so the hotkeys work on
|
|
any keyboard/layout without hardcoded keycodes. */
|
|
static void xkb_state_reload(void)
|
|
{
|
|
if (server.xkb_state) {
|
|
xkb_state_unref(server.xkb_state);
|
|
server.xkb_state = NULL;
|
|
}
|
|
if (!server.xkb_keymap)
|
|
return;
|
|
server.xkb_state = xkb_state_new(server.xkb_keymap);
|
|
server.xkb_prefix_mod = xkb_keymap_mod_get_index(server.xkb_keymap,
|
|
tab_prefix == PREFIX_SUPER ? XKB_PREFIX_MOD4_NAME
|
|
: XKB_MOD_NAME_ALT);
|
|
kick_log(KICK_LOG_DEBUG, "kick: prefix modifier index=%u "
|
|
"(%s)\n", (unsigned)server.xkb_prefix_mod,
|
|
tab_prefix == PREFIX_SUPER ? "Super/Mod4" : "Alt");
|
|
}
|
|
|
|
static void host_kbd_keymap(void *data, struct wl_keyboard *kbd, uint32_t format,
|
|
int32_t fd, uint32_t size)
|
|
{
|
|
(void)data; (void)kbd;
|
|
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 || size == 0 || !server.xkb_ctx) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "kick: host keymap: format=%u size=%u\n",
|
|
format, size);
|
|
|
|
/* The keymap is an shm file (size includes the trailing NUL), so mmap it
|
|
like wlroots' own clients do; fall back to a plain read loop. */
|
|
char *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
bool mapped = buf != MAP_FAILED;
|
|
size_t total = mapped ? size : 0;
|
|
if (!mapped) {
|
|
buf = malloc(size + 1);
|
|
if (!buf) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
while (total < size) {
|
|
ssize_t rd = read(fd, buf + total, size - total);
|
|
if (rd > 0) {
|
|
total += rd;
|
|
} else if (rd < 0 && (errno == EAGAIN || errno == EINTR)) {
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
|
|
nanosleep(&ts, NULL);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
buf[total] = '\0';
|
|
if (total < size)
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"kick: host keymap: short read %zu/%u bytes\n",
|
|
total, size);
|
|
}
|
|
|
|
struct xkb_keymap *km = xkb_keymap_new_from_string(
|
|
server.xkb_ctx, buf, XKB_KEYMAP_FORMAT_TEXT_V1, 0);
|
|
if (km) {
|
|
wlr_keyboard_set_keymap(&server.keyboard, km);
|
|
if (server.xkb_keymap)
|
|
xkb_keymap_unref(server.xkb_keymap);
|
|
server.xkb_keymap = km;
|
|
xkb_state_reload();
|
|
kick_log(KICK_LOG_DEBUG, "kick: host keymap parsed OK\n");
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "kick: failed to parse host "
|
|
"keymap (%u bytes); keeping fallback keymap\n", size);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
char hex[129];
|
|
size_t n = total < 64 ? total : 64;
|
|
for (size_t i = 0; i < n; i++)
|
|
snprintf(hex + i * 2, sizeof(hex) - i * 2, "%02x",
|
|
(unsigned char)buf[i]);
|
|
kick_log(KICK_LOG_DEBUG, "kick: keymap head (%zu/%u "
|
|
"bytes): %s\n", n, size, hex);
|
|
}
|
|
}
|
|
|
|
if (mapped)
|
|
munmap(buf, size);
|
|
else
|
|
free(buf);
|
|
close(fd);
|
|
}
|
|
|
|
static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
|
struct wl_surface *surface, struct wl_array *keys)
|
|
{
|
|
(void)data; (void)kbd; (void)surface; (void)keys;
|
|
/* Remember the enter serial for host clipboard set_selection. Only the
|
|
focus-based wl_data_device backend can hold a stale echo guard (a
|
|
set_selection sent with a no-longer-valid serial gets no reply): drop it
|
|
on refocus there. The data-control backend echoes independently of
|
|
focus, so its in-flight guard must be preserved here, or the host's echo
|
|
of our own set_selection is mistaken for a real selection and mirrored
|
|
back into an endless set_selection/receive loop. */
|
|
bridge.kbd_serial = serial;
|
|
if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE) {
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
}
|
|
if (bridge.host_set_pending && server.seat->selection_source &&
|
|
server.seat->selection_source->impl != &bridge_inner_source_impl) {
|
|
bridge.host_set_pending = false;
|
|
bridge_host_set_selection(server.seat->selection_source);
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[kbd] enter surface=%p serial=%u active=%p\n",
|
|
(void *)surface, serial, (void *)active_toplevel());
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
if (w && w->toplevel)
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
w->toplevel->base->surface, server.keyboard.keycodes,
|
|
server.keyboard.num_keycodes, &server.keyboard.modifiers);
|
|
}
|
|
|
|
static void host_kbd_leave(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
|
struct wl_surface *surface)
|
|
{
|
|
(void)data; (void)kbd; (void)serial; (void)surface;
|
|
kick_log(KICK_LOG_DEBUG, "[kbd] leave surface=%p serial=%u\n",
|
|
(void *)surface, serial);
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
}
|
|
|
|
/* Temporary grey-band diagnosis: dump the scene buffer (client content as
|
|
wlroots rendered it) to /tmp/scene_N.ppm. The buffer memory is top-down,
|
|
XRGB8888. */
|
|
static void dump_scene_buffer(struct wlr_buffer *buf) {
|
|
if (!buf) return;
|
|
void *data;
|
|
uint32_t format;
|
|
size_t stride;
|
|
if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
|
|
&data, &format, &stride)) {
|
|
kick_log(KICK_LOG_INFO, "[dump] scene buffer not CPU-readable "
|
|
"(%dx%d)\n", buf->width, buf->height);
|
|
return;
|
|
}
|
|
/* [diag] classify the buffer: count non-black pixels so we can tell
|
|
"client painted nothing" from "we failed to import". */
|
|
int w = buf->width, h = buf->height;
|
|
unsigned long nonblack = 0, lum = 0;
|
|
unsigned char *p = data;
|
|
for (int y = 0; y < h; y++) {
|
|
unsigned char *row = p + (size_t)y * stride;
|
|
for (int x = 0; x < w; x++) {
|
|
uint32_t b = row[x*4+0];
|
|
unsigned long v = b + row[x*4+1] + row[x*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] scene %dx%d fmt=0x%x "
|
|
"nonblack=%lu/%lu mean=%lu\n", w, h, format, nonblack,
|
|
(unsigned long)w * h, lum / ((unsigned long)w * h + 1));
|
|
char path[512];
|
|
dump_path(path, sizeof(path), "scene", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
int w = buf->width, h = buf->height;
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
unsigned char *p = data;
|
|
for (int y = 0; y < h; y++) {
|
|
unsigned char *row = p + (size_t)y * stride;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] scene buffer -> %s (%dx%d, "
|
|
"fmt=0x%x)\n", path, w, h, format);
|
|
}
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* Dump what we are about to present to the host (our composited back buffer).
|
|
Must be called with the host EGL context current and back buffer freshly
|
|
drawn. GL readback is bottom-up, so flip vertically. */
|
|
static void dump_host_back(void) {
|
|
int w = host.width, h = host.height;
|
|
unsigned char *px = malloc((size_t)w * h * 4);
|
|
if (!px) return;
|
|
/* The blit draws are queued on the GPU; without a finish the readback can
|
|
race the previous frame's swap and return stale/mid-clear content. */
|
|
glFinish();
|
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
|
unsigned long nonblack = 0, lum = 0;
|
|
for (int y = 0; y < h * w; y++) {
|
|
unsigned long v = px[y*4+0] + px[y*4+1] + px[y*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
|
|
w, h, nonblack, (unsigned long)w * h,
|
|
lum / ((unsigned long)w * h + 1));
|
|
char path[512];
|
|
dump_path(path, sizeof(path), "host", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
for (int y = h - 1; y >= 0; y--) {
|
|
unsigned char *row = px + (size_t)y * w * 4;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] host back buffer -> %s (%dx%d)\n",
|
|
path, w, h);
|
|
}
|
|
free(px);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* Dump the standalone frame buffer as it will be presented (scene + overlay
|
|
composited by the wlroots renderer). With a GLES2 renderer we glReadPixels
|
|
the buffer's FBO in the wlroots EGL context; otherwise fall back to a CPU
|
|
read (e.g. the shm-backed buffers of the pixman renderer). Uses the same
|
|
"[dump] host back buffer ->" message as the nested dump so tests can parse
|
|
either mode with one helper. */
|
|
static void dump_standalone_buffer(struct wlr_buffer *buf) {
|
|
int w = buf->width, h = buf->height;
|
|
unsigned char *px = malloc((size_t)w * h * 4);
|
|
if (!px) return;
|
|
bool flip = false;
|
|
struct wlr_egl *egl = wlr_gles2_renderer_get_egl(server.renderer);
|
|
GLuint fbo = egl ? wlr_gles2_renderer_get_buffer_fbo(server.renderer, buf)
|
|
: 0;
|
|
EGLDisplay d = egl ? wlr_egl_get_display(egl) : EGL_NO_DISPLAY;
|
|
EGLContext c = egl ? wlr_egl_get_context(egl) : EGL_NO_CONTEXT;
|
|
if (d != EGL_NO_DISPLAY && c != EGL_NO_CONTEXT && fbo) {
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, c);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
flip = true;
|
|
} else {
|
|
void *data;
|
|
uint32_t format;
|
|
size_t stride;
|
|
if (!wlr_buffer_begin_data_ptr_access(buf,
|
|
WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &format, &stride)) {
|
|
free(px);
|
|
kick_log(KICK_LOG_INFO,
|
|
"[dump] standalone buffer not CPU-readable\n");
|
|
return;
|
|
}
|
|
for (int y = 0; y < h; y++)
|
|
memcpy(px + (size_t)y * w * 4, data + (size_t)y * stride,
|
|
(size_t)w * 4);
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
}
|
|
|
|
unsigned long nonblack = 0, lum = 0;
|
|
for (int y = 0; y < h * w; y++) {
|
|
unsigned long v = px[y*4+0] + px[y*4+1] + px[y*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
|
|
w, h, nonblack, (unsigned long)w * h,
|
|
lum / ((unsigned long)w * h + 1));
|
|
char path[512];
|
|
dump_path(path, sizeof(path), "host", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
for (int y = 0; y < h; y++) {
|
|
int sy = flip ? h - 1 - y : y;
|
|
unsigned char *row = px + (size_t)sy * w * 4;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] host back buffer -> %s (%dx%d)\n",
|
|
path, w, h);
|
|
if (server.cursor)
|
|
kick_log(KICK_LOG_INFO,
|
|
"[dump] cursor at (%f,%f)\n", server.cursor->x, server.cursor->y);
|
|
}
|
|
free(px);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* KICK_SYNTH_INPUT=1: ignore the real host keyboard entirely and accept
|
|
input only from the stdin event feed (the test/driver channel). The host
|
|
keyboard listener stays attached (its enter/leave events still drive the
|
|
clipboard bridge), but key and modifier events from the real host are
|
|
dropped so a stray physical press can't cancel a synthesized repeat or
|
|
clobber the injected modifier state. */
|
|
static bool synth_input_only;
|
|
|
|
/* Forward declarations for the shared handlers (defined just below
|
|
host_kbd_key / host_ptr_motion). */
|
|
static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
|
uint32_t mods_depressed);
|
|
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group);
|
|
static void host_ptr_motion(uint32_t time, double sx, double sy);
|
|
/* Host pointer/touch listeners (defined with the host seat handlers). The
|
|
CTL_CMD_HOST_PTR/HOST_TOUCH feed drives them with NULL objects -- every
|
|
listener voids its Wayland object -- the same way CTL_CMD_KEY already
|
|
drives host_kbd_key with NULLs. */
|
|
static void host_ptr_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *surface, wl_fixed_t sx,
|
|
wl_fixed_t sy);
|
|
static void host_ptr_leave(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *surface);
|
|
static void host_ptr_motion_event(void *data, struct wl_pointer *p,
|
|
uint32_t time, wl_fixed_t sx, wl_fixed_t sy);
|
|
static void host_ptr_button(void *data, struct wl_pointer *p, uint32_t serial,
|
|
uint32_t time, uint32_t button, uint32_t state);
|
|
static void host_ptr_axis(void *data, struct wl_pointer *p, uint32_t time,
|
|
uint32_t axis, wl_fixed_t value);
|
|
static void host_ptr_axis_source(void *data, struct wl_pointer *p,
|
|
uint32_t source);
|
|
static void host_ptr_axis_discrete(void *data, struct wl_pointer *p,
|
|
uint32_t axis, int32_t discrete);
|
|
static void host_ptr_axis_stop(void *data, struct wl_pointer *p,
|
|
uint32_t time, uint32_t axis);
|
|
static void host_ptr_frame(void *data, struct wl_pointer *p);
|
|
static void host_touch_down(void *data, struct wl_touch *touch,
|
|
uint32_t serial, uint32_t time,
|
|
struct wl_surface *surface, int32_t id,
|
|
wl_fixed_t fx, wl_fixed_t fy);
|
|
static void host_touch_motion(void *data, struct wl_touch *touch,
|
|
uint32_t time, int32_t id,
|
|
wl_fixed_t fx, wl_fixed_t fy);
|
|
static void host_touch_up(void *data, struct wl_touch *touch,
|
|
uint32_t serial, uint32_t time, int32_t id);
|
|
static void host_touch_cancel(void *data, struct wl_touch *touch);
|
|
static void host_touch_frame(void *data, struct wl_touch *touch);
|
|
|
|
/* Touch delivery shared by native touchscreens, the nested host forwarder
|
|
and the ctl feed. `action` is a KICK_TOUCH_* value; (ox,oy) are
|
|
output-local pixels. */
|
|
enum kick_touch_action { KICK_TOUCH_UP = 0, KICK_TOUCH_DOWN = 1,
|
|
KICK_TOUCH_MOTION = 2 };
|
|
static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action,
|
|
double ox, double oy);
|
|
|
|
static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
|
uint32_t time, uint32_t key, uint32_t state)
|
|
{
|
|
(void)data;
|
|
if (synth_input_only && kbd)
|
|
return;
|
|
(void)kbd;
|
|
bridge.kbd_serial = serial;
|
|
if (!process_key_event(time, key, state, host.host_mods_depressed)) {
|
|
/* forward the key to our clients, like a native device would */
|
|
if (im_grab_forward_key(time, key, state))
|
|
return;
|
|
struct wlr_keyboard_key_event event = {
|
|
.time_msec = time,
|
|
.keycode = key,
|
|
.update_state = true,
|
|
.state = state,
|
|
};
|
|
wlr_keyboard_notify_key(&server.keyboard, &event);
|
|
wlr_seat_set_keyboard(server.seat, &server.keyboard);
|
|
wlr_seat_keyboard_notify_key(server.seat, time, key, state);
|
|
}
|
|
}
|
|
|
|
/* Force a whole-output repaint of the standalone frame. The overlay is drawn
|
|
into the scene-output buffer outside the scene graph -- and on the plane
|
|
path removed from the primary -- so the scene's damage trackers never learn
|
|
about its old pixels; a swapchain slot reused after the overlay was drawn
|
|
keeps the stale overlay baked in unless we repaint the whole frame on every
|
|
transition. Use this on every overlay show/hide and cursor motion. */
|
|
static void standalone_overlay_damage(void) {
|
|
if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
}
|
|
|
|
/* set_title / set_app_id for xdg toplevels: refresh the overlay and push the
|
|
new title/app_id onto the taskbar handles. The event's data is the
|
|
toplevel, from which we recover the owning window. */
|
|
static void handle_window_meta(struct wl_listener *l, void *data) {
|
|
(void)l;
|
|
struct wlr_xdg_toplevel *toplevel = data;
|
|
struct kick_window *w = window_for_toplevel(toplevel);
|
|
/* The new title paints on the next frame while held. */
|
|
if (w) {
|
|
foreign_sync_title(w);
|
|
foreign_sync_app_id(w);
|
|
/* Emit the row title at the data source, so a title change is
|
|
observable in logs even when no overlay frame is drawn (under
|
|
load there may be no draw pass, and [title] is otherwise only
|
|
logged from the draw loops). tab_row_title() is self-deduping
|
|
and safe to call outside a draw. */
|
|
struct kick_tab *t = w->tab;
|
|
if (t)
|
|
(void)tab_row_title(t);
|
|
}
|
|
}
|
|
|
|
/* Smooth-scroll accumulation (output pixels) towards one overlay tab step.
|
|
Carries the remainder across events; cleared whenever the overlay hides
|
|
so no stale fraction leaks into the next session. */
|
|
static double overlay_wheel_acc = 0.0;
|
|
|
|
static void update_overlay_visible(bool alt) {
|
|
if (alt == host.overlay_visible) return;
|
|
if (alt) {
|
|
/* Prefix went down: snapshot the shown window. Every focus change
|
|
from here until release is a preview -- window_enter leaves the
|
|
MRU stack alone -- and marks the session navigated. */
|
|
server.mru_alt_anchor = mru_current();
|
|
server.mru_alt_navigated = false;
|
|
} else {
|
|
/* Prefix released: the window we are on becomes most recent, but
|
|
only if we actually moved. A bare prefix tap/hold that never
|
|
navigates must not reshuffle the stack. */
|
|
if (server.mru_alt_navigated) {
|
|
struct kick_window *cur = mru_current();
|
|
if (cur) {
|
|
mru_move_to_front(cur);
|
|
kick_log(KICK_LOG_INFO, "[mru] commit app=%d\n",
|
|
tab_index(cur->tab));
|
|
}
|
|
}
|
|
server.mru_alt_anchor = NULL;
|
|
server.mru_alt_navigated = false;
|
|
overlay_wheel_acc = 0.0;
|
|
}
|
|
host.overlay_visible = alt;
|
|
kick_log(KICK_LOG_DEBUG, "[render] overlay %s\n",
|
|
alt ? "show" : "hide");
|
|
/* A rejected plane goes back to UNKNOWN on the next hold so the fresh
|
|
allocation is probed again instead of staying composited forever. */
|
|
if (alt && server.overlay_plane == OVERLAY_PLANE_OFF)
|
|
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
|
|
/* Scanout lock ownership: when the overlay rides a hardware plane the lock
|
|
must stay off so direct scanout of the focused client keeps running
|
|
underneath; otherwise it must be held for the whole hold so the second
|
|
paint pass lands on our own composited buffer. Releasing it re-enables
|
|
scanout automatically (same pattern labwc uses for its panels). */
|
|
overlay_sync_render_lock();
|
|
overlay_sync_cursor_lock();
|
|
/* The overlay is baked into the scene output's swapchain buffers in both
|
|
modes now, so a transition must force those buffers to repaint from
|
|
clean scene content: standalone via a whole-output repaint, nested via
|
|
a whole-ring damage (its window blit no longer redraws the overlay away
|
|
-- it just copies whatever the buffer holds). */
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
else if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* handle_new_output() forces software cursors on every standalone output, so
|
|
the pointer is always composited into our frame by the scene renderer --
|
|
which only repaints where the frame was damaged. Cursor motion damages the
|
|
*output*, not the scene, so force whole-output damage on every move or the
|
|
pointer would never be painted. */
|
|
static void standalone_cursor_motion(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
}
|
|
|
|
static struct wl_listener standalone_cursor_motion_listener = {
|
|
.notify = standalone_cursor_motion,
|
|
};
|
|
|
|
/* Is the configured tab-management prefix (Alt, or Super/Mod4 with
|
|
--modifier super) currently pressed? Uses the xkb modifier index when a
|
|
keymap is compiled; with no keymap (xkb init failed) it falls back to the
|
|
raw wl_keyboard modifier bit: mod1/alt = 1<<3, mod4/super = 1<<6. */
|
|
static bool prefix_active(uint32_t mods_depressed) {
|
|
if (server.xkb_state)
|
|
return xkb_state_mod_index_is_active(server.xkb_state,
|
|
server.xkb_prefix_mod, XKB_STATE_MODS_EFFECTIVE);
|
|
return (mods_depressed & (tab_prefix == PREFIX_SUPER ? (1u << 6)
|
|
: (1u << 3))) != 0;
|
|
}
|
|
|
|
/* Shared key handling for both the host keyboard and native input devices:
|
|
tab-switch bindings + overlay visibility, with the caller forwarding the
|
|
key to the seat afterwards. `mods_depressed` uses the wl_keyboard modifier
|
|
layout (shift=1<<0, ctrl=1<<2, alt=1<<3). Returns true when the key was
|
|
consumed by a binding and must not be forwarded to the seat. */
|
|
static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
|
uint32_t mods_depressed)
|
|
{
|
|
(void)time;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
if (server.xkb_state)
|
|
xkb_state_update_key(server.xkb_state, key + 8,
|
|
state == WL_KEYBOARD_KEY_STATE_PRESSED
|
|
? XKB_KEY_DOWN : XKB_KEY_UP);
|
|
xkb_keysym_t sym = server.xkb_state
|
|
? xkb_state_key_get_one_sym(server.xkb_state, key + 8)
|
|
: XKB_KEY_NoSymbol;
|
|
/* With no compiled keymap (xkb init failed) fall back to the raw
|
|
evdev keycodes / modifier bits for the hotkey matching below. */
|
|
bool alt = prefix_active(mods_depressed);
|
|
if (alt != host.overlay_visible)
|
|
update_overlay_visible(alt);
|
|
bool ctrl = (mods_depressed & (1u << 2)) != 0;
|
|
char symbuf[32];
|
|
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[kbd] key=%u state=%u sym=%s ctrl=%d alt=%d repeat_key=%u\n",
|
|
key, state, sym ? symbuf : "-",
|
|
(mods_depressed & (1u << 2)) != 0, alt, host.repeat_key);
|
|
/* Confirm-before-quit modal active: every key is swallowed so focused
|
|
clients never see them. Y/Enter quits on the press (the first press
|
|
only -- the exit runs before any repeat could re-fire); N/Escape
|
|
cancels, and repeats just re-cancel the already-cleared state. */
|
|
if (confirm_quit_pending) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
bool yes = server.xkb_state
|
|
? (sym == XKB_KEY_y || sym == XKB_KEY_Y ||
|
|
sym == XKB_KEY_Return || sym == XKB_KEY_KP_Enter)
|
|
: (key == 21 || key == 28);
|
|
bool no = server.xkb_state
|
|
? (sym == XKB_KEY_n || sym == XKB_KEY_N ||
|
|
sym == XKB_KEY_Escape)
|
|
: (key == 49 || key == 1);
|
|
if (yes) {
|
|
kick_log(KICK_LOG_INFO, "kick: quit confirmed\n");
|
|
set_confirm_quit(false);
|
|
do_quit();
|
|
} else if (no) {
|
|
kick_log(KICK_LOG_INFO, "kick: quit cancelled\n");
|
|
set_confirm_quit(false);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/* Session locked: swallow every compositor grab chord so the user can't
|
|
cycle tabs, close windows or open terminals behind the lock screen
|
|
(passive keys still flow to the focused lock surface). The quit chord
|
|
and confirm modal above run first and remain available, as does the VT
|
|
switch below (a matching compositor session action). */
|
|
if (server.session_locked)
|
|
return true;
|
|
/* A focused surface with an active shortcuts inhibitor (game/VM) grabs
|
|
the compositor's chords: swallow the tab/window grabs so they reach
|
|
the client. Quit and VT switch above/below stay live. */
|
|
if (kb_inhibit_focused())
|
|
return true;
|
|
/* Ctrl+Alt+F1..F12 switch virtual terminals in standalone mode. wlroots
|
|
has no built-in handler; the compositor must notice the key and ask the
|
|
session (libseat/logind) to activate another VT. Both the press and
|
|
release are consumed so the focused client never sees the chord. */
|
|
if (server.standalone && server.session &&
|
|
sym >= XKB_KEY_XF86Switch_VT_1 &&
|
|
sym <= XKB_KEY_XF86Switch_VT_12) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
unsigned vt = sym - XKB_KEY_XF86Switch_VT_1 + 1;
|
|
kick_log(KICK_LOG_INFO,
|
|
"kick: vt switch -> %u\n", vt);
|
|
wlr_session_change_vt(server.session, vt);
|
|
}
|
|
return true;
|
|
}
|
|
/* Function keys are not grabbed (prefix+F11 above is the one exception):
|
|
they pass through to the focused client, which may have its own
|
|
bindings (e.g. a browser's F11 fullscreen request, which
|
|
handle_request_fullscreen contains inside kick). Frame dumps are
|
|
available via SIGUSR1 instead. */
|
|
/* Alt+Up / Alt+Down cycle tabs. Grabbed so the focused client never
|
|
sees them. The host sends a single physical press, so repeats are
|
|
synthesized by kbd_repeat_start(); subsequent host presses of an
|
|
already-held key are ignored so a host-sent repeat never cycles twice.
|
|
Plain arrow keys (not Fn+arrow) are used because many laptop firmware
|
|
swallow Alt+Fn+PageUp (the Fn key reports as XF86WakeUp there), which
|
|
makes Alt+PageUp/PageDown unreliable. */
|
|
bool prev, next;
|
|
if (server.xkb_state) {
|
|
prev = sym == XKB_KEY_Up;
|
|
next = sym == XKB_KEY_Down;
|
|
} else {
|
|
prev = key == 103;
|
|
next = key == 108;
|
|
}
|
|
/* Alt+Shift+Up / Alt+Shift+Down move the active tab within the overlay
|
|
list (wrapping at the ends), without switching the active surface.
|
|
Checked before the plain Alt+arrow cycle below so Shift always means
|
|
"reorder, don't switch"; hold to keep moving via kbd_repeat_start.
|
|
Grabbed so the focused client never sees the chord (a terminal would
|
|
otherwise act on Shift+arrow). */
|
|
bool shift = (mods_depressed & (1u << 0)) != 0;
|
|
if (alt && shift && (prev || next)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
int dir = next ? 1 : -1;
|
|
reorder_active_tab(dir);
|
|
kbd_repeat_start(key, dir, REPEAT_REORDER);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
if (alt && (prev || next)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
int dir = next ? 1 : -1;
|
|
cycle_tab(dir);
|
|
kbd_repeat_start(key, dir, REPEAT_CYCLE);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
/* prefix+Tab / prefix+1 walk the MRU window stack one step per press
|
|
(hold to keep walking via REPEAT_MRU): each press previews the next
|
|
window after the shown one, wrapping at the bottom, without touching
|
|
the stack itself -- the shown window moves to the front on prefix
|
|
release. Shift is ignored (no reverse direction): with Shift held
|
|
this still walks forward so no stray Tab/1 leaks to the client
|
|
mid-menu. Alt+1 is the reliable MRU chord on hosts that grab the
|
|
switcher keys: GNOME takes Alt+Tab, Alt+Esc AND Alt+Backtick (the
|
|
metacity-era switch_group default) before a nested compositor ever
|
|
sees them, but leaves prefix+1 free; Alt+Tab is kept for hosts that
|
|
don't grab it. Grabbed so the focused client never sees the chord. */
|
|
{
|
|
bool is_tab = server.xkb_state
|
|
? (sym == XKB_KEY_Tab || sym == XKB_KEY_ISO_Left_Tab)
|
|
: key == 15;
|
|
bool is_one = server.xkb_state
|
|
? (sym == XKB_KEY_1)
|
|
: key == 2;
|
|
if (alt && (is_tab || is_one)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
mru_step();
|
|
kbd_repeat_start(key, 1, REPEAT_MRU);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* Alt+Left / Alt+Right rotate the active tab's windows: Right
|
|
steps forward to the next window, Left steps backward to the previous
|
|
one, wrapping at the ends. Grabbed (consumed) so the focused client
|
|
never sees the chord -- a terminal would otherwise act on Left/Right.
|
|
When the tab has a single window the chord is still consumed as a
|
|
no-op so Alt+arrow never leaks to the client. */
|
|
{
|
|
bool left = server.xkb_state ? (sym == XKB_KEY_Left) : (key == 105);
|
|
bool right = server.xkb_state ? (sym == XKB_KEY_Right) : (key == 106);
|
|
if (alt && !shift && (left || right)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
int dir = right ? 1 : -1;
|
|
cycle_tab_windows(server.active_tab, dir);
|
|
kbd_repeat_start(key, dir, REPEAT_SUBCYCLE);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* Alt+Shift+Left / Alt+Shift+Right move the current window within its
|
|
tab's window list -- the window analogue of Alt+Shift+Up/Down tab
|
|
reordering. Wraps at the ends. Grabbed so terminals never see
|
|
Shift+arrow, and it summons the overlay like every other navigation
|
|
chord (the reordered rows are the visible effect). With two windows
|
|
the move still visibly swaps the two overlay rows, so there is no
|
|
dead zone at n==2. */
|
|
{
|
|
bool mleft = server.xkb_state ? (sym == XKB_KEY_Left) : (key == 105);
|
|
bool mright = server.xkb_state ? (sym == XKB_KEY_Right) : (key == 106);
|
|
if (alt && shift && (mleft || mright)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
update_overlay_visible(true);
|
|
if (host.repeat_key != key) {
|
|
int dir = mright ? 1 : -1;
|
|
struct kick_window *w = tab_current_window(server.active_tab);
|
|
if (w)
|
|
reorder_window(w, dir);
|
|
kbd_repeat_start(key, dir, REPEAT_MEMBER_MOVE);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* Ctrl+Alt+Q quits kick (both the host close and this chord share
|
|
quit_requested): with more than one window open a modal asks before
|
|
anything is torn down. Grabbed so the focused client never sees the
|
|
chord; a latch drops host-synthesized repeats so one hold can't fire
|
|
the request (or the confirm) more than once. */
|
|
{ bool is_q = server.xkb_state
|
|
? (sym == XKB_KEY_q || sym == XKB_KEY_Q)
|
|
: key == 16;
|
|
if (alt && ctrl && is_q) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!quit_key_held) {
|
|
quit_key_held = true;
|
|
quit_requested();
|
|
}
|
|
} else {
|
|
quit_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* prefix+Delete closes the active window (or the whole tab when it is the
|
|
only window). Grabbed so the focused client never sees the key; a latch
|
|
drops host-synthesized repeats so one hold can't fire it per repeat.
|
|
When the whole tab closes, the active surface moves to the right
|
|
neighbor inside close_tab(), so the tab is gone this frame even before
|
|
the client process exits. */
|
|
{ bool is_delete = server.xkb_state
|
|
? (sym == XKB_KEY_Delete)
|
|
: key == 111;
|
|
if (alt && is_delete) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!delete_key_held) {
|
|
delete_key_held = true;
|
|
close_active_window();
|
|
}
|
|
} else {
|
|
delete_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* Alt+Shift+T opens a new terminal tab ($TERMINAL, default alacritty).
|
|
The tab is inserted right after the current one and becomes active when
|
|
it maps. Grabbed so the focused client never sees the chord; a latch
|
|
drops host-synthesized repeats so one hold can't open a terminal per
|
|
repeat. */
|
|
{
|
|
bool is_t = server.xkb_state
|
|
? (sym == XKB_KEY_t || sym == XKB_KEY_T)
|
|
: key == 20;
|
|
if (alt && shift && is_t) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!term_key_held) {
|
|
term_key_held = true;
|
|
if (!cmd_in_path(term_cmd)) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"kick: '%s' not found in PATH; "
|
|
"no terminal opened (set $TERMINAL)\n",
|
|
term_cmd);
|
|
} else {
|
|
add_tab(term_cmd);
|
|
}
|
|
}
|
|
} else {
|
|
term_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* prefix+F11 toggles fullscreen for the focused overlay entry only.
|
|
Nested it fullscreens the entry directly and raises the kick window
|
|
chrome in the host compositor when that isn't up yet (an already
|
|
fullscreen chrome stays put); exiting drops the chrome. Siblings,
|
|
stacked members and other tabs stay maximized. A black active tab
|
|
picks it up at map via host.fullscreen. Standalone has no host
|
|
window, so it flips the focused entry directly. Grabbed so the
|
|
focused client never sees the chord; a latch drops host-synthesized
|
|
repeats so one hold can't flip-flop the toggle. Plain unprefixed
|
|
F11 still passes through below. */
|
|
{ bool is_f11 = server.xkb_state
|
|
? (sym == XKB_KEY_F11)
|
|
: key == 87;
|
|
if (alt && is_f11) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!f11_key_held) {
|
|
f11_key_held = true;
|
|
host_toggle_fullscreen();
|
|
}
|
|
} else {
|
|
f11_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* any other key activity stops the tab-switch repeat */
|
|
kbd_repeat_cancel();
|
|
return false;
|
|
}
|
|
|
|
/* Shared modifier handling for the host keyboard and native devices. */
|
|
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group)
|
|
{
|
|
host.host_mods_depressed = mods_depressed;
|
|
if (server.xkb_state)
|
|
xkb_state_update_mask(server.xkb_state, mods_depressed,
|
|
mods_latched, mods_locked, group, group, group);
|
|
bool alt = prefix_active(mods_depressed);
|
|
if (alt != host.overlay_visible)
|
|
update_overlay_visible(alt);
|
|
if (host.repeat_key && !alt)
|
|
kbd_repeat_cancel();
|
|
}
|
|
|
|
static void host_kbd_modifiers(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
|
uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group)
|
|
{
|
|
(void)data; (void)serial;
|
|
if (synth_input_only && kbd)
|
|
return;
|
|
(void)kbd;
|
|
process_modifiers(mods_depressed, mods_latched, mods_locked, group);
|
|
wlr_keyboard_notify_modifiers(&server.keyboard, mods_depressed,
|
|
mods_latched, mods_locked, group);
|
|
if (im_grab_forward_modifiers(&server.keyboard))
|
|
return;
|
|
wlr_seat_keyboard_notify_modifiers(server.seat, &server.keyboard.modifiers);
|
|
}
|
|
|
|
static void host_kbd_repeat_info(void *data, struct wl_keyboard *kbd,
|
|
int32_t rate, int32_t delay)
|
|
{
|
|
(void)data; (void)kbd;
|
|
wlr_keyboard_set_repeat_info(&server.keyboard, rate, delay);
|
|
}
|
|
|
|
static const struct wl_keyboard_listener host_kbd_listener = {
|
|
.keymap = host_kbd_keymap,
|
|
.enter = host_kbd_enter,
|
|
.leave = host_kbd_leave,
|
|
.key = host_kbd_key,
|
|
.modifiers = host_kbd_modifiers,
|
|
.repeat_info = host_kbd_repeat_info,
|
|
};
|
|
|
|
/* ============ Standalone native input devices ============
|
|
In standalone mode real keyboard/pointer devices come from the backend
|
|
(libinput). Their events are routed through the same hotkey logic and are
|
|
then forwarded into the shared seat keyboard exactly like host input.
|
|
|
|
The keyboard listeners are per-device (one struct standalone_kbdev per
|
|
libinput keyboard): ending a single global slot makes modifier state come
|
|
from whichever device attached last (often an HDMI-CEC or consumer-control
|
|
node), so Shift held on the real keyboard never reached the seat. */
|
|
|
|
static void standalone_kbd_key(struct wl_listener *listener, void *data) {
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, key);
|
|
struct wlr_keyboard_key_event *ev = data;
|
|
uint32_t mods = wlr_keyboard_get_modifiers(kd->keyboard);
|
|
if (!process_key_event(ev->time_msec, ev->keycode, ev->state, mods)) {
|
|
if (im_grab_forward_key(ev->time_msec, ev->keycode, ev->state))
|
|
return;
|
|
wlr_seat_set_keyboard(server.seat, kd->keyboard);
|
|
wlr_seat_keyboard_notify_key(server.seat, ev->time_msec, ev->keycode,
|
|
ev->state);
|
|
}
|
|
}
|
|
|
|
static void standalone_kbd_modifiers(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, modifiers);
|
|
struct wlr_keyboard *kb = kd->keyboard;
|
|
process_modifiers(kb->modifiers.depressed, kb->modifiers.latched,
|
|
kb->modifiers.locked, kb->modifiers.group);
|
|
wlr_seat_set_keyboard(server.seat, kb);
|
|
if (im_grab_forward_modifiers(kb))
|
|
return;
|
|
wlr_seat_keyboard_notify_modifiers(server.seat, &kb->modifiers);
|
|
}
|
|
|
|
/* The device is going away (libinput suspends all devices when the session
|
|
loses its VT). Drop our listeners so wlr_keyboard_finish's assert that
|
|
every event list is empty holds, then free the struct. wlroots emits the
|
|
input device's destroy signal from within wlr_keyboard_finish(), so this
|
|
runs before the assert. */
|
|
static void standalone_kbd_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, destroy);
|
|
wl_list_remove(&kd->destroy.link);
|
|
wl_list_remove(&kd->key.link);
|
|
wl_list_remove(&kd->modifiers.link);
|
|
wl_list_remove(&kd->link);
|
|
free(kd);
|
|
}
|
|
|
|
/* Forward the wlr_cursor's position to the seat as the effect/click point.
|
|
wlroots is the single source of truth for where the pointer is: it clamps
|
|
cursor->x/y against the output layout (wlr_cursor_warp_closest), so the
|
|
visible arrow and the coordinates forwarded here stay in the same space,
|
|
even when a requested --screen-mode mode differs from the geometry the
|
|
output layout derives from the committed state. Never hand-clamp a second
|
|
copy. */
|
|
static void standalone_forward_pointer(uint32_t time) {
|
|
struct wlr_output *output = server.output;
|
|
if (!server.cursor || !output) return;
|
|
double x = server.cursor->x;
|
|
double y = server.cursor->y;
|
|
wlr_output_layout_output_coords(server.output_layout, output, &x, &y);
|
|
host_ptr_motion(time, x, y);
|
|
}
|
|
|
|
/* Shared standalone pointer-motion handling, used by both the libinput device
|
|
listeners and the stdin ctl feed (which has no device, hence NULL). The
|
|
wlr_cursor never moves on its own in wlroots 0.19 -- the device wiring only
|
|
re-emits motion on cursor->events.motion -- so every relative delta must be
|
|
turned into wlr_cursor_move() or the visible pointer stays pinned while the
|
|
seat (and therefore client hover) keeps moving. With a NULL device the move
|
|
falls back to the output layout's closest-point mapping. */
|
|
/* ---------- pointer constraints + relative pointer ---------- */
|
|
|
|
/* Forward a relative pointer delta to clients (zwp_relative_pointer_v1).
|
|
The manager scopes delivery to clients that bound it on the seat. */
|
|
static void send_relative_motion(uint32_t time_msec, double dx, double dy) {
|
|
if (!server.relative_pointer_mgr || !server.seat) return;
|
|
kick_log(KICK_LOG_INFO, "[relative] motion dx=%d dy=%d\n",
|
|
(int)dx, (int)dy);
|
|
wlr_relative_pointer_manager_v1_send_relative_motion(
|
|
server.relative_pointer_mgr, server.seat,
|
|
(uint64_t)time_msec * 1000, dx, dy, dx, dy);
|
|
}
|
|
|
|
/* Find the scene origin (in scene/layout coordinates) of a surface's buffer
|
|
node, so a constraint region in surface-local space can be mapped to the
|
|
output. Surfaces live under per-app scene trees, so the walk is
|
|
recursive. */
|
|
static bool scene_surface_origin_in(struct wlr_scene_tree *tree,
|
|
struct wlr_surface *surface,
|
|
double *ox, double *oy) {
|
|
struct wlr_scene_node *node;
|
|
wl_list_for_each(node, &tree->children, link) {
|
|
if (node->type == WLR_SCENE_NODE_BUFFER) {
|
|
struct wlr_scene_buffer *sb = wlr_scene_buffer_from_node(node);
|
|
struct wlr_scene_surface *ss = wlr_scene_surface_try_from_buffer(sb);
|
|
if (ss && ss->surface == surface) {
|
|
int lx = 0, ly = 0;
|
|
if (!wlr_scene_node_coords(node, &lx, &ly)) return false;
|
|
*ox = (double)lx;
|
|
*oy = (double)ly;
|
|
return true;
|
|
}
|
|
} else if (node->type == WLR_SCENE_NODE_TREE) {
|
|
if (scene_surface_origin_in(wlr_scene_tree_from_node(node),
|
|
surface, ox, oy))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool scene_surface_origin(struct wlr_surface *surface,
|
|
double *ox, double *oy) {
|
|
return scene_surface_origin_in(&server.scene->tree, surface, ox, oy);
|
|
}
|
|
|
|
static void constraint_deactivate(void) {
|
|
if (!server.active_constraint) return;
|
|
wlr_pointer_constraint_v1_send_deactivated(server.active_constraint);
|
|
wl_list_remove(&server.constraint_destroy.link);
|
|
server.active_constraint = NULL;
|
|
}
|
|
|
|
static void handle_constraint_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_server *s = wl_container_of(listener, s, constraint_destroy);
|
|
kick_log(KICK_LOG_DEBUG, "[constraint] destroyed\n");
|
|
/* Detach like constraint_deactivate does: wlroots asserts every
|
|
listener is gone before it frees the constraint. */
|
|
wl_list_remove(&s->constraint_destroy.link);
|
|
s->active_constraint = NULL;
|
|
}
|
|
|
|
static void constraint_activate(struct wlr_surface *surface) {
|
|
if (!server.pointer_constraints || !surface) return;
|
|
struct wlr_pointer_constraint_v1 *c = wlr_pointer_constraints_v1_constraint_for_surface(
|
|
server.pointer_constraints, surface, server.seat);
|
|
if (!c) return;
|
|
server.active_constraint = c;
|
|
if (!scene_surface_origin(surface, &server.constraint_ox, &server.constraint_oy)) {
|
|
server.constraint_ox = 0.0;
|
|
server.constraint_oy = 0.0;
|
|
}
|
|
server.lock_x = server.last_ptr_x;
|
|
server.lock_y = server.last_ptr_y;
|
|
server.constraint_destroy.notify = handle_constraint_destroy;
|
|
wl_signal_add(&c->events.destroy, &server.constraint_destroy);
|
|
wlr_pointer_constraint_v1_send_activated(c);
|
|
if (c->type == WLR_POINTER_CONSTRAINT_V1_LOCKED)
|
|
kick_log(KICK_LOG_INFO, "[constraint] locked on surface\n");
|
|
else
|
|
kick_log(KICK_LOG_INFO, "[constraint] confined on surface\n");
|
|
}
|
|
|
|
/* Focus change is the only place the pointer's surface can change, so it is
|
|
the natural point to swap the active constraint. */
|
|
static void handle_seat_pointer_focus(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_seat_pointer_focus_change_event *ev = data;
|
|
constraint_deactivate();
|
|
if (ev->new_surface)
|
|
constraint_activate(ev->new_surface);
|
|
}
|
|
|
|
/* ===================== Text input + input method (OSK/IME) =================
|
|
Local-only relay between the focused tab's text-input fields
|
|
(zwp_text_input_v3, e.g. toolkit entries) and an in-session input method
|
|
(zwp_input_method_v2, e.g. fcitx5). The seat's keyboard focus is the single
|
|
source of truth: text-input objects get enter/leave as focus moves, the
|
|
focused field's surrounding-text/content-type go to the IME, and the IME's
|
|
preedit/commit strings go back to the field. While the IME holds its
|
|
keyboard grab, keys bypass the seat and are sent to the grab instead (the
|
|
tab-management chords in process_key_event still run first, so the user
|
|
can always cycle away). IME candidate popups render into
|
|
server.input_popup_tree, above the tab trees. */
|
|
|
|
struct kick_text_input {
|
|
struct wlr_text_input_v3 *ti;
|
|
struct wl_listener enable;
|
|
struct wl_listener commit;
|
|
struct wl_listener disable;
|
|
struct wl_listener destroy;
|
|
};
|
|
|
|
struct kick_input_method {
|
|
struct wlr_input_method_v2 *im;
|
|
struct wl_listener commit;
|
|
struct wl_listener new_popup;
|
|
struct wl_listener grab_keyboard;
|
|
struct wl_listener destroy;
|
|
struct wl_list link; /* server.input_methods */
|
|
};
|
|
|
|
struct kick_input_popup {
|
|
struct wlr_input_popup_surface_v2 *popup;
|
|
struct wlr_scene_tree *scene;
|
|
struct wl_listener destroy;
|
|
struct wl_listener commit;
|
|
struct wl_list link; /* server.input_popups */
|
|
};
|
|
|
|
/* The input method currently available (the first bound one; a session runs
|
|
a single IME). Tracked in our own wrapper list: the manager's
|
|
input_methods nodes are the wl_resources' links, not the
|
|
wlr_input_method_v2 structs' own, so container_of on them is garbage. */
|
|
static struct wlr_input_method_v2 *active_input_method(void) {
|
|
if (wl_list_empty(&server.input_methods))
|
|
return NULL;
|
|
struct kick_input_method *kim = wl_container_of(
|
|
server.input_methods.next, kim, link);
|
|
return kim->im;
|
|
}
|
|
|
|
/* The text input entered on the keyboard-focused surface, if any. */
|
|
static struct wlr_text_input_v3 *focused_text_input(void) {
|
|
struct wlr_surface *focused = server.seat->keyboard_state.focused_surface;
|
|
if (!focused || !server.text_input_mgr)
|
|
return NULL;
|
|
struct wlr_text_input_v3 *ti;
|
|
wl_list_for_each(ti, &server.text_input_mgr->text_inputs, link) {
|
|
if (ti->focused_surface == focused)
|
|
return ti;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Forward the focused field's state to the IME. Only the features the field
|
|
actually committed are sent; the change cause + done always complete the
|
|
sequence so the IME never waits on a half-applied state. */
|
|
static void relay_text_input_to_im(struct wlr_text_input_v3 *ti) {
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
if (!im)
|
|
return;
|
|
if ((ti->current.features &
|
|
WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT) &&
|
|
ti->current.surrounding.text)
|
|
wlr_input_method_v2_send_surrounding_text(im,
|
|
ti->current.surrounding.text, ti->current.surrounding.cursor,
|
|
ti->current.surrounding.anchor);
|
|
if (ti->current.features & WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE)
|
|
wlr_input_method_v2_send_content_type(im,
|
|
ti->current.content_type.hint, ti->current.content_type.purpose);
|
|
wlr_input_method_v2_send_text_change_cause(im,
|
|
ti->current.text_change_cause);
|
|
wlr_input_method_v2_send_done(im);
|
|
kick_log(KICK_LOG_DEBUG, "[im] field -> IME (done)\n");
|
|
}
|
|
|
|
/* Forward the IME's pending strings to the focused field. */
|
|
static void relay_im_to_text_input(struct wlr_input_method_v2 *im) {
|
|
struct wlr_text_input_v3 *ti = focused_text_input();
|
|
if (!ti)
|
|
return;
|
|
if (im->current.preedit.text)
|
|
wlr_text_input_v3_send_preedit_string(ti, im->current.preedit.text,
|
|
im->current.preedit.cursor_begin, im->current.preedit.cursor_end);
|
|
if (im->current.commit_text)
|
|
wlr_text_input_v3_send_commit_string(ti, im->current.commit_text);
|
|
if (im->current.delete.before_length || im->current.delete.after_length)
|
|
wlr_text_input_v3_send_delete_surrounding_text(ti,
|
|
im->current.delete.before_length, im->current.delete.after_length);
|
|
wlr_text_input_v3_send_done(ti);
|
|
kick_log(KICK_LOG_DEBUG, "[im] IME -> field (done)\n");
|
|
}
|
|
|
|
/* Tell the IME where the focused field's cursor sits (output pixels), so its
|
|
candidate popup lands next to the caret. The cursor rectangle is
|
|
surface-local; scene_surface_origin() maps the surface into the scene. */
|
|
static void im_send_cursor_rectangles(void) {
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
struct wlr_text_input_v3 *ti = focused_text_input();
|
|
if (!im || !ti || wl_list_empty(&im->popup_surfaces))
|
|
return;
|
|
if (!(ti->current.features & WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE))
|
|
return;
|
|
struct wlr_box rect = ti->current.cursor_rectangle;
|
|
double ox, oy;
|
|
if (ti->focused_surface && scene_surface_origin(ti->focused_surface,
|
|
&ox, &oy)) {
|
|
rect.x += (int)ox;
|
|
rect.y += (int)oy;
|
|
}
|
|
struct wlr_input_popup_surface_v2 *popup;
|
|
wl_list_for_each(popup, &im->popup_surfaces, link)
|
|
wlr_input_popup_surface_v2_send_text_input_rectangle(popup, &rect);
|
|
}
|
|
|
|
static void handle_text_input_enable(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_text_input *kti = wl_container_of(listener, kti, enable);
|
|
struct wlr_text_input_v3 *ti = data;
|
|
(void)ti;
|
|
kick_log(KICK_LOG_DEBUG, "[im] text input enabled\n");
|
|
if (kti->ti == focused_text_input()) {
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
if (im)
|
|
wlr_input_method_v2_send_activate(im);
|
|
relay_text_input_to_im(kti->ti);
|
|
im_send_cursor_rectangles();
|
|
}
|
|
}
|
|
|
|
static void handle_text_input_commit(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_text_input *kti = wl_container_of(listener, kti, commit);
|
|
struct wlr_text_input_v3 *ti = data;
|
|
(void)ti;
|
|
if (kti->ti == focused_text_input()) {
|
|
relay_text_input_to_im(kti->ti);
|
|
im_send_cursor_rectangles();
|
|
}
|
|
}
|
|
|
|
static void handle_text_input_disable(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_text_input *kti = wl_container_of(listener, kti, disable);
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[im] text input disabled\n");
|
|
if (kti->ti == focused_text_input()) {
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
if (im)
|
|
wlr_input_method_v2_send_deactivate(im);
|
|
}
|
|
}
|
|
|
|
static void handle_text_input_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_text_input *kti = wl_container_of(listener, kti, destroy);
|
|
(void)data;
|
|
wl_list_remove(&kti->enable.link);
|
|
wl_list_remove(&kti->commit.link);
|
|
wl_list_remove(&kti->disable.link);
|
|
wl_list_remove(&kti->destroy.link);
|
|
free(kti);
|
|
}
|
|
|
|
static void handle_text_input_new(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_text_input_v3 *ti = data;
|
|
struct kick_text_input *kti = calloc(1, sizeof(*kti));
|
|
if (!kti)
|
|
return;
|
|
kti->ti = ti;
|
|
kti->enable.notify = handle_text_input_enable;
|
|
wl_signal_add(&ti->events.enable, &kti->enable);
|
|
kti->commit.notify = handle_text_input_commit;
|
|
wl_signal_add(&ti->events.commit, &kti->commit);
|
|
kti->disable.notify = handle_text_input_disable;
|
|
wl_signal_add(&ti->events.disable, &kti->disable);
|
|
kti->destroy.notify = handle_text_input_destroy;
|
|
wl_signal_add(&ti->events.destroy, &kti->destroy);
|
|
/* The field may belong to the already-focused surface (bound late); enter
|
|
it right away so it can enable without waiting for a focus switch. */
|
|
struct wlr_surface *focused = server.seat->keyboard_state.focused_surface;
|
|
if (focused && ti->focused_surface == NULL &&
|
|
wl_resource_get_client(ti->resource) ==
|
|
wl_resource_get_client(focused->resource))
|
|
wlr_text_input_v3_send_enter(ti, focused);
|
|
}
|
|
|
|
static void handle_input_method_commit(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_method *kim = wl_container_of(listener, kim, commit);
|
|
struct wlr_input_method_v2 *im = data;
|
|
(void)im;
|
|
kick_log(KICK_LOG_DEBUG, "[im] IME commit\n");
|
|
relay_im_to_text_input(kim->im);
|
|
}
|
|
|
|
static void handle_input_popup_commit(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_popup *pop = wl_container_of(listener, pop, commit);
|
|
(void)data;
|
|
im_send_cursor_rectangles();
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_input_popup_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_popup *pop = wl_container_of(listener, pop, destroy);
|
|
(void)data;
|
|
wl_list_remove(&pop->destroy.link);
|
|
wl_list_remove(&pop->commit.link);
|
|
wl_list_remove(&pop->link);
|
|
if (pop->scene)
|
|
wlr_scene_node_destroy(&pop->scene->node);
|
|
free(pop);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void handle_input_method_new_popup(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_method *kim = wl_container_of(listener, kim, new_popup);
|
|
(void)kim;
|
|
struct wlr_input_popup_surface_v2 *popup = data;
|
|
struct kick_input_popup *pop = calloc(1, sizeof(*pop));
|
|
if (!pop)
|
|
return;
|
|
pop->popup = popup;
|
|
/* Plain surface with possible subsurfaces: a subsurface tree renders the
|
|
buffer and its children without any xdg positioning. */
|
|
pop->scene = wlr_scene_subsurface_tree_create(server.input_popup_tree,
|
|
popup->surface);
|
|
pop->destroy.notify = handle_input_popup_destroy;
|
|
wl_signal_add(&popup->events.destroy, &pop->destroy);
|
|
pop->commit.notify = handle_input_popup_commit;
|
|
wl_signal_add(&popup->surface->events.commit, &pop->commit);
|
|
wl_list_insert(&server.input_popups, &pop->link);
|
|
im_send_cursor_rectangles();
|
|
kick_request_redraw();
|
|
kick_log(KICK_LOG_INFO, "[im] IME popup opened\n");
|
|
}
|
|
|
|
static void handle_input_method_grab_keyboard(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_method *kim = wl_container_of(listener, kim,
|
|
grab_keyboard);
|
|
(void)kim;
|
|
struct wlr_input_method_keyboard_grab_v2 *grab = data;
|
|
/* Give the IME our keymap/repeat info so it interprets grabbed keys. */
|
|
struct wlr_keyboard *kb = wlr_seat_get_keyboard(server.seat);
|
|
if (!kb)
|
|
kb = &server.keyboard;
|
|
wlr_input_method_keyboard_grab_v2_set_keyboard(grab, kb);
|
|
kick_log(KICK_LOG_INFO, "[im] IME grabbed keyboard\n");
|
|
}
|
|
|
|
static void handle_input_method_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_input_method *kim = wl_container_of(listener, kim, destroy);
|
|
(void)data;
|
|
wl_list_remove(&kim->commit.link);
|
|
wl_list_remove(&kim->new_popup.link);
|
|
wl_list_remove(&kim->grab_keyboard.link);
|
|
wl_list_remove(&kim->destroy.link);
|
|
wl_list_remove(&kim->link);
|
|
free(kim);
|
|
kick_log(KICK_LOG_INFO, "[im] input method gone\n");
|
|
}
|
|
|
|
static void handle_input_method_new(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_input_method_v2 *im = data;
|
|
struct kick_input_method *kim = calloc(1, sizeof(*kim));
|
|
if (!kim)
|
|
return;
|
|
kim->im = im;
|
|
kim->commit.notify = handle_input_method_commit;
|
|
wl_signal_add(&im->events.commit, &kim->commit);
|
|
kim->new_popup.notify = handle_input_method_new_popup;
|
|
wl_signal_add(&im->events.new_popup_surface, &kim->new_popup);
|
|
kim->grab_keyboard.notify = handle_input_method_grab_keyboard;
|
|
wl_signal_add(&im->events.grab_keyboard, &kim->grab_keyboard);
|
|
kim->destroy.notify = handle_input_method_destroy;
|
|
wl_signal_add(&im->events.destroy, &kim->destroy);
|
|
wl_list_insert(&server.input_methods, &kim->link);
|
|
kick_log(KICK_LOG_INFO, "[im] input method ready\n");
|
|
/* An IME binding late still learns the focused field immediately. */
|
|
struct wlr_text_input_v3 *ti = focused_text_input();
|
|
if (ti && ti->current_enabled) {
|
|
wlr_input_method_v2_send_activate(im);
|
|
relay_text_input_to_im(ti);
|
|
} else {
|
|
wlr_input_method_v2_send_deactivate(im);
|
|
}
|
|
}
|
|
|
|
/* Keyboard focus moved: leave the old surface's fields, enter the new
|
|
surface's same-client fields, and (de)activate the IME. This single hook
|
|
covers every focus path (tab switches, maps, lock, layer focus). */
|
|
static void handle_seat_keyboard_focus(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_seat_keyboard_focus_change_event *ev = data;
|
|
if (server.text_input_mgr) {
|
|
struct wlr_text_input_v3 *ti;
|
|
wl_list_for_each(ti, &server.text_input_mgr->text_inputs, link) {
|
|
if (ev->old_surface && ti->focused_surface == ev->old_surface)
|
|
wlr_text_input_v3_send_leave(ti);
|
|
}
|
|
if (ev->new_surface) {
|
|
wl_list_for_each(ti, &server.text_input_mgr->text_inputs, link) {
|
|
if (ti->focused_surface == NULL &&
|
|
wl_resource_get_client(ti->resource) ==
|
|
wl_resource_get_client(ev->new_surface->resource))
|
|
wlr_text_input_v3_send_enter(ti, ev->new_surface);
|
|
}
|
|
}
|
|
}
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
if (im) {
|
|
struct wlr_text_input_v3 *ti = focused_text_input();
|
|
if (ti && ti->current_enabled) {
|
|
wlr_input_method_v2_send_activate(im);
|
|
relay_text_input_to_im(ti);
|
|
im_send_cursor_rectangles();
|
|
} else {
|
|
wlr_input_method_v2_send_deactivate(im);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The IME's keyboard grab, when held, receives keys/modifiers instead of the
|
|
seat's focused client. */
|
|
static struct wlr_input_method_keyboard_grab_v2 *im_grab(void) {
|
|
struct wlr_input_method_v2 *im = active_input_method();
|
|
return im ? im->keyboard_grab : NULL;
|
|
}
|
|
|
|
static bool im_grab_forward_key(uint32_t time, uint32_t key, uint32_t state) {
|
|
struct wlr_input_method_keyboard_grab_v2 *grab = im_grab();
|
|
if (!grab)
|
|
return false;
|
|
wlr_input_method_keyboard_grab_v2_send_key(grab, time, key, state);
|
|
return true;
|
|
}
|
|
|
|
static bool im_grab_forward_modifiers(struct wlr_keyboard *kb) {
|
|
struct wlr_input_method_keyboard_grab_v2 *grab = im_grab();
|
|
if (!grab || !kb)
|
|
return false;
|
|
wlr_input_method_keyboard_grab_v2_send_modifiers(grab, &kb->modifiers);
|
|
return true;
|
|
}
|
|
|
|
/* ===================== xdg-dialog (modal toplevels) =================
|
|
A modal dialog demands attention: when it maps (or is set modal while
|
|
mapped) its tab is shown and it takes window focus, even under the
|
|
no-steal rule and even with --tab-groups off (where it still owns its
|
|
own tab, so it stays individually reachable). Placement itself is
|
|
untouched: ancestry grouping stacks it with --tab-groups on exactly as
|
|
before. Session lock still wins (set_active_tab/window_enter refuse
|
|
while locked). */
|
|
|
|
/* Show the modal's tab (unless already shown) and focus the dialog. */
|
|
static void modal_activate(struct kick_window *w) {
|
|
if (!w || !w->mapped || !w->modal)
|
|
return;
|
|
if (w->tab != server.active_tab)
|
|
set_active_tab(w->tab);
|
|
window_enter(w);
|
|
kick_log(KICK_LOG_INFO, "[dialog] modal app=%d\n", tab_index(w->tab));
|
|
}
|
|
|
|
static void dialog_unwire(struct kick_window *w) {
|
|
if (!w || !w->dialog_wired)
|
|
return;
|
|
wl_list_remove(&w->dialog_set_modal.link);
|
|
wl_list_remove(&w->dialog_destroy.link);
|
|
w->dialog_wired = false;
|
|
w->dialog = NULL;
|
|
w->modal = false;
|
|
}
|
|
|
|
static void handle_dialog_set_modal(struct wl_listener *listener,
|
|
void *data) {
|
|
struct kick_window *w = wl_container_of(listener, w, dialog_set_modal);
|
|
(void)data; /* wlroots emits NULL; the object lives on the window. */
|
|
if (!w->dialog)
|
|
return;
|
|
w->modal = w->dialog->modal;
|
|
kick_log(KICK_LOG_DEBUG, "[dialog] modal=%d app=%d\n",
|
|
w->modal, tab_index(w->tab));
|
|
if (w->modal && w->mapped)
|
|
modal_activate(w);
|
|
}
|
|
|
|
static void handle_dialog_destroy(struct wl_listener *listener, void *data) {
|
|
struct kick_window *w = wl_container_of(listener, w, dialog_destroy);
|
|
(void)data;
|
|
dialog_unwire(w);
|
|
}
|
|
|
|
/* Mirror an xdg_dialog_v1 object onto its window. The object may appear
|
|
before claim (same commit as the toplevel), between claim and map, or
|
|
long after mapping -- so claim, manager-new-dialog and map all funnel
|
|
here. Re-wiring replaces a dead object when the client makes a second
|
|
one for the same toplevel. */
|
|
static void dialog_wire(struct kick_window *w,
|
|
struct wlr_xdg_dialog_v1 *dialog) {
|
|
if (!w || !dialog)
|
|
return;
|
|
dialog_unwire(w);
|
|
w->dialog = dialog;
|
|
w->modal = dialog->modal;
|
|
w->dialog_set_modal.notify = handle_dialog_set_modal;
|
|
wl_signal_add(&dialog->events.set_modal, &w->dialog_set_modal);
|
|
w->dialog_destroy.notify = handle_dialog_destroy;
|
|
wl_signal_add(&dialog->events.destroy, &w->dialog_destroy);
|
|
w->dialog_wired = true;
|
|
kick_log(KICK_LOG_DEBUG, "[dialog] wired modal=%d app=%d\n",
|
|
w->modal, tab_index(w->tab));
|
|
}
|
|
|
|
static void handle_dialog_new(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_dialog_v1 *dialog = data;
|
|
struct kick_window *w = window_for_toplevel(dialog->xdg_toplevel);
|
|
if (!w)
|
|
return;
|
|
dialog_wire(w, dialog);
|
|
if (w->modal && w->mapped)
|
|
modal_activate(w);
|
|
}
|
|
|
|
/* Window decorations (zxdg_decoration_manager_v1): every tab is maximized
|
|
and kick draws no frames of its own, so clients always stay client-side
|
|
(their headerbars keep working). Later server-side requests are simply
|
|
never honored: wlroots only changes mode via set_mode, which nothing
|
|
here calls again. Pinning happens at map (or at creation for late
|
|
binds): set_mode asserts on a never-committed surface, and the normal
|
|
order is decoration-before-first-commit. */
|
|
static void decoration_pin_client_side(struct wlr_xdg_toplevel *toplevel) {
|
|
if (!toplevel || !server.decoration_mgr)
|
|
return;
|
|
struct wlr_xdg_toplevel_decoration_v1 *deco;
|
|
wl_list_for_each(deco, &server.decoration_mgr->decorations, link) {
|
|
if (deco->toplevel == toplevel &&
|
|
deco->current.mode !=
|
|
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE) {
|
|
wlr_xdg_toplevel_decoration_v1_set_mode(
|
|
deco, WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
|
|
kick_log(KICK_LOG_INFO, "[decoration] client-side\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void handle_decoration_new(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_toplevel_decoration_v1 *deco = data;
|
|
if (deco->toplevel->base->initialized)
|
|
decoration_pin_client_side(deco->toplevel);
|
|
}
|
|
|
|
/* Clamp an absolute pointer position to whatever the active constraint
|
|
allows, in place. LOCKED pins to the lock position; CONFINED keeps the
|
|
pointer inside the constraint region (empty region = whole surface). */
|
|
static void constraint_filter_abs(double *x, double *y) {
|
|
struct wlr_pointer_constraint_v1 *c = server.active_constraint;
|
|
if (!c) return;
|
|
struct wlr_surface *surface = c->surface;
|
|
if (c->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) {
|
|
*x = server.lock_x;
|
|
*y = server.lock_y;
|
|
return;
|
|
}
|
|
int w = surface->current.width;
|
|
int h = surface->current.height;
|
|
if (w <= 0 || h <= 0) return;
|
|
int x1 = 0, y1 = 0, x2 = w, y2 = h;
|
|
if (pixman_region32_not_empty(&c->region)) {
|
|
pixman_box32_t *ext = pixman_region32_extents(&c->region);
|
|
x1 = ext->x1;
|
|
y1 = ext->y1;
|
|
x2 = ext->x2;
|
|
y2 = ext->y2;
|
|
if (x2 > w) x2 = w;
|
|
if (y2 > h) y2 = h;
|
|
}
|
|
if (x1 >= x2) x1 = x2 - 1;
|
|
if (y1 >= y2) y1 = y2 - 1;
|
|
double lx = *x - server.constraint_ox;
|
|
double ly = *y - server.constraint_oy;
|
|
if (lx < x1) lx = x1;
|
|
else if (lx >= x2) lx = x2 - 1;
|
|
if (ly < y1) ly = y1;
|
|
else if (ly >= y2) ly = y2 - 1;
|
|
*x = server.constraint_ox + lx;
|
|
*y = server.constraint_oy + ly;
|
|
}
|
|
|
|
static void native_ptr_motion(uint32_t time, struct wlr_input_device *dev,
|
|
double dx, double dy) {
|
|
if (!server.cursor) return;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
send_relative_motion(time, dx, dy);
|
|
wlr_cursor_move(server.cursor, dev, dx, dy);
|
|
/* Keep the visible arrow inside the active constraint's region (or
|
|
pinned, when locked). wlr_cursor is the source of truth for the
|
|
position, so clamp it rather than forwarding a second copy. */
|
|
if (server.active_constraint) {
|
|
double x = server.cursor->x, y = server.cursor->y;
|
|
constraint_filter_abs(&x, &y);
|
|
/* Re-warp through wlroots so the output cursor (the rendered arrow)
|
|
follows: writing cursor->x/y directly would leave it behind. */
|
|
if (x != server.cursor->x || y != server.cursor->y)
|
|
wlr_cursor_warp(server.cursor, NULL, x, y);
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] pos=%d,%d\n",
|
|
(int)server.cursor->x, (int)server.cursor->y);
|
|
standalone_forward_pointer(time);
|
|
}
|
|
|
|
static void native_ptr_abs(uint32_t time, struct wlr_input_device *dev,
|
|
double x, double y) {
|
|
if (!server.cursor) return;
|
|
wlr_cursor_warp_absolute(server.cursor, dev, x, y);
|
|
/* Same clamp as the relative path: an absolute tablet (or a virtual
|
|
pointer) must not escape a lock/confinement the game requested. */
|
|
if (server.active_constraint) {
|
|
double cx = server.cursor->x, cy = server.cursor->y;
|
|
constraint_filter_abs(&cx, &cy);
|
|
if (cx != server.cursor->x || cy != server.cursor->y)
|
|
wlr_cursor_warp(server.cursor, NULL, cx, cy);
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] pos=%d,%d\n",
|
|
(int)server.cursor->x, (int)server.cursor->y);
|
|
standalone_forward_pointer(time);
|
|
}
|
|
|
|
/* Native touchscreen events: warp the (invisible) cursor to the normalized
|
|
absolute position so the rest of the pipeline -- layout coords, constraint
|
|
clamping -- matches pointer semantics exactly, then deliver through the
|
|
shared seat path. */
|
|
static void standalone_touch_deliver_from(struct wlr_touch *touch,
|
|
uint32_t time_msec, int32_t id, int action, double x, double y) {
|
|
if (!server.cursor) return;
|
|
wlr_cursor_warp_absolute(server.cursor, &touch->base, x, y);
|
|
double ox = server.cursor->x, oy = server.cursor->y;
|
|
if (server.output)
|
|
wlr_output_layout_output_coords(server.output_layout,
|
|
server.output, &ox, &oy);
|
|
seat_touch_deliver(time_msec, id, action, ox, oy);
|
|
}
|
|
|
|
static void standalone_touch_down_event(struct wl_listener *listener,
|
|
void *data) {
|
|
struct standalone_touchdev *td =
|
|
wl_container_of(listener, td, down);
|
|
struct wlr_touch_down_event *ev = data;
|
|
standalone_touch_deliver_from(td->touch, ev->time_msec, ev->touch_id,
|
|
KICK_TOUCH_DOWN, ev->x, ev->y);
|
|
}
|
|
|
|
static void standalone_touch_motion_event(struct wl_listener *listener,
|
|
void *data) {
|
|
struct standalone_touchdev *td =
|
|
wl_container_of(listener, td, motion);
|
|
struct wlr_touch_motion_event *ev = data;
|
|
standalone_touch_deliver_from(td->touch, ev->time_msec, ev->touch_id,
|
|
KICK_TOUCH_MOTION, ev->x, ev->y);
|
|
}
|
|
|
|
static void standalone_touch_up_event(struct wl_listener *listener,
|
|
void *data) {
|
|
struct standalone_touchdev *td = wl_container_of(listener, td, up);
|
|
struct wlr_touch_up_event *ev = data;
|
|
seat_touch_deliver(ev->time_msec, ev->touch_id, KICK_TOUCH_UP, 0, 0);
|
|
}
|
|
|
|
static void standalone_touch_destroy(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct standalone_touchdev *td = wl_container_of(listener, td, destroy);
|
|
wl_list_remove(&td->link);
|
|
wl_list_remove(&td->down.link);
|
|
wl_list_remove(&td->motion.link);
|
|
wl_list_remove(&td->up.link);
|
|
wl_list_remove(&td->destroy.link);
|
|
free(td);
|
|
}
|
|
|
|
static void standalone_ptr_motion_event(struct wl_listener *listener, void *data) {
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, motion);
|
|
struct wlr_pointer_motion_event *ev = data;
|
|
native_ptr_motion(ev->time_msec,
|
|
(struct wlr_input_device *)&pd->pointer->base,
|
|
ev->delta_x, ev->delta_y);
|
|
}
|
|
|
|
static void standalone_ptr_abs_event(struct wl_listener *listener, void *data) {
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, motion_absolute);
|
|
struct wlr_pointer_motion_absolute_event *ev = data;
|
|
native_ptr_abs(ev->time_msec,
|
|
(struct wlr_input_device *)&pd->pointer->base,
|
|
ev->x, ev->y);
|
|
}
|
|
|
|
/* ===================== Pointer gestures =====================
|
|
Touchpad swipe/pinch/hold fan out to gesture clients holding seat
|
|
pointer focus (wlroots routes by focus; with no focused surface the
|
|
sends are no-ops). Both sources -- libinput devices in standalone
|
|
mode and the stdin test feed in any mode -- drive these shared
|
|
forwarders. Nested mode has no host gesture source (the host only
|
|
offers wl_pointer), so gestures there come from the feed alone. */
|
|
|
|
static void gesture_activity(void) {
|
|
if (server.idle_notifier && server.seat)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier,
|
|
server.seat);
|
|
}
|
|
|
|
static void gesture_swipe_begin(uint32_t time, uint32_t fingers) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_swipe_begin(server.gestures, server.seat,
|
|
time, fingers);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] swipe begin fingers=%u\n", fingers);
|
|
}
|
|
|
|
static void gesture_swipe_update(uint32_t time, uint32_t fingers,
|
|
double dx, double dy) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_swipe_update(server.gestures, server.seat,
|
|
time, dx, dy);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] swipe update fingers=%u dx=%.1f dy=%.1f\n",
|
|
fingers, dx, dy);
|
|
}
|
|
|
|
static void gesture_swipe_end(uint32_t time, bool cancelled) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_swipe_end(server.gestures, server.seat,
|
|
time, cancelled);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] swipe %s\n",
|
|
cancelled ? "cancelled" : "end");
|
|
}
|
|
|
|
static void gesture_pinch_begin(uint32_t time, uint32_t fingers) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_pinch_begin(server.gestures, server.seat,
|
|
time, fingers);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] pinch begin fingers=%u\n", fingers);
|
|
}
|
|
|
|
static void gesture_pinch_update(uint32_t time, uint32_t fingers, double dx,
|
|
double dy, double scale, double rotation) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_pinch_update(server.gestures, server.seat,
|
|
time, dx, dy,
|
|
scale, rotation);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] pinch update fingers=%u scale=%.2f rot=%.1f\n",
|
|
fingers, scale, rotation);
|
|
}
|
|
|
|
static void gesture_pinch_end(uint32_t time, bool cancelled) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_pinch_end(server.gestures, server.seat,
|
|
time, cancelled);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] pinch %s\n",
|
|
cancelled ? "cancelled" : "end");
|
|
}
|
|
|
|
static void gesture_hold_begin(uint32_t time, uint32_t fingers) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_hold_begin(server.gestures, server.seat,
|
|
time, fingers);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] hold begin fingers=%u\n", fingers);
|
|
}
|
|
|
|
static void gesture_hold_end(uint32_t time, bool cancelled) {
|
|
if (!server.gestures || !server.seat) return;
|
|
gesture_activity();
|
|
wlr_pointer_gestures_v1_send_hold_end(server.gestures, server.seat,
|
|
time, cancelled);
|
|
kick_log(KICK_LOG_DEBUG, "[gesture] hold %s\n",
|
|
cancelled ? "cancelled" : "end");
|
|
}
|
|
|
|
static void standalone_ptr_swipe_begin(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_swipe_begin_event *ev = data;
|
|
gesture_swipe_begin(ev->time_msec, ev->fingers);
|
|
}
|
|
|
|
static void standalone_ptr_swipe_update(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_swipe_update_event *ev = data;
|
|
gesture_swipe_update(ev->time_msec, ev->fingers, ev->dx, ev->dy);
|
|
}
|
|
|
|
static void standalone_ptr_swipe_end(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_swipe_end_event *ev = data;
|
|
gesture_swipe_end(ev->time_msec, ev->cancelled);
|
|
}
|
|
|
|
static void standalone_ptr_pinch_begin(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_pinch_begin_event *ev = data;
|
|
gesture_pinch_begin(ev->time_msec, ev->fingers);
|
|
}
|
|
|
|
static void standalone_ptr_pinch_update(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_pinch_update_event *ev = data;
|
|
gesture_pinch_update(ev->time_msec, ev->fingers, ev->dx, ev->dy,
|
|
ev->scale, ev->rotation);
|
|
}
|
|
|
|
static void standalone_ptr_pinch_end(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_pinch_end_event *ev = data;
|
|
gesture_pinch_end(ev->time_msec, ev->cancelled);
|
|
}
|
|
|
|
static void standalone_ptr_hold_begin(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_hold_begin_event *ev = data;
|
|
gesture_hold_begin(ev->time_msec, ev->fingers);
|
|
}
|
|
|
|
static void standalone_ptr_hold_end(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_hold_end_event *ev = data;
|
|
gesture_hold_end(ev->time_msec, ev->cancelled);
|
|
}
|
|
|
|
static void standalone_ptr_button_event(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_button_event *ev = data;
|
|
if (!server.seat) return;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
/* Clicks while the overlay is up are swallowed: hover already previewed,
|
|
and the client beneath must not see a press/release pair. */
|
|
if (overlay_eats_pointer()) {
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
wlr_seat_pointer_notify_button(server.seat, ev->time_msec, ev->button,
|
|
ev->state);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void standalone_ptr_axis_event(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_axis_event *ev = data;
|
|
if (!server.seat) return;
|
|
/* libinput scales discrete steps by WLR_POINTER_AXIS_DISCRETE_STEP
|
|
(±120 per notch; the seat divides it back off for clients), while
|
|
overlay_handle_axis speaks Wayland wire units (±1). Normalize only
|
|
for the overlay call -- the client forward below must stay raw.
|
|
Sub-notch fractions truncate to 0 and fall through to the smooth
|
|
accumulator inside the helper. */
|
|
if (overlay_handle_axis(ev->orientation, ev->delta,
|
|
ev->delta_discrete / WLR_POINTER_AXIS_DISCRETE_STEP))
|
|
return;
|
|
wlr_seat_pointer_notify_axis(server.seat, ev->time_msec, ev->orientation,
|
|
ev->delta, ev->delta_discrete, ev->source,
|
|
WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void standalone_ptr_frame_event(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
}
|
|
|
|
/* The device is going away (libinput suspends all devices when the session
|
|
loses its VT). Drop our listeners so wlr_pointer_finish's assert that
|
|
every event list is empty holds, then free the struct. */
|
|
static void standalone_ptr_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, destroy);
|
|
wl_list_remove(&pd->destroy.link);
|
|
wl_list_remove(&pd->motion.link);
|
|
wl_list_remove(&pd->motion_absolute.link);
|
|
wl_list_remove(&pd->button.link);
|
|
wl_list_remove(&pd->axis.link);
|
|
wl_list_remove(&pd->frame.link);
|
|
wl_list_remove(&pd->swipe_begin.link);
|
|
wl_list_remove(&pd->swipe_update.link);
|
|
wl_list_remove(&pd->swipe_end.link);
|
|
wl_list_remove(&pd->pinch_begin.link);
|
|
wl_list_remove(&pd->pinch_update.link);
|
|
wl_list_remove(&pd->pinch_end.link);
|
|
wl_list_remove(&pd->hold_begin.link);
|
|
wl_list_remove(&pd->hold_end.link);
|
|
wl_list_remove(&pd->link);
|
|
free(pd);
|
|
}
|
|
|
|
static void handle_new_input(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_input_device *device = data;
|
|
|
|
switch (device->type) {
|
|
case WLR_INPUT_DEVICE_KEYBOARD: {
|
|
struct wlr_keyboard *kb = wlr_keyboard_from_input_device(device);
|
|
/* Without a keymap the device never emits modifier events; give it
|
|
ours so both the hotkeys and the seat work. */
|
|
if (server.xkb_keymap)
|
|
wlr_keyboard_set_keymap(kb, server.xkb_keymap);
|
|
struct standalone_kbdev *kd = calloc(1, sizeof(*kd));
|
|
if (!kd) break;
|
|
kd->keyboard = kb;
|
|
kd->key.notify = standalone_kbd_key;
|
|
wl_signal_add(&kb->events.key, &kd->key);
|
|
kd->modifiers.notify = standalone_kbd_modifiers;
|
|
wl_signal_add(&kb->events.modifiers, &kd->modifiers);
|
|
kd->destroy.notify = standalone_kbd_destroy;
|
|
wl_signal_add(&kb->base.events.destroy, &kd->destroy);
|
|
wl_list_insert(server.kbdevs.prev, &kd->link);
|
|
kick_log(KICK_LOG_INFO, "standalone: keyboard '%s'\n", device->name);
|
|
break;
|
|
}
|
|
case WLR_INPUT_DEVICE_POINTER: {
|
|
struct wlr_pointer *ptr = wlr_pointer_from_input_device(device);
|
|
if (server.cursor)
|
|
wlr_cursor_attach_input_device(server.cursor, device);
|
|
struct standalone_ptrdev *pd = calloc(1, sizeof(*pd));
|
|
if (!pd) break;
|
|
pd->pointer = ptr;
|
|
pd->motion.notify = standalone_ptr_motion_event;
|
|
wl_signal_add(&ptr->events.motion, &pd->motion);
|
|
pd->motion_absolute.notify = standalone_ptr_abs_event;
|
|
wl_signal_add(&ptr->events.motion_absolute, &pd->motion_absolute);
|
|
pd->button.notify = standalone_ptr_button_event;
|
|
wl_signal_add(&ptr->events.button, &pd->button);
|
|
pd->axis.notify = standalone_ptr_axis_event;
|
|
wl_signal_add(&ptr->events.axis, &pd->axis);
|
|
pd->frame.notify = standalone_ptr_frame_event;
|
|
wl_signal_add(&ptr->events.frame, &pd->frame);
|
|
pd->swipe_begin.notify = standalone_ptr_swipe_begin;
|
|
wl_signal_add(&ptr->events.swipe_begin, &pd->swipe_begin);
|
|
pd->swipe_update.notify = standalone_ptr_swipe_update;
|
|
wl_signal_add(&ptr->events.swipe_update, &pd->swipe_update);
|
|
pd->swipe_end.notify = standalone_ptr_swipe_end;
|
|
wl_signal_add(&ptr->events.swipe_end, &pd->swipe_end);
|
|
pd->pinch_begin.notify = standalone_ptr_pinch_begin;
|
|
wl_signal_add(&ptr->events.pinch_begin, &pd->pinch_begin);
|
|
pd->pinch_update.notify = standalone_ptr_pinch_update;
|
|
wl_signal_add(&ptr->events.pinch_update, &pd->pinch_update);
|
|
pd->pinch_end.notify = standalone_ptr_pinch_end;
|
|
wl_signal_add(&ptr->events.pinch_end, &pd->pinch_end);
|
|
pd->hold_begin.notify = standalone_ptr_hold_begin;
|
|
wl_signal_add(&ptr->events.hold_begin, &pd->hold_begin);
|
|
pd->hold_end.notify = standalone_ptr_hold_end;
|
|
wl_signal_add(&ptr->events.hold_end, &pd->hold_end);
|
|
pd->destroy.notify = standalone_ptr_destroy;
|
|
wl_signal_add(&ptr->base.events.destroy, &pd->destroy);
|
|
wl_list_insert(server.ptrdevs.prev, &pd->link);
|
|
kick_log(KICK_LOG_INFO, "standalone: pointer '%s'\n", device->name);
|
|
break;
|
|
}
|
|
case WLR_INPUT_DEVICE_TOUCH: {
|
|
struct wlr_touch *touch = wlr_touch_from_input_device(device);
|
|
if (server.cursor)
|
|
wlr_cursor_attach_input_device(server.cursor, device);
|
|
struct standalone_touchdev *td = calloc(1, sizeof(*td));
|
|
if (!td) break;
|
|
td->touch = touch;
|
|
td->down.notify = standalone_touch_down_event;
|
|
wl_signal_add(&touch->events.down, &td->down);
|
|
td->motion.notify = standalone_touch_motion_event;
|
|
wl_signal_add(&touch->events.motion, &td->motion);
|
|
td->up.notify = standalone_touch_up_event;
|
|
wl_signal_add(&touch->events.up, &td->up);
|
|
td->destroy.notify = standalone_touch_destroy;
|
|
wl_signal_add(&touch->base.events.destroy, &td->destroy);
|
|
wl_list_insert(server.touchdevs.prev, &td->link);
|
|
kick_log(KICK_LOG_INFO, "standalone: touch '%s'\n", device->name);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ============ stdin event feed (test/driver channel) ============
|
|
Reads raw input events from stdin and dispatches them into the same
|
|
handlers a real device would drive, so the input paths stay under test.
|
|
Records (after the u8 command byte, u32s little-endian):
|
|
|
|
1 | u32 key | u32 state (WL_KEYBOARD_KEY_STATE_*)
|
|
2 | u32 mods | u32 latched | u32 locked | u32 group
|
|
3 | u32 x | u32 y (absolute output pixels)
|
|
4 | i32 dx | i32 dy (relative motion)
|
|
5 | u32 btn | u32 state (evdev button; 1 press / 0 release)
|
|
6 | u32 slot | u32 action | u32 x | u32 y
|
|
(touch: action 1 down / 2 motion / 0 up,
|
|
x,y absolute output pixels)
|
|
7 | u32 phase | u32 fingers | i32 dx | i32 dy
|
|
(swipe: phase 0 begin / 1 update /
|
|
2 end / 3 cancelled end)
|
|
8 | u32 phase | u32 fingers | i32 dx | i32 dy | i32 scale1000 |
|
|
i32 rot1000 (pinch: scale/rotation in thousandths;
|
|
phases as swipe)
|
|
9 | u32 phase | u32 fingers (hold: phase 0 begin / 1 end /
|
|
2 cancelled end)
|
|
*/
|
|
#define CTL_CMD_KEY 1
|
|
#define CTL_CMD_MODS 2
|
|
#define CTL_CMD_WARP 3
|
|
#define CTL_CMD_PTR_MOTION 4
|
|
#define CTL_CMD_PTR_BUTTON 5
|
|
#define CTL_CMD_TOUCH 6
|
|
#define CTL_CMD_SWIPE 7
|
|
#define CTL_CMD_PINCH 8
|
|
#define CTL_CMD_HOLD 9
|
|
/* HOST_PTR/HOST_TOUCH drive the real host wl_pointer/wl_touch listeners with
|
|
synthesized events (a 17-byte record: sub-command + three u32 args). The
|
|
listeners void their Wayland objects, the same way CTL_CMD_KEY already
|
|
drives host_kbd_key with NULLs, so the test feed covers the nested input
|
|
path instead of bypassing it. */
|
|
#define CTL_CMD_HOST_PTR 10
|
|
#define CTL_CMD_HOST_TOUCH 11
|
|
|
|
#define CTL_MAX_MSG 32 /* pinch carries six u32s after padding */
|
|
#define CTL_BUF_SIZE 512
|
|
|
|
static uint32_t ctl_serial;
|
|
static uint32_t ctl_time;
|
|
|
|
static void ctl_dispatch(const uint8_t *msg, size_t len) {
|
|
uint32_t u32[6];
|
|
if (len < 5) return;
|
|
switch (msg[0]) {
|
|
case CTL_CMD_KEY:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
host_kbd_key(NULL, NULL, ++ctl_serial, ctl_time, u32[0], u32[1]);
|
|
break;
|
|
case CTL_CMD_MODS:
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
host_kbd_modifiers(NULL, NULL, ++ctl_serial, u32[0], u32[1],
|
|
u32[2], u32[3]);
|
|
break;
|
|
case CTL_CMD_WARP:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Absolute standalone-output-pixel position, so tests can place the
|
|
visible cursor (wlr_cursor_warp with a NULL device). Warp doesn't
|
|
emit a motion event, so damage the scene here like motion would, and
|
|
forward the new position to the seat so the effect point stays where
|
|
the arrow lands. Nested mode has no wlr_cursor: feed the position
|
|
straight into the seat instead, so the ctl feed stays usable in both
|
|
modes (e.g. to give a client a pointer enter so it can request a
|
|
cursor shape). */
|
|
if (server.standalone) {
|
|
double wx = (double)u32[0], wy = (double)u32[1];
|
|
constraint_filter_abs(&wx, &wy);
|
|
wlr_cursor_warp(server.cursor, NULL, wx, wy);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] pos=%d,%d\n",
|
|
(int)wx, (int)wy);
|
|
standalone_forward_pointer(ctl_time);
|
|
standalone_overlay_damage();
|
|
} else {
|
|
host_ptr_motion(ctl_time, (double)u32[0], (double)u32[1]);
|
|
}
|
|
break;
|
|
case CTL_CMD_PTR_MOTION:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Relative delta through the same path a libinput pointer device
|
|
takes (native_ptr_motion -> wlr_cursor_move). With no device there
|
|
is no cursor->events.motion, so damage the scene explicitly. */
|
|
native_ptr_motion(ctl_time, NULL, (double)u32[0], (double)u32[1]);
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
break;
|
|
case CTL_CMD_PTR_BUTTON:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Button press/release at the current pointer position, through the
|
|
same seat entry real devices use. Button codes are evdev-style
|
|
(BTN_LEFT = 0x110); state 1 = press, 0 = release. Swallowed
|
|
while the overlay is up, like every other button path. */
|
|
if (!server.seat) return;
|
|
if (overlay_eats_pointer()) {
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
wlr_seat_pointer_notify_button(server.seat, ctl_time,
|
|
u32[0], u32[1]);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
break;
|
|
case CTL_CMD_TOUCH:
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
/* Touch event at absolute output pixels, through the same delivery
|
|
path native devices and the nested forwarder use. */
|
|
seat_touch_deliver(ctl_time, (int32_t)u32[0],
|
|
u32[1] > 2 ? KICK_TOUCH_UP : (int)u32[1],
|
|
(double)(int32_t)u32[2],
|
|
(double)(int32_t)u32[3]);
|
|
break;
|
|
case CTL_CMD_SWIPE:
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
/* Touchpad swipe through the shared gesture forwarders. */
|
|
switch (u32[0]) {
|
|
case 0: gesture_swipe_begin(ctl_time, u32[1]); break;
|
|
case 1: gesture_swipe_update(ctl_time, u32[1],
|
|
(double)(int32_t)u32[2], (double)(int32_t)u32[3]); break;
|
|
case 2: gesture_swipe_end(ctl_time, false); break;
|
|
case 3: gesture_swipe_end(ctl_time, true); break;
|
|
}
|
|
break;
|
|
case CTL_CMD_PINCH:
|
|
if (len < 25) return;
|
|
memcpy(u32, msg + 1, 24);
|
|
ctl_time += 1;
|
|
/* Touchpad pinch; scale/rotation ride as thousandths. */
|
|
switch (u32[0]) {
|
|
case 0: gesture_pinch_begin(ctl_time, u32[1]); break;
|
|
case 1: gesture_pinch_update(ctl_time, u32[1],
|
|
(double)(int32_t)u32[2], (double)(int32_t)u32[3],
|
|
(double)(int32_t)u32[4] / 1000.0,
|
|
(double)(int32_t)u32[5] / 1000.0); break;
|
|
case 2: gesture_pinch_end(ctl_time, false); break;
|
|
case 3: gesture_pinch_end(ctl_time, true); break;
|
|
}
|
|
break;
|
|
case CTL_CMD_HOLD:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Touchpad hold through the shared gesture forwarders. */
|
|
switch (u32[0]) {
|
|
case 0: gesture_hold_begin(ctl_time, u32[1]); break;
|
|
case 1: gesture_hold_end(ctl_time, false); break;
|
|
case 2: gesture_hold_end(ctl_time, true); break;
|
|
}
|
|
break;
|
|
case CTL_CMD_HOST_PTR:
|
|
/* Synthesized host pointer event through the real wl_pointer
|
|
listener (sub, a, b, c). Coordinates are output pixels. */
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
switch (u32[0]) {
|
|
case 0: /* enter: serial, x, y */
|
|
host_ptr_enter(NULL, NULL, u32[1], NULL,
|
|
wl_fixed_from_int((int)u32[2]),
|
|
wl_fixed_from_int((int)u32[3]));
|
|
break;
|
|
case 1: /* leave: serial */
|
|
host_ptr_leave(NULL, NULL, u32[1], NULL);
|
|
break;
|
|
case 2: /* motion: x, y */
|
|
host_ptr_motion_event(NULL, NULL, ctl_time,
|
|
wl_fixed_from_int((int)u32[1]),
|
|
wl_fixed_from_int((int)u32[2]));
|
|
break;
|
|
case 3: /* button: button, state */
|
|
host_ptr_button(NULL, NULL, ++ctl_serial, ctl_time,
|
|
u32[1], u32[2]);
|
|
break;
|
|
case 4: /* axis: axis, value (pixels) */
|
|
host_ptr_axis(NULL, NULL, ctl_time, u32[1],
|
|
wl_fixed_from_int((int)u32[2]));
|
|
break;
|
|
case 5: /* axis_discrete: axis, discrete */
|
|
host_ptr_axis_discrete(NULL, NULL, u32[1], (int32_t)u32[2]);
|
|
break;
|
|
case 6: /* axis_source: source */
|
|
host_ptr_axis_source(NULL, NULL, u32[1]);
|
|
break;
|
|
case 7: /* axis_stop: axis */
|
|
host_ptr_axis_stop(NULL, NULL, ctl_time, u32[1]);
|
|
break;
|
|
case 8: /* frame */
|
|
host_ptr_frame(NULL, NULL);
|
|
break;
|
|
}
|
|
break;
|
|
case CTL_CMD_HOST_TOUCH:
|
|
/* Synthesized host touch event through the real wl_touch listener
|
|
(sub, a, b, c). Coordinates are output pixels. */
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
switch (u32[0]) {
|
|
case 0: /* down: slot, x, y */
|
|
host_touch_down(NULL, NULL, ++ctl_serial, ctl_time, NULL,
|
|
(int32_t)u32[1],
|
|
wl_fixed_from_int((int)u32[2]),
|
|
wl_fixed_from_int((int)u32[3]));
|
|
break;
|
|
case 1: /* motion: slot, x, y */
|
|
host_touch_motion(NULL, NULL, ctl_time, (int32_t)u32[1],
|
|
wl_fixed_from_int((int)u32[2]),
|
|
wl_fixed_from_int((int)u32[3]));
|
|
break;
|
|
case 2: /* up: slot */
|
|
host_touch_up(NULL, NULL, ++ctl_serial, ctl_time,
|
|
(int32_t)u32[1]);
|
|
break;
|
|
case 3: /* cancel */
|
|
host_touch_cancel(NULL, NULL);
|
|
break;
|
|
case 4: /* frame */
|
|
host_touch_frame(NULL, NULL);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int ctl_cb(int fd, uint32_t mask, void *data) {
|
|
static uint8_t buf[CTL_BUF_SIZE];
|
|
static size_t have = 0;
|
|
(void)data;
|
|
|
|
/* stdin closed: stop watching but keep the compositor running */
|
|
if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) {
|
|
wl_event_source_remove(server.ctl_source);
|
|
server.ctl_source = NULL;
|
|
return 0;
|
|
}
|
|
|
|
ssize_t n = read(fd, buf + have, sizeof(buf) - have);
|
|
if (n <= 0) {
|
|
if (n == 0) {
|
|
wl_event_source_remove(server.ctl_source);
|
|
server.ctl_source = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
have += (size_t)n;
|
|
|
|
uint8_t msg[CTL_MAX_MSG];
|
|
while (have >= 5) {
|
|
/* MODS/SWIPE/TOUCH/HOST_PTR/HOST_TOUCH carry four u32s, PINCH six
|
|
after the command byte; HOLD is a single u32 pair like the rest. */
|
|
size_t need = (buf[0] == CTL_CMD_MODS || buf[0] == CTL_CMD_TOUCH ||
|
|
buf[0] == CTL_CMD_SWIPE || buf[0] == CTL_CMD_HOST_PTR ||
|
|
buf[0] == CTL_CMD_HOST_TOUCH)
|
|
? 17
|
|
: buf[0] == CTL_CMD_PINCH ? 25
|
|
: 9;
|
|
if (have < need) break;
|
|
memcpy(msg, buf, need);
|
|
ctl_dispatch(msg, need);
|
|
memmove(buf, buf + need, have - need);
|
|
have -= need;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- host pointer ---------- */
|
|
|
|
static struct wlr_surface *surface_at(double x, double y,
|
|
double *sx, double *sy)
|
|
{
|
|
struct wlr_scene_node *node = wlr_scene_node_at(
|
|
&server.scene->tree.node, x, y, sx, sy);
|
|
if (node && node->type == WLR_SCENE_NODE_BUFFER) {
|
|
struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node);
|
|
struct wlr_scene_surface *scene_surface =
|
|
wlr_scene_surface_try_from_buffer(buffer);
|
|
if (scene_surface)
|
|
return scene_surface->surface;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Live touch slots kick originated (any source). On host cancel every
|
|
sequence ends with a clean up instead of stranding clients. */
|
|
#define KICK_TOUCH_MAX_SLOTS 16
|
|
static int32_t touch_slots[KICK_TOUCH_MAX_SLOTS];
|
|
static size_t touch_slot_count;
|
|
/* Touch ids swallowed by the overlay while it is visible (a tap that
|
|
selected a row, or a tap outside every row that is ignored). Their
|
|
motion/up must not leak to clients as a partial gesture. */
|
|
static int32_t overlay_touch_slots[KICK_TOUCH_MAX_SLOTS];
|
|
static size_t overlay_touch_count;
|
|
/* First touch id that went down while the overlay was up: the only finger
|
|
whose motion re-targets the preview (first-touch-wins). -1 when no
|
|
overlay drag is active. */
|
|
static int32_t overlay_touch_driver = -1;
|
|
|
|
static bool overlay_touch_consumed(int32_t id) {
|
|
for (size_t i = 0; i < overlay_touch_count; i++)
|
|
if (overlay_touch_slots[i] == id) return true;
|
|
return false;
|
|
}
|
|
|
|
static void overlay_touch_consume(int32_t id) {
|
|
if (overlay_touch_consumed(id)) return;
|
|
if (overlay_touch_count < KICK_TOUCH_MAX_SLOTS)
|
|
overlay_touch_slots[overlay_touch_count++] = id;
|
|
}
|
|
|
|
static void overlay_touch_release(int32_t id) {
|
|
for (size_t i = 0; i < overlay_touch_count; i++)
|
|
if (overlay_touch_slots[i] == id) {
|
|
overlay_touch_slots[i] = overlay_touch_slots[--overlay_touch_count];
|
|
break;
|
|
}
|
|
if (id == overlay_touch_driver)
|
|
overlay_touch_driver = -1;
|
|
}
|
|
|
|
/* Preview a window while the overlay is up (the Alt release commits it to
|
|
the MRU front via the usual overlay path). Shared by touch taps/drags
|
|
and pointer hover. Mirrors mru_step's set_active_tab + window_enter
|
|
pinning so a stacked member is selected exactly, not just its tab's
|
|
current window. Landing on the already-shown window is a no-op (false),
|
|
so motion-rate events never spam the log; callers log on true. */
|
|
static bool overlay_preview_select(struct kick_window *w) {
|
|
if (!w || server.session_locked) return false;
|
|
if (w == mru_current()) return false;
|
|
struct kick_tab *tab = w->tab;
|
|
if (!tab || !tab_overlay_row(tab)) return false;
|
|
if (tab != server.active_tab)
|
|
set_active_tab(tab);
|
|
window_enter(w);
|
|
return true;
|
|
}
|
|
|
|
static bool overlay_eats_pointer(void) {
|
|
return host.overlay_visible && !confirm_quit_pending &&
|
|
!server.session_locked;
|
|
}
|
|
|
|
/* Smooth-scroll pixels per overlay tab step. Notched wheels send discrete
|
|
ticks (one step each) alongside their pixel value; smooth-only devices
|
|
send value alone, which accumulates here. */
|
|
#define OVERLAY_WHEEL_STEP 15.0
|
|
|
|
/* Whether the wheel has anywhere to go: two or more live, non-closing tabs
|
|
(the same predicate cycle_tab steps over). With fewer, wheel input is
|
|
still swallowed but steps and logs nothing. */
|
|
static bool overlay_can_cycle(void) {
|
|
int n = 0;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link)
|
|
if (tab_live(t) && !tab_closing(t) && ++n >= 2)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* Vertical wheel while the overlay eats pointer input: cycle tabs exactly
|
|
like Alt+Up/Down (preview now, Alt release commits via the usual overlay
|
|
path). discrete > 0 (wheel down) steps to the next tab, < 0 to the
|
|
previous one; a zero discrete accumulates value towards one step.
|
|
Returns true when eaten (the axis must not reach the client). */
|
|
static bool overlay_handle_axis(uint32_t axis, double value, int32_t discrete) {
|
|
if (axis != 0 || !overlay_eats_pointer())
|
|
return false;
|
|
if (server.seat)
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
if (!overlay_can_cycle())
|
|
return true;
|
|
if (discrete != 0) {
|
|
int dir = discrete > 0 ? 1 : -1;
|
|
int32_t n = discrete > 0 ? discrete : -discrete;
|
|
for (int32_t i = 0; i < n; i++) {
|
|
cycle_tab(dir);
|
|
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
|
|
}
|
|
return true;
|
|
}
|
|
overlay_wheel_acc += value;
|
|
while (overlay_wheel_acc >= OVERLAY_WHEEL_STEP ||
|
|
overlay_wheel_acc <= -OVERLAY_WHEEL_STEP) {
|
|
int dir = overlay_wheel_acc > 0 ? 1 : -1;
|
|
cycle_tab(dir);
|
|
kick_log(KICK_LOG_INFO, "[wheel] cycle dir=%+d\n", dir);
|
|
overlay_wheel_acc -= dir * OVERLAY_WHEEL_STEP;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void seat_touch_deliver(uint32_t time_msec, int32_t id, int action,
|
|
double ox, double oy) {
|
|
if (!server.seat) return;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
/* While the Alt overlay is up, touches never reach clients: a tap on
|
|
a row previews that window (single tap selects, drag motion
|
|
re-targets the preview as the finger slides between titles),
|
|
anything else is ignored. Checked before constraint clamping --
|
|
the overlay sits outside any constrained surface. */
|
|
if (host.overlay_visible && !confirm_quit_pending && !server.session_locked) {
|
|
if (action == KICK_TOUCH_DOWN) {
|
|
/* First-touch-wins: only the finger that claims the idle
|
|
driver previews; concurrent fingers are swallowed silently
|
|
(and never promote when the driver lifts, to avoid jumps). */
|
|
if (overlay_touch_driver == -1) {
|
|
struct kick_tab *ht = NULL;
|
|
struct kick_window *hw = NULL;
|
|
if (overlay_hit_test(ox, oy, &ht, &hw) && hw &&
|
|
overlay_preview_select(hw))
|
|
kick_log(KICK_LOG_INFO, "[touch] select app=%d\n",
|
|
tab_index(ht));
|
|
overlay_touch_consume(id);
|
|
overlay_touch_driver = id;
|
|
} else {
|
|
overlay_touch_consume(id);
|
|
}
|
|
wlr_seat_touch_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
if (overlay_touch_consumed(id)) {
|
|
if (action == KICK_TOUCH_MOTION) {
|
|
/* Drag: sliding into another row's title previews it;
|
|
sliding out of every row sticks to the last preview. */
|
|
if (id == overlay_touch_driver) {
|
|
struct kick_tab *ht = NULL;
|
|
struct kick_window *hw = NULL;
|
|
if (overlay_hit_test(ox, oy, &ht, &hw) && hw &&
|
|
overlay_preview_select(hw))
|
|
kick_log(KICK_LOG_INFO, "[touch] select app=%d\n",
|
|
tab_index(ht));
|
|
}
|
|
} else {
|
|
overlay_touch_release(id);
|
|
}
|
|
wlr_seat_touch_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
} else if (overlay_touch_consumed(id)) {
|
|
/* Overlay went away mid-gesture: drop the tail, never forward a
|
|
partial sequence to the client. */
|
|
if (action != KICK_TOUCH_MOTION)
|
|
overlay_touch_release(id);
|
|
wlr_seat_touch_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
constraint_filter_abs(&ox, &oy);
|
|
switch (action) {
|
|
case KICK_TOUCH_DOWN: {
|
|
double nx = 0.0, ny = 0.0;
|
|
struct wlr_surface *surface = surface_at(ox, oy, &nx, &ny);
|
|
/* A down without a client surface underneath is dropped: touch
|
|
points stick to the surface they land on. */
|
|
if (surface) {
|
|
wlr_seat_touch_notify_down(server.seat, surface,
|
|
time_msec, id, nx, ny);
|
|
if (touch_slot_count < KICK_TOUCH_MAX_SLOTS)
|
|
touch_slots[touch_slot_count++] = id;
|
|
}
|
|
break;
|
|
}
|
|
case KICK_TOUCH_MOTION:
|
|
/* Motion routes to the point's original client; coordinates may
|
|
leave that surface while dragging. */
|
|
wlr_seat_touch_notify_motion(server.seat, time_msec, id, ox, oy);
|
|
break;
|
|
default: { /* KICK_TOUCH_UP */
|
|
wlr_seat_touch_notify_up(server.seat, time_msec, id);
|
|
for (size_t i = 0; i < touch_slot_count; i++)
|
|
if (touch_slots[i] == id) {
|
|
touch_slots[i] = touch_slots[--touch_slot_count];
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
wlr_seat_touch_notify_frame(server.seat);
|
|
}
|
|
|
|
/* End every live touch sequence with an up: the host cancelled (gesture
|
|
policy, session teardown) and clients must not wait forever. */
|
|
static void seat_touch_cancel_all(void) {
|
|
/* Overlay-swallowed touches were never forwarded: just drop them. */
|
|
overlay_touch_count = 0;
|
|
overlay_touch_driver = -1;
|
|
while (touch_slot_count > 0)
|
|
seat_touch_deliver(0, touch_slots[touch_slot_count - 1],
|
|
KICK_TOUCH_UP, 0, 0);
|
|
}
|
|
|
|
static void host_ptr_motion(uint32_t time, double sx, double sy) {
|
|
if (!server.seat) return;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
server.last_ptr_x = sx;
|
|
server.last_ptr_y = sy;
|
|
/* Hovering the overlay previews rows like a touch drag (no click
|
|
needed); motion never reaches clients while it is up. Raw coords:
|
|
the overlay sits outside any constrained surface. */
|
|
if (overlay_eats_pointer()) {
|
|
struct kick_tab *ht = NULL;
|
|
struct kick_window *hw = NULL;
|
|
if (overlay_hit_test(sx, sy, &ht, &hw) && hw &&
|
|
overlay_preview_select(hw))
|
|
kick_log(KICK_LOG_INFO, "[pointer] select app=%d\n",
|
|
tab_index(ht));
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
constraint_filter_abs(&sx, &sy);
|
|
double nx = 0.0, ny = 0.0;
|
|
struct wlr_surface *surface = surface_at(sx, sy, &nx, &ny);
|
|
if (surface) {
|
|
wlr_seat_pointer_notify_enter(server.seat, surface, nx, ny);
|
|
wlr_seat_pointer_notify_motion(server.seat, time, nx, ny);
|
|
} else {
|
|
wlr_seat_pointer_notify_clear_focus(server.seat);
|
|
/* Pointer over no nested surface (black tab): drop any client shape. */
|
|
apply_cursor_shape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
|
}
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void host_ptr_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *surface, wl_fixed_t sx, wl_fixed_t sy)
|
|
{
|
|
(void)data; (void)p; (void)surface;
|
|
host.ptr_serial = serial;
|
|
host_ptr_motion(0, wl_fixed_to_double(sx), wl_fixed_to_double(sy));
|
|
}
|
|
|
|
static void host_ptr_leave(void *data, struct wl_pointer *p, uint32_t serial,
|
|
struct wl_surface *surface)
|
|
{
|
|
(void)data; (void)p; (void)serial; (void)surface;
|
|
if (server.seat) {
|
|
wlr_seat_pointer_notify_clear_focus(server.seat);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
}
|
|
|
|
static void host_ptr_motion_event(void *data, struct wl_pointer *p, uint32_t time,
|
|
wl_fixed_t sx, wl_fixed_t sy)
|
|
{
|
|
(void)data; (void)p;
|
|
double nx = wl_fixed_to_double(sx), ny = wl_fixed_to_double(sy);
|
|
/* Nested mode has no wlr_cursor: derive the relative delta from the
|
|
consecutive absolute host positions. (Standalone deltas are sent from
|
|
native_ptr_motion instead.) */
|
|
send_relative_motion(time, nx - server.last_ptr_x, ny - server.last_ptr_y);
|
|
host_ptr_motion(time, nx, ny);
|
|
}
|
|
|
|
static void host_ptr_button(void *data, struct wl_pointer *p, uint32_t serial,
|
|
uint32_t time, uint32_t button, uint32_t state)
|
|
{
|
|
(void)data; (void)p; (void)serial;
|
|
if (!server.seat) return;
|
|
if (server.idle_notifier)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
if (overlay_eats_pointer()) {
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
return;
|
|
}
|
|
wlr_seat_pointer_notify_button(server.seat, time, button, state);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void host_ptr_axis(void *data, struct wl_pointer *p, uint32_t time,
|
|
uint32_t axis, wl_fixed_t value)
|
|
{
|
|
(void)data; (void)p;
|
|
if (!server.seat) return;
|
|
int32_t discrete = axis < 2 ? host.axis_discrete[axis] : 0;
|
|
host.axis_discrete[axis] = 0;
|
|
if (overlay_handle_axis(axis, wl_fixed_to_double(value), discrete))
|
|
return;
|
|
wlr_seat_pointer_notify_axis(server.seat, time, axis,
|
|
wl_fixed_to_double(value), discrete,
|
|
host.axis_source,
|
|
WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void host_ptr_axis_source(void *data, struct wl_pointer *p, uint32_t source)
|
|
{
|
|
(void)data; (void)p;
|
|
host.axis_source = source;
|
|
}
|
|
|
|
static void host_ptr_axis_discrete(void *data, struct wl_pointer *p, uint32_t axis,
|
|
int32_t discrete)
|
|
{
|
|
(void)data; (void)p;
|
|
if (axis < 2)
|
|
host.axis_discrete[axis] += discrete;
|
|
}
|
|
|
|
static void host_ptr_axis_stop(void *data, struct wl_pointer *p, uint32_t time,
|
|
uint32_t axis)
|
|
{
|
|
(void)data; (void)p; (void)time; (void)axis;
|
|
}
|
|
|
|
static void host_ptr_frame(void *data, struct wl_pointer *p) {
|
|
(void)data; (void)p;
|
|
}
|
|
|
|
static void host_touch_deliver(void *data, struct wl_touch *touch,
|
|
uint32_t time, int32_t id, wl_fixed_t fx,
|
|
wl_fixed_t fy, int action) {
|
|
(void)data; (void)touch;
|
|
/* Host touch coordinates are surface-local on our host window, which
|
|
is the same space the pointer path treats as output-local. */
|
|
seat_touch_deliver(time, id, action,
|
|
wl_fixed_to_double(fx), wl_fixed_to_double(fy));
|
|
}
|
|
|
|
static void host_touch_down(void *data, struct wl_touch *touch,
|
|
uint32_t serial, uint32_t time,
|
|
struct wl_surface *surface, int32_t id,
|
|
wl_fixed_t fx, wl_fixed_t fy) {
|
|
(void)serial; (void)surface;
|
|
host_touch_deliver(data, touch, time, id, fx, fy, KICK_TOUCH_DOWN);
|
|
}
|
|
|
|
static void host_touch_motion(void *data, struct wl_touch *touch,
|
|
uint32_t time, int32_t id,
|
|
wl_fixed_t fx, wl_fixed_t fy) {
|
|
host_touch_deliver(data, touch, time, id, fx, fy, KICK_TOUCH_MOTION);
|
|
}
|
|
|
|
static void host_touch_up(void *data, struct wl_touch *touch,
|
|
uint32_t serial, uint32_t time, int32_t id) {
|
|
(void)data; (void)touch; (void)serial;
|
|
seat_touch_deliver(time, id, KICK_TOUCH_UP, 0, 0);
|
|
}
|
|
|
|
static void host_touch_cancel(void *data, struct wl_touch *touch) {
|
|
(void)data; (void)touch;
|
|
seat_touch_cancel_all();
|
|
kick_log(KICK_LOG_DEBUG, "[seat] host touch cancelled\n");
|
|
}
|
|
|
|
static void host_touch_frame(void *data, struct wl_touch *touch) {
|
|
(void)data; (void)touch;
|
|
if (server.seat)
|
|
wlr_seat_touch_notify_frame(server.seat);
|
|
}
|
|
|
|
static const struct wl_touch_listener host_touch_listener = {
|
|
.down = host_touch_down,
|
|
.motion = host_touch_motion,
|
|
.up = host_touch_up,
|
|
.cancel = host_touch_cancel,
|
|
.frame = host_touch_frame,
|
|
};
|
|
|
|
static const struct wl_pointer_listener host_ptr_listener = {
|
|
.enter = host_ptr_enter,
|
|
.leave = host_ptr_leave,
|
|
.motion = host_ptr_motion_event,
|
|
.button = host_ptr_button,
|
|
.axis = host_ptr_axis,
|
|
.frame = host_ptr_frame,
|
|
.axis_source = host_ptr_axis_source,
|
|
.axis_stop = host_ptr_axis_stop,
|
|
.axis_discrete = host_ptr_axis_discrete,
|
|
};
|
|
|
|
/* When the host compositor (labwc on a VT round-trip) removes a seat
|
|
capability, every object created from that seat -- our wl_keyboard /
|
|
wl_pointer -- is invalidated, yet the client keeps running silently with
|
|
no input ever arriving again. Re-create the objects as capabilities are
|
|
re-gained; the compositor re-sends keymap/modifiers/enter on the fresh
|
|
wl_keyboard, and host_kbd_enter() re-focuses our nested seat. */
|
|
static void host_seat_capabilities(void *data, struct wl_seat *seat,
|
|
uint32_t capabilities)
|
|
{
|
|
(void)data; (void)seat;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] capabilities=0x%x\n", capabilities);
|
|
const bool have_kbd = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0;
|
|
const bool have_ptr = (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0;
|
|
if (have_kbd && !host.keyboard) {
|
|
host.keyboard = wl_seat_get_keyboard(host.seat);
|
|
if (host.keyboard) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] re-gained keyboard\n");
|
|
wl_keyboard_add_listener(host.keyboard, &host_kbd_listener, NULL);
|
|
}
|
|
} else if (!have_kbd && host.keyboard) {
|
|
wl_keyboard_release(host.keyboard);
|
|
host.keyboard = NULL;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] keyboard capability lost\n");
|
|
}
|
|
if (have_ptr && !host.pointer) {
|
|
host.pointer = wl_seat_get_pointer(host.seat);
|
|
if (host.pointer) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] re-gained pointer\n");
|
|
wl_pointer_add_listener(host.pointer, &host_ptr_listener, NULL);
|
|
}
|
|
} else if (!have_ptr && host.pointer) {
|
|
host.shape_dev = NULL; /* the shape device goes inert too */
|
|
wl_pointer_release(host.pointer);
|
|
host.pointer = NULL;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] pointer capability lost\n");
|
|
}
|
|
const bool have_touch =
|
|
(capabilities & WL_SEAT_CAPABILITY_TOUCH) != 0;
|
|
if (have_touch && !host.touch) {
|
|
host.touch = wl_seat_get_touch(host.seat);
|
|
if (host.touch) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] re-gained touch\n");
|
|
wl_touch_add_listener(host.touch, &host_touch_listener, NULL);
|
|
}
|
|
} else if (!have_touch && host.touch) {
|
|
seat_touch_cancel_all(); /* end sequences cleanly before release */
|
|
wl_touch_release(host.touch);
|
|
host.touch = NULL;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] touch capability lost\n");
|
|
}
|
|
/* The cursor-shape device rides on the host pointer: (re)create it once
|
|
the pointer is (re)available. */
|
|
if (host.pointer)
|
|
host_cursor_shape_get_pointer();
|
|
}
|
|
|
|
/* ===================== Host init / EGL ===================== */
|
|
|
|
static bool host_init(void) {
|
|
const char *disp = getenv("WAYLAND_DISPLAY");
|
|
if (!disp)
|
|
disp = "wayland-0";
|
|
host.display = wl_display_connect(NULL);
|
|
if (!host.display) {
|
|
fprintf(stderr, "kick: cannot connect to host compositor "
|
|
"(WAYLAND_DISPLAY=%s). Run me inside a running Wayland session.\n",
|
|
disp);
|
|
return false;
|
|
}
|
|
|
|
host.registry = wl_display_get_registry(host.display);
|
|
wl_registry_add_listener(host.registry, &host_registry_listener, NULL);
|
|
wl_display_roundtrip(host.display);
|
|
host.registry_synced = true;
|
|
|
|
if (!host.compositor || !host.wm_base) {
|
|
fprintf(stderr, "kick: host lacks compositor/xdg_wm_base\n");
|
|
return false;
|
|
}
|
|
|
|
host.surface = wl_compositor_create_surface(host.compositor);
|
|
|
|
/* Backend selection: compositors advertising zxdg_decoration_manager_v1
|
|
honor server-side decorations, so kick owns the shell objects and
|
|
talks xdg-shell directly -- no set_window_geometry hints, immediate
|
|
acks, and fullscreen state application that cannot anchor to stale
|
|
geometry (the labwc 0.9.8 misplacement). Hosts without it force
|
|
CSD (Mutter/GNOME): hand the window to libdecor, which draws its own
|
|
titlebar. KICK_LIBDECOR=1 forces the libdecor path for testing,
|
|
KICK_LIBDECOR=0 forces the direct path (decoration-less hosts). */
|
|
const char *ld = getenv("KICK_LIBDECOR");
|
|
host.use_libdecor = (ld && strcmp(ld, "0") != 0) || (!ld && !host.decoration);
|
|
|
|
xdg_wm_base_add_listener(host.wm_base, &host_xdg_wm_listener, NULL);
|
|
if (host.use_libdecor) {
|
|
host.libdecor = libdecor_new(host.display, &host_libdecor_iface);
|
|
if (!host.libdecor) {
|
|
fprintf(stderr, "kick: libdecor failed to initialise\n");
|
|
return false;
|
|
}
|
|
host.frame = libdecor_decorate(host.libdecor, host.surface,
|
|
&host_frame_iface, NULL);
|
|
if (!host.frame) {
|
|
fprintf(stderr, "kick: libdecor_decorate failed\n");
|
|
return false;
|
|
}
|
|
libdecor_frame_set_title(host.frame, "kick");
|
|
libdecor_frame_set_app_id(host.frame, "kick");
|
|
/* libdecor negotiates xdg decorations with the host: a server-side
|
|
title bar when offered, its own client-side titlebar otherwise. */
|
|
libdecor_frame_map(host.frame);
|
|
wl_surface_commit(host.surface);
|
|
wl_display_roundtrip(host.display);
|
|
kick_log(KICK_LOG_INFO, "host window: libdecor-managed decorations\n");
|
|
if (host.use_libdecor)
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO,
|
|
"host window: direct xdg-shell with server-side decorations\n");
|
|
host.xdg_surface = xdg_wm_base_get_xdg_surface(host.wm_base,
|
|
host.surface);
|
|
host.toplevel = xdg_surface_get_toplevel(host.xdg_surface);
|
|
xdg_toplevel_set_title(host.toplevel, "kick");
|
|
xdg_toplevel_set_app_id(host.toplevel, "kick");
|
|
/* No decoration manager (e.g. KICK_LIBDECOR=0 on a bare host):
|
|
run undecorated rather than crashing on a NULL manager. */
|
|
if (host.decoration) {
|
|
host.decoration_obj =
|
|
zxdg_decoration_manager_v1_get_toplevel_decoration(
|
|
host.decoration, host.toplevel);
|
|
zxdg_toplevel_decoration_v1_add_listener(host.decoration_obj,
|
|
&host_decoration_listener, NULL);
|
|
zxdg_toplevel_decoration_v1_set_mode(host.decoration_obj,
|
|
ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
|
}
|
|
xdg_surface_add_listener(host.xdg_surface,
|
|
&host_xdg_surface_listener, NULL);
|
|
xdg_toplevel_add_listener(host.toplevel,
|
|
&host_xdg_toplevel_listener, NULL);
|
|
wl_surface_commit(host.surface);
|
|
wl_display_roundtrip(host.display);
|
|
}
|
|
|
|
/* Host keyboard/pointer are created by the seat capabilities handler
|
|
during the registry round-trip above; nothing more to fetch here. */
|
|
bridge_init();
|
|
|
|
/* Try EGL on the host wayland platform */
|
|
pfn_eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)
|
|
eglGetProcAddress("eglGetPlatformDisplayEXT");
|
|
if (!pfn_eglGetPlatformDisplayEXT) {
|
|
fprintf(stderr, "kick: no EGL platform support; "
|
|
"falling back to software rendering\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
|
|
host.egl_display = pfn_eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT,
|
|
host.display, NULL);
|
|
if (host.egl_display == EGL_NO_DISPLAY) {
|
|
fprintf(stderr, "kick: EGL display failed; "
|
|
"falling back to software rendering\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
if (!eglInitialize(host.egl_display, NULL, NULL)) {
|
|
fprintf(stderr, "kick: eglInitialize failed; "
|
|
"falling back to software rendering\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
|
|
eglBindAPI(EGL_OPENGL_ES_API);
|
|
|
|
const EGLint config_attribs[] = {
|
|
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
|
EGL_RED_SIZE, 8,
|
|
EGL_GREEN_SIZE, 8,
|
|
EGL_BLUE_SIZE, 8,
|
|
EGL_ALPHA_SIZE, 0,
|
|
EGL_NONE
|
|
};
|
|
EGLint ncfg = 0;
|
|
if (!eglChooseConfig(host.egl_display, config_attribs, &host.egl_config,
|
|
1, &ncfg) || ncfg < 1) {
|
|
fprintf(stderr, "kick: no EGL config; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
|
|
const EGLint ctx_attribs[] = {
|
|
EGL_CONTEXT_CLIENT_VERSION, 2,
|
|
EGL_NONE
|
|
};
|
|
host.egl_context = eglCreateContext(host.egl_display, host.egl_config,
|
|
EGL_NO_CONTEXT, ctx_attribs);
|
|
if (host.egl_context == EGL_NO_CONTEXT) {
|
|
fprintf(stderr, "kick: EGL context failed; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
|
|
/* An eager host (kick-in-kick) may have configured the window inside
|
|
the startup roundtrip already; keep that real size. Otherwise install
|
|
the default until the first (async) configure overwrites it.
|
|
Resetting unconditionally desyncs the swapchain (sized from the
|
|
configure) from the host size and trips wlroots' build_state assert. */
|
|
if (!host.configured) {
|
|
host.width = 1280;
|
|
host.height = 800;
|
|
}
|
|
host.egl_window = wl_egl_window_create(host.surface, host.width, host.height);
|
|
host.egl_surface = eglCreateWindowSurface(host.egl_display, host.egl_config,
|
|
(EGLNativeWindowType)host.egl_window, NULL);
|
|
if (host.egl_surface == EGL_NO_SURFACE) {
|
|
fprintf(stderr, "kick: EGL window surface failed; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
|
|
if (!eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
|
|
host.egl_context)) {
|
|
fprintf(stderr, "kick: eglMakeCurrent failed; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
eglSwapInterval(host.egl_display, 0);
|
|
|
|
pfn_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)egl_proc("eglCreateImageKHR");
|
|
pfn_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)egl_proc("eglDestroyImageKHR");
|
|
pfn_glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)
|
|
egl_proc("glEGLImageTargetTexture2DOES");
|
|
|
|
if (!gl_setup()) {
|
|
fprintf(stderr, "kick: GL setup failed; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
host.egl_ok = true;
|
|
kick_log(KICK_LOG_INFO, "kick: EGL (GPU) rendering enabled\n");
|
|
return true;
|
|
}
|
|
|
|
/* ===================== Scene buffer -> host blit ===================== */
|
|
|
|
static bool import_scene_buffer(struct wlr_buffer *buf, GLuint tex) {
|
|
struct wlr_dmabuf_attributes attrs;
|
|
if (wlr_buffer_get_dmabuf(buf, &attrs)) {
|
|
if (attrs.n_planes < 1) return false;
|
|
|
|
EGLint att[48];
|
|
int n = 0;
|
|
att[n++] = EGL_WIDTH; att[n++] = attrs.width;
|
|
att[n++] = EGL_HEIGHT; att[n++] = attrs.height;
|
|
att[n++] = EGL_LINUX_DRM_FOURCC_EXT; att[n++] = (EGLint)attrs.format;
|
|
for (int p = 0; p < attrs.n_planes; p++) {
|
|
att[n++] = EGL_DMA_BUF_PLANE0_FD_EXT + p * 3; att[n++] = attrs.fd[p];
|
|
att[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT + p * 3; att[n++] = (EGLint)attrs.offset[p];
|
|
att[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT + p * 3; att[n++] = (EGLint)attrs.stride[p];
|
|
}
|
|
if (attrs.modifier != DRM_FORMAT_MOD_INVALID) {
|
|
att[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
|
|
att[n++] = (EGLint)(attrs.modifier & 0xffffffff);
|
|
att[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
|
|
att[n++] = (EGLint)(attrs.modifier >> 32);
|
|
}
|
|
att[n++] = EGL_NONE;
|
|
|
|
EGLImageKHR img = pfn_eglCreateImageKHR(host.egl_display, EGL_NO_CONTEXT,
|
|
EGL_LINUX_DMA_BUF_EXT,
|
|
(EGLClientBuffer)NULL, att);
|
|
if (img == EGL_NO_IMAGE_KHR) {
|
|
kick_log(KICK_LOG_DEBUG, "[import] eglCreateImageKHR failed "
|
|
"(format 0x%x modifier 0x%llx)\n", attrs.format,
|
|
(unsigned long long)attrs.modifier);
|
|
return false;
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
pfn_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img);
|
|
pfn_eglDestroyImageKHR(host.egl_display, img);
|
|
return true;
|
|
}
|
|
|
|
/* Fallback for non-dmabuf buffers (on a headless backend the allocator
|
|
yields shm buffers). Upload via CPU read so shm clients and the shm
|
|
swapchain both render into the host window. */
|
|
void *data;
|
|
uint32_t fmt;
|
|
size_t stride;
|
|
if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
|
|
&data, &fmt, &stride)) {
|
|
kick_log(KICK_LOG_DEBUG, "[import] buffer is neither dmabuf nor "
|
|
"CPU-readable\n");
|
|
return false;
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
switch (fmt) {
|
|
case DRM_FORMAT_XRGB8888:
|
|
case DRM_FORMAT_ARGB8888:
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, buf->width, buf->height, 0,
|
|
GL_BGRA_EXT, GL_UNSIGNED_BYTE, data);
|
|
break;
|
|
default:
|
|
kick_log(KICK_LOG_DEBUG, "[import] unsupported CPU fallback format "
|
|
"0x%x\n", fmt);
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
return false;
|
|
}
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
return true;
|
|
}
|
|
|
|
/* Pace presents to the host compositor: this fires after the host has shown
|
|
the buffer we queued, so we know we may queue the next one. Any frame drawn
|
|
while a present is still pending is coalesced into the back buffer and
|
|
presented here, keeping exactly one outstanding buffer (no burst, no
|
|
tearing on busy repainters like terminals). */
|
|
static const struct wl_callback_listener host_frame_listener;
|
|
/* Request a host frame callback and present the drawing in the back buffer.
|
|
The callback must be requested *before* the swap: frame requests are part
|
|
of the state committed by eglSwapBuffers, so requesting it after would wait
|
|
on a commit that never comes. */
|
|
static void present_to_host(void) {
|
|
host.frame_cb = wl_surface_frame(host.surface);
|
|
wl_callback_add_listener(host.frame_cb, &host_frame_listener, NULL);
|
|
double t_swap = kick_monotonic_ms();
|
|
eglSwapBuffers(host.egl_display, host.egl_surface);
|
|
t_swap = kick_monotonic_ms() - t_swap;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG && t_swap >= 1.0)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] t=%.0f host swap %.1fms\n",
|
|
kick_monotonic_ms(), t_swap);
|
|
host.present_pending = true;
|
|
}
|
|
|
|
static void host_frame_done(void *data, struct wl_callback *callback,
|
|
uint32_t callback_data)
|
|
{
|
|
(void)data; (void)callback; (void)callback_data;
|
|
if (host.frame_cb) {
|
|
wl_callback_destroy(host.frame_cb);
|
|
host.frame_cb = NULL;
|
|
}
|
|
host.present_pending = false;
|
|
|
|
/* The pipeline just drained: apply a queued fullscreen change now, while
|
|
no swap is outstanding. Frames presented afterwards (back_dirty) are
|
|
pre-acknowledge and harmless; the first commit after the configure's
|
|
ack will carry the freshly sized buffer. */
|
|
if (host.fs_queued) {
|
|
host.fs_queued = false;
|
|
host_apply_fullscreen(host.fs_target);
|
|
}
|
|
|
|
if (host.back_dirty) {
|
|
host.back_dirty = false;
|
|
present_to_host();
|
|
}
|
|
}
|
|
|
|
static const struct wl_callback_listener host_frame_listener = {
|
|
.done = host_frame_done,
|
|
};
|
|
|
|
/* The scene is rendered by the wlroots GLES2 renderer, which owns its own
|
|
EGL display/context, while we sample that buffer from a *different* EGL
|
|
context (host.egl_display). Without an explicit barrier the GPU may still
|
|
be executing the renderer's draw commands when we import+read the dmabuf,
|
|
which shows as flicker/tearing on content that repaints continuously.
|
|
Make the renderer's context current on this thread and glFinish() so the
|
|
buffer is guaranteed fully rendered before we blit it. */
|
|
static void sync_renderer_context(void) {
|
|
struct wlr_egl *src = wlr_gles2_renderer_get_egl(server.renderer);
|
|
if (!src) return;
|
|
EGLDisplay d = wlr_egl_get_display(src);
|
|
EGLContext c = wlr_egl_get_context(src);
|
|
if (d == EGL_NO_DISPLAY || c == EGL_NO_CONTEXT) return;
|
|
if (d == host.egl_display && c == host.egl_context) return;
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, c);
|
|
glFinish();
|
|
eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
|
|
host.egl_context);
|
|
}
|
|
|
|
static void blit_scene_buffer(struct wlr_buffer *buf) {
|
|
int w = host.width, h = host.height;
|
|
glViewport(0, 0, w, h);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
bool imported = buf && import_scene_buffer(buf, host.import_tex);
|
|
if (imported) {
|
|
static const GLfloat white[4] = { 1, 1, 1, 1 };
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
draw_texture_rect(host.import_tex, buf->width, buf->height,
|
|
0, 0, buf->width, buf->height, 0, 0,
|
|
buf->width, buf->height, w, h, white);
|
|
glDisable(GL_BLEND);
|
|
}
|
|
if (dump_host_req) {
|
|
dump_host_req = false;
|
|
dump_host_back();
|
|
}
|
|
|
|
if (!imported) {
|
|
/* Never present a frame that is only the clear colour: keep the last
|
|
good frame on the host. A later frame with a good import presents
|
|
normally. */
|
|
return;
|
|
}
|
|
|
|
if (host.present_pending) {
|
|
/* Host hasn't shown the previous buffer yet: redraw into the back
|
|
buffer and coalesce the swap until the frame callback arrives. */
|
|
host.back_dirty = true;
|
|
} else {
|
|
present_to_host();
|
|
}
|
|
}
|
|
|
|
/* Standalone (native-output) frame: composite the scene into our swapchain,
|
|
then draw the Alt overlay as a second pass on the same buffer from wlr
|
|
textures. There is no host EGL surface to blit to and no host frame-call
|
|
pacing; the native output's own commit drives presentation. */
|
|
|
|
/* Output powered off: overwrite the just-composited scene buffer with opaque
|
|
black so the committed frame is truly dark (not a stale scene frame).
|
|
Called in both render paths right after a successful build_state. */
|
|
static void draw_power_blank(struct wlr_buffer *buf) {
|
|
struct wlr_render_pass *pass =
|
|
wlr_renderer_begin_buffer_pass(server.renderer, buf, NULL);
|
|
if (!pass) return;
|
|
struct wlr_render_rect_options opts = {
|
|
.box = { .x = 0, .y = 0, .width = buf->width, .height = buf->height },
|
|
.color = { .r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f },
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
|
|
};
|
|
wlr_render_pass_add_rect(pass, &opts);
|
|
wlr_render_pass_submit(pass);
|
|
}
|
|
|
|
/* --- Overlay hardware-plane path (standalone DRM) ---
|
|
The tab overlay / confirm-quit modal is normally composited as a second
|
|
pass on top of the scene buffer. On a real DRM output kick instead asks to
|
|
present it on a hardware plane via wlr_output_layer: the overlay lives in
|
|
its own box-sized buffer and libliftoff scans it out, so the primary buffer
|
|
stays untouched and direct scanout of the focused client keeps running
|
|
underneath. The headless backend claims every overlay layer is accepted
|
|
but renders nothing, so only genuine DRM outputs get a layer.
|
|
|
|
wlroots exposes no static "does the hardware have a spare plane" query; the
|
|
only trustworthy answer comes from a real commit, whose accepted flag says
|
|
whether libliftoff landed the layer. So the first overlay hold of each
|
|
output *is* the probe: commit once, read accepted, and stamp the verdict
|
|
for the session -- fixed hardware gives a fixed answer. After that every
|
|
frame just asks the plane (layer with a buffer) or does not (buffer NULL). */
|
|
|
|
static bool overlay_plane_knob(void) {
|
|
static int knob = -1;
|
|
if (knob < 0) {
|
|
const char *e = getenv("KICK_OVERLAY_PLANE");
|
|
knob = !e || strcmp(e, "0") != 0;
|
|
}
|
|
return knob;
|
|
}
|
|
|
|
/* May this frame try the plane? USED holds are the settled path; UNKNOWN
|
|
means the very next overlay frame is the one-shot probe that decides.
|
|
OFF (headless, nested, knob turned down, or a failed probe) means
|
|
composite always. Whether the persistent layer object exists never
|
|
gates this: creation failure simply degrades to composite. */
|
|
static bool overlay_plane_capable(void) {
|
|
return server.standalone && !server.standalone_headless &&
|
|
overlay_plane_knob() &&
|
|
(server.overlay_plane == OVERLAY_PLANE_USED ||
|
|
server.overlay_plane == OVERLAY_PLANE_UNKNOWN);
|
|
}
|
|
|
|
/* Take or release the attach-render lock that suppresses direct scanout.
|
|
The composite path needs it (the overlay's second pass would otherwise be
|
|
painted over a scanned-out client buffer); the plane path must not hold it
|
|
-- that is the whole point of offloading. Idempotent. */
|
|
static void overlay_set_render_lock(bool want) {
|
|
if (want == server.overlay_render_lock) return;
|
|
if (server.output)
|
|
wlr_output_lock_attach_render(server.output, want);
|
|
server.overlay_render_lock = want;
|
|
}
|
|
|
|
/* Recompute the scanout-lock state from the current overlay state: held while
|
|
the overlay is up on the composite path, open on the plane path. Called
|
|
whenever the overlay appears or disappears. */
|
|
static void overlay_sync_render_lock(void) {
|
|
bool up = host.overlay_visible || confirm_quit_pending;
|
|
overlay_set_render_lock(up && !overlay_plane_capable());
|
|
}
|
|
|
|
/* Take or release the software-cursor lock while the overlay is up on a
|
|
real DRM output. With no cursor layer in the allocation there is
|
|
nothing for the menu layer to trade planes with, so placement stays
|
|
deterministic; the cursor itself is painted into the plane buffer
|
|
(overlay_pass_cursor, gated on this lock) and re-added after the menu
|
|
on the composite path, so it stays visible and topmost. Idempotent;
|
|
headless outputs are already locked permanently at output init and
|
|
never touched here. */
|
|
static void overlay_set_cursor_lock(bool want) {
|
|
if (want == server.overlay_cursor_lock) return;
|
|
if (server.output && server.standalone && !server.standalone_headless)
|
|
wlr_output_lock_software_cursors(server.output, want);
|
|
server.overlay_cursor_lock = want;
|
|
}
|
|
|
|
/* Recompute the cursor-lock state alongside the render lock: held whenever
|
|
the overlay (tab list or modal) is up on DRM. Called from the same
|
|
three places as overlay_sync_render_lock. */
|
|
static void overlay_sync_cursor_lock(void) {
|
|
overlay_set_cursor_lock(host.overlay_visible || confirm_quit_pending);
|
|
}
|
|
|
|
/* (Re)create the box-sized overlay swapchain. The overlay is opaque, so
|
|
XRGB8888 (no alpha) is the plane format; a plane can only ever use a
|
|
format it can actually scan out, and if none fits libliftoff reports the
|
|
layer back for composition so kick falls through to the composite path. */
|
|
static void overlay_plane_resize(int w, int h) {
|
|
if (w < 1) w = 1;
|
|
if (h < 1) h = 1;
|
|
if (server.overlay_swapchain && server.overlay_swapchain->width == w &&
|
|
server.overlay_swapchain->height == h)
|
|
return;
|
|
static uint64_t mods[] = { DRM_FORMAT_MOD_LINEAR };
|
|
struct wlr_drm_format fmt = {
|
|
.format = DRM_FORMAT_XRGB8888,
|
|
.len = 1,
|
|
.capacity = 1,
|
|
.modifiers = mods,
|
|
};
|
|
if (server.overlay_swapchain) {
|
|
wlr_swapchain_destroy(server.overlay_swapchain);
|
|
server.overlay_swapchain = NULL;
|
|
}
|
|
server.overlay_swapchain =
|
|
wlr_swapchain_create(server.allocator, w, h, &fmt);
|
|
}
|
|
|
|
/* Render the current overlay into the box-sized plane buffer. Returns the
|
|
locked buffer (caller unlocks it after the commit that presented it). */
|
|
static struct wlr_buffer *overlay_render_plane(const struct overlay_box *box) {
|
|
if (!server.overlay_swapchain) return NULL;
|
|
struct wlr_buffer *buf = wlr_swapchain_acquire(server.overlay_swapchain);
|
|
if (!buf) return NULL;
|
|
struct wlr_render_pass *pass =
|
|
wlr_renderer_begin_buffer_pass(server.renderer, buf, NULL);
|
|
if (!pass) {
|
|
wlr_buffer_unlock(buf);
|
|
return NULL;
|
|
}
|
|
if (confirm_quit_pending)
|
|
draw_confirm_quit_pass(pass, box->x, box->y);
|
|
else
|
|
draw_tab_overlay_pass(pass, box->x, box->y);
|
|
/* Cursor last: above overlay above content, wherever this buffer lands. */
|
|
overlay_pass_cursor(pass, box->x, box->y);
|
|
wlr_render_pass_submit(pass);
|
|
return buf;
|
|
}
|
|
|
|
static void render_standalone(void) {
|
|
if (!server.scene_output || !server.swapchain) return;
|
|
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
apply_pending_output_resize(&state);
|
|
|
|
bool overlay_up = host.overlay_visible || confirm_quit_pending;
|
|
double t_build = 0, t_commit = 0;
|
|
bool frame_committed = false;
|
|
|
|
/* Plane path: paint the overlay into its own small buffer and present
|
|
it on the output's persistent layer, which libliftoff scans out on a
|
|
hardware plane. The layer lives as long as the output; every commit
|
|
carries its state (a NULL buffer disables it when the menu is
|
|
hidden), so the backend's allocation settles instead of re-solving
|
|
around a fresh layer object on every frame. The first hold of each
|
|
output is the one-shot probe: the commit tells us whether libliftoff
|
|
accepted the layer, which becomes the session answer. */
|
|
struct overlay_box box;
|
|
bool have_box = false;
|
|
struct wlr_buffer *plane_buf = NULL;
|
|
/* A rejected plane frame falls back to composite for the rest of the
|
|
hold; the next hold re-probes (see update_overlay_visible). */
|
|
bool plane_attempt = overlay_up && overlay_plane_capable() &&
|
|
server.overlay_layer;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG && server.standalone)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] frame start t=%.0f vis=%d plane=%d\n",
|
|
kick_monotonic_ms(), overlay_up, plane_attempt);
|
|
if (plane_attempt) {
|
|
int nrows = confirm_quit_pending ? overlay_confirm_box(&box)
|
|
: overlay_tab_box(&box);
|
|
if (nrows > 0) {
|
|
overlay_plane_resize(box.w, box.h);
|
|
plane_buf = overlay_render_plane(&box);
|
|
have_box = plane_buf != NULL;
|
|
}
|
|
plane_attempt = have_box;
|
|
}
|
|
if (server.overlay_layer) {
|
|
/* Always describe the one persistent layer: with a buffer while
|
|
asking for the plane, NULL to park it while hidden (FB_ID 0
|
|
drops the plane). The array must stay valid until finish. */
|
|
server.overlay_layer_state.layer = server.overlay_layer;
|
|
server.overlay_layer_state.buffer = plane_buf;
|
|
if (have_box) {
|
|
server.overlay_layer_state.src_box = (struct wlr_fbox){
|
|
.x = 0, .y = 0, .width = box.w, .height = box.h };
|
|
server.overlay_layer_state.dst_box = (struct wlr_box){
|
|
.x = box.x, .y = box.y, .width = box.w, .height = box.h };
|
|
} else {
|
|
server.overlay_layer_state.src_box = (struct wlr_fbox){0};
|
|
server.overlay_layer_state.dst_box = (struct wlr_box){0};
|
|
}
|
|
server.overlay_layer_state.damage = NULL;
|
|
wlr_output_state_set_layers(&state, &server.overlay_layer_state, 1);
|
|
}
|
|
|
|
/* Composite path: the overlay joins the scene's own buffer. Its box is
|
|
invisible to the scene's damage ring, so while it is up every frame
|
|
must repaint the whole scene beneath it (a partial repaint would stack
|
|
coats of the translucent backdrop toward opaque). */
|
|
bool composite_overlay = overlay_up && !plane_attempt;
|
|
if (composite_overlay) {
|
|
overlay_set_render_lock(true);
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
}
|
|
|
|
struct wlr_scene_output_state_options options = {
|
|
.swapchain = server.swapchain,
|
|
};
|
|
double t_begin = kick_monotonic_ms();
|
|
bool rendered = wlr_scene_output_build_state(server.scene_output, &state,
|
|
&options);
|
|
t_build = kick_monotonic_ms() - t_begin;
|
|
|
|
if (!rendered || !state.buffer) {
|
|
/* Nothing to repaint. An idle hold keeps whatever the plane already
|
|
scans out (planes persist across frames); every overlay show/hide
|
|
forces a repaint through standalone_overlay_damage(), so a menu
|
|
appearing or clearing always goes out on a rendered frame.
|
|
Drop this frame's plane buffer before finishing the (empty)
|
|
state exactly once; the persistent layer entry stays described
|
|
(with this buffer still attached) so the next attempt reuses the
|
|
settled allocation. */
|
|
kick_log(KICK_LOG_DEBUG, "[render] build_state failed\n");
|
|
if (plane_buf) {
|
|
wlr_buffer_unlock(plane_buf);
|
|
plane_buf = NULL;
|
|
server.overlay_layer_state.buffer = NULL;
|
|
}
|
|
plane_attempt = false;
|
|
goto frame_done;
|
|
}
|
|
|
|
/* Output powered off (wlopm): blank the composited frame before commit. */
|
|
if (!server.output_powered)
|
|
draw_power_blank(state.buffer);
|
|
|
|
/* Same diagnostic as the nested path below: what did wlroots composite? */
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
struct wlr_output *out = server.output;
|
|
kick_log(KICK_LOG_DEBUG, "[diag] frame buffer %dx%d (output %dx%d)\n",
|
|
state.buffer->width, state.buffer->height,
|
|
out ? out->width : 0, out ? out->height : 0);
|
|
}
|
|
|
|
if (dump_scene_req) {
|
|
dump_scene_req = false;
|
|
dump_scene_buffer(state.buffer);
|
|
}
|
|
|
|
if (composite_overlay) {
|
|
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
|
server.renderer, state.buffer, NULL);
|
|
if (pass) {
|
|
draw_tab_overlay_pass(pass, 0, 0);
|
|
/* Cursor above overlay above content: the scene pass baked a
|
|
software cursor into the primary underneath the menu, so
|
|
re-add it on top (a no-op while a hardware cursor plane is
|
|
active -- that cursor already scans out above every layer). */
|
|
wlr_output_add_software_cursors_to_render_pass(server.output,
|
|
pass, NULL);
|
|
wlr_render_pass_submit(pass);
|
|
}
|
|
}
|
|
|
|
if (dump_host_req) {
|
|
dump_host_req = false;
|
|
dump_standalone_buffer(state.buffer);
|
|
}
|
|
|
|
/* The plane buffer is referenced (not locked) by the layer state, so it
|
|
must stay alive through the commit below; unlock only after commit
|
|
(or in frame_done for the no-commit paths). */
|
|
apply_tearing_hint(&state);
|
|
double t_c = kick_monotonic_ms();
|
|
bool committed = wlr_output_commit_state(server.output, &state);
|
|
t_commit = kick_monotonic_ms() - t_c;
|
|
frame_committed = committed;
|
|
if (!committed) {
|
|
/* The commit did not land (e.g. a DRM golden frame during a modeset).
|
|
Whatever the last landed frame left on the plane stays until the
|
|
next landed one, so re-ask the same intent next frame; there is no
|
|
cross-frame state to fix up. */
|
|
kick_log(KICK_LOG_DEBUG, "[render] output commit failed, retrying\n");
|
|
kick_request_redraw();
|
|
goto frame_done;
|
|
}
|
|
output_mgmt_sync();
|
|
|
|
/* Witness for the overlay plane: whether libliftoff placed this frame's
|
|
layer, plus the box it was asked to scan out (WxH+X+Y in output
|
|
pixels). KMS offers no per-layer name, so the box -- which sizes the
|
|
plane's FB exactly -- is the off-process handle: a VT test matches
|
|
the live kernel plane by FB size and hold/release timing. */
|
|
if (plane_attempt) {
|
|
kick_log(KICK_LOG_DEBUG, "[render] overlay plane accepted=%d "
|
|
"box=%dx%d+%d+%d layer=%p buf=%dx%d\n",
|
|
server.overlay_layer_state.accepted ? 1 : 0,
|
|
box.w, box.h, box.x, box.y,
|
|
(void *)server.overlay_layer,
|
|
plane_buf ? plane_buf->width : 0,
|
|
plane_buf ? plane_buf->height : 0);
|
|
}
|
|
|
|
if (plane_attempt && server.overlay_plane == OVERLAY_PLANE_UNKNOWN) {
|
|
/* One-shot probe: the accepted flag is only populated by a real
|
|
commit -- libliftoff never writes it on a test-only path -- so this
|
|
is the authoritative "does the hardware have a plane for us"
|
|
answer. */
|
|
if (server.overlay_layer_state.accepted) {
|
|
kick_log(KICK_LOG_DEBUG, "[render] overlay plane available\n");
|
|
server.overlay_plane = OVERLAY_PLANE_USED;
|
|
} else {
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[render] overlay plane not available, compositing\n");
|
|
server.overlay_plane = OVERLAY_PLANE_OFF;
|
|
/* The probe frame already committed with the layer dropped: make
|
|
the menu appear anyway by re-rendering it composited. */
|
|
kick_request_redraw();
|
|
}
|
|
} else if (plane_attempt && !server.overlay_layer_state.accepted) {
|
|
/* Rejected mid-hold (allocation shifted under us): composite for
|
|
the rest of the hold so the menu never drops a frame. The next
|
|
hold re-probes. */
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[render] overlay plane rejected, compositing\n");
|
|
server.overlay_plane = OVERLAY_PLANE_OFF;
|
|
overlay_set_render_lock(true);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
frame_done:
|
|
/* Finish the output state first: the layers array must stay valid until
|
|
finish returns. The layer itself persists across frames (created with
|
|
the output); only unlock this frame's plane buffer, which the commit
|
|
just consumed. */
|
|
wlr_output_state_finish(&state);
|
|
if (plane_buf) {
|
|
wlr_buffer_unlock(plane_buf);
|
|
plane_buf = NULL;
|
|
}
|
|
if (kick_log_level() >= KICK_LOG_DEBUG && server.standalone)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] frame result t=%.0f vis=%d "
|
|
"plane=%d build=%.1f commit=%.1f ok=%d\n",
|
|
kick_monotonic_ms(), overlay_up, plane_attempt,
|
|
t_build, t_commit, frame_committed);
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
wlr_scene_output_send_frame_done(server.scene_output, &now);
|
|
}
|
|
|
|
static void render_and_present(void) {
|
|
if (server.standalone) {
|
|
render_standalone();
|
|
return;
|
|
}
|
|
if (!host.egl_ok || !host.configured) return;
|
|
if (!server.scene_output || !server.swapchain) return;
|
|
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
apply_pending_output_resize(&state);
|
|
/* Same rationale as render_standalone(): the overlay paints outside the
|
|
scene graph, so partial repaints would stack coats on its box. */
|
|
if (host.overlay_visible)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
struct wlr_scene_output_state_options options = {
|
|
.swapchain = server.swapchain,
|
|
};
|
|
double t_build = kick_monotonic_ms();
|
|
bool rendered = wlr_scene_output_build_state(server.scene_output, &state,
|
|
&options);
|
|
t_build = kick_monotonic_ms() - t_build;
|
|
if (!rendered || !state.buffer) {
|
|
kick_log(KICK_LOG_DEBUG, "[render] build_state failed\n");
|
|
wlr_output_state_finish(&state);
|
|
goto frame_done;
|
|
}
|
|
|
|
/* Output powered off (wlopm): blank the composited frame before commit. */
|
|
if (!server.output_powered)
|
|
draw_power_blank(state.buffer);
|
|
|
|
/* [diag] What did wlroots composite? A swapchain-sized buffer means we
|
|
composited the scene; anything else implies direct scanout of a client
|
|
buffer (which our blit may or may not import). */
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
struct wlr_output *out = server.output;
|
|
kick_log(KICK_LOG_DEBUG, "[diag] frame buffer %dx%d (output %dx%d)\n",
|
|
state.buffer->width, state.buffer->height,
|
|
out ? out->width : 0, out ? out->height : 0);
|
|
}
|
|
|
|
/* Barrier between the wlr renderer's context and our host blit. */
|
|
double t_sync = kick_monotonic_ms();
|
|
sync_renderer_context();
|
|
t_sync = kick_monotonic_ms() - t_sync;
|
|
|
|
if (dump_scene_req) {
|
|
dump_scene_req = false;
|
|
dump_scene_buffer(state.buffer);
|
|
}
|
|
|
|
static int autodump = -1;
|
|
if (autodump < 0)
|
|
autodump = getenv("KICK_AUTODUMP") ? 1 : 0;
|
|
if (autodump > 0 && client_dumped && --autodump == 0) {
|
|
dump_scene_req = true;
|
|
dump_host_req = true;
|
|
/* The flags are consumed by the *next* render pass; under the
|
|
idle-stop gate that pass needs an explicit redraw request. */
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Overlay joins the frame before the blit, drawn exactly as standalone
|
|
draws it -- one renderer for both modes; the blit stays a pure copy. */
|
|
double t_overlay = 0.0;
|
|
if (host.overlay_visible || confirm_quit_pending) {
|
|
t_overlay = kick_monotonic_ms();
|
|
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
|
server.renderer, state.buffer, NULL);
|
|
if (pass) {
|
|
draw_tab_overlay_pass(pass, 0, 0);
|
|
wlr_render_pass_submit(pass);
|
|
}
|
|
t_overlay = kick_monotonic_ms() - t_overlay;
|
|
}
|
|
|
|
double t_blit = kick_monotonic_ms();
|
|
blit_scene_buffer(state.buffer);
|
|
t_blit = kick_monotonic_ms() - t_blit;
|
|
|
|
apply_tearing_hint(&state);
|
|
double t_commit = kick_monotonic_ms();
|
|
if (!wlr_output_commit_state(server.output, &state))
|
|
kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n");
|
|
else
|
|
output_mgmt_sync();
|
|
t_commit = kick_monotonic_ms() - t_commit;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] nested frame t=%.0f "
|
|
"build=%.1f sync=%.1f overlay=%.1f blit=%.1f commit=%.1f\n",
|
|
kick_monotonic_ms(),
|
|
t_build, t_sync, t_overlay, t_blit, t_commit);
|
|
|
|
frame_done:
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
wlr_scene_output_send_frame_done(server.scene_output, &now);
|
|
|
|
wlr_output_state_finish(&state);
|
|
}
|
|
|
|
/* ===================== Launch client ===================== */
|
|
|
|
|
|
/* D-Bus/systemd activation environment, mirroring what labwc does on
|
|
launch (DRM backend): D-Bus-activated services and systemd user units
|
|
otherwise keep whatever WAYLAND_DISPLAY etc. they were started with --
|
|
typically a previous compositor's dead socket or nothing -- and clients
|
|
talking to them (panels, portals) stall into timeouts. Standalone only:
|
|
a nested kick must never rewrite its host session's activation
|
|
environment. Requires a session bus (like labwc, gated on
|
|
DBUS_SESSION_BUS_ADDRESS) and the dbus-update-activation-environment
|
|
helper; fire-and-forget -- sigchld_handler reaps the child, and the
|
|
helper failing (no bus, no binary) is silent by design. */
|
|
static void activation_run(bool clear) {
|
|
static const char *const vars[] = {
|
|
"WAYLAND_DISPLAY",
|
|
"DISPLAY",
|
|
"XDG_CURRENT_DESKTOP",
|
|
"XDG_SESSION_TYPE",
|
|
"XCURSOR_SIZE",
|
|
"XCURSOR_THEME",
|
|
};
|
|
/* WAYLAND_DISPLAY is always exported (our socket, set below); the rest
|
|
only when present in our own environment, so absent values are never
|
|
pushed as empty over a live session's settings -- except on clear,
|
|
where empty is exactly labwc's documented way of blanking them. */
|
|
const char *args[2 + 7 + 1];
|
|
int argc = 0;
|
|
args[argc++] = "dbus-update-activation-environment";
|
|
args[argc++] = "--systemd";
|
|
char wayland_value[128];
|
|
char clear_values[6][256];
|
|
int nclear = 0;
|
|
for (size_t i = 0; i < sizeof(vars) / sizeof(vars[0]); i++) {
|
|
if (strcmp(vars[i], "WAYLAND_DISPLAY") == 0) {
|
|
snprintf(wayland_value, sizeof(wayland_value), "WAYLAND_DISPLAY=%s",
|
|
clear ? "" : kick_socket);
|
|
args[argc++] = wayland_value;
|
|
} else if (getenv(vars[i])) {
|
|
if (clear) {
|
|
if (nclear < 6) {
|
|
snprintf(clear_values[nclear], sizeof(clear_values[0]),
|
|
"%s=", vars[i]);
|
|
args[argc++] = clear_values[nclear++];
|
|
}
|
|
} else {
|
|
args[argc++] = vars[i];
|
|
}
|
|
}
|
|
}
|
|
args[argc] = NULL;
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
/* Child: export into the updater's environment, then replace. */
|
|
if (!clear)
|
|
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
|
else
|
|
setenv("WAYLAND_DISPLAY", "", 1);
|
|
execvp(args[0], (char *const *)args);
|
|
_exit(127);
|
|
}
|
|
}
|
|
|
|
/* Push kick's session variables into the D-Bus/systemd activation
|
|
environment once the socket is listening (call before first clients).
|
|
Registers the matching clear for every exit() path; the SIGTERM/SIGINT
|
|
loop callback clears explicitly before its _exit(). Blank the variables
|
|
again on the way out so later activations in this login session don't
|
|
inherit kick's now-removed socket (labwc parity). */
|
|
static void activation_clear_env(void) {
|
|
if (!server.standalone)
|
|
return;
|
|
if (!getenv("DBUS_SESSION_BUS_ADDRESS"))
|
|
return;
|
|
if (!cmd_in_path("dbus-update-activation-environment"))
|
|
return;
|
|
activation_run(true);
|
|
}
|
|
|
|
static void activation_update(void) {
|
|
if (!server.standalone)
|
|
return;
|
|
if (!getenv("DBUS_SESSION_BUS_ADDRESS"))
|
|
return;
|
|
if (!cmd_in_path("dbus-update-activation-environment"))
|
|
return;
|
|
kick_log(KICK_LOG_INFO, "activation environment updated\n");
|
|
activation_run(false);
|
|
atexit(activation_clear_env);
|
|
}
|
|
|
|
/* Does the first whitespace-delimited word of `cmd` resolve to an executable
|
|
on PATH? Mirrors how `sh -c <cmd>` resolves the first word, so this is a
|
|
cheap pre-launch "which" check. Returns 1 if found (or uncheckable, e.g.
|
|
a word too long or no PATH), 0 if the word is definitely missing. */
|
|
static int cmd_in_path(const char *cmd) {
|
|
while (*cmd == ' ' || *cmd == '\t')
|
|
cmd++;
|
|
const char *p = cmd;
|
|
while (*p && *p != ' ' && *p != '\t' && *p != '\n')
|
|
p++;
|
|
if (p == cmd)
|
|
return 0;
|
|
char word[256];
|
|
size_t n = (size_t)(p - cmd);
|
|
if (n >= sizeof(word))
|
|
return 1; /* give up on absurdly long commands */
|
|
memcpy(word, cmd, n);
|
|
word[n] = '\0';
|
|
|
|
if (strchr(word, '/'))
|
|
return access(word, X_OK) == 0;
|
|
|
|
const char *path_env = getenv("PATH");
|
|
if (!path_env) path_env = "/usr/local/bin:/usr/bin:/bin";
|
|
char buf[512];
|
|
for (const char *d = path_env; d && *d; ) {
|
|
const char *colon = strchr(d, ':');
|
|
size_t len = colon ? (size_t)(colon - d) : strlen(d);
|
|
if (len == 0) {
|
|
len = 1;
|
|
buf[0] = '.';
|
|
} else {
|
|
if (len + 1 + n >= sizeof(buf)) {
|
|
if (colon) d = colon + 1; else d = NULL;
|
|
continue;
|
|
}
|
|
memcpy(buf, d, len);
|
|
buf[len] = '/';
|
|
}
|
|
memcpy(buf + len + 1, word, n);
|
|
buf[len + 1 + n] = '\0';
|
|
if (access(buf, X_OK) == 0)
|
|
return 1;
|
|
if (colon) d = colon + 1; else d = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* Kick runs its own plain Xwayland (see the wlr_xwayland_create call site):
|
|
no -auth, so any local client connects without a magic cookie and nothing
|
|
is written to the session Xauthority. This is the normal wlroots default
|
|
and avoids the legacy X11 access-control dance entirely. */
|
|
#endif
|
|
|
|
static void launch_client(struct kick_window *w) {
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
/* Own process group so we can kill the whole client tree on exit. */
|
|
setpgid(0, 0);
|
|
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!xdg_runtime) xdg_runtime = "/tmp";
|
|
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
|
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
|
|
/* Mode selection is kick's own config: KICK_MODE/WLR_BACKENDS must
|
|
never leak into children, or a nested kick started from a tab
|
|
inherits standalone (or forced backends) and dies failing to
|
|
open DRM -- the same reason the test harness strips both for
|
|
nested spawns. Explicit per-command values still work: the
|
|
command's own assignments happen after this scrub. */
|
|
unsetenv("KICK_MODE");
|
|
unsetenv("WLR_BACKENDS");
|
|
/* Toolkits self-select their connection: Wayland-capable ones see
|
|
only our socket above, X11-only ones (emacs-gtk, xmessage) take
|
|
DISPLAY below. Never force GDK_BACKEND/QT_QPA_PLATFORM to
|
|
"wayland" here: an explicit value removes the toolkit's fallback,
|
|
so an X11-only GTK build dies with a misleading "cannot open
|
|
display" even though our Xwayland is reachable. */
|
|
unsetenv("XAUTHORITY");
|
|
#if HAVE_XWAYLAND
|
|
/* X11 commands connect through our permissive Xwayland. */
|
|
if (server.xwayland && server.xwayland->display_name) {
|
|
setenv("DISPLAY", server.xwayland->display_name, 1);
|
|
} else
|
|
unsetenv("DISPLAY");
|
|
#else
|
|
unsetenv("DISPLAY");
|
|
#endif
|
|
/* Unless debugging, don't let client chatter spill onto the launching
|
|
terminal; the client inherits our stdout/stderr. */
|
|
if (kick_log_level() < KICK_LOG_DEBUG) {
|
|
int nullfd = open("/dev/null", O_WRONLY);
|
|
if (nullfd >= 0) {
|
|
dup2(nullfd, STDOUT_FILENO);
|
|
dup2(nullfd, STDERR_FILENO);
|
|
if (nullfd > STDERR_FILENO) close(nullfd);
|
|
}
|
|
}
|
|
execlp("/bin/sh", "sh", "-c", w->cmd, NULL);
|
|
_exit(127);
|
|
}
|
|
if (pid > 0) {
|
|
/* Claim the child's process group from the parent side too: the
|
|
child setpgid's itself, but under load it can sit descheduled
|
|
between fork and setpgid while we already run -- and a SIGCHLD in
|
|
that window makes sweep_dead_startup_groups conclude ESRCH on a
|
|
group that simply does not exist yet, dropping a live tab (the
|
|
orphaned command keeps running with no tab). Either side's
|
|
setpgid wins; failure here just means the child beat us to it
|
|
(or died already, which the sweep then reports honestly). */
|
|
setpgid(pid, pid);
|
|
w->pid = pid;
|
|
kick_log(KICK_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
|
|
tab_index(w->tab), pid, w->cmd);
|
|
}
|
|
}
|
|
|
|
/* Fork every command-line command at startup, in command-line order. Each
|
|
tab keeps its position because handle_new_toplevel claims its pending
|
|
window by process group (launch_client runs each command in its own
|
|
setpgid'd group, whose id equals w->pid), so the tab order mirrors the
|
|
command line no matter which client connects first. A command that never
|
|
connects leaves its tab black; a command whose process group exits without
|
|
ever connecting is swept away later (see sweep_dead_startup_groups). */
|
|
static void launch_all_cli(void) {
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
struct kick_window *w;
|
|
wl_list_for_each(w, &tab->windows, link)
|
|
if (w->origin == ORIGIN_CLI)
|
|
launch_client(w);
|
|
}
|
|
}
|
|
|
|
/* Drop black tabs whose command's whole process group has exited (e.g.
|
|
`kick /bin/false` -- sh runs false, the group empties, and the
|
|
tab would otherwise stay black forever). Runs from the main loop after a
|
|
SIGCHLD, never from the handler: it frees slots. Group liveness, not the
|
|
launcher pid's, decides: `sh -c "cmd &"` returns while cmd is still
|
|
starting, but cmd holds the group open, so kill(-groupid,0) succeeds and
|
|
the tab survives. */
|
|
/* Human words for a raw waitpid status, for the sweep's diagnostics. */
|
|
static void describe_child_status(int status, char *buf, size_t cap) {
|
|
if (WIFEXITED(status))
|
|
snprintf(buf, cap, "exit code %d", WEXITSTATUS(status));
|
|
else if (WIFSIGNALED(status))
|
|
snprintf(buf, cap, "killed by signal %d", WTERMSIG(status));
|
|
else
|
|
snprintf(buf, cap, "status 0x%x", status);
|
|
}
|
|
|
|
static void sweep_dead_startup_groups(void) {
|
|
struct kick_tab *tab, *tab_tmp;
|
|
wl_list_for_each_safe(tab, tab_tmp, &server.tabs, link) {
|
|
struct kick_window *w, *wt;
|
|
wl_list_for_each_safe(w, wt, &tab->windows, link) {
|
|
if (w->toplevel || !w->pending_claim || w->closing || w->pid <= 0)
|
|
continue;
|
|
errno = 0;
|
|
if (kill(-w->pid, 0) == 0 || errno == EPERM)
|
|
continue; /* the group still has a member */
|
|
if (errno != ESRCH)
|
|
continue;
|
|
/* The group id is not observable, but the launcher itself may
|
|
still be alive -- e.g. descheduled between fork and setpgid,
|
|
or daemonized out of its group. Sweeping now would drop a
|
|
live tab (and orphan its command); leave it for the next
|
|
SIGCHLD, by which time a dead launcher fails both kills. */
|
|
if (w->pid > 0 && (kill(w->pid, 0) == 0 || errno == EPERM))
|
|
continue;
|
|
/* Say why on stderr regardless of log level: at default levels the
|
|
client's own output is /dev/null'd and kick_log INFO is hidden,
|
|
so a command that dies before opening its window would otherwise
|
|
end the session (last tab) or shrink it with no visible
|
|
explanation. */
|
|
char why[64];
|
|
why[0] = '\0';
|
|
if ((pid_t)last_child_pid == w->pid)
|
|
describe_child_status((int)last_child_status, why, sizeof(why));
|
|
const char *cmd = w->cmd ? w->cmd : "?";
|
|
if (why[0])
|
|
fprintf(stderr, "kick: tab %d (%s): command exited without "
|
|
"opening a window (%s)\n", tab_index(tab), cmd, why);
|
|
else
|
|
fprintf(stderr, "kick: tab %d (%s): command exited without "
|
|
"opening a window\n", tab_index(tab), cmd);
|
|
/* Drop only this dead placeholder: the tab may still hold live
|
|
windows (group members, a racing claim), which must survive.
|
|
Succession runs only when the tab empties; remove_tab on an
|
|
empty tab just unlinks and frees the shell. */
|
|
if (w->close_timer) {
|
|
wl_event_source_remove(w->close_timer);
|
|
w->close_timer = NULL;
|
|
}
|
|
if (w->icon_wlr) {
|
|
wlr_texture_destroy(w->icon_wlr);
|
|
w->icon_wlr = NULL;
|
|
}
|
|
icon_proto_release(w);
|
|
mru_remove(w);
|
|
wl_list_remove(&w->link);
|
|
free(w->cmd);
|
|
free(w->tag);
|
|
free(w->tag_desc);
|
|
free(w);
|
|
if (wl_list_empty(&tab->windows)) {
|
|
if (server.active_tab == tab) {
|
|
server.active_tab = NULL;
|
|
struct kick_tab *succ = pick_successor(tab, 0);
|
|
if (!succ) succ = pick_successor(tab, 1);
|
|
if (succ)
|
|
set_active_tab(succ);
|
|
else {
|
|
/* the last tab died before ever connecting: tear down */
|
|
fprintf(stderr, "kick: no tabs left to show, exiting\n");
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
}
|
|
remove_tab(tab);
|
|
break; /* the tab is gone; stop walking its windows */
|
|
}
|
|
}
|
|
}
|
|
/* If the sweep's succession handed active to a tab that this same pass
|
|
then removed, put the crown back on some live tab (or nothing). */
|
|
if (!server.active_tab) {
|
|
struct kick_tab *succ = NULL;
|
|
struct kick_tab *t;
|
|
wl_list_for_each(t, &server.tabs, link)
|
|
if (tab_live(t) && !tab_closing(t)) {
|
|
succ = t;
|
|
break;
|
|
}
|
|
if (succ)
|
|
set_active_tab(succ);
|
|
}
|
|
}
|
|
|
|
/* Drain the SIGCHLD latch and reap dead startup groups; called from both
|
|
event loops after each dispatch. */
|
|
static void sweep_dead_startup(void) {
|
|
if (!sigchld_seen) return;
|
|
sigchld_seen = 0;
|
|
sweep_dead_startup_groups();
|
|
}
|
|
|
|
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
|
|
given as a command-line argument. The tab is inserted in the tab order
|
|
right after the currently active tab (falling back to the far right if
|
|
none is active), so the new tab sits to its right in the cycle. It
|
|
carries a still-black pending window, exactly like a command-line
|
|
command, until its client claims the slot. Returns the new tab's index,
|
|
or -1 on allocation failure. */
|
|
static int add_tab(const char *cmd) {
|
|
struct kick_tab *tab = calloc(1, sizeof(*tab));
|
|
if (!tab) return -1;
|
|
wl_list_init(&tab->windows);
|
|
/* Anchor on the active tab itself, not its toplevel: while the active
|
|
tab is still black (its client has not connected yet) its pending
|
|
window has no toplevel either, and inserting after the tab is what
|
|
keeps the new terminal beside the current tab. */
|
|
insert_tab_after(tab, server.active_tab);
|
|
|
|
struct kick_window *w = window_alloc(tab);
|
|
if (!w) {
|
|
wl_list_remove(&tab->link);
|
|
free(tab);
|
|
return -1;
|
|
}
|
|
w->cmd = strdup(cmd);
|
|
if (!w->cmd) {
|
|
wl_list_remove(&w->link);
|
|
wl_list_remove(&tab->link);
|
|
free(tab);
|
|
return -1;
|
|
}
|
|
w->pid = -1;
|
|
w->pending_claim = true;
|
|
w->origin = ORIGIN_ADD_TAB;
|
|
launch_client(w);
|
|
/* Input binding added a visible row: repaint like any other
|
|
keybinding. (The fork/exec itself produces no commit, damage or
|
|
signal, so without this nothing would schedule the frame.) */
|
|
if (host.overlay_visible) {
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
else if (server.scene_output)
|
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
|
kick_request_redraw();
|
|
}
|
|
return tab_index(tab);
|
|
}
|
|
|
|
/* ===================== Crash handler ===================== */
|
|
|
|
/* Unreachable by design in tests: only runs on SIGSEGV/SIGABRT --
|
|
excluded from line coverage. */
|
|
/* GCOVR_EXCL_START */
|
|
static void crash_handler(int sig, siginfo_t *info, void *ctx) {
|
|
(void)sig; (void)info; (void)ctx;
|
|
void *bt[64];
|
|
int n = backtrace(bt, 64);
|
|
write(2, "\n=== BACKTRACE ===\n", 19);
|
|
backtrace_symbols_fd(bt, n, 2);
|
|
remove_socket_file();
|
|
_exit(139);
|
|
}
|
|
/* GCOVR_EXCL_STOP */
|
|
|
|
static void sigchld_handler(int sig) {
|
|
(void)sig;
|
|
sigchld_seen = 1;
|
|
int status;
|
|
pid_t pid;
|
|
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
last_child_pid = (sig_atomic_t)pid;
|
|
last_child_status = (sig_atomic_t)status;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
/* snprintf()/write() are async-signal-safe; fprintf() is not (it
|
|
takes the FILE lock, which can deadlock if we interrupt a logging
|
|
fprintf in the main loop). */
|
|
char buf[64];
|
|
int n = snprintf(buf, sizeof(buf),
|
|
"[child] pid %d exited status 0x%x\n", pid, status);
|
|
if (n > 0 && (size_t)n < sizeof(buf))
|
|
(void)!write(STDERR_FILENO, buf, (size_t)n);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* On SIGTERM or Ctrl-C (SIGINT), take down the clients and exit cleanly
|
|
rather than leaving them orphaned when the compositor dies. The clients
|
|
live in their own process groups, so a terminal Ctrl-C only reaches us.
|
|
|
|
These signals are delivered through the event loop (wl_event_loop_add_signal
|
|
in main) rather than a raw sigaction handler: the full teardown calls
|
|
malloc/free and libwayland proxy destroys, which are not async-signal-safe
|
|
and deadlock if the signal interrupts fork()/malloc()/logging (observed:
|
|
SIGTERM landing inside launch_client's fork() left libc's allocator lock
|
|
held, and shutdown_cleanup() -> bridge_cleanup() -> calloc() blocked on it
|
|
forever). Running it from a normal loop callback makes that impossible. */
|
|
static int close_signal_cb(int sig, void *data) {
|
|
(void)sig;
|
|
(void)data;
|
|
shutdown_cleanup();
|
|
activation_clear_env();
|
|
#ifdef KICK_COVERAGE
|
|
/* gcov's atexit flush never runs on _exit(); dump counters explicitly
|
|
so SIGTERM-driven test teardown still records coverage. */
|
|
extern void __gcov_dump(void);
|
|
__gcov_dump();
|
|
#endif
|
|
_exit(0);
|
|
return 0;
|
|
}
|
|
|
|
/* SIGUSR1: save the current scene buffer and host back buffer on the next
|
|
frame, for inspecting what the client has actually drawn so far. */
|
|
static void sigusr1_handler(int sig) {
|
|
(void)sig;
|
|
dump_scene_req = 1;
|
|
dump_host_req = 1;
|
|
needs_redraw = 1;
|
|
/* A signal can land while the loop is blocked in poll on an idle
|
|
compositor; the write wakes it so the dump still renders. */
|
|
if (server.sig_pipe[1] >= 0) {
|
|
char c = 1;
|
|
(void)!write(server.sig_pipe[1], &c, 1);
|
|
}
|
|
}
|
|
|
|
/* Drain the SIGUSR1 self-pipe wake; the render gate (not this callback)
|
|
decides what to present. */
|
|
static int sigpipe_cb(int fd, uint32_t mask, void *data) {
|
|
(void)mask; (void)data;
|
|
char buf[64];
|
|
while (read(fd, buf, sizeof(buf)) > 0)
|
|
;
|
|
return 0;
|
|
}
|
|
|
|
/* Does the comma-separated WLR_BACKENDS list contain `name`? */
|
|
static bool backends_include(const char *env, const char *name) {
|
|
size_t len = strlen(name);
|
|
for (const char *p = env; p && *p; ) {
|
|
const char *comma = strchr(p, ',');
|
|
size_t n = comma ? (size_t)(comma - p) : strlen(p);
|
|
if (n == len && strncmp(p, name, len) == 0)
|
|
return true;
|
|
p = comma ? comma + 1 : NULL;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ===================== Main ===================== */
|
|
|
|
/* Parse a --screen-mode value: WxH or WxH@Hz (the Hz is matched at the
|
|
mode's mHz precision, so "60" == 60.000). Stores into mode_req_w/h/
|
|
refresh; returns false on a malformed value. */
|
|
static bool parse_screen_mode(const char *val) {
|
|
int w = 0, h = 0;
|
|
const char *p = val;
|
|
for (; *p >= '0' && *p <= '9'; p++)
|
|
w = w * 10 + (*p - '0');
|
|
if (*p != 'x')
|
|
return false;
|
|
p++;
|
|
for (; *p >= '0' && *p <= '9'; p++)
|
|
h = h * 10 + (*p - '0');
|
|
if (w <= 0 || h <= 0)
|
|
return false;
|
|
unsigned refresh_mhz = 0;
|
|
if (*p == '@') {
|
|
char *end = NULL;
|
|
double hz = strtod(p + 1, &end);
|
|
if (end == p + 1 || hz <= 0.0 || *end != '\0')
|
|
return false;
|
|
refresh_mhz = (unsigned)(hz * 1000.0 + 0.5);
|
|
} else if (*p != '\0') {
|
|
return false;
|
|
}
|
|
mode_req_w = w;
|
|
mode_req_h = h;
|
|
mode_req_refresh = (int)refresh_mhz;
|
|
return true;
|
|
}
|
|
|
|
/* True when this process is the graphical session itself (standalone) rather
|
|
than a client of one (nested): our controlling terminal is a real VT
|
|
(/dev/tty<digits>) and there is no ambient graphical session (neither
|
|
WAYLAND_DISPLAY nor DISPLAY) that nested mode could open its window in --
|
|
or a display manager launched us as its Wayland session (XDG_SESSION_TYPE
|
|
says so; every major DM/greeter sets it, and the VT test can never fire
|
|
there since stdin is no virtual console). SSH sessions, terminal
|
|
emulators and pipes all fail both tests (/dev/pts/N, /dev/ttyS*, not a
|
|
tty; no session type), so they keep the nested default. */
|
|
static bool started_on_console(void) {
|
|
char name[64];
|
|
if (getenv("WAYLAND_DISPLAY") || getenv("DISPLAY"))
|
|
return false;
|
|
const char *session_type = getenv("XDG_SESSION_TYPE");
|
|
if (session_type && strcmp(session_type, "wayland") == 0)
|
|
return true;
|
|
if (!isatty(STDIN_FILENO))
|
|
return false;
|
|
if (ttyname_r(STDIN_FILENO, name, sizeof(name)) != 0)
|
|
return false;
|
|
if (strncmp(name, "/dev/tty", 8) != 0 || name[8] == '\0')
|
|
return false;
|
|
for (const char *p = name + 8; *p; p++)
|
|
if (*p < '0' || *p > '9')
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/* Split display/render platforms (vc4 display + v3d render): the GLES2
|
|
renderer resolves to the display node, which has no 3D engine -- and
|
|
wlroots would then advertise that node as the dmabuf main device, so
|
|
Mesa clients try to render there and fall back to llvmpipe (mpv goes
|
|
software). Find a real render node and report it instead; rendering,
|
|
allocation and scanout paths are untouched, only the advertisement.
|
|
Skips software rasterizers; the caller keeps the renderer-derived
|
|
device when nothing usable is found. */
|
|
static bool dmabuf_render_node(dev_t *dev_out) {
|
|
DIR *d = opendir("/dev/dri");
|
|
if (!d)
|
|
return false;
|
|
bool found = false;
|
|
struct dirent *e;
|
|
while ((e = readdir(d)) != NULL) {
|
|
if (strncmp(e->d_name, "renderD", 7) != 0)
|
|
continue;
|
|
char path[16 + NAME_MAX];
|
|
snprintf(path, sizeof(path), "/dev/dri/%s", e->d_name);
|
|
int fd = open(path, O_RDWR | O_CLOEXEC);
|
|
if (fd < 0)
|
|
continue;
|
|
drmVersionPtr v = drmGetVersion(fd);
|
|
bool hw = v && v->name && strcmp(v->name, "swrast") != 0 &&
|
|
strcmp(v->name, "kmsro") != 0;
|
|
struct stat st;
|
|
if (hw && fstat(fd, &st) == 0) {
|
|
*dev_out = st.st_rdev;
|
|
found = true;
|
|
}
|
|
if (v)
|
|
drmFreeVersion(v);
|
|
close(fd);
|
|
if (found)
|
|
break;
|
|
}
|
|
closedir(d);
|
|
return found;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
snprintf(kick_socket, sizeof(kick_socket), "kick-%d",
|
|
(int)getpid());
|
|
const char *runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!runtime) runtime = "/tmp";
|
|
snprintf(socket_path, sizeof(socket_path), "%s/%s", runtime, kick_socket);
|
|
/* Clear a stale leftover (e.g. from a crashed previous run with the same
|
|
name) before we try to bind; harmless if there is none. */
|
|
remove_socket_file();
|
|
|
|
/* Resolve KICK_LOG up front so signal handlers can read the cached
|
|
level without touching non-async-signal-safe functions. */
|
|
kick_log_level_init();
|
|
|
|
/* KICK_DUMP_DIR: where frame-dump .ppm files land (default /tmp).
|
|
Test workers point each compositor at its own directory so the
|
|
per-boot dump_seq counters cannot collide on filenames. */
|
|
{
|
|
const char *d = getenv("KICK_DUMP_DIR");
|
|
if (d && d[0])
|
|
snprintf(dump_dir, sizeof(dump_dir), "%s", d);
|
|
}
|
|
|
|
/* KICK_SYNTH_INPUT: keep real host keyboard events out of the test
|
|
channel so the stdin feed is the only source of key/modifier input. */
|
|
{
|
|
const char *v = getenv("KICK_SYNTH_INPUT");
|
|
synth_input_only = v && v[0] && v[0] != '0';
|
|
}
|
|
|
|
/* Alt+Shift+T opens a terminal named by $TERMINAL when set, else the
|
|
alacritty default. Every command-line argument is one tab launcher,
|
|
except --screen-mode WxH[@Hz], which selects the native output mode and
|
|
is consumed here, --modifier alt|super, which picks the modifier
|
|
prefix for the tab-management chords, and --start-cmd-index N, which
|
|
picks the command (1-based) that starts focused. Handle all of this up
|
|
front so a missing command (or a bad request) can be reported and the
|
|
process can exit before any window is created. */
|
|
{
|
|
const char *env_term = getenv("TERMINAL");
|
|
if (env_term && env_term[0])
|
|
term_cmd = env_term;
|
|
}
|
|
char **appv = calloc((size_t)argc, sizeof(*appv));
|
|
if (!appv) {
|
|
fprintf(stderr, "kick: out of memory\n");
|
|
return 1;
|
|
}
|
|
int napps = 0;
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
fprintf(stderr,
|
|
"kick Tab Compositor\n"
|
|
"\n"
|
|
"kick opens one tab per command-line argument. Each "
|
|
"argument is a shell\n"
|
|
"command; quote it when it takes arguments. With no "
|
|
"arguments a single\n"
|
|
"terminal tab opens ($TERMINAL, default alacritty).\n"
|
|
"\n"
|
|
"Tabs are managed with chords on a modifier prefix -- "
|
|
"Alt by default;\n"
|
|
"--modifier super moves them to Super.\n"
|
|
"\n"
|
|
"Usage:\n"
|
|
" %s [options] [prog...]\n"
|
|
" %s -h | --help\n"
|
|
"\n"
|
|
"Example:\n"
|
|
" # kick \"firefox -no-remote\"\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" --screen-mode WxH[@Hz] Select the native output "
|
|
"mode (standalone\n"
|
|
" only), e.g. 1920x1080 or "
|
|
"1920x1080@60. A\n"
|
|
" request nothing supports "
|
|
"prints every mode\n"
|
|
" and exits.\n"
|
|
" --modifier alt|super Pick the tab-management "
|
|
"modifier (default\n"
|
|
" alt). 'super' suits "
|
|
"standalone/DRM and\n"
|
|
" labwc-style hosts.\n"
|
|
" --start-cmd-index N Pick the command (1-based) "
|
|
"that starts\n"
|
|
" focused; without it the "
|
|
"last command does.\n"
|
|
" --tab-groups on|off How a client's related "
|
|
"windows group with their tab:\n"
|
|
" 'on' (default) stacks them "
|
|
"over the parent tab,\n"
|
|
" sharing its visibility and "
|
|
"focus (a browser-style\n"
|
|
" tab group); 'off' gives each "
|
|
"related window its own\n"
|
|
" tab right of the parent.\n"
|
|
" --panel CMD Launch a wlr-layer-shell "
|
|
"panel (e.g. waybar) as an\n"
|
|
" always-on-top bar that "
|
|
"sizes the tabs underneath it.\n"
|
|
" The panel shares the login "
|
|
"session's D-Bus bus,\n"
|
|
" so a single-instance panel "
|
|
"already running on the\n"
|
|
" host (e.g. wf-panel-pi) "
|
|
"exits without mapping;\n"
|
|
" use a multi-instance panel "
|
|
"or stop the host copy.\n"
|
|
" --focus-priority Favor the active window: "
|
|
"it runs at normal\n"
|
|
" CPU/IO priority (nice 0, "
|
|
"best-effort/4) while every\n"
|
|
" other window is pushed down "
|
|
"(nice +10,\n"
|
|
" best-effort/7). Off by default; "
|
|
"needs no sudo.\n"
|
|
" -h, --help Show the full help.\n"
|
|
"\n"
|
|
"Keys (hold the modifier):\n"
|
|
" Up / Down Cycle tabs; hold to keep "
|
|
"cycling.\n"
|
|
" Tab / 1 Walk windows most-recently-used first; "
|
|
"hold to keep\n"
|
|
" walking. The stack only changes on "
|
|
"release.\n"
|
|
" Shift+Up / Shift+Down Reorder the active tab.\n"
|
|
" Delete Close the active window (or the tab when it is the only window).\n"
|
|
" Shift+T Open a new terminal tab "
|
|
"($TERMINAL).\n"
|
|
" Left / Right Cycle the active tab's "
|
|
"tab-group windows (Left = previous, Right = next); hold "
|
|
"to keep cycling.\n"
|
|
" Ctrl+Alt+F1..F12 Switch virtual terminals "
|
|
"(standalone only).\n"
|
|
" Ctrl+Alt+Q Quit kick: asks for "
|
|
"confirmation when more than\n"
|
|
" one window is open "
|
|
"(Y/Enter quits, N/Escape keeps running).\n",
|
|
argv[0], argv[0]);
|
|
free(appv);
|
|
return 0;
|
|
}
|
|
const char *val = NULL;
|
|
if (strcmp(argv[i], "--screen-mode") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --screen-mode needs a value, "
|
|
"e.g. --screen-mode 1920x1080\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
val = argv[++i];
|
|
} else if (strncmp(argv[i], "--screen-mode=", 14) == 0) {
|
|
val = argv[i] + 14;
|
|
} else if (strcmp(argv[i], "--modifier") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --modifier needs a value, "
|
|
"e.g. --modifier super\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *m = argv[++i];
|
|
if (strcmp(m, "alt") == 0) {
|
|
tab_prefix = PREFIX_ALT;
|
|
} else if (strcmp(m, "super") == 0) {
|
|
tab_prefix = PREFIX_SUPER;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --modifier '%s' "
|
|
"(use 'alt' or 'super')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strncmp(argv[i], "--modifier=", 11) == 0) {
|
|
const char *m = argv[i] + 11;
|
|
if (strcmp(m, "alt") == 0) {
|
|
tab_prefix = PREFIX_ALT;
|
|
} else if (strcmp(m, "super") == 0) {
|
|
tab_prefix = PREFIX_SUPER;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --modifier '%s' "
|
|
"(use 'alt' or 'super')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strcmp(argv[i], "--tab-groups") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --tab-groups needs a value "
|
|
"(on or off)\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *m = argv[++i];
|
|
if (strcmp(m, "off") == 0) {
|
|
tab_groups = false;
|
|
} else if (strcmp(m, "on") == 0) {
|
|
tab_groups = true;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --tab-groups '%s' "
|
|
"(use 'on' or 'off')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strncmp(argv[i], "--tab-groups=", 13) == 0) {
|
|
const char *m = argv[i] + 13;
|
|
if (strcmp(m, "off") == 0) {
|
|
tab_groups = false;
|
|
} else if (strcmp(m, "on") == 0) {
|
|
tab_groups = true;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --tab-groups '%s' "
|
|
"(use 'on' or 'off')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strcmp(argv[i], "--focus-priority") == 0) {
|
|
opt_focus_priority = true;
|
|
continue;
|
|
} else if (strcmp(argv[i], "--panel") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --panel needs a value, "
|
|
"e.g. --panel waybar\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
if (server.panel_cmd) {
|
|
fprintf(stderr, "kick: only one --panel is allowed\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
server.panel_cmd = strdup(argv[++i]);
|
|
continue;
|
|
} else if (strncmp(argv[i], "--panel=", 8) == 0) {
|
|
if (server.panel_cmd) {
|
|
fprintf(stderr, "kick: only one --panel is allowed\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
server.panel_cmd = strdup(argv[i] + 8);
|
|
continue;
|
|
} else if (strcmp(argv[i], "--start-cmd-index") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --start-cmd-index needs a "
|
|
"value, e.g. --start-cmd-index 2\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *n = argv[++i];
|
|
char *end = NULL;
|
|
long idx = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || idx < 1 || idx > 2147483647L) {
|
|
fprintf(stderr, "kick: bad --start-cmd-index '%s' "
|
|
"(use a 1-based command number, e.g. --start-cmd-index "
|
|
"2)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
/* Convert to the program's 0-based index as soon as possible. */
|
|
start_cmd_index = (int)idx - 1;
|
|
continue;
|
|
} else if (strncmp(argv[i], "--start-cmd-index=",
|
|
sizeof("--start-cmd-index=") - 1) == 0) {
|
|
const char *n = argv[i] + sizeof("--start-cmd-index=") - 1;
|
|
char *end = NULL;
|
|
long idx = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || idx < 1 || idx > 2147483647L) {
|
|
fprintf(stderr, "kick: bad --start-cmd-index '%s' "
|
|
"(use a 1-based command number)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
start_cmd_index = (int)idx - 1;
|
|
continue;
|
|
}
|
|
if (val) {
|
|
if (!parse_screen_mode(val)) {
|
|
fprintf(stderr, "kick: bad --screen-mode '%s' "
|
|
"(use WxH or WxH@Hz, e.g. 1920x1080 or "
|
|
"1920x1080@60)\n", val);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
}
|
|
appv[napps++] = argv[i];
|
|
}
|
|
int total_apps = napps;
|
|
bool default_tab = false;
|
|
if (total_apps == 0) {
|
|
/* No arguments: open one terminal tab, as Alt+Shift+T would. */
|
|
default_tab = true;
|
|
total_apps = 1;
|
|
}
|
|
if (start_cmd_index >= 0 && start_cmd_index >= total_apps) {
|
|
fprintf(stderr, "kick: --start-cmd-index %d is out of range: "
|
|
"1..%d (%d command%s)\n", start_cmd_index + 1, total_apps,
|
|
total_apps, total_apps == 1 ? "" : "s");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
for (int i = 0; i < total_apps; i++) {
|
|
const char *cmd = default_tab ? term_cmd : appv[i];
|
|
if (!cmd_in_path(cmd)) {
|
|
fprintf(stderr, "kick: '%s' not found in PATH%s\n",
|
|
cmd, default_tab ? " (set $TERMINAL)" : "");
|
|
free(appv);
|
|
return 1;
|
|
}
|
|
}
|
|
if (server.panel_cmd && !cmd_in_path(server.panel_cmd)) {
|
|
fprintf(stderr, "kick: --panel '%s' not found in PATH\n",
|
|
server.panel_cmd);
|
|
free(appv);
|
|
return 1;
|
|
}
|
|
|
|
struct sigaction crash_sa = {
|
|
.sa_sigaction = crash_handler,
|
|
.sa_flags = SA_SIGINFO | SA_NODEFER,
|
|
};
|
|
sigaction(SIGSEGV, &crash_sa, NULL);
|
|
sigaction(SIGABRT, &crash_sa, NULL);
|
|
server.sig_pipe[0] = server.sig_pipe[1] = -1;
|
|
if (pipe(server.sig_pipe) != 0) {
|
|
fprintf(stderr, "kick: failed to create SIGUSR1 wake pipe\n");
|
|
return 1;
|
|
}
|
|
int pipe_flags = fcntl(server.sig_pipe[0], F_GETFL, 0);
|
|
if (pipe_flags < 0
|
|
|| fcntl(server.sig_pipe[0], F_SETFL,
|
|
pipe_flags | O_NONBLOCK) != 0) {
|
|
fprintf(stderr, "kick: failed to make SIGUSR1 pipe non-blocking\n");
|
|
return 1;
|
|
}
|
|
signal(SIGCHLD, sigchld_handler);
|
|
signal(SIGUSR1, sigusr1_handler);
|
|
/* A client that disconnects mid-write (paste abandoned, tab racing an
|
|
event) must fail the write, not kill the compositor: stock practice
|
|
for Wayland compositors, whose libwayland cleans up dead clients on
|
|
EPIPE once the signal is out of the way. */
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
/* wlroots logs follow KICK_LOG: quiet silences everything, info drops
|
|
protocol noise, debug shows everything. Quiet must be SILENT, not
|
|
ERROR: wlroots only redirects Xwayland's stderr (xkbcomp warnings)
|
|
when verbosity is below ERROR, and the xwm xcb errors are routine
|
|
non-fatal noise. Kick's own fatal errors use fprintf directly, so
|
|
they still show when quiet. */
|
|
switch (kick_log_level()) {
|
|
case KICK_LOG_DEBUG:
|
|
wlr_log_init(WLR_DEBUG, NULL);
|
|
break;
|
|
case KICK_LOG_INFO:
|
|
wlr_log_init(WLR_INFO, NULL);
|
|
break;
|
|
default:
|
|
wlr_log_init(WLR_SILENT, NULL);
|
|
break;
|
|
}
|
|
|
|
/* Mode selection: KICK_MODE=standalone (or any forced WLR_BACKENDS)
|
|
drives a native output directly (DRM, or headless for tests); the
|
|
default nested mode blits a headless output into a host window.
|
|
With neither variable set, launching from a Linux virtual console
|
|
or as a display manager's Wayland session picks standalone
|
|
automatically so plain `kick` works at a TTY and in lightdm/greetd
|
|
entries; KICK_MODE=nested pins the old behaviour there. */
|
|
server.standalone = false;
|
|
const char *mode_reason = "default";
|
|
{
|
|
const char *mode = getenv("KICK_MODE");
|
|
const char *wlr_backends = getenv("WLR_BACKENDS");
|
|
const char *session_type = getenv("XDG_SESSION_TYPE");
|
|
if (mode && strcmp(mode, "standalone") == 0) {
|
|
server.standalone = true;
|
|
mode_reason = "KICK_MODE";
|
|
} else if (mode && mode[0]) {
|
|
/* any other explicit value (e.g. KICK_MODE=nested) pins nested */
|
|
mode_reason = "KICK_MODE";
|
|
} else if (!wlr_backends || !wlr_backends[0]) {
|
|
server.standalone = started_on_console();
|
|
if (server.standalone)
|
|
mode_reason = (session_type &&
|
|
strcmp(session_type, "wayland") == 0) ?
|
|
"session" : "console";
|
|
}
|
|
if (wlr_backends && wlr_backends[0]) {
|
|
server.standalone = true;
|
|
mode_reason = "WLR_BACKENDS";
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[mode] %s (%s)\n",
|
|
server.standalone ? "standalone" : "nested", mode_reason);
|
|
|
|
/* Nested inside another kick with the same Alt prefix: the outer
|
|
compositor grabs every Alt chord (cycle, MRU, close, new-terminal),
|
|
so this session never sees them -- its tab switching feels stuck or
|
|
slow while the outer session moves instead. A labwc-style host grabs
|
|
no Alt+Up/Down, which is why nesting there feels fine. Suggest
|
|
--modifier super for one of the two. The outer session names its
|
|
socket kick-<pid>, so match that basename. */
|
|
if (!server.standalone && tab_prefix == PREFIX_ALT) {
|
|
const char *host_disp = getenv("WAYLAND_DISPLAY");
|
|
const char *slash = host_disp ? strrchr(host_disp, '/') : NULL;
|
|
const char *base = slash ? slash + 1 : host_disp;
|
|
if (base && strncmp(base, "kick-", 5) == 0)
|
|
fprintf(stderr,
|
|
"kick: warning: host display '%s' looks like another kick "
|
|
"using the default Alt prefix; the outer session grabs "
|
|
"every Alt chord, so this session's tab switching will "
|
|
"feel slow -- run one of them with --modifier super\n",
|
|
host_disp);
|
|
}
|
|
|
|
server.wl_display = wl_display_create();
|
|
if (!server.wl_display) {
|
|
fprintf(stderr, "kick: failed to create Wayland display\n");
|
|
return 1;
|
|
}
|
|
server.loop = wl_display_get_event_loop(server.wl_display);
|
|
wl_list_init(&server.kbdevs);
|
|
wl_list_init(&server.ptrdevs);
|
|
wl_list_init(&server.touchdevs);
|
|
|
|
/* SIGUSR1 dump wake: the read end registers an event source so the byte
|
|
written by the handler interrupts the blocking dispatch below. */
|
|
server.sigpipe_src = wl_event_loop_add_fd(server.loop, server.sig_pipe[0],
|
|
WL_EVENT_READABLE, sigpipe_cb, NULL);
|
|
|
|
/* Route SIGTERM/SIGINT through the event loop (signalfd) so shutdown runs
|
|
in normal context; see close_signal_cb for why a raw handler is unsafe. */
|
|
wl_event_loop_add_signal(server.loop, SIGTERM, close_signal_cb, NULL);
|
|
wl_event_loop_add_signal(server.loop, SIGINT, close_signal_cb, NULL);
|
|
|
|
/* stdin event feed: lets tests/drivers inject raw keyboard events
|
|
into the real host key handlers without a host compositor. */
|
|
server.ctl_source = wl_event_loop_add_fd(server.loop, STDIN_FILENO,
|
|
WL_EVENT_READABLE,
|
|
ctl_cb, NULL);
|
|
|
|
if (server.standalone) {
|
|
/* autocreate honours WLR_BACKENDS and otherwise picks the current
|
|
session's wayland/x11 backend (we run inside some compositor
|
|
elsewhere), so force DRM+libinput unless told otherwise. */
|
|
const char *wlr_backends = getenv("WLR_BACKENDS");
|
|
if (!wlr_backends || !wlr_backends[0]) {
|
|
wlr_backends = "drm,libinput";
|
|
setenv("WLR_BACKENDS", wlr_backends, 1);
|
|
}
|
|
server.standalone_headless = backends_include(wlr_backends, "headless");
|
|
/* wlroots' default DRM "atomic" interface ignores WLR_OUTPUT_STATE_LAYERS
|
|
(layer FBs are imported but never added to the atomic request), so a
|
|
wlr_output_layer can never reach a hardware plane without libliftoff.
|
|
The libliftoff interface is only selected via WLR_DRM_FORCE_LIBLIFTOFF,
|
|
so wire that in for real (non-headless) standalone runs; the overlay
|
|
plane path depends on it. Leave a user-set value alone. */
|
|
if (!server.standalone_headless &&
|
|
!getenv("WLR_DRM_FORCE_LIBLIFTOFF"))
|
|
setenv("WLR_DRM_FORCE_LIBLIFTOFF", "1", 1);
|
|
/* autocreate wraps every backend in a wlr_multi_backend, which breaks
|
|
the plain headless backend's manual wlr_headless_add_output() below
|
|
(it asserts wlr_backend_is_headless()), so create the headless
|
|
backend directly instead. */
|
|
if (server.standalone_headless)
|
|
server.backend = wlr_headless_backend_create(server.loop);
|
|
else
|
|
server.backend = wlr_backend_autocreate(server.loop, &server.session);
|
|
} else {
|
|
server.backend = wlr_headless_backend_create(server.loop);
|
|
}
|
|
if (!server.backend) {
|
|
fprintf(stderr, "kick: failed to create %s backend\n",
|
|
server.standalone ? "backend (autocreate)"
|
|
: "headless backend");
|
|
return 1;
|
|
}
|
|
|
|
server.renderer = wlr_renderer_autocreate(server.backend);
|
|
if (!server.renderer) {
|
|
fprintf(stderr, "kick: no GPU renderer available\n");
|
|
return 1;
|
|
}
|
|
/* wl_shm and zwp_linux_dmabuf_v1 are advertised once each, explicitly,
|
|
further below. The legacy wlr_renderer_init_wl_display() shortcut
|
|
must NOT be used here: it creates a second wl_shm (v2) and a second
|
|
linux-dmabuf (v4) beside the explicit ones, and clients that
|
|
initialize state per advertised global -- Qt's Wayland platform
|
|
re-enters its dmabuf feedback setup for every instance -- segfault
|
|
on the duplicate (VLC died in QWaylandDisplay::registry_global). */
|
|
|
|
/* Overlay text setup is CPU-side (Pango) and mode-independent. */
|
|
overlay_font_init();
|
|
|
|
server.allocator = wlr_allocator_autocreate(server.backend, server.renderer);
|
|
if (!server.allocator) {
|
|
fprintf(stderr, "kick: failed to create allocator\n");
|
|
return 1;
|
|
}
|
|
|
|
/* wl_surface at v4: legacy clients (e.g. VLC's Wayland vout) place video
|
|
via non-zero wl_surface.attach offsets, which is a protocol violation
|
|
from v5 on. Advertise v4 so those offsets are accepted and ignored,
|
|
like before the v5 breaking change. */
|
|
server.compositor = wlr_compositor_create(server.wl_display, 4,
|
|
server.renderer);
|
|
wlr_subcompositor_create(server.wl_display);
|
|
wlr_shm_create_with_renderer(server.wl_display, 1, server.renderer);
|
|
/* Advertise a render node, not the display node, as the dmabuf main
|
|
device (see dmabuf_render_node): on failure keep wlroots' default
|
|
renderer-derived device rather than dropping dmabuf entirely. */
|
|
struct wlr_linux_dmabuf_feedback_v1_init_options dmabuf_opts = {
|
|
.main_renderer = server.renderer,
|
|
};
|
|
struct wlr_linux_dmabuf_feedback_v1 dmabuf_feedback = {0};
|
|
if (wlr_linux_dmabuf_feedback_v1_init_with_options(&dmabuf_feedback,
|
|
&dmabuf_opts)) {
|
|
dev_t render_dev;
|
|
if (dmabuf_render_node(&render_dev)) {
|
|
dmabuf_feedback.main_device = render_dev;
|
|
kick_log(KICK_LOG_INFO, "[dmabuf] main device %u:%u (render node)\n",
|
|
major(render_dev), minor(render_dev));
|
|
}
|
|
wlr_linux_dmabuf_v1_create(server.wl_display, 5, &dmabuf_feedback);
|
|
wlr_linux_dmabuf_feedback_v1_finish(&dmabuf_feedback);
|
|
} else {
|
|
wlr_linux_dmabuf_v1_create_with_renderer(server.wl_display, 5,
|
|
server.renderer);
|
|
}
|
|
wlr_data_device_manager_create(server.wl_display);
|
|
wlr_ext_data_control_manager_v1_create(server.wl_display, 1);
|
|
|
|
/* Direct scanout policy:
|
|
- standalone (DRM/VT console): ENABLED. Scanout presents a fullscreen
|
|
client's buffer straight to the display -- true zero-copy, no
|
|
composite, no blit. An attach-render lock is held while the tab
|
|
overlay is up (see update_overlay_visible) so the overlay's second
|
|
pass lands on the composited frame instead of being painted onto
|
|
the client's own buffer.
|
|
- nested: DISABLED. A "scanned-out" buffer on the headless output is
|
|
displayed nowhere by itself; kick must still blit every presented
|
|
buffer into the host window, importing the *client's* dmabuf into
|
|
the host EGL context each frame. VLC's scanout-feedback-tuned
|
|
buffers reject that import per frame (EGL_BAD_PARAMETER "modifier
|
|
attributes not equal") and the video goes black behind playing
|
|
audio. Revisit if Mesa/wlroots ever accept those buffers here;
|
|
KICK_NESTED_SCANOUT=1 forces it back on for testing either way. */
|
|
if (!server.standalone && !getenv("KICK_NESTED_SCANOUT"))
|
|
setenv("WLR_SCENE_DISABLE_DIRECT_SCANOUT", "1", 1);
|
|
|
|
server.scene = wlr_scene_create();
|
|
server.xdg_shell = wlr_xdg_shell_create(server.wl_display, 6);
|
|
|
|
/* Decorations stay client-side: tabs are maximized and kick draws no
|
|
frames of its own (see handle_decoration_new). */
|
|
server.decoration_mgr =
|
|
wlr_xdg_decoration_manager_v1_create(server.wl_display);
|
|
if (server.decoration_mgr) {
|
|
server.decoration_new.notify = handle_decoration_new;
|
|
wl_signal_add(&server.decoration_mgr->events.new_toplevel_decoration,
|
|
&server.decoration_new);
|
|
}
|
|
|
|
/* Layer-shell (waybar/panels): the surfaces live in an always-on-top
|
|
scene tree, kept a sibling of the tab window trees so tabs and the bar
|
|
never interfere. */
|
|
wl_list_init(&server.layer_surfaces);
|
|
server.layer_tree = wlr_scene_tree_create(&server.scene->tree);
|
|
/* Dedicated tree for parentless popups (bars' menus): rooted at the
|
|
output origin so their geometry maps directly to pixels. */
|
|
server.popup_tree = wlr_scene_tree_create(&server.scene->tree);
|
|
/* IME candidate popups: same always-on-top treatment as layer_tree. */
|
|
server.input_popup_tree = wlr_scene_tree_create(&server.scene->tree);
|
|
wl_list_init(&server.input_popups);
|
|
wl_list_init(&server.input_methods);
|
|
server.layer_shell = wlr_layer_shell_v1_create(server.wl_display, 3);
|
|
server.new_layer_surface.notify = handle_new_layer_surface;
|
|
wl_signal_add(&server.layer_shell->events.new_surface,
|
|
&server.new_layer_surface);
|
|
|
|
/* Passive protocol globals that cost one global each and let clients run
|
|
at their best: presentation-time for frame pacing, xdg-output for
|
|
logical output geometry, viewporter for viewport-scaled buffers (e.g.
|
|
video). wlr_scene applies viewports to xdg surfaces automatically. */
|
|
server.presentation = wlr_presentation_create(server.wl_display,
|
|
server.backend, 1);
|
|
server.viewporter = wlr_viewporter_create(server.wl_display);
|
|
/* Content-type hints plus single-pixel buffers: one global each, fully
|
|
passive (wlroots tags surfaces and allocates 1x1 buffers itself). */
|
|
server.content_type_mgr =
|
|
wlr_content_type_manager_v1_create(server.wl_display, 1);
|
|
server.single_pixel_mgr =
|
|
wlr_single_pixel_buffer_manager_v1_create(server.wl_display);
|
|
/* Alpha modifier: one passive global (see the field comment). */
|
|
server.alpha_modifier_mgr =
|
|
wlr_alpha_modifier_v1_create(server.wl_display);
|
|
|
|
/* Let nested clients pick their cursor shape (cursor-shape-v1); the
|
|
manager is passive, so we handle request_set_shape ourselves below:
|
|
apply it to the standalone cursor or forward it to the host pointer. */
|
|
server.shape_mgr = wlr_cursor_shape_manager_v1_create(server.wl_display, 1);
|
|
if (server.shape_mgr) {
|
|
server.shape_listener.notify = handle_shape_set;
|
|
wl_signal_add(&server.shape_mgr->events.request_set_shape,
|
|
&server.shape_listener);
|
|
}
|
|
|
|
server.seat = wlr_seat_create(server.wl_display, "kick");
|
|
wlr_seat_set_capabilities(server.seat,
|
|
WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD |
|
|
WL_SEAT_CAPABILITY_TOUCH);
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland: X11 apps get tabs like Wayland clients. Lazy start keeps an
|
|
idle session free of an Xwayland process; display_name is reserved at
|
|
create time, so launch_client can export DISPLAY right away. The XWM
|
|
bridges ICCCM focus and X11 selections onto the seat, which rides the
|
|
existing clipboard bridge out to the host. */
|
|
if (!getenv("KICK_NO_XWAYLAND")) {
|
|
/* Run our own plain Xwayland: ignore any WLR_XWAYLAND wrapper the
|
|
environment may point at (e.g. one that forces `-auth` and the
|
|
legacy X11 cookie dance). wlroots then execs the compiled-in
|
|
Xwayland with no -auth, so local clients connect freely and we
|
|
never touch the session Xauthority file. */
|
|
unsetenv("WLR_XWAYLAND");
|
|
server.xwayland = wlr_xwayland_create(server.wl_display,
|
|
server.compositor, true);
|
|
if (server.xwayland) {
|
|
wl_list_init(&server.or_windows);
|
|
wl_list_init(&server.x11_pendings);
|
|
wlr_xwayland_set_seat(server.xwayland, server.seat);
|
|
server.xwayland_new_surface.notify = handle_xwayland_new_surface;
|
|
wl_signal_add(&server.xwayland->events.new_surface,
|
|
&server.xwayland_new_surface);
|
|
server.xwayland_ready.notify = handle_xwayland_ready;
|
|
wl_signal_add(&server.xwayland->events.ready,
|
|
&server.xwayland_ready);
|
|
server.xwayland_destroy.notify = handle_xwayland_destroy;
|
|
wl_signal_add(&server.xwayland->events.destroy,
|
|
&server.xwayland_destroy);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "kick: XWayland unavailable "
|
|
"(no Xwayland binary?)\n");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Pointer constraints + relative pointer: games and creative apps ask for
|
|
these to grab the cursor and to get raw relative motion. The constraint
|
|
is activated/deactivated on seat pointer focus change; relative deltas
|
|
are forwarded from the input paths. */
|
|
server.pointer_constraints = wlr_pointer_constraints_v1_create(server.wl_display);
|
|
server.relative_pointer_mgr = wlr_relative_pointer_manager_v1_create(server.wl_display);
|
|
server.seat_pointer_focus.notify = handle_seat_pointer_focus;
|
|
wl_signal_add(&server.seat->pointer_state.events.focus_change,
|
|
&server.seat_pointer_focus);
|
|
/* Pointer gestures: touchpad swipe/pinch/hold fan out to gesture
|
|
clients holding seat pointer focus (see the gesture section;
|
|
libinput devices feed it in standalone mode, the stdin feed in
|
|
any mode). */
|
|
server.gestures = wlr_pointer_gestures_v1_create(server.wl_display);
|
|
/* Text input + input method (OSK/IME): advertise both managers so
|
|
in-session clients and IMEs (fcitx5, OSKs) can bind; the relay between
|
|
the focused field and the IME runs off the seat's keyboard focus
|
|
(see handle_seat_keyboard_focus). */
|
|
server.text_input_mgr =
|
|
wlr_text_input_manager_v3_create(server.wl_display);
|
|
server.input_method_mgr =
|
|
wlr_input_method_manager_v2_create(server.wl_display);
|
|
server.text_input_new.notify = handle_text_input_new;
|
|
wl_signal_add(&server.text_input_mgr->events.new_text_input,
|
|
&server.text_input_new);
|
|
server.input_method_new.notify = handle_input_method_new;
|
|
wl_signal_add(&server.input_method_mgr->events.new_input_method,
|
|
&server.input_method_new);
|
|
server.seat_keyboard_focus.notify = handle_seat_keyboard_focus;
|
|
wl_signal_add(&server.seat->keyboard_state.events.focus_change,
|
|
&server.seat_keyboard_focus);
|
|
/* Primary selection (zwp_primary_selection_v1): advertise the manager so
|
|
clients like text editors can set the selection with their own serial
|
|
(no roundtrip needed); the request handler installs it on the seat,
|
|
which distributes it to the other focused tab clients. */
|
|
server.primary_selection_mgr =
|
|
wlr_primary_selection_v1_device_manager_create(server.wl_display);
|
|
server.request_set_primary_selection.notify = seat_request_set_primary_selection;
|
|
wl_signal_add(&server.seat->events.request_set_primary_selection,
|
|
&server.request_set_primary_selection);
|
|
/* The request_set_selection handler is what actually performs the
|
|
wlr_seat_set_selection for clients using the data-control manager; the
|
|
host-forwarding it triggers is a no-op without an outer host (backend
|
|
NONE), so standalone needs it too or wl-copy-style clipboard requests
|
|
are silently dropped. */
|
|
bridge_attach_seat();
|
|
|
|
/* Foreign toplevel management: let taskbars/switchers (waybar taskbar,
|
|
disorder, etc.) enumerate kick's windows and request activation/close.
|
|
One wlr-foreign handle plus one ext-foreign handle per live window;
|
|
sync happens in foreign_sync_* and the window claim/teardown paths. */
|
|
server.foreign_mgr = wlr_foreign_toplevel_manager_v1_create(server.wl_display);
|
|
server.ext_foreign_list = wlr_ext_foreign_toplevel_list_v1_create(server.wl_display, 1);
|
|
|
|
/* Idle notification: report user inactivity and honor idle-inhibit.
|
|
Activity is claimed on every input event (keyboard/pointer/touch);
|
|
an inhibitor flips the notifier to inhibited so a screenblank/DPMS
|
|
consumer stays awake while a client asks. */
|
|
server.idle_notifier = wlr_idle_notifier_v1_create(server.wl_display);
|
|
wl_list_init(&server.idle_inhibitors);
|
|
server.idle_inhibit_mgr = wlr_idle_inhibit_v1_create(server.wl_display);
|
|
server.idle_new_inhibitor.notify = handle_idle_new_inhibitor;
|
|
wl_signal_add(&server.idle_inhibit_mgr->events.new_inhibitor,
|
|
&server.idle_new_inhibitor);
|
|
|
|
/* Session lock (ext-session-lock-v1): let a lock client (swaylock) grab
|
|
the session and blank the screen. While locked, no window may take
|
|
focus and the grab chords are swallowed (see process_key_event). */
|
|
server.session_lock_manager =
|
|
wlr_session_lock_manager_v1_create(server.wl_display);
|
|
server.session_lock_new_lock.notify = handle_session_lock_new_lock;
|
|
wl_signal_add(&server.session_lock_manager->events.new_lock,
|
|
&server.session_lock_new_lock);
|
|
|
|
/* Screen capture (ext-image-copy-capture-v1), for grim & co. wlroots
|
|
runs the whole capture flow itself off the output commit path. */
|
|
server.copy_capture_mgr = wlr_ext_image_copy_capture_manager_v1_create(
|
|
server.wl_display, 1);
|
|
server.output_capture_mgr = wlr_ext_output_image_capture_source_manager_v1_create(
|
|
server.wl_display, 1);
|
|
|
|
/* Tearing-control + fractional-scale: the shown surface's tearing hint
|
|
drives the commit's async-flip flag (see apply_tearing_hint), and
|
|
surfaces learn the output scale at claim. */
|
|
server.tearing_mgr = wlr_tearing_control_manager_v1_create(server.wl_display, 1);
|
|
server.tearing_new_object.notify = handle_tearing_new_object;
|
|
wl_signal_add(&server.tearing_mgr->events.new_object,
|
|
&server.tearing_new_object);
|
|
server.fractional_scale_mgr =
|
|
wlr_fractional_scale_manager_v1_create(server.wl_display, 1);
|
|
|
|
/* Keyboard shortcuts inhibit: let a focused fullscreen client grab the
|
|
compositor's chords (see kb_inhibit_focused / process_key_event). */
|
|
server.kb_inhibit_mgr =
|
|
wlr_keyboard_shortcuts_inhibit_v1_create(server.wl_display);
|
|
server.kb_inhibit_new_inhibitor.notify = handle_kb_inhibit_new_inhibitor;
|
|
wl_signal_add(&server.kb_inhibit_mgr->events.new_inhibitor,
|
|
&server.kb_inhibit_new_inhibitor);
|
|
|
|
/* Output power management: blank/unblank the single output on demand.
|
|
Starts powered; set_mode toggles the scene tree (see handler). */
|
|
server.output_powered = true;
|
|
server.output_power_mgr =
|
|
wlr_output_power_manager_v1_create(server.wl_display);
|
|
server.output_power_set_mode.notify = handle_output_power_set_mode;
|
|
wl_signal_add(&server.output_power_mgr->events.set_mode,
|
|
&server.output_power_set_mode);
|
|
|
|
/* Output management: publish the single head; client resizes apply
|
|
standalone and are refused nested (see the section). */
|
|
server.output_mgr = wlr_output_manager_v1_create(server.wl_display);
|
|
server.output_mgr_apply.notify = handle_output_mgr_apply;
|
|
wl_signal_add(&server.output_mgr->events.apply, &server.output_mgr_apply);
|
|
server.output_mgr_test.notify = handle_output_mgr_test;
|
|
wl_signal_add(&server.output_mgr->events.test, &server.output_mgr_test);
|
|
|
|
/* Gamma control: night-light clients set the output ramp (see
|
|
handle_gamma_set); wlroots itself fails controls for outputs with
|
|
no LUT, so headless/nested clients learn it is unsupported. */
|
|
server.gamma_mgr = wlr_gamma_control_manager_v1_create(server.wl_display);
|
|
server.gamma_set.notify = handle_gamma_set;
|
|
wl_signal_add(&server.gamma_mgr->events.set_gamma, &server.gamma_set);
|
|
|
|
/* Virtual keyboard/pointer: synthesised input feeds the seat via the
|
|
same paths as real devices (see handle_virtual_kb_new/ptr_new). */
|
|
wl_list_init(&server.virtual_kbs);
|
|
wl_list_init(&server.virtual_ptrs);
|
|
server.virtual_kb_mgr =
|
|
wlr_virtual_keyboard_manager_v1_create(server.wl_display);
|
|
server.virtual_kb_new.notify = handle_virtual_kb_new;
|
|
wl_signal_add(&server.virtual_kb_mgr->events.new_virtual_keyboard,
|
|
&server.virtual_kb_new);
|
|
server.virtual_ptr_mgr =
|
|
wlr_virtual_pointer_manager_v1_create(server.wl_display);
|
|
server.virtual_ptr_new.notify = handle_virtual_ptr_new;
|
|
wl_signal_add(&server.virtual_ptr_mgr->events.new_virtual_pointer,
|
|
&server.virtual_ptr_new);
|
|
|
|
/* Xdg-activation: focus windows by token (see
|
|
handle_xdg_activation_request). */
|
|
server.xdg_activation = wlr_xdg_activation_v1_create(server.wl_display);
|
|
server.xdg_activation_request.notify = handle_xdg_activation_request;
|
|
wl_signal_add(&server.xdg_activation->events.request_activate,
|
|
&server.xdg_activation_request);
|
|
|
|
/* xdg-dialog: modal toplevels demand attention (see modal_activate).
|
|
Version 1 is the only one wlroots speaks. */
|
|
server.dialog_mgr = wlr_xdg_wm_dialog_v1_create(server.wl_display, 1);
|
|
server.dialog_new.notify = handle_dialog_new;
|
|
wl_signal_add(&server.dialog_mgr->events.new_dialog, &server.dialog_new);
|
|
|
|
/* Toplevel icons: exact client artwork for the overlay (see the icon
|
|
section). Advertise the overlay's draw size so clients upload a
|
|
fitting buffer. */
|
|
server.icon_mgr =
|
|
wlr_xdg_toplevel_icon_manager_v1_create(server.wl_display, 1); int icon_sizes[] = { OVERLAY_ICON_SZ };
|
|
wlr_xdg_toplevel_icon_manager_v1_set_sizes(server.icon_mgr, icon_sizes,
|
|
1);
|
|
server.icon_set.notify = handle_icon_set;
|
|
wl_signal_add(&server.icon_mgr->events.set_icon, &server.icon_set);
|
|
|
|
/* wl_fixes: core-protocol repairs (registry destroy, global-remove
|
|
acks) for clients like Firefox. wlroots serves the requests; the
|
|
compositor only advertises the global. */
|
|
server.fixes = wlr_fixes_create(server.wl_display, 1);
|
|
|
|
/* Toplevel tags: clients label toplevels ("settings", ...) with a tag
|
|
plus a translated description (see the tag section). Version 1 is
|
|
the only one wlroots speaks. */
|
|
wl_list_init(&server.pending_tags);
|
|
server.tag_mgr =
|
|
wlr_xdg_toplevel_tag_manager_v1_create(server.wl_display, 1);
|
|
server.tag_set.notify = handle_tag_set;
|
|
wl_signal_add(&server.tag_mgr->events.set_tag, &server.tag_set);
|
|
server.tag_set_desc.notify = handle_tag_set_desc;
|
|
wl_signal_add(&server.tag_mgr->events.set_description,
|
|
&server.tag_set_desc);
|
|
|
|
/* Color representation: surfaces declare their alpha mode / matrix
|
|
coefficients / range; wlroots validates client values against the
|
|
advertised set. kick is an SDR sRGB session, so advertise exactly
|
|
that: premultiplied-electrical alpha with identity coefficients in
|
|
full range (explicit options rather than the renderer's query, which
|
|
reports an empty set on the GLES2 renderer and would leave clients
|
|
with nothing to negotiate). */
|
|
{
|
|
static const enum wp_color_representation_surface_v1_alpha_mode
|
|
color_alpha_modes[] = {
|
|
WP_COLOR_REPRESENTATION_SURFACE_V1_ALPHA_MODE_PREMULTIPLIED_ELECTRICAL,
|
|
};
|
|
static const struct wlr_color_representation_v1_coeffs_and_range
|
|
color_coeffs_ranges[] = {
|
|
{
|
|
.coeffs = WP_COLOR_REPRESENTATION_SURFACE_V1_COEFFICIENTS_IDENTITY,
|
|
.range = WP_COLOR_REPRESENTATION_SURFACE_V1_RANGE_FULL,
|
|
},
|
|
};
|
|
const struct wlr_color_representation_v1_options color_opts = {
|
|
.supported_alpha_modes = color_alpha_modes,
|
|
.supported_alpha_modes_len = 1,
|
|
.supported_coeffs_and_ranges = color_coeffs_ranges,
|
|
.supported_coeffs_and_ranges_len = 1,
|
|
};
|
|
server.color_repr_mgr = wlr_color_representation_manager_v1_create(
|
|
server.wl_display, 1, &color_opts);
|
|
}
|
|
|
|
/* Color management: advertise the SDR sRGB session so color-aware
|
|
clients can negotiate image descriptions; the scene handles
|
|
per-surface feedback once linked. */
|
|
{
|
|
static const enum wp_color_manager_v1_render_intent
|
|
color_render_intents[] = {
|
|
WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL,
|
|
};
|
|
static const enum wp_color_manager_v1_transfer_function
|
|
color_transfer_functions[] = {
|
|
WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB,
|
|
};
|
|
static const enum wp_color_manager_v1_primaries
|
|
color_primaries[] = {
|
|
WP_COLOR_MANAGER_V1_PRIMARIES_SRGB,
|
|
};
|
|
const struct wlr_color_manager_v1_options color_mgmt_opts = {
|
|
.features = {0},
|
|
.render_intents = color_render_intents,
|
|
.render_intents_len = 1,
|
|
.transfer_functions = color_transfer_functions,
|
|
.transfer_functions_len = 1,
|
|
.primaries = color_primaries,
|
|
.primaries_len = 1,
|
|
};
|
|
server.color_manager = wlr_color_manager_v1_create(
|
|
server.wl_display, 1, &color_mgmt_opts);
|
|
if (server.color_manager)
|
|
wlr_scene_set_color_manager_v1(server.scene,
|
|
server.color_manager);
|
|
}
|
|
|
|
/* System bell: clients ring the bell (see handle_bell_ring).
|
|
Version 1 is the only one wlroots speaks. */
|
|
server.bell_mgr = wlr_xdg_system_bell_v1_create(server.wl_display, 1);
|
|
if (server.bell_mgr) {
|
|
server.bell_ring.notify = handle_bell_ring;
|
|
wl_signal_add(&server.bell_mgr->events.ring, &server.bell_ring);
|
|
}
|
|
|
|
/* The output layout exists in both modes so xdg-output can advertise
|
|
logical output geometry to clients; nested mode simply has one output
|
|
(the host window) in the layout. */
|
|
server.output_layout = wlr_output_layout_create(server.wl_display);
|
|
if (!server.output_layout) {
|
|
fprintf(stderr, "kick: failed to create output layout\n");
|
|
return 1;
|
|
}
|
|
server.xdg_output_mgr = wlr_xdg_output_manager_v1_create(server.wl_display,
|
|
server.output_layout);
|
|
|
|
/* Standalone has no host compositor to draw a pointer, so give the
|
|
output a real wlr_cursor. Theme/size follow the XDG cursor convention:
|
|
XCURSOR_THEME (default "default") and XCURSOR_SIZE (default 24px).
|
|
wlroots resolves the theme itself (search path, Inherits chains) and,
|
|
when nothing is installed, substitutes a builtin arrow -- so the pointer
|
|
is never invisible. We only report when the requested theme was
|
|
silently swapped for the builtin. */
|
|
if (server.standalone) {
|
|
server.cursor = wlr_cursor_create();
|
|
if (!server.cursor) {
|
|
fprintf(stderr, "kick: failed to create cursor\n");
|
|
return 1;
|
|
}
|
|
wlr_cursor_attach_output_layout(server.cursor, server.output_layout);
|
|
|
|
const char *theme_env = getenv("XCURSOR_THEME");
|
|
const char *theme = theme_env && *theme_env ? theme_env : "default";
|
|
|
|
uint32_t size = 24;
|
|
const char *size_env = getenv("XCURSOR_SIZE");
|
|
if (size_env) {
|
|
char *end = NULL;
|
|
unsigned long v = strtoul(size_env, &end, 10);
|
|
if (end != size_env && v > 0)
|
|
size = (uint32_t)v;
|
|
}
|
|
|
|
server.xcursor_mgr = wlr_xcursor_manager_create(theme, size);
|
|
if (server.xcursor_mgr)
|
|
wlr_xcursor_manager_load(server.xcursor_mgr, 1);
|
|
|
|
/* Detect the builtin from the manager's own state: wlr_xcursor_theme_load
|
|
renames the theme object to "default" when it substitutes the builtin
|
|
arrow (cursor_count == 0), so a mismatch with the requested name
|
|
means the theme didn't exist on disk. A theme literally named
|
|
"default" is indistinguishable (the builtin adopts that name), so
|
|
skip the warning there and only fail warm either way. */
|
|
struct wlr_xcursor *cur = server.xcursor_mgr ?
|
|
wlr_xcursor_manager_get_xcursor(server.xcursor_mgr, "default", 1)
|
|
: NULL;
|
|
if (cur && strcmp(theme, "default") != 0 &&
|
|
!wl_list_empty(&server.xcursor_mgr->scaled_themes)) {
|
|
struct wlr_xcursor_manager_theme *mt = wl_container_of(
|
|
server.xcursor_mgr->scaled_themes.next, mt, link);
|
|
if (mt->theme && mt->theme->name &&
|
|
strcmp(mt->theme->name, theme) != 0)
|
|
kick_log(KICK_LOG_INFO,
|
|
"[cursor] theme '%s' not found; wlroots builtin cursor "
|
|
"in use\n", theme);
|
|
}
|
|
if (!cur) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"[cursor] no cursor theme available; no pointer shown\n");
|
|
} else {
|
|
wlr_cursor_set_xcursor(server.cursor, server.xcursor_mgr, "default");
|
|
wl_signal_add(&server.cursor->events.motion,
|
|
&standalone_cursor_motion_listener);
|
|
}
|
|
}
|
|
|
|
/* virtual keyboard fed from the host keyboard */
|
|
static const struct wlr_keyboard_impl keyboard_impl = {
|
|
.name = "kick-keyboard",
|
|
};
|
|
wlr_keyboard_init(&server.keyboard, &keyboard_impl, "kick-keyboard");
|
|
|
|
/* fallback keymap until the host sends one */
|
|
server.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if (server.xkb_ctx) {
|
|
xkb_context_set_log_level(server.xkb_ctx, XKB_LOG_LEVEL_DEBUG);
|
|
xkb_context_set_log_fn(server.xkb_ctx, kick_xkb_log);
|
|
struct xkb_rule_names names = {
|
|
.rules = NULL, .model = "pc105",
|
|
.layout = "us", .variant = NULL, .options = NULL,
|
|
};
|
|
server.xkb_keymap = xkb_keymap_new_from_names(
|
|
server.xkb_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
if (server.xkb_keymap) {
|
|
wlr_keyboard_set_keymap(&server.keyboard, server.xkb_keymap);
|
|
xkb_state_reload();
|
|
}
|
|
}
|
|
wlr_seat_set_keyboard(server.seat, &server.keyboard);
|
|
|
|
server.new_output.notify = handle_new_output;
|
|
wl_signal_add(&server.backend->events.new_output, &server.new_output);
|
|
server.new_toplevel.notify = handle_new_toplevel;
|
|
wl_signal_add(&server.xdg_shell->events.new_toplevel, &server.new_toplevel);
|
|
server.new_popup.notify = handle_new_popup;
|
|
wl_signal_add(&server.xdg_shell->events.new_popup, &server.new_popup);
|
|
|
|
/* Standalone: attach the native (libinput) input devices to the seat. */
|
|
if (server.standalone) {
|
|
server.new_input.notify = handle_new_input;
|
|
wl_signal_add(&server.backend->events.new_input, &server.new_input);
|
|
}
|
|
|
|
/* The headless backend (nested always, and standalone under
|
|
WLR_BACKENDS=headless when no real display is available) has no
|
|
outputs of its own, so add ours manually; DRM emits its outputs via
|
|
new_output. new_output fires during backend start. */
|
|
if (!server.standalone || server.standalone_headless) {
|
|
int w = server.standalone && mode_req_w > 0 ? mode_req_w : 1280;
|
|
int h = server.standalone && mode_req_h > 0 ? mode_req_h : 800;
|
|
host.width = w;
|
|
host.height = h;
|
|
wlr_headless_add_output(server.backend, host.width, host.height);
|
|
}
|
|
|
|
if (!wlr_backend_start(server.backend)) {
|
|
fprintf(stderr, "kick: failed to start backend\n");
|
|
return 1;
|
|
}
|
|
|
|
if (wl_display_add_socket(server.wl_display, kick_socket) < 0) {
|
|
fprintf(stderr, "kick: failed to add socket '%s': %s\n",
|
|
kick_socket, strerror(errno));
|
|
return 1;
|
|
}
|
|
|
|
/* The tab/MRU lists must exist before host_init(): a host that sends
|
|
its first configure synchronously inside the startup roundtrip drives
|
|
the resize path (which walks server.tabs) before any tab exists. */
|
|
wl_list_init(&server.tabs);
|
|
wl_list_init(&server.mru);
|
|
server.mru_alt_anchor = NULL;
|
|
server.mru_alt_navigated = false;
|
|
|
|
/* Standalone mode presents onto the native output directly; nested mode
|
|
connects to the host compositor and presents inside it. */
|
|
if (!server.standalone && !host_init()) {
|
|
fprintf(stderr, "kick: could not start nested session\n");
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
|
|
kick_log(KICK_LOG_INFO, "Wayland socket: %s\n", kick_socket);
|
|
|
|
/* Build the tab list: one tab per command-line command, in command-line
|
|
order, each carrying a still-black pending window the command's client
|
|
claims when it connects (the process-group match in
|
|
handle_new_toplevel puts each client on its own slot no matter which
|
|
connects first). The lists themselves were initialized before
|
|
host_init above. */
|
|
int focus_idx = start_cmd_index >= 0 ? start_cmd_index : total_apps - 1;
|
|
struct kick_tab *focus_tab = NULL;
|
|
for (int i = 0; i < total_apps; i++) {
|
|
struct kick_tab *tab = calloc(1, sizeof(*tab));
|
|
if (!tab) {
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
wl_list_init(&tab->windows);
|
|
wl_list_insert(server.tabs.prev, &tab->link);
|
|
|
|
struct kick_window *w = window_alloc(tab);
|
|
if (!w) {
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
const char *cmd = default_tab ? term_cmd : appv[i];
|
|
w->cmd = strdup(cmd);
|
|
w->pid = -1;
|
|
w->origin = ORIGIN_CLI;
|
|
/* Live-but-black until the command claims the slot with a toplevel. */
|
|
w->pending_claim = true;
|
|
if (i == focus_idx)
|
|
focus_tab = tab;
|
|
|
|
if (cmd[0] == '-') {
|
|
if (i > 0) {
|
|
fprintf(stderr,
|
|
"kick: warning: %s starts with '-'; it is "
|
|
"run as its own tab, not an option to %s\n"
|
|
" If intended, quote it: \"%s %s\"\n",
|
|
cmd, appv[i - 1], appv[i - 1], cmd);
|
|
} else {
|
|
fprintf(stderr,
|
|
"kick: warning: %s starts with '-'; if this "
|
|
"is meant for a first command, quote them together, "
|
|
"e.g. \"%s\"\n",
|
|
cmd, cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The selected command-line tab is active from the very first frame:
|
|
black, titled by its command line, until the client maps. The user
|
|
waits only on that one tab. --start-cmd-index picks it (1-based);
|
|
without the flag the last command is the one shown, so the startup tab
|
|
is always the one the caller can count on. */
|
|
set_active_tab(focus_tab);
|
|
|
|
/* Fork every command-line command at once; the process-group match in
|
|
handle_new_toplevel gives each its own slot on arrival. Refresh the
|
|
D-Bus/systemd activation environment first so services started from
|
|
here on see this session's socket. */
|
|
activation_update();
|
|
launch_all_cli();
|
|
/* The --panel command, if any: a layer-shell client, forked separately since
|
|
it never claims a tab. */
|
|
launch_panel();
|
|
|
|
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
|
kick_log(KICK_LOG_INFO, "kick: running. "
|
|
"%s+Up/Down switches tabs; %s+Tab/%s+1 walks recent windows; "
|
|
"%s+Shift+Up/Down reorders; "
|
|
"%s+Delete closes the active window (or the tab when it's the only window); "
|
|
"%s+Shift+T opens a terminal (%s).\n",
|
|
prefix, prefix, prefix, prefix, prefix, prefix, term_cmd);
|
|
|
|
/* Standalone event loop: no host display, so drive the server loop alone.
|
|
The native output's frame signal (or the headless 60Hz timer) wakes the
|
|
dispatcher and triggers a render. The loop fd is polled directly and
|
|
dispatched with timeout 0: dispatch(-1) would re-poll inside libwayland
|
|
after draining the SIGUSR1 self-pipe and never return, leaving the gate
|
|
below unevaluated on an idle screen. */
|
|
if (server.standalone) {
|
|
int server_loop_fd = wl_event_loop_get_fd(server.loop);
|
|
for (;;) {
|
|
struct pollfd fds[1] = {
|
|
{ .fd = server_loop_fd, .events = POLLIN },
|
|
};
|
|
int ret;
|
|
do {
|
|
ret = poll(fds, 1, -1);
|
|
} while (ret < 0 && errno == EINTR);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
do {
|
|
ret = wl_event_loop_dispatch(server.loop, 0);
|
|
} while (ret == -1 && errno == EINTR);
|
|
wl_display_flush_clients(server.wl_display);
|
|
sweep_dead_startup();
|
|
if (server.output && (wlr_output_is_headless(server.output)
|
|
|| !server.output->frame_pending)
|
|
&& (needs_redraw || server.output->needs_frame)) {
|
|
frame_pending = false;
|
|
needs_redraw = false;
|
|
render_and_present();
|
|
/* A render queues frame-done (and other) events for clients;
|
|
flush them before blocking in poll below, or they sit in
|
|
the connection until the next wakeup (e.g. the headless
|
|
60Hz timer ~16ms later), visibly slowing every client --
|
|
including a nested kick, whose swaps pace on frame-done. */
|
|
wl_display_flush_clients(server.wl_display);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Nested event loop: service the server, the host display, and libdecor
|
|
(whose decoration plugin runs its own event handling). */
|
|
int server_fd = wl_event_loop_get_fd(server.loop);
|
|
int host_fd = wl_display_get_fd(host.display);
|
|
int decor_fd = host.libdecor ? libdecor_get_fd(host.libdecor) : -1;
|
|
|
|
for (;;) {
|
|
wl_display_dispatch_pending(host.display);
|
|
if (host.use_libdecor)
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
/* Flush queued nested-client messages before blocking: a data-control
|
|
`send` queued while dispatching the host side would otherwise sit
|
|
in the client connection until some later event wakes the poll
|
|
below, stalling host-pasted clipboard data for seconds. */
|
|
wl_display_flush_clients(server.wl_display);
|
|
|
|
/* Never block in a host read: use the non-blocking prepare/read
|
|
pattern so the server loop (headless frame timer) is always
|
|
serviced while waiting on the host socket. */
|
|
if (wl_display_prepare_read(host.display) == 0) {
|
|
wl_display_flush(host.display);
|
|
struct pollfd fds[3] = {
|
|
{ .fd = server_fd, .events = POLLIN },
|
|
{ .fd = host_fd, .events = POLLIN },
|
|
{ .fd = decor_fd, .events = POLLIN },
|
|
};
|
|
int nfds = decor_fd >= 0 ? 3 : 2;
|
|
int ret;
|
|
do {
|
|
ret = poll(fds, nfds, -1);
|
|
} while (ret < 0 && errno == EINTR);
|
|
|
|
if (ret > 0 && (fds[1].revents & (POLLIN | POLLERR)))
|
|
wl_display_read_events(host.display);
|
|
else
|
|
wl_display_cancel_read(host.display);
|
|
|
|
if (ret > 0 && (fds[0].revents & (POLLIN | POLLERR)))
|
|
wl_event_loop_dispatch(server.loop, 0);
|
|
} else {
|
|
wl_event_loop_dispatch(server.loop, 0);
|
|
}
|
|
wl_display_flush_clients(server.wl_display);
|
|
sweep_dead_startup();
|
|
wl_display_dispatch_pending(host.display);
|
|
if (host.use_libdecor)
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
|
|
/* Render only when something changed (kick's own redraws or scene
|
|
damage) and no page flip is in flight; an idle screen never
|
|
repaints. wl_scene schedules its own frame wake on client
|
|
damage, so a dirty scene always gets here. */
|
|
if (server.output && (wlr_output_is_headless(server.output)
|
|
|| !server.output->frame_pending)
|
|
&& (needs_redraw || server.output->needs_frame)) {
|
|
frame_pending = false;
|
|
if (!host.present_pending || dump_scene_req || dump_host_req) {
|
|
needs_redraw = false;
|
|
render_and_present();
|
|
/* Same as the standalone loop above: a render queues
|
|
frame-done (and other) events for clients; flush them
|
|
before blocking in poll, or they sit until the next
|
|
wakeup -- stalling a nested kick, whose swaps pace on
|
|
frame-done. */
|
|
wl_display_flush_clients(server.wl_display);
|
|
} else if (server.scene_output) {
|
|
/* A host present is still outstanding, so a full render
|
|
now (scene build + GPU blit + swap) would only be
|
|
overwritten before it is ever shown -- one wasted
|
|
1080p pass per hold-repeat tick at 25Hz, blocking the
|
|
loop the next tick needs. Send just the frame-done
|
|
instead, keeping clients paced, and leave the redraw
|
|
flags set so the ack paints everything accumulated.
|
|
(present_pending is only ever true in nested mode;
|
|
standalone always takes the full path. Dumps punch
|
|
through because they need a real buffer.) */
|
|
struct timespec cheap_now;
|
|
clock_gettime(CLOCK_MONOTONIC, &cheap_now);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[diag] cheap frame (present pending)\n");
|
|
wlr_scene_output_send_frame_done(server.scene_output,
|
|
&cheap_now);
|
|
wl_display_flush_clients(server.wl_display);
|
|
}
|
|
}
|
|
wl_display_flush(host.display);
|
|
}
|
|
|
|
/* Unreachable in practice: every shutdown path exits via
|
|
shutdown_cleanup(). Kept for completeness. */
|
|
shutdown_cleanup();
|
|
return 0;
|
|
}
|