Hidden tabs live in disabled scene trees, so wlr_scene_output_send_frame_done() never reaches them and frame-callback-throttled clients stop committing: only the shown window's thumbnail tracked live while the rest sat at their last frame. Fan frame-done out to every hidden tab window once per output frame while the overlay is held (overlay_send_hidden_frames after the scene frame-done in both render_standalone and render_and_present). The walk covers main plus subsurface trees for both xdg and X11 windows via window_surface(), and skips the shown window the scene path already handles. Gated on --overlay-previews plus overlay_visible, so the steady state with the menu hidden costs nothing and background clients are not woken. The existing thumb_capture snapshot and draw-time tree composite paths are unchanged; they simply receive the commits they were missing. Tests: new test_standalone_headless_overlay_previews_hidden_live uses a hidden --pulse tab and asserts the red thumbnail count swings while Alt is held. README overlay-previews paragraph updated from freeze to live-while-held. Verified: ./build.sh clean; overlay-previews suite 6 passed; full suite 179 passed with -n4.
15160 lines
638 KiB
C
15160 lines
638 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 surface in a window's subsurface tree watched for thumbnail commits.
|
|
Lives in w->thumb_subs from thumb_sub_wire until the surface dies or the
|
|
window goes away (thumb_release); either side frees it exactly once. */
|
|
struct kick_thumb_sub {
|
|
struct wl_list link; /* w->thumb_subs */
|
|
struct kick_window *w;
|
|
struct wlr_surface *surface;
|
|
struct wl_listener commit;
|
|
struct wl_listener destroy;
|
|
struct wl_listener new_subsurface;
|
|
};
|
|
|
|
/* 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;
|
|
|
|
/* 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;
|
|
/* Live overlay-preview thumbnail (--overlay-previews): the window's
|
|
latest frame. Windows without subsurfaces route each commit to
|
|
whichever source is fresh -- a CPU snapshot (fresh SHM attaches),
|
|
a locked dma-buf (GPU attaches), or the surface's own texture
|
|
(in-place SHM updates, where current.buffer is already NULL at
|
|
commit time). Windows with subsurface content (e.g. Firefox)
|
|
composite at overlay-draw time from live surface textures instead,
|
|
which outlive current.buffer. Released with the window. */
|
|
struct wlr_texture *thumb_tex;
|
|
struct wlr_buffer *thumb_buf;
|
|
uint32_t *thumb_px;
|
|
int thumb_pw, thumb_ph;
|
|
bool thumb_dirty;
|
|
enum { THUMB_SRC_NONE, THUMB_SRC_SNAP, THUMB_SRC_DMABUF,
|
|
THUMB_SRC_LIVE } thumb_src;
|
|
/* Subsurface commit tracking for overlay thumbnails: one entry per
|
|
surface in the window's subsurface tree (main surface first). A
|
|
subsurface-only paint fires no toplevel commit, so each surface
|
|
carries its own commit listener re-running thumb_capture, and
|
|
new_subsurface chains surfaces created after the claim. Emptied by
|
|
thumb_release with the window. */
|
|
struct wl_list thumb_subs; /* kick_thumb_sub.link */
|
|
/* 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_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;
|
|
/* 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: full-colour tile (grayscale
|
|
antialiasing, premultiplied) drawn through the existing blend path, so
|
|
row dimming (global alpha fade) 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. White text stays white; colour glyphs (e.g. colour emoji) keep
|
|
their colour. */
|
|
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 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;
|
|
|
|
/* --overlay-previews: show window thumbnails in the tab overlay instead
|
|
of icon+title rows. Each row is the window's icon followed by a small
|
|
view of the window as it last appeared (live for the shown window, which
|
|
commits continuously); tabs with no mapped window keep their command-line
|
|
text row. Off by default. */
|
|
static bool opt_overlay_previews = false;
|
|
|
|
/* --cursor-size N: standalone pointer size in px (8..128). -1 means unset:
|
|
fall back to $XCURSOR_SIZE, then the 24px default. Nested mode has no
|
|
cursor of its own (the host draws the pointer), so it is ignored there. */
|
|
static int opt_cursor_size = -1;
|
|
|
|
/* ===================== 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);
|
|
/* 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 tab_index(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)
|
|
remove_tab(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->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->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 full-colour tile (grayscale
|
|
antialiasing: subpixel rendering over our translucent blending path
|
|
misbehaves, per labwc's findings) and upload it like the icon path.
|
|
White text renders white; colour glyphs (colour emoji) keep their
|
|
colour. Dimming is a global-alpha fade at draw time, so no tinting
|
|
here. 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 little-endian BGRA, which is exactly
|
|
the byte order wlr_texture_from_pixels expects for
|
|
DRM_FORMAT_ARGB8888 (same as the icon/cursor upload paths), so copy
|
|
rows verbatim: white text stays white with coverage alpha, colour
|
|
glyphs keep their colour. */
|
|
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;
|
|
memcpy(drow, srow, (size_t)w * 4);
|
|
}
|
|
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) {
|
|
int n = snprintf(roots[n_roots], sizeof(roots[0]), "%s/icons",
|
|
data_home);
|
|
if (n > 0 && (size_t)n < sizeof(roots[0]))
|
|
n_roots++;
|
|
}
|
|
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]) {
|
|
int n = snprintf(roots[n_roots], sizeof(roots[0]), "%s/icons", p);
|
|
if (n > 0 && (size_t)n < sizeof(roots[0]))
|
|
n_roots++;
|
|
}
|
|
}
|
|
free(copy);
|
|
|
|
if (strchr(name, '/') && access(name, R_OK) == 0 &&
|
|
(overlay_endswith(name, ".png") ||
|
|
overlay_endswith(name, ".svg")))
|
|
return strdup(name);
|
|
|
|
/* Icon names become a single path component below; anything absurdly
|
|
long can never match a real file, so fail fast instead of probing
|
|
a truncated path. The %.1024s bound keeps every snprintf below
|
|
provably within buf (worst case ~1.6k < 4096). */
|
|
if (strlen(name) > 1024)
|
|
return NULL;
|
|
|
|
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++) {
|
|
int n = snprintf(buf, sizeof(buf), "%s/%s/%s/apps/%.1024s.%s",
|
|
roots[tr], themes[t], sizes[s], name, exts[e]);
|
|
if (n < 0 || (size_t)n >= sizeof(buf))
|
|
continue;
|
|
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++) {
|
|
int n = snprintf(buf, sizeof(buf), "%s/%.1024s.%s", pixmap_roots[pr], name, exts[e]);
|
|
if (n < 0 || (size_t)n >= sizeof(buf))
|
|
continue;
|
|
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);
|
|
}
|
|
|
|
/* --overlay-previews geometry: thumbnail rows divide the menu, which is
|
|
content-sized and centered with a 50px top/bottom margin. Each row would
|
|
fill an equal share of the margined height less the selected-window
|
|
header block, but no thumbnail grows past a quarter of the screen
|
|
height; the menu then wraps the header plus the capped rows.
|
|
Thumbnails keep a fixed 16:9 size; the preview row pads them like a text
|
|
row pads its font (+8). Pending (windowless) rows share the uniform row
|
|
height. */
|
|
#define OVERLAY_MENU_MARGIN_V 50
|
|
static int overlay_title_row_h(void) {
|
|
int rh = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
if (rh < OVERLAY_ICON_SZ + 4) rh = OVERLAY_ICON_SZ + 4;
|
|
return rh;
|
|
}
|
|
/* Last computed preview metrics (see overlay_preview_metrics): the commit
|
|
path has no row count handy, so the snapshot size follows this cache.
|
|
A slightly stale snapshot only changes the effective downscale filtering
|
|
at draw time, which stretches any snapshot to the live thumb box. */
|
|
static int preview_thumb_w_cached, preview_thumb_h_cached;
|
|
static int overlay_preview_head_h(void);
|
|
static void overlay_preview_metrics(int n_rows, int *row_h, int *thumb_w,
|
|
int *thumb_h) {
|
|
int boxpad = 6, row_gap = 8;
|
|
int avail_h = host.height - 2 * OVERLAY_MENU_MARGIN_V -
|
|
overlay_preview_head_h();
|
|
int fill = 0;
|
|
if (n_rows > 0)
|
|
fill = (avail_h - 2 * boxpad - (n_rows - 1) * row_gap) / n_rows - 8;
|
|
int th = fill;
|
|
int cap = host.height / 4;
|
|
if (th > cap) th = cap;
|
|
if (th < 1) th = 1;
|
|
preview_thumb_w_cached = th * 16 / 9;
|
|
preview_thumb_h_cached = th;
|
|
if (thumb_w) *thumb_w = th * 16 / 9;
|
|
if (thumb_h) *thumb_h = th;
|
|
if (row_h) *row_h = th + 8;
|
|
}
|
|
/* Snapshot pixels: twice the box in each dimension. The extra density feeds
|
|
the GPU bilinear downscale at draw time, so text survives as shapes
|
|
instead of nearest-neighbor speckle. */
|
|
#define OVERLAY_PREVIEW_SUPERSAMPLE 2
|
|
static int overlay_preview_snap_w(void) {
|
|
int tw = preview_thumb_w_cached;
|
|
if (tw <= 0) tw = overlay_title_row_h() * 3 * 16 / 9;
|
|
return tw * OVERLAY_PREVIEW_SUPERSAMPLE;
|
|
}
|
|
static int overlay_preview_snap_h(void) {
|
|
int th = preview_thumb_h_cached;
|
|
if (th <= 0) th = overlay_title_row_h() * 3;
|
|
return th * OVERLAY_PREVIEW_SUPERSAMPLE;
|
|
}
|
|
|
|
/* Live overlay-preview thumbnail (--overlay-previews). Capture runs in the
|
|
commit handler and routes each commit to its fresh source: a fresh attach
|
|
snapshots (SHM) or locks (dma-buf) the committed buffer; an in-place SHM
|
|
update (current.buffer already NULL) leaves the pixels in the surface's
|
|
own texture, which wlroots refreshed from the dropped buffer. Windows
|
|
whose content lives in subsurfaces (e.g. Firefox) instead composite the
|
|
whole surface tree at overlay-draw time from live textures, which
|
|
outlive current.buffer. Draw serves whichever source fits the window. */
|
|
static void thumb_sub_release(struct kick_thumb_sub *sub) {
|
|
if (!sub) return;
|
|
wl_list_remove(&sub->commit.link);
|
|
wl_list_remove(&sub->destroy.link);
|
|
wl_list_remove(&sub->new_subsurface.link);
|
|
wl_list_remove(&sub->link);
|
|
free(sub);
|
|
}
|
|
|
|
static void thumb_release(struct kick_window *w) {
|
|
if (!w) return;
|
|
struct kick_thumb_sub *sub, *tmp;
|
|
wl_list_for_each_safe(sub, tmp, &w->thumb_subs, link)
|
|
thumb_sub_release(sub);
|
|
if (w->thumb_tex) {
|
|
wlr_texture_destroy(w->thumb_tex);
|
|
w->thumb_tex = NULL;
|
|
}
|
|
if (w->thumb_buf) {
|
|
wlr_buffer_unlock(w->thumb_buf);
|
|
w->thumb_buf = NULL;
|
|
}
|
|
free(w->thumb_px);
|
|
w->thumb_px = NULL;
|
|
w->thumb_pw = w->thumb_ph = 0;
|
|
w->thumb_dirty = false;
|
|
w->thumb_src = THUMB_SRC_NONE;
|
|
}
|
|
|
|
/* Single-surface capture: the legacy fast path for windows without
|
|
subsurfaces. `s` is the main surface, `b` its current buffer. */
|
|
static void thumb_capture_single(struct kick_window *w, struct wlr_surface *s,
|
|
struct wlr_buffer *b) {
|
|
if (b && b->width > 0 && b->height > 0) {
|
|
/* Route dma-buf straight to the GPU path: probing it with a CPU
|
|
data-pointer access first can stall on GPU fences at 60Hz, which
|
|
visibly slows GPU clients like alacritty. get_dmabuf is a cheap
|
|
attribute read with no mapping or sync involved. */
|
|
struct wlr_dmabuf_attributes dma;
|
|
if (wlr_buffer_get_dmabuf(b, &dma)) {
|
|
if (b != w->thumb_buf) {
|
|
if (w->thumb_buf)
|
|
wlr_buffer_unlock(w->thumb_buf);
|
|
w->thumb_buf = wlr_buffer_lock(b);
|
|
}
|
|
w->thumb_src = THUMB_SRC_DMABUF;
|
|
w->thumb_dirty = true;
|
|
} else {
|
|
void *data = NULL;
|
|
uint32_t fmt = 0;
|
|
size_t stride = 0;
|
|
bool begun = wlr_buffer_begin_data_ptr_access(
|
|
b, WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &fmt, &stride);
|
|
bool cpu = begun &&
|
|
(fmt == DRM_FORMAT_ARGB8888 || fmt == DRM_FORMAT_XRGB8888);
|
|
if (cpu) {
|
|
int bw = b->width, bh = b->height;
|
|
int tw = overlay_preview_snap_w(), th = overlay_preview_snap_h();
|
|
if (!w->thumb_px || w->thumb_pw != tw || w->thumb_ph != th) {
|
|
free(w->thumb_px);
|
|
w->thumb_px = malloc((size_t)tw * th * 4);
|
|
w->thumb_pw = tw;
|
|
w->thumb_ph = th;
|
|
}
|
|
if (w->thumb_px) {
|
|
/* Box-averaged downscale: average every source pixel in
|
|
each destination pixel's block instead of sampling one,
|
|
so glyph strokes survive as shapes rather than speckle.
|
|
Average in premultiplied space. */
|
|
for (int y = 0; y < th; y++) {
|
|
int sy0 = (int)((int64_t)y * bh / th);
|
|
int sy1 = (int)(((int64_t)(y + 1) * bh + th - 1) / th);
|
|
if (sy1 <= sy0) sy1 = sy0 + 1;
|
|
if (sy1 > bh) sy1 = bh;
|
|
for (int x = 0; x < tw; x++) {
|
|
int sx0 = (int)((int64_t)x * bw / tw);
|
|
int sx1 = (int)(((int64_t)(x + 1) * bw + tw - 1) / tw);
|
|
if (sx1 <= sx0) sx1 = sx0 + 1;
|
|
if (sx1 > bw) sx1 = bw;
|
|
unsigned long sa = 0, sr = 0, sg = 0, sb = 0;
|
|
unsigned long n = 0;
|
|
for (int sy = sy0; sy < sy1; sy++) {
|
|
const uint32_t *row = (const uint32_t *)
|
|
((const char *)data + (size_t)sy * stride);
|
|
for (int sx = sx0; sx < sx1; sx++) {
|
|
uint32_t p = row[sx];
|
|
unsigned a = (p >> 24) & 0xff;
|
|
if (fmt == DRM_FORMAT_XRGB8888 || a == 0)
|
|
a = 0xff;
|
|
unsigned r = (p >> 16) & 0xff,
|
|
g = (p >> 8) & 0xff,
|
|
bl = p & 0xff;
|
|
sa += a;
|
|
sr += r * a / 255;
|
|
sg += g * a / 255;
|
|
sb += bl * a / 255;
|
|
n++;
|
|
}
|
|
}
|
|
if (n == 0) n = 1;
|
|
unsigned a = (unsigned)(sa / n);
|
|
unsigned r = (unsigned)(sr / n);
|
|
unsigned g = (unsigned)(sg / n);
|
|
unsigned bl = (unsigned)(sb / n);
|
|
w->thumb_px[(size_t)y * tw + x] =
|
|
(a << 24) | (r << 16) | (g << 8) | bl;
|
|
}
|
|
}
|
|
w->thumb_src = THUMB_SRC_SNAP;
|
|
w->thumb_dirty = true;
|
|
}
|
|
wlr_buffer_end_data_ptr_access(b);
|
|
if (w->thumb_buf) {
|
|
/* Source switched back to SHM: drop the GPU buffer lock. */
|
|
wlr_buffer_unlock(w->thumb_buf);
|
|
w->thumb_buf = NULL;
|
|
}
|
|
} else {
|
|
/* Readable but unexpected format: hold a lock and import. */
|
|
if (begun)
|
|
wlr_buffer_end_data_ptr_access(b);
|
|
if (b != w->thumb_buf) {
|
|
if (w->thumb_buf)
|
|
wlr_buffer_unlock(w->thumb_buf);
|
|
w->thumb_buf = wlr_buffer_lock(b);
|
|
}
|
|
w->thumb_src = THUMB_SRC_DMABUF;
|
|
w->thumb_dirty = true;
|
|
}
|
|
}
|
|
} else if (s && wlr_surface_get_texture(s)) {
|
|
/* In-place SHM update: the pixels went straight into the surface
|
|
texture (current.buffer was already consumed), which stays live
|
|
through every applied commit. */
|
|
if (w->thumb_buf) {
|
|
wlr_buffer_unlock(w->thumb_buf);
|
|
w->thumb_buf = NULL;
|
|
}
|
|
w->thumb_src = THUMB_SRC_LIVE;
|
|
w->thumb_dirty = true;
|
|
} else {
|
|
return;
|
|
}
|
|
/* A hidden window's commit damages no output, so repaint explicitly for
|
|
the preview to track the window live. */
|
|
if (host.overlay_visible)
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Tree-thumbnail collection: two wlr_surface_for_each_surface walks over the
|
|
window (bottom to top) at overlay-draw time. Positions are root-relative
|
|
surface coordinates; sizes come from each surface's live texture, so the
|
|
composite matches what the scene renders -- including subsurface content
|
|
whose current.buffer is long gone by draw time. Textures persist on the
|
|
surface from the last commit, so hidden tabs show their last frame. */
|
|
struct thumb_extent {
|
|
int minx, miny, maxx, maxy;
|
|
bool any;
|
|
};
|
|
|
|
struct thumb_draw_ctx {
|
|
struct wlr_render_pass *pass;
|
|
float alpha;
|
|
int ox, oy; /* extent origin in root coordinates */
|
|
double scale; /* window pixels -> thumb pixels */
|
|
int dx0, dy0; /* thumb-box origin of the fitted extent */
|
|
};
|
|
|
|
static void thumb_extent_iter(struct wlr_surface *surface, int sx, int sy,
|
|
void *data) {
|
|
struct thumb_extent *e = data;
|
|
struct wlr_texture *tex = surface ? wlr_surface_get_texture(surface) : NULL;
|
|
if (!tex || tex->width == 0 || tex->height == 0) return;
|
|
int tw = (int)tex->width;
|
|
int th = (int)tex->height;
|
|
if (!e->any) {
|
|
e->minx = sx;
|
|
e->miny = sy;
|
|
e->maxx = sx + tw;
|
|
e->maxy = sy + th;
|
|
e->any = true;
|
|
return;
|
|
}
|
|
if (sx < e->minx) e->minx = sx;
|
|
if (sy < e->miny) e->miny = sy;
|
|
if (sx + tw > e->maxx) e->maxx = sx + tw;
|
|
if (sy + th > e->maxy) e->maxy = sy + th;
|
|
}
|
|
|
|
static void thumb_draw_iter(struct wlr_surface *surface, int sx, int sy,
|
|
void *data) {
|
|
struct thumb_draw_ctx *c = data;
|
|
struct wlr_texture *tex = surface ? wlr_surface_get_texture(surface) : NULL;
|
|
if (!tex || tex->width <= 0 || tex->height <= 0) return;
|
|
struct wlr_render_texture_options o = {
|
|
.texture = tex,
|
|
.src_box = { .x = 0, .y = 0,
|
|
.width = (float)tex->width, .height = (float)tex->height },
|
|
.dst_box = { .x = (float)(c->dx0 + (sx - c->ox) * c->scale),
|
|
.y = (float)(c->dy0 + (sy - c->oy) * c->scale),
|
|
.width = (float)(tex->width * c->scale),
|
|
.height = (float)(tex->height * c->scale) },
|
|
.alpha = &c->alpha,
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(c->pass, &o);
|
|
}
|
|
|
|
/* Draw-time tree thumbnail: composite every live surface texture at its
|
|
root-relative offset, aspect-fit into the thumb box like the
|
|
single-surface path. Textures outlive current.buffer (cleared by commit
|
|
finalization), so this sees subsurface content no commit hook can. */
|
|
static void overlay_pass_tree_thumb(struct wlr_render_pass *pass,
|
|
struct kick_window *w,
|
|
int tx, int ty, int tw, int th,
|
|
float alpha) {
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s) return;
|
|
struct thumb_extent e = { 0 };
|
|
wlr_surface_for_each_surface(s, thumb_extent_iter, &e);
|
|
if (!e.any || e.maxx <= e.minx || e.maxy <= e.miny) return;
|
|
int ew = e.maxx - e.minx, eh = e.maxy - e.miny;
|
|
int dw = tw, dh = (int)((int64_t)tw * eh / ew);
|
|
if (dh > th) {
|
|
dh = th;
|
|
dw = (int)((int64_t)th * ew / eh);
|
|
}
|
|
if (dw <= 0 || dh <= 0) return;
|
|
struct thumb_draw_ctx c = {
|
|
.pass = pass,
|
|
.alpha = alpha,
|
|
.ox = e.minx, .oy = e.miny,
|
|
.scale = (double)dw / (double)ew,
|
|
.dx0 = tx + (tw - dw) / 2,
|
|
.dy0 = ty + (th - dh) / 2,
|
|
};
|
|
wlr_surface_for_each_surface(s, thumb_draw_iter, &c);
|
|
}
|
|
|
|
/* Whether the window owns subsurface content: such windows composite at
|
|
draw time from live textures (see overlay_pass_tree_thumb). */
|
|
static bool window_has_subsurfaces(struct kick_window *w) {
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s) return false;
|
|
return !wl_list_empty(&s->current.subsurfaces_above) ||
|
|
!wl_list_empty(&s->current.subsurfaces_below);
|
|
}
|
|
|
|
/* Tree windows composite at draw time from live textures; capture only
|
|
repaints while the overlay is held. */
|
|
static void thumb_capture_tree(struct kick_window *w) {
|
|
(void)w;
|
|
if (host.overlay_visible)
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Route a commit to its thumbnail source. Windows without subsurfaces
|
|
snapshot the committed buffer synchronously (current.buffer is only
|
|
valid inside the commit handler); windows with subsurface content
|
|
composite at overlay-draw time from live textures instead, so every
|
|
commit side -- the toplevel commit, each subsurface's commit, a sync
|
|
child's arrival -- only has to repaint while the overlay is held. */
|
|
static void thumb_capture(struct kick_window *w) {
|
|
if (!w || !opt_overlay_previews) return;
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s) return;
|
|
if (!window_has_subsurfaces(w)) {
|
|
struct wlr_buffer *b = s->current.buffer;
|
|
thumb_capture_single(w, s, b);
|
|
return;
|
|
}
|
|
thumb_capture_tree(w);
|
|
}
|
|
|
|
static void thumb_sub_wire(struct kick_window *w, struct wlr_surface *s);
|
|
|
|
static void thumb_sub_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_thumb_sub *sub = wl_container_of(listener, sub, commit);
|
|
thumb_capture(sub->w);
|
|
}
|
|
|
|
static void thumb_sub_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_thumb_sub *sub = wl_container_of(listener, sub, destroy);
|
|
thumb_sub_release(sub);
|
|
}
|
|
|
|
static void thumb_sub_new(struct wl_listener *listener, void *data) {
|
|
struct kick_thumb_sub *sub = wl_container_of(listener, sub,
|
|
new_subsurface);
|
|
/* new_subsurface carries the fresh wlr_subsurface. */
|
|
struct wlr_subsurface *wsub = data;
|
|
if (!wsub || !wsub->surface) return;
|
|
thumb_sub_wire(sub->w, wsub->surface);
|
|
/* The tree gained a surface: repaint while the overlay is held so the
|
|
new content appears without waiting for the next commit. */
|
|
thumb_capture(sub->w);
|
|
}
|
|
|
|
static bool thumb_sub_wired(struct kick_window *w, struct wlr_surface *s) {
|
|
struct kick_thumb_sub *sub;
|
|
wl_list_for_each(sub, &w->thumb_subs, link)
|
|
if (sub->surface == s) return true;
|
|
return false;
|
|
}
|
|
|
|
/* Watch `s` (and, via the walk in thumb_sub_wire_tree, its existing
|
|
children) for thumbnail commits. Surfaces created later arrive through
|
|
new_subsurface, which wlroots emits on the parent at their first
|
|
commit. */
|
|
static void thumb_sub_wire(struct kick_window *w, struct wlr_surface *s) {
|
|
if (!w || !s || thumb_sub_wired(w, s)) return;
|
|
struct kick_thumb_sub *sub = calloc(1, sizeof(*sub));
|
|
if (!sub) return;
|
|
sub->w = w;
|
|
sub->surface = s;
|
|
sub->commit.notify = thumb_sub_commit;
|
|
wl_signal_add(&s->events.commit, &sub->commit);
|
|
sub->destroy.notify = thumb_sub_destroy;
|
|
wl_signal_add(&s->events.destroy, &sub->destroy);
|
|
sub->new_subsurface.notify = thumb_sub_new;
|
|
wl_signal_add(&s->events.new_subsurface, &sub->new_subsurface);
|
|
wl_list_insert(w->thumb_subs.prev, &sub->link);
|
|
}
|
|
|
|
static void thumb_wire_iter(struct wlr_surface *surface, int sx, int sy,
|
|
void *data) {
|
|
(void)sx;
|
|
(void)sy;
|
|
struct kick_window *w = data;
|
|
/* The main surface already has the window's own commit listener:
|
|
watching it twice would snapshot every frame twice. */
|
|
if (surface == window_surface(w)) return;
|
|
thumb_sub_wire(w, surface);
|
|
}
|
|
|
|
static void thumb_sub_wire_tree(struct kick_window *w) {
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s) return;
|
|
wlr_surface_for_each_surface(s, thumb_wire_iter, w);
|
|
}
|
|
|
|
/* Live thumbnails while the overlay is held: hidden windows sit in disabled
|
|
scene trees, so wlr_scene_output_send_frame_done() never reaches them and
|
|
frame-callback-throttled clients stop committing (only the shown window's
|
|
thumbnail tracks live). Fan frame-done out to every hidden tab window's
|
|
surface tree once per output frame while held, so each hidden client keeps
|
|
producing frames that thumb_capture() snapshots. Gated on
|
|
--overlay-previews + overlay_visible, so the steady state (menu hidden)
|
|
costs nothing. */
|
|
static void overlay_hidden_frame_iter(struct wlr_surface *surface, int sx,
|
|
int sy, void *data) {
|
|
(void)sx;
|
|
(void)sy;
|
|
struct timespec *now = data;
|
|
if (surface && now)
|
|
wlr_surface_send_frame_done(surface, now);
|
|
}
|
|
|
|
static void overlay_send_hidden_frames(struct timespec *now) {
|
|
if (!opt_overlay_previews || !host.overlay_visible || !now)
|
|
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 (!w->tree || w->tree->node.enabled)
|
|
continue; /* shown: scene path already sent frame-done */
|
|
struct wlr_surface *s = window_surface(w);
|
|
if (!s)
|
|
continue;
|
|
wlr_surface_for_each_surface(s, overlay_hidden_frame_iter, now);
|
|
}
|
|
}
|
|
}
|
|
|
|
static struct wlr_texture *overlay_thumb_tex(struct kick_window *w) {
|
|
if (!w || !server.renderer) return NULL;
|
|
if (w->thumb_src == THUMB_SRC_LIVE) {
|
|
struct wlr_surface *s = window_surface(w);
|
|
struct wlr_texture *live = s ? wlr_surface_get_texture(s) : NULL;
|
|
if (live) return live;
|
|
}
|
|
if (w->thumb_tex && !w->thumb_dirty)
|
|
return w->thumb_tex;
|
|
struct wlr_texture *tex = NULL;
|
|
if (w->thumb_src == THUMB_SRC_DMABUF && w->thumb_buf) {
|
|
tex = wlr_texture_from_buffer(server.renderer, w->thumb_buf);
|
|
} else if (w->thumb_src == THUMB_SRC_SNAP && w->thumb_px) {
|
|
tex = wlr_texture_from_pixels(
|
|
server.renderer, DRM_FORMAT_ARGB8888,
|
|
(uint32_t)w->thumb_pw * 4,
|
|
(uint32_t)w->thumb_pw, (uint32_t)w->thumb_ph, w->thumb_px);
|
|
} else {
|
|
return w->thumb_tex;
|
|
}
|
|
if (!tex) {
|
|
/* A persistent NULL here is a black thumbnail (e.g. a dma-buf the
|
|
renderer cannot import): log once per window so KICK_LOG=2 shows
|
|
which tab never produced a texture. */
|
|
if (!w->thumb_tex)
|
|
kick_log(KICK_LOG_DEBUG, "[thumb] import failed app=%d src=%d\n",
|
|
tab_index(w->tab), (int)w->thumb_src);
|
|
return w->thumb_tex; /* keep the previous frame; retry later */
|
|
}
|
|
if (w->thumb_tex)
|
|
wlr_texture_destroy(w->thumb_tex);
|
|
w->thumb_tex = tex;
|
|
w->thumb_dirty = false;
|
|
return w->thumb_tex;
|
|
}
|
|
|
|
/* Icon + aspect-fit live thumbnail. The thumb box is letterboxed (black)
|
|
and the window's current frame is centered inside, downscaled with
|
|
bilinear filtering; a window with no frame yet keeps the letterbox. */
|
|
static void overlay_pass_thumb(struct wlr_render_pass *pass,
|
|
struct kick_window *w,
|
|
int tx, int ty, int tw, int th, float alpha) {
|
|
overlay_pass_rect(pass, tx, ty, tw, th, 0.0f, 0.0f, 0.0f, 1.0f);
|
|
/* Subsurface content (e.g. Firefox) never sits in the main surface's
|
|
buffer: composite the live tree instead of the single snapshot. */
|
|
if (window_has_subsurfaces(w)) {
|
|
overlay_pass_tree_thumb(pass, w, tx, ty, tw, th, alpha);
|
|
return;
|
|
}
|
|
struct wlr_texture *tex = overlay_thumb_tex(w);
|
|
if (!tex || tex->width == 0 || tex->height == 0) return;
|
|
int dw = tw, dh = (int)((int64_t)tw * tex->height / tex->width);
|
|
if (dh > th) {
|
|
dh = th;
|
|
dw = (int)((int64_t)th * tex->width / tex->height);
|
|
}
|
|
if (dw <= 0 || dh <= 0) return;
|
|
struct wlr_render_texture_options o = {
|
|
.texture = tex,
|
|
.src_box = { .x = 0, .y = 0,
|
|
.width = (float)tex->width, .height = (float)tex->height },
|
|
.dst_box = { .x = tx + (tw - dw) / 2, .y = ty + (th - dh) / 2,
|
|
.width = dw, .height = dh },
|
|
.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;
|
|
};
|
|
|
|
/* Visible overlay rows, shared by measure, draw and hit-test so all three
|
|
agree on the count before laying out. */
|
|
static int overlay_count_rows(void) {
|
|
int n_rows = 0;
|
|
struct kick_tab *tab;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
if (!tab_overlay_row(tab)) continue;
|
|
n_rows++;
|
|
}
|
|
return n_rows;
|
|
}
|
|
|
|
/* 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 thumb_w = 0;
|
|
if (opt_overlay_previews)
|
|
overlay_preview_metrics(overlay_count_rows(), &row_h, &thumb_w,
|
|
NULL);
|
|
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;
|
|
if (opt_overlay_previews) {
|
|
row_w += icon_sz + icon_gap + thumb_w;
|
|
} else {
|
|
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 = overlay_preview_head_h() + n_rows * row_h +
|
|
(n_rows - 1) * row_gap + 2 * boxpad;
|
|
if (bw > host.width - 2 * margin) bw = host.width - 2 * margin;
|
|
/* Preview menus are content-sized by construction; the margin clamp is
|
|
only a safety net for degenerate (tiny) screens. */
|
|
int vmargin = opt_overlay_previews ? OVERLAY_MENU_MARGIN_V : margin;
|
|
if (bh > host.height - 2 * vmargin) bh = host.height - 2 * vmargin;
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* Selected-window header (preview mode): the active tab's current window
|
|
-- what the highlight sits on -- as a full-width header row at the top
|
|
of the menu: icon left-anchored, title centered in the remaining width
|
|
and ellipsized to it. Full width keeps the menu size independent of
|
|
the selection, so moving the highlight never resizes anything. Reuses
|
|
the row machinery: tab_row_title for the string (with its pending-tab
|
|
command fallback), the icon loader, and a budgeted text tile. */
|
|
static struct kick_window *overlay_selected_window(void) {
|
|
if (!server.active_tab) return NULL;
|
|
return tab_current_window(server.active_tab);
|
|
}
|
|
|
|
/* Vertical block the header reserves above the thumbnail rows: the title
|
|
row plus one standard row gap. Zero outside preview mode. */
|
|
static int overlay_preview_head_h(void) {
|
|
if (!opt_overlay_previews) return 0;
|
|
return overlay_title_row_h() + 8; /* row_gap */
|
|
}
|
|
|
|
static void overlay_pass_selected_header(struct wlr_render_pass *pass,
|
|
int box_x, int box_y, int boxpad,
|
|
int row_inner) {
|
|
struct kick_window *w = overlay_selected_window();
|
|
if (!w) return;
|
|
const char *t = tab_row_title(server.active_tab);
|
|
if (!t) t = "";
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8;
|
|
int row_h = overlay_title_row_h();
|
|
int hy = box_y + boxpad;
|
|
|
|
overlay_pass_rect(pass, box_x + boxpad - 2, hy - 1, row_inner + 4,
|
|
row_h, 0.22f, 0.22f, 0.25f, 1.0f);
|
|
if (overlay_window_icon_needs_load(w))
|
|
overlay_icon_load_window(w);
|
|
int ix = box_x + boxpad, iy = hy + (row_h - icon_sz) / 2;
|
|
if (w->icon_wlr) {
|
|
overlay_pass_icon_tex(pass, w->icon_wlr, ix, iy, icon_sz, 1.0f);
|
|
} else {
|
|
overlay_pass_rect(pass, ix, iy, icon_sz, icon_sz,
|
|
0.45f, 0.45f, 0.48f, 0.85f);
|
|
overlay_pass_rect(pass, ix + 3, iy + 3, icon_sz - 6,
|
|
icon_sz - 6, 0.07f, 0.07f, 0.09f, 1.0f);
|
|
}
|
|
int rx = ix + icon_sz + icon_gap;
|
|
int rw = row_inner - (icon_sz + icon_gap);
|
|
if (rw < 0) rw = 0;
|
|
struct overlay_text_entry *te = overlay_text_tile(t, rw);
|
|
int tw = te ? te->w : 0;
|
|
int tx = rx + (rw - tw) / 2;
|
|
if (tx < rx) tx = rx;
|
|
overlay_pass_text(pass, t, rw, tx,
|
|
hy + (row_h - host.overlay_font_h) / 2, 1.0f);
|
|
}
|
|
|
|
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;
|
|
int thumb_w = 0, thumb_h = 0;
|
|
if (opt_overlay_previews) {
|
|
overlay_preview_metrics(overlay_count_rows(), &row_h, &thumb_w,
|
|
&thumb_h);
|
|
}
|
|
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);
|
|
|
|
/* Selected-window header first (preview mode); thumbnail rows follow
|
|
below it with the standard gap. */
|
|
int head_h = overlay_preview_head_h();
|
|
int row_inner = box_w - 2 * boxpad;
|
|
if (opt_overlay_previews && head_h > 0)
|
|
overlay_pass_selected_header(pass, box_x, box_y, boxpad, row_inner);
|
|
|
|
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 + head_h + row * (row_h + row_gap);
|
|
bool active = tab == server.active_tab;
|
|
|
|
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++) {
|
|
if (opt_overlay_previews) {
|
|
/* No captions in preview mode: fixed thumbnail boxes. */
|
|
widths[k] = thumb_w;
|
|
total_w += icon_sz + icon_gap + widths[k];
|
|
continue;
|
|
}
|
|
/* 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;
|
|
int ty = ry + (row_h - thumb_h) / 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);
|
|
}
|
|
if (opt_overlay_previews) {
|
|
overlay_pass_thumb(pass, w, mx + icon_sz + icon_gap, ty,
|
|
thumb_w, thumb_h,
|
|
active ? 1.0f : 0.72f);
|
|
} else {
|
|
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;
|
|
int thumb_w = 0;
|
|
if (opt_overlay_previews) {
|
|
overlay_preview_metrics(overlay_count_rows(), &row_h, &thumb_w,
|
|
NULL);
|
|
}
|
|
struct kick_tab *tab;
|
|
|
|
/* The selected-window header is display-only: taps on it change
|
|
nothing. */
|
|
int head_h = overlay_preview_head_h();
|
|
if (head_h > 0 && oy >= box_y + boxpad &&
|
|
oy < box_y + boxpad + head_h)
|
|
return false;
|
|
|
|
int row = 0;
|
|
wl_list_for_each(tab, &server.tabs, link) {
|
|
if (!tab_overlay_row(tab)) continue;
|
|
int ry = box_y + boxpad + head_h + 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++) {
|
|
if (opt_overlay_previews) {
|
|
widths[k] = thumb_w;
|
|
total_w += icon_sz + icon_gap + widths[k];
|
|
continue;
|
|
}
|
|
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;
|
|
/* No scheduling from this handler: wlr_output_schedule_frame re-emits
|
|
this same event via idle, so scheduling here can wedge dispatch.
|
|
Frame readiness is polled in the main loops via
|
|
server.output->frame_pending / needs_frame. */
|
|
}
|
|
|
|
/* 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;
|
|
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);
|
|
thumb_release(w);
|
|
icon_proto_release(w);
|
|
foreign_handle_destroy(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);
|
|
thumb_release(w);
|
|
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);
|
|
if (!w->close_timer) {
|
|
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);
|
|
/* --overlay-previews: a commit is a new thumbnail frame. */
|
|
thumb_capture(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[1024];
|
|
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_init(&w->thumb_subs);
|
|
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);
|
|
|
|
/* Subsurface paints bypass the toplevel commit: watch the whole tree
|
|
so --overlay-previews thumbnails track subsurface clients too. */
|
|
thumb_sub_wire_tree(w);
|
|
|
|
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 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);
|
|
thumb_capture(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) {
|
|
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[1024];
|
|
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[1024];
|
|
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[1024];
|
|
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 (server.session_locked) 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;
|
|
}
|
|
/* 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;
|
|
}
|
|
/* 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. Above the inhibitor so quit stays live. */
|
|
{ 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;
|
|
}
|
|
}
|
|
/* Inhibited (game/VM grabs chords): swallow tab/window grabs below so
|
|
they reach the client. VT switch and quit above stay live. */
|
|
if (kb_inhibit_focused())
|
|
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;
|
|
}
|
|
}
|
|
/* 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.idle_notifier && server.seat)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
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 or quit confirm 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.idle_notifier && server.seat)
|
|
wlr_idle_notifier_v1_notify_activity(server.idle_notifier, server.seat);
|
|
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);
|
|
overlay_send_hidden_frames(&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);
|
|
overlay_send_hidden_frames(&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;
|
|
}
|
|
|
|
/* 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. */
|
|
|
|
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;
|
|
}
|
|
thumb_release(w);
|
|
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"
|
|
" --cursor-size N Standalone pointer size "
|
|
"in px (8..128;\n"
|
|
" default 24, or "
|
|
"$XCURSOR_SIZE when set).\n"
|
|
" Ignored when nested: the "
|
|
"host draws the\n"
|
|
" pointer there.\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"
|
|
" --overlay-previews Show window thumbnails "
|
|
"in the tab overlay\n"
|
|
" instead of icon+title rows: "
|
|
"each row is the icon\n"
|
|
" plus a small view of "
|
|
"the window as it last\n"
|
|
" appeared (live for the "
|
|
"shown window). Off by\n"
|
|
" default.\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], "--overlay-previews") == 0) {
|
|
opt_overlay_previews = 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;
|
|
} else if (strcmp(argv[i], "--cursor-size") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --cursor-size needs a "
|
|
"value, e.g. --cursor-size 32\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *n = argv[++i];
|
|
char *end = NULL;
|
|
long sz = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || sz < 8 || sz > 128) {
|
|
fprintf(stderr, "kick: bad --cursor-size '%s' "
|
|
"(use 8..128, e.g. --cursor-size 32)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
opt_cursor_size = (int)sz;
|
|
continue;
|
|
} else if (strncmp(argv[i], "--cursor-size=",
|
|
sizeof("--cursor-size=") - 1) == 0) {
|
|
const char *n = argv[i] + sizeof("--cursor-size=") - 1;
|
|
char *end = NULL;
|
|
long sz = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || sz < 8 || sz > 128) {
|
|
fprintf(stderr, "kick: bad --cursor-size '%s' "
|
|
"(use 8..128, e.g. --cursor-size 32)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
opt_cursor_size = (int)sz;
|
|
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),
|
|
with --cursor-size overriding the size when given.
|
|
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;
|
|
}
|
|
/* Explicit --cursor-size wins over the environment. */
|
|
if (opt_cursor_size > 0)
|
|
size = (uint32_t)opt_cursor_size;
|
|
|
|
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)) {
|
|
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)) {
|
|
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;
|
|
}
|