summaryrefslogtreecommitdiff
path: root/src/platform/x11
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-13 13:44:13 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-13 13:44:13 +0300
commitc059e3aa7f93da470f052a5cd8ee0d8dc969e679 (patch)
tree63ffa378287ed2c606eda2e940501b9edc3d2b8b /src/platform/x11
parent87d0bc50d53dc216efb2b04495f090616318bc5c (diff)
rename and simplification
Diffstat (limited to 'src/platform/x11')
-rw-r--r--src/platform/x11/keymap.c137
-rw-r--r--src/platform/x11/keymap.h8
-rw-r--r--src/platform/x11/meson.build1
-rw-r--r--src/platform/x11/native.c14
-rw-r--r--src/platform/x11/window.c2064
5 files changed, 2224 insertions, 0 deletions
diff --git a/src/platform/x11/keymap.c b/src/platform/x11/keymap.c
new file mode 100644
index 0000000..3c566b3
--- /dev/null
+++ b/src/platform/x11/keymap.c
@@ -0,0 +1,137 @@
+#include "keymap.h"
+#include <string.h>
+
+/* XKB standard physical key names, independent of server numeric keycodes. */
+enum yasl_key yasl_key_from_xkb_name(const char name[4])
+{
+ static const struct { char name[4]; enum yasl_key key; } keys[] = {
+ {{'T', 'L', 'D', 'E'}, YASL_KEY_GRAVE},
+ {{'A', 'E', '0', '1'}, YASL_KEY_1},
+ {{'A', 'E', '0', '2'}, YASL_KEY_2},
+ {{'A', 'E', '0', '3'}, YASL_KEY_3},
+ {{'A', 'E', '0', '4'}, YASL_KEY_4},
+ {{'A', 'E', '0', '5'}, YASL_KEY_5},
+ {{'A', 'E', '0', '6'}, YASL_KEY_6},
+ {{'A', 'E', '0', '7'}, YASL_KEY_7},
+ {{'A', 'E', '0', '8'}, YASL_KEY_8},
+ {{'A', 'E', '0', '9'}, YASL_KEY_9},
+ {{'A', 'E', '1', '0'}, YASL_KEY_0},
+ {{'A', 'E', '1', '1'}, YASL_KEY_MINUS},
+ {{'A', 'E', '1', '2'}, YASL_KEY_EQUAL},
+ {{'B', 'K', 'S', 'P'}, YASL_KEY_BACKSPACE},
+ {{'T', 'A', 'B', 0}, YASL_KEY_TAB},
+ {{'A', 'D', '0', '1'}, YASL_KEY_Q},
+ {{'A', 'D', '0', '2'}, YASL_KEY_W},
+ {{'A', 'D', '0', '3'}, YASL_KEY_E},
+ {{'A', 'D', '0', '4'}, YASL_KEY_R},
+ {{'A', 'D', '0', '5'}, YASL_KEY_T},
+ {{'A', 'D', '0', '6'}, YASL_KEY_Y},
+ {{'A', 'D', '0', '7'}, YASL_KEY_U},
+ {{'A', 'D', '0', '8'}, YASL_KEY_I},
+ {{'A', 'D', '0', '9'}, YASL_KEY_O},
+ {{'A', 'D', '1', '0'}, YASL_KEY_P},
+ {{'A', 'D', '1', '1'}, YASL_KEY_LEFT_BRACKET},
+ {{'A', 'D', '1', '2'}, YASL_KEY_RIGHT_BRACKET},
+ {{'B', 'K', 'S', 'L'}, YASL_KEY_BACKSLASH},
+ {{'R', 'T', 'R', 'N'}, YASL_KEY_ENTER},
+ {{'C', 'A', 'P', 'S'}, YASL_KEY_CAPS_LOCK},
+ {{'A', 'C', '0', '1'}, YASL_KEY_A},
+ {{'A', 'C', '0', '2'}, YASL_KEY_S},
+ {{'A', 'C', '0', '3'}, YASL_KEY_D},
+ {{'A', 'C', '0', '4'}, YASL_KEY_F},
+ {{'A', 'C', '0', '5'}, YASL_KEY_G},
+ {{'A', 'C', '0', '6'}, YASL_KEY_H},
+ {{'A', 'C', '0', '7'}, YASL_KEY_J},
+ {{'A', 'C', '0', '8'}, YASL_KEY_K},
+ {{'A', 'C', '0', '9'}, YASL_KEY_L},
+ {{'A', 'C', '1', '0'}, YASL_KEY_SEMICOLON},
+ {{'A', 'C', '1', '1'}, YASL_KEY_APOSTROPHE},
+ {{'L', 'F', 'S', 'H'}, YASL_KEY_LEFT_SHIFT},
+ {{'L', 'S', 'G', 'T'}, YASL_KEY_ISO_BACKSLASH},
+ {{'A', 'B', '0', '1'}, YASL_KEY_Z},
+ {{'A', 'B', '0', '2'}, YASL_KEY_X},
+ {{'A', 'B', '0', '3'}, YASL_KEY_C},
+ {{'A', 'B', '0', '4'}, YASL_KEY_V},
+ {{'A', 'B', '0', '5'}, YASL_KEY_B},
+ {{'A', 'B', '0', '6'}, YASL_KEY_N},
+ {{'A', 'B', '0', '7'}, YASL_KEY_M},
+ {{'A', 'B', '0', '8'}, YASL_KEY_COMMA},
+ {{'A', 'B', '0', '9'}, YASL_KEY_PERIOD},
+ {{'A', 'B', '1', '0'}, YASL_KEY_SLASH},
+ {{'R', 'T', 'S', 'H'}, YASL_KEY_RIGHT_SHIFT},
+ {{'L', 'C', 'T', 'L'}, YASL_KEY_LEFT_CONTROL},
+ {{'L', 'W', 'I', 'N'}, YASL_KEY_LEFT_SUPER},
+ {{'L', 'A', 'L', 'T'}, YASL_KEY_LEFT_ALT},
+ {{'S', 'P', 'C', 'E'}, YASL_KEY_SPACE},
+ {{'R', 'A', 'L', 'T'}, YASL_KEY_RIGHT_ALT},
+ {{'R', 'W', 'I', 'N'}, YASL_KEY_RIGHT_SUPER},
+ {{'C', 'O', 'M', 'P'}, YASL_KEY_MENU},
+ {{'R', 'C', 'T', 'L'}, YASL_KEY_RIGHT_CONTROL},
+ {{'E', 'S', 'C', 0}, YASL_KEY_ESCAPE},
+ {{'F', 'K', '0', '1'}, YASL_KEY_F1},
+ {{'F', 'K', '0', '2'}, YASL_KEY_F2},
+ {{'F', 'K', '0', '3'}, YASL_KEY_F3},
+ {{'F', 'K', '0', '4'}, YASL_KEY_F4},
+ {{'F', 'K', '0', '5'}, YASL_KEY_F5},
+ {{'F', 'K', '0', '6'}, YASL_KEY_F6},
+ {{'F', 'K', '0', '7'}, YASL_KEY_F7},
+ {{'F', 'K', '0', '8'}, YASL_KEY_F8},
+ {{'F', 'K', '0', '9'}, YASL_KEY_F9},
+ {{'F', 'K', '1', '0'}, YASL_KEY_F10},
+ {{'F', 'K', '1', '1'}, YASL_KEY_F11},
+ {{'F', 'K', '1', '2'}, YASL_KEY_F12},
+ {{'P', 'R', 'S', 'C'}, YASL_KEY_PRINT_SCREEN},
+ {{'S', 'C', 'L', 'K'}, YASL_KEY_SCROLL_LOCK},
+ {{'P', 'A', 'U', 'S'}, YASL_KEY_PAUSE},
+ {{'I', 'N', 'S', 0}, YASL_KEY_INSERT},
+ {{'H', 'O', 'M', 'E'}, YASL_KEY_HOME},
+ {{'P', 'G', 'U', 'P'}, YASL_KEY_PAGE_UP},
+ {{'D', 'E', 'L', 'E'}, YASL_KEY_DELETE},
+ {{'E', 'N', 'D', 0}, YASL_KEY_END},
+ {{'P', 'G', 'D', 'N'}, YASL_KEY_PAGE_DOWN},
+ {{'U', 'P', 0, 0}, YASL_KEY_UP},
+ {{'L', 'E', 'F', 'T'}, YASL_KEY_LEFT},
+ {{'D', 'O', 'W', 'N'}, YASL_KEY_DOWN},
+ {{'R', 'G', 'H', 'T'}, YASL_KEY_RIGHT},
+ {{'N', 'M', 'L', 'K'}, YASL_KEY_NUM_LOCK},
+ {{'K', 'P', 'D', 'V'}, YASL_KEY_KP_DIVIDE},
+ {{'K', 'P', 'M', 'U'}, YASL_KEY_KP_MULTIPLY},
+ {{'K', 'P', 'S', 'U'}, YASL_KEY_KP_SUBTRACT},
+ {{'K', 'P', '7', 0}, YASL_KEY_KP_7},
+ {{'K', 'P', '8', 0}, YASL_KEY_KP_8},
+ {{'K', 'P', '9', 0}, YASL_KEY_KP_9},
+ {{'K', 'P', 'A', 'D'}, YASL_KEY_KP_ADD},
+ {{'K', 'P', '4', 0}, YASL_KEY_KP_4},
+ {{'K', 'P', '5', 0}, YASL_KEY_KP_5},
+ {{'K', 'P', '6', 0}, YASL_KEY_KP_6},
+ {{'K', 'P', '1', 0}, YASL_KEY_KP_1},
+ {{'K', 'P', '2', 0}, YASL_KEY_KP_2},
+ {{'K', 'P', '3', 0}, YASL_KEY_KP_3},
+ {{'K', 'P', 'E', 'N'}, YASL_KEY_KP_ENTER},
+ {{'K', 'P', '0', 0}, YASL_KEY_KP_0},
+ {{'K', 'P', 'D', 'L'}, YASL_KEY_KP_DECIMAL},
+ {{'K', 'P', 'E', 'Q'}, YASL_KEY_KP_EQUAL},
+ {{'F', 'K', '1', '3'}, YASL_KEY_F13},
+ {{'F', 'K', '1', '4'}, YASL_KEY_F14},
+ {{'F', 'K', '1', '5'}, YASL_KEY_F15},
+ {{'F', 'K', '1', '6'}, YASL_KEY_F16},
+ {{'F', 'K', '1', '7'}, YASL_KEY_F17},
+ {{'F', 'K', '1', '8'}, YASL_KEY_F18},
+ {{'F', 'K', '1', '9'}, YASL_KEY_F19},
+ {{'F', 'K', '2', '0'}, YASL_KEY_F20},
+ {{'F', 'K', '2', '1'}, YASL_KEY_F21},
+ {{'F', 'K', '2', '2'}, YASL_KEY_F22},
+ {{'F', 'K', '2', '3'}, YASL_KEY_F23},
+ {{'F', 'K', '2', '4'}, YASL_KEY_F24},
+ {{'H', 'K', 'T', 'G'}, YASL_KEY_KANA},
+ {{'A', 'B', '1', '1'}, YASL_KEY_RO},
+ {{'H', 'E', 'N', 'K'}, YASL_KEY_CONVERT},
+ {{'M', 'U', 'H', 'E'}, YASL_KEY_NONCONVERT},
+ {{'A', 'E', '1', '3'}, YASL_KEY_YEN},
+ {{'M', 'U', 'T', 'E'}, YASL_KEY_MUTE},
+ {{'I', '1', '2', '9'}, YASL_KEY_KP_COMMA},
+ };
+ for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++)
+ if (memcmp(keys[i].name, name, 4) == 0) return keys[i].key;
+ return YASL_KEY_UNKNOWN;
+}
diff --git a/src/platform/x11/keymap.h b/src/platform/x11/keymap.h
new file mode 100644
index 0000000..2ec4295
--- /dev/null
+++ b/src/platform/x11/keymap.h
@@ -0,0 +1,8 @@
+#ifndef YASL_X11_KEYMAP_H
+#define YASL_X11_KEYMAP_H
+
+#include "yasl/yasl.h"
+
+enum yasl_key yasl_key_from_xkb_name(const char name[4]);
+
+#endif
diff --git a/src/platform/x11/meson.build b/src/platform/x11/meson.build
new file mode 100644
index 0000000..6b85e46
--- /dev/null
+++ b/src/platform/x11/meson.build
@@ -0,0 +1 @@
+sources += files('window.c', 'keymap.c', 'native.c')
diff --git a/src/platform/x11/native.c b/src/platform/x11/native.c
new file mode 100644
index 0000000..249adec
--- /dev/null
+++ b/src/platform/x11/native.c
@@ -0,0 +1,14 @@
+#include "common/backend.h"
+#include "yasl/native/x11.h"
+
+Display *yasl_x11_display_get_display(struct yasl_display *d)
+{
+ return (d && yasl_display_backend(d) == YASL_BACKEND_X11) ?
+ (Display *)yasl_display_get_native_handle(d) : NULL;
+}
+
+uintptr_t yasl_x11_surface_get_window(struct yasl_surface *s)
+{
+ return (s && s->display && yasl_display_backend(s->display) == YASL_BACKEND_X11) ?
+ (uintptr_t)yasl_surface_get_native_handle(s) : 0;
+}
diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c
new file mode 100644
index 0000000..7891fff
--- /dev/null
+++ b/src/platform/x11/window.c
@@ -0,0 +1,2064 @@
+#define _POSIX_C_SOURCE 200809L
+#define _DEFAULT_SOURCE
+#include "common/backend.h"
+#include "common/input.h"
+#include "common/keyboard.h"
+#include "keymap.h"
+#include "common/utf8.h"
+#include "common/timing.h"
+
+#include <X11/Xatom.h>
+#include <X11/XKBlib.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/cursorfont.h>
+#include <X11/keysym.h>
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/XShm.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#if YASL_HAS_XRANDR
+#include <X11/extensions/Xrandr.h>
+#endif
+#include <xkbcommon/xkbcommon.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <math.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#define TEXT_LIMIT (16 * 1024 * 1024)
+
+#ifndef likely
+#define likely(x) __builtin_expect(!!(x), 1)
+#endif
+#ifndef unlikely
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#endif
+
+struct x11_surface {
+ struct yasl_surface base;
+ struct x11_surface *next;
+ struct x11_surface *dead_next;
+ Window window;
+ GC gc;
+ XIC ic;
+ XImage *image;
+ uint32_t *pixels;
+ bool shm_active;
+ XShmSegmentInfo shm_info;
+ struct yasl_vector2i size;
+ const struct yasl_window_cbs *cbs;
+ void *ud;
+ struct input_queue input;
+ struct yasl_frame frame;
+ struct yasl_damage_list damage;
+ struct yasl_frame_clock clock;
+ uint32_t state;
+ bool want_draw, drawing, presented, destroyed;
+ struct x11_surface *parent;
+ bool is_popup;
+ struct yasl_vector2i content_offset;
+ char *ime_preedit;
+ int ime_cursor_begin;
+ int ime_cursor_end;
+ struct yasl_rect2i ime_caret;
+};
+
+struct paste_request {
+ struct paste_request *next;
+ struct x11_surface *target;
+ Window window;
+};
+
+struct tablet_axis {
+ int number;
+ double min, max, value;
+};
+
+struct x11_pointer {
+ struct x11_pointer *next;
+ int id;
+ enum yasl_pointer_device device;
+ struct tablet_axis pressure, tilt_x, tilt_y;
+};
+
+struct x11_display {
+ struct yasl_display base;
+ Display *native;
+ int screen;
+ struct yasl_loop *loop;
+ int watch;
+ struct x11_surface *surfaces, *dead;
+ int depth;
+ XIM im;
+ Window selection_window;
+ Atom protocols, delete_window, utf8, clipboard, targets, paste_property, incr;
+ Atom net_name, net_state, fullscreen, maximized_h, maximized_v;
+ Atom xdnd_aware, xdnd_enter, xdnd_position, xdnd_status, xdnd_leave, xdnd_drop, xdnd_finished;
+ Atom xdnd_selection, xdnd_action_copy, xdnd_type_list, uri_list;
+ char *selection[2];
+ struct paste_request *pastes;
+ uint32_t mods;
+ char *app_id;
+ bool owns_loop;
+ int xi_opcode;
+ bool xi2;
+ bool has_shm;
+ bool has_xkb;
+ int xkb_event;
+ enum yasl_key keys[256];
+ Cursor cursors[16];
+ Atom abs_pressure, abs_tilt_x, abs_tilt_y, tool_type, eraser;
+ struct x11_pointer *pointers;
+ struct {
+ bool active;
+ struct x11_surface *origin;
+ struct x11_surface *carried;
+ struct yasl_vector2i carry_offset;
+ struct yasl_drag_listener listener;
+ void *ud;
+ struct x11_surface *hover;
+ struct yasl_vector2i hover_pos;
+ Window icon_window;
+ Pixmap icon_pixmap;
+ struct yasl_vector2i hotspot;
+ } drag;
+ struct {
+ Window source;
+ int version;
+ bool accepts;
+ bool has_uri_list;
+ struct x11_surface *surface;
+ struct yasl_vector2i pos;
+ Time time;
+ } xdnd;
+ struct {
+ bool active;
+ Window source;
+ struct x11_surface *surface;
+ Time time;
+ } pending_drop;
+};
+
+static struct x11_display *display_of(const struct yasl_surface *s)
+{
+ return (struct x11_display *)s->display;
+}
+
+static bool place(struct yasl_surface *base, struct yasl_vector2i at);
+static void drag_end(struct x11_display *d, enum yasl_drag_end how);
+static void drag_cancel(struct yasl_display *base);
+static void schedule_redraw(struct yasl_surface *base);
+
+static void leave(struct x11_display *d)
+{
+ if (--d->depth) return;
+ while (d->dead) {
+ struct x11_surface *s = d->dead;
+ d->dead = s->dead_next;
+ free(s);
+ }
+}
+
+static void push(struct x11_surface *s, struct yasl_input_event event,
+ const char *text, size_t len)
+{
+ if (s->destroyed) return;
+ if (text) {
+ if (event.kind == YASL_INPUT_TEXT) {
+ event.text.text = text;
+ event.text.len = len;
+ } else if (event.kind == YASL_INPUT_DROP_FILES) {
+ event.drop_files.files = text;
+ event.drop_files.len = len;
+ } else {
+ event.paste.text = text;
+ event.paste.len = len;
+ }
+ }
+ event.surface = &s->base;
+ input_queue_push(&s->input, &event);
+}
+
+static unsigned long component(uint32_t color, unsigned long mask)
+{
+ if (unlikely(!mask)) return 0;
+ unsigned shift = (unsigned)__builtin_ctzl(mask);
+ unsigned long m = mask >> shift;
+ return (((unsigned long)color * m + 127) / 255) << shift;
+}
+
+static void drag_set_hover(struct x11_display *d, struct x11_surface *s, struct yasl_vector2i pos)
+{
+ if (!d->drag.active) return;
+ if (d->drag.hover && d->drag.hover != s) {
+ struct x11_surface *old = d->drag.hover;
+ struct yasl_vector2i old_pos = d->drag.hover_pos;
+ d->drag.hover = NULL;
+ if (!old->destroyed && old->cbs && old->cbs->drag)
+ old->cbs->drag(&old->base, YASL_DRAG_LEAVE, old_pos, old->ud);
+ if (!d->drag.active) return;
+ }
+ if (s && s->destroyed) return;
+ d->drag.hover = s;
+ d->drag.hover_pos = pos;
+ if (s && s->cbs && s->cbs->drag)
+ s->cbs->drag(&s->base, YASL_DRAG_OVER, pos, s->ud);
+}
+
+static void drag_leave_hover(struct x11_display *d, struct x11_surface *s, struct yasl_vector2i pos)
+{
+ if (!d->drag.active || d->drag.hover != s) return;
+ d->drag.hover = NULL;
+ if (s && !s->destroyed && s->cbs && s->cbs->drag)
+ s->cbs->drag(&s->base, YASL_DRAG_LEAVE, pos, s->ud);
+}
+
+static void drag_drop_hover(struct x11_display *d)
+{
+ if (!d->drag.active) return;
+ drag_end(d, YASL_DRAG_DROPPED);
+}
+
+static int yasl_x11_error_handler(Display *native, XErrorEvent *event)
+{
+ (void)native;
+ (void)event;
+ /* Suppress asynchronous errors (BadWindow, BadDrawable, BadMatch)
+ * during window close, destruction races, or speculative property queries. */
+ return 0;
+}
+
+static struct x11_surface *surface_at_root_pos(struct x11_display *d, int rx, int ry, struct yasl_vector2i *out_pos)
+{
+ if (!d->native) return NULL;
+ Window ignore_icon = d->drag.icon_window;
+ Window ignore_carried = d->drag.carried ? d->drag.carried->window : None;
+
+ for (struct x11_surface *s = d->surfaces; s; s = s->next) {
+ if (s->destroyed || s->window == ignore_icon || s->window == ignore_carried) continue;
+ int tx = 0, ty = 0;
+ Window sub = None;
+ if (XTranslateCoordinates(d->native, RootWindow(d->native, d->screen),
+ s->window, rx, ry, &tx, &ty, &sub)) {
+ if (tx >= 0 && ty >= 0 && tx < s->size.x && ty < s->size.y) {
+ if (out_pos) *out_pos = (struct yasl_vector2i){tx, ty};
+ return s;
+ }
+ }
+ }
+ return NULL;
+}
+
+static uint32_t modifiers(unsigned state)
+{
+ return ((state & ShiftMask) ? YASL_MOD_SHIFT : 0) |
+ ((state & ControlMask) ? YASL_MOD_CTRL : 0) |
+ ((state & Mod1Mask) ? YASL_MOD_ALT : 0) |
+ ((state & Mod4Mask) ? YASL_MOD_SUPER : 0) |
+ ((state & LockMask) ? YASL_MOD_CAPS : 0);
+}
+
+static void pointer_info(struct x11_display *d, struct x11_pointer *p,
+ const XIDeviceInfo *info)
+{
+ p->id = info->deviceid;
+ p->device = YASL_PTR_DEV_MOUSE;
+ p->pressure.number = p->tilt_x.number = p->tilt_y.number = -1;
+ bool touch = false;
+ for (int i = 0; i < info->num_classes; i++) {
+ XIAnyClassInfo *c = info->classes[i];
+ if (c->type == XITouchClass) touch = true;
+ if (c->type != XIValuatorClass) continue;
+ XIValuatorClassInfo *v = (XIValuatorClassInfo *)c;
+ if (v->mode != XIModeAbsolute) continue;
+ struct tablet_axis *axis = v->label == d->abs_pressure ? &p->pressure :
+ v->label == d->abs_tilt_x ? &p->tilt_x :
+ v->label == d->abs_tilt_y ? &p->tilt_y : NULL;
+ if (axis) *axis = (struct tablet_axis){v->number, v->min, v->max, v->value};
+ }
+ if (!touch && p->pressure.number >= 0) {
+ p->device = YASL_PTR_DEV_PEN;
+ /* Xorg's Wacom and libinput drivers expose separate eraser devices.
+ * Use the tool property when available, with the device name as a
+ * fallback for servers (including Xwayland) without that property. */
+ if (info->name) {
+ for (const char *s = info->name; *s; s++)
+ if (!strncasecmp(s, "eraser", 6)) { p->device = YASL_PTR_DEV_ERASER; break; }
+ }
+ }
+}
+
+static struct x11_pointer *pointer_for(struct x11_display *d, int id)
+{
+ for (struct x11_pointer *p = d->pointers; p; p = p->next)
+ if (p->id == id) return p;
+ if (!d->native) return NULL;
+ int count = 0;
+ XIDeviceInfo *info = XIQueryDevice(d->native, id, &count);
+ if (!info) return NULL;
+ struct x11_pointer *p = count ? calloc(1, sizeof(*p)) : NULL;
+ if (p) {
+ pointer_info(d, p, info);
+ if (p->device == YASL_PTR_DEV_PEN || p->device == YASL_PTR_DEV_ERASER) {
+ Atom type;
+ int format;
+ unsigned long n, after;
+ unsigned char *value = NULL;
+ if (XIGetProperty(d->native, id, d->tool_type, 0, 1, False,
+ XA_ATOM, &type, &format, &n, &after, &value) == Success &&
+ type == XA_ATOM && format == 32 && n == 1 && value)
+ /* XIGetProperty packs format-32 values as uint32_t, unlike
+ * XGetWindowProperty's array of native-sized longs. */
+ p->device = *(uint32_t *)value == d->eraser ? YASL_PTR_DEV_ERASER : p->device;
+ if (value) XFree(value);
+ }
+ p->next = d->pointers;
+ d->pointers = p;
+ }
+ XIFreeDeviceInfo(info);
+ return p;
+}
+
+static void clear_pointers(struct x11_display *d)
+{
+ while (d->pointers) {
+ struct x11_pointer *p = d->pointers;
+ d->pointers = p->next;
+ free(p);
+ }
+}
+
+static void update_axes(struct x11_pointer *p, const XIValuatorState *state)
+{
+ const double *value = state->values;
+ for (int i = 0; i < state->mask_len * 8; i++) {
+ if (!XIMaskIsSet(state->mask, i)) continue;
+ struct tablet_axis *axes[] = {&p->pressure, &p->tilt_x, &p->tilt_y};
+ for (unsigned a = 0; a < sizeof(axes) / sizeof(axes[0]); a++)
+ if (axes[a]->number == i && isfinite(*value)) axes[a]->value = *value;
+ value++;
+ }
+}
+
+static uint16_t pressure_value(const struct tablet_axis *a)
+{
+ if (a->number < 0 || !(a->max > a->min) || !isfinite(a->value)) return 0;
+ double v = (a->value - a->min) / (a->max - a->min);
+ return (uint16_t)lround(fmax(0, fmin(1, v)) * 65535);
+}
+
+static int8_t tilt_value(const struct tablet_axis *a)
+{
+ /* Xorg tablet tilt valuators are already in degrees. */
+ return a->number < 0 || !isfinite(a->value) ? 0 :
+ (int8_t)lround(fmax(-90, fmin(90, a->value)));
+}
+
+static int pointer_button(unsigned button)
+{
+ return button == 1 ? 0 : button == 2 ? 1 : button == 3 ? 2 :
+ button >= 8 && button <= INT16_MAX ? (int)button - 5 : 3;
+}
+
+static void xi_pointer_event(struct x11_surface *s, const XIDeviceEvent *ev)
+{
+ struct x11_display *d = display_of(&s->base);
+ struct x11_pointer *p = pointer_for(d, ev->sourceid);
+ struct yasl_mouse_event mouse = {
+ .pos = {yasl_scale_to_device(ev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(ev->event_y, YASL_SCALE_UNIT)},
+ .device = p ? p->device : YASL_PTR_DEV_MOUSE,
+ .mods = modifiers(ev->mods.effective),
+ .button = 3,
+ };
+ if (p && (p->device == YASL_PTR_DEV_PEN || p->device == YASL_PTR_DEV_ERASER)) {
+ update_axes(p, &ev->valuators);
+ if (ev->evtype == XI_ButtonRelease && ev->detail == 1)
+ p->pressure.value = p->pressure.min;
+ mouse.pen = (struct yasl_pointer_pen){
+ pressure_value(&p->pressure), tilt_value(&p->tilt_x), tilt_value(&p->tilt_y)};
+ }
+ if (ev->evtype == XI_Motion) mouse.kind = YASL_PTR_MOTION;
+ else if (ev->detail >= 4 && ev->detail <= 7) {
+ if (ev->evtype == XI_ButtonRelease) return;
+ mouse.kind = YASL_PTR_SCROLL;
+ if (ev->detail <= 5) mouse.scroll.y = ev->detail == 4 ? -1 : 1;
+ else mouse.scroll.x = ev->detail == 6 ? -1 : 1;
+ } else {
+ mouse.kind = ev->evtype == XI_ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
+ mouse.button = pointer_button(ev->detail);
+ }
+ d->mods = mouse.mods;
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_MOUSE, .mouse = mouse}, NULL, 0);
+}
+
+static void xi_event(struct x11_display *d, XGenericEventCookie *cookie)
+{
+ if (cookie->evtype == XI_HierarchyChanged || cookie->evtype == XI_DeviceChanged) {
+ if (cookie->evtype == XI_DeviceChanged &&
+ ((XIDeviceChangedEvent *)cookie->data)->reason == XISlaveSwitch) return;
+ clear_pointers(d);
+ if (cookie->evtype == XI_HierarchyChanged &&
+ (((XIHierarchyEvent *)cookie->data)->flags &
+ (XISlaveRemoved | XIDeviceDisabled | XISlaveDetached | XIMasterRemoved))) {
+ /* Removal/disable may interrupt a drag before a release arrives. */
+ if (d->drag.active)
+ drag_end(d, YASL_DRAG_CANCELLED);
+ for (struct x11_surface *s = d->surfaces; s; s = s->next)
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_RESET}, NULL, 0);
+ }
+ return;
+ }
+ bool crossing = cookie->evtype == XI_Enter || cookie->evtype == XI_Leave;
+ if (!crossing && cookie->evtype != XI_Motion && cookie->evtype != XI_ButtonPress &&
+ cookie->evtype != XI_ButtonRelease) return;
+ Window window = crossing ? ((XIEnterEvent *)cookie->data)->event :
+ ((XIDeviceEvent *)cookie->data)->event;
+ struct x11_surface *s = d->surfaces;
+ while (s && s->window != window) s = s->next;
+ if (cookie->evtype == XI_Motion && d->drag.active) {
+ const XIDeviceEvent *mev = cookie->data;
+ if (d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
+ else if (d->drag.icon_window && d->native)
+ XMoveWindow(d->native, d->drag.icon_window,
+ (int)round(mev->root_x) - d->drag.hotspot.x,
+ (int)round(mev->root_y) - d->drag.hotspot.y);
+ struct yasl_vector2i hpos = {yasl_scale_to_device(mev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(mev->event_y, YASL_SCALE_UNIT)};
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, (int)round(mev->root_x), (int)round(mev->root_y), &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ } else if (cookie->evtype == XI_Enter && d->drag.active) {
+ const XIEnterEvent *eev = cookie->data;
+ struct yasl_vector2i hpos = {yasl_scale_to_device(eev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(eev->event_y, YASL_SCALE_UNIT)};
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, (int)round(eev->root_x), (int)round(eev->root_y), &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ } else if (cookie->evtype == XI_Leave && d->drag.active) {
+ const XIEnterEvent *lev = cookie->data;
+ struct yasl_vector2i lpos = {yasl_scale_to_device(lev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(lev->event_y, YASL_SCALE_UNIT)};
+ drag_leave_hover(d, s, lpos);
+ } else if (cookie->evtype == XI_ButtonRelease && d->drag.active) {
+ drag_drop_hover(d);
+ }
+ if (!s || s->destroyed) return;
+ if (!crossing) xi_pointer_event(s, cookie->data);
+ else {
+ const XIEnterEvent *ev = cookie->data;
+ struct x11_pointer *p = pointer_for(d, ev->sourceid);
+ if (p) p->pressure.value = p->pressure.min;
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_MOUSE, .mouse = {
+ .pos = {yasl_scale_to_device(ev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(ev->event_y, YASL_SCALE_UNIT)},
+ .device = p ? p->device : YASL_PTR_DEV_MOUSE,
+ .button = 3, .mods = modifiers(ev->mods.effective),
+ .kind = cookie->evtype == XI_Leave ? YASL_PTR_LEAVE : YASL_PTR_MOTION,
+ }}, NULL, 0);
+ }
+}
+
+static bool resize(struct x11_surface *s, struct yasl_vector2i size)
+{
+ if (size.x <= 0 || size.y <= 0 || size.x > 32767 || size.y > 32767 ||
+ (size_t)size.x > SIZE_MAX / sizeof(uint32_t) / (size_t)size.y)
+ return false;
+ if (s->image && size.x == s->size.x && size.y == s->size.y) return true;
+ struct x11_display *d = display_of(&s->base);
+ if (!d->native) {
+ s->size = size;
+ s->want_draw = true;
+ return true;
+ }
+ uint64_t start = yasl_loop_now_ns();
+
+ if (s->shm_active) {
+ XShmDetach(d->native, &s->shm_info);
+ if (s->image) XDestroyImage(s->image);
+ if (s->shm_info.shmaddr) shmdt(s->shm_info.shmaddr);
+ s->image = NULL;
+ s->pixels = NULL;
+ s->shm_active = false;
+ } else {
+ if (s->image) XDestroyImage(s->image);
+ free(s->pixels);
+ s->image = NULL;
+ s->pixels = NULL;
+ }
+
+ XImage *image = NULL;
+ uint32_t *pixels = NULL;
+ bool shm_ok = false;
+ if (d->has_shm) {
+ size_t bytes = (size_t)size.x * (size_t)size.y * sizeof(uint32_t);
+ s->shm_info.shmid = shmget(IPC_PRIVATE, bytes, IPC_CREAT | 0600);
+ if (s->shm_info.shmid >= 0) {
+ s->shm_info.shmaddr = (char *)shmat(s->shm_info.shmid, NULL, 0);
+ if (s->shm_info.shmaddr != (char *)-1) {
+ s->shm_info.readOnly = False;
+ image = XShmCreateImage(d->native, DefaultVisual(d->native, d->screen),
+ (unsigned)DefaultDepth(d->native, d->screen), ZPixmap,
+ s->shm_info.shmaddr, &s->shm_info,
+ (unsigned)size.x, (unsigned)size.y);
+ if (image) {
+ shmctl(s->shm_info.shmid, IPC_RMID, NULL);
+ if (XShmAttach(d->native, &s->shm_info)) {
+ pixels = (uint32_t *)s->shm_info.shmaddr;
+ memset(pixels, 0, bytes);
+ shm_ok = true;
+ } else {
+ XDestroyImage(image);
+ image = NULL;
+ shmdt(s->shm_info.shmaddr);
+ s->shm_info.shmaddr = NULL;
+ }
+ } else {
+ shmdt(s->shm_info.shmaddr);
+ s->shm_info.shmaddr = NULL;
+ shmctl(s->shm_info.shmid, IPC_RMID, NULL);
+ }
+ } else {
+ s->shm_info.shmaddr = NULL;
+ shmctl(s->shm_info.shmid, IPC_RMID, NULL);
+ }
+ }
+ }
+ if (!shm_ok) {
+ image = XCreateImage(d->native, DefaultVisual(d->native, d->screen),
+ (unsigned)DefaultDepth(d->native, d->screen), ZPixmap, 0, NULL,
+ (unsigned)size.x, (unsigned)size.y, 32, 0);
+ pixels = calloc((size_t)size.x * (size_t)size.y, sizeof(*pixels));
+ if (!image || !pixels) {
+ if (image) XDestroyImage(image);
+ free(pixels);
+ return false;
+ }
+ image->data = calloc((size_t)image->bytes_per_line, (size_t)size.y);
+ if (!image->data) {
+ XDestroyImage(image);
+ free(pixels);
+ return false;
+ }
+ }
+ s->shm_active = shm_ok;
+ s->image = image;
+ s->pixels = pixels;
+ s->size = size;
+ s->base.size = size;
+ s->base.scale_120 = YASL_SCALE_UNIT;
+ s->want_draw = true;
+ yasl_clock_note_resize(&s->clock, yasl_loop_now_ns() - start);
+ return true;
+}
+
+static void cancel_pastes(struct x11_display *d, struct x11_surface *target)
+{
+ for (struct paste_request **p = &d->pastes; *p;) {
+ struct paste_request *r = *p;
+ if (!target) {
+ *p = r->next;
+ XDestroyWindow(d->native, r->window);
+ free(r);
+ } else {
+ /* Keep the requestor alive until SelectionNotify: the owner may
+ * still be writing to its property. Never retain a dead target. */
+ if (r->target == target) r->target = NULL;
+ p = &r->next;
+ }
+ }
+}
+
+static void surface_destroy(struct yasl_surface *base)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (s->destroyed) return;
+ struct x11_display *d = display_of(base);
+ if (d->drag.active) {
+ if (d->drag.origin == s || d->drag.carried == s)
+ drag_end(d, YASL_DRAG_CANCELLED);
+ else if (d->drag.hover == s)
+ d->drag.hover = NULL;
+ }
+ if (d->xdnd.surface == s)
+ d->xdnd.surface = NULL;
+ if (d->pending_drop.surface == s) d->pending_drop.surface = NULL;
+ for (struct x11_surface *p = d->surfaces; p; p = p->next) {
+ if (p->parent == s) p->parent = NULL;
+ }
+ s->destroyed = true;
+ for (struct x11_surface **p = &d->surfaces; *p; p = &(*p)->next) {
+ if (*p == s) { *p = s->next; break; }
+ }
+ cancel_pastes(d, s);
+ free(s->ime_preedit);
+ s->ime_preedit = NULL;
+ if (s->ic) XDestroyIC(s->ic);
+ if (s->shm_active) {
+ XShmDetach(d->native, &s->shm_info);
+ if (s->image) XDestroyImage(s->image);
+ if (s->shm_info.shmaddr) shmdt(s->shm_info.shmaddr);
+ s->image = NULL;
+ s->pixels = NULL;
+ s->shm_active = false;
+ } else {
+ if (s->image) XDestroyImage(s->image);
+ free(s->pixels);
+ s->image = NULL;
+ s->pixels = NULL;
+ }
+ if (d->native) {
+ if (s->gc) XFreeGC(d->native, s->gc);
+ if (s->window) XDestroyWindow(d->native, s->window);
+ }
+ input_queue_clear(&s->input);
+ if (d->depth) {
+ s->dead_next = d->dead;
+ d->dead = s;
+ } else free(s);
+}
+
+static void set_callbacks(struct yasl_surface *base,
+ const struct yasl_window_cbs *cbs, void *ud)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (s->destroyed) return;
+ s->base.callbacks = cbs ? *cbs : (struct yasl_window_cbs){0};
+ s->cbs = &s->base.callbacks;
+ s->ud = ud;
+}
+
+static void set_title(struct yasl_surface *base, const char *title)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (s->destroyed || !title) return;
+ struct x11_display *d = display_of(base);
+ XStoreName(d->native, s->window, title);
+ XChangeProperty(d->native, s->window, d->net_name, d->utf8, 8,
+ PropModeReplace, (const unsigned char *)title, (int)strlen(title));
+}
+
+static void net_message(struct x11_surface *s, Atom type,
+ long a, long b, long c, long e, long f)
+{
+ struct x11_display *d = display_of(&s->base);
+ XEvent event = {0};
+ event.xclient = (XClientMessageEvent){
+ .type = ClientMessage, .display = d->native, .window = s->window,
+ .message_type = type, .format = 32, .data.l = {a, b, c, e, f},
+ };
+ XSendEvent(d->native, RootWindow(d->native, d->screen), False,
+ SubstructureRedirectMask | SubstructureNotifyMask, &event);
+}
+
+static struct yasl_surface *create_toplevel(struct yasl_display *base,
+ const struct yasl_window_spec *spec, void *graphics_device)
+{
+ (void)graphics_device;
+ if (!spec) return NULL;
+ struct x11_display *d = (struct x11_display *)base;
+ struct x11_surface *s = calloc(1, sizeof(*s));
+ if (!s) return NULL;
+ s->base.display = base;
+ s->clock.refresh_ns = YASL_REFRESH_DEFAULT_NS;
+ if (!resize(s, spec->size)) { free(s); return NULL; }
+ s->window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen),
+ 0, 0, (unsigned)spec->size.x, (unsigned)spec->size.y, 0, 0, 0);
+ s->base.native_handle = (void *)(uintptr_t)s->window;
+ s->base.size = s->size;
+ s->base.scale_120 = YASL_SCALE_UNIT;
+ s->gc = XCreateGC(d->native, s->window, 0, NULL);
+ XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
+ FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
+ (d->xi2 ? 0 : ButtonPressMask | ButtonReleaseMask |
+ PointerMotionMask | LeaveWindowMask | EnterWindowMask));
+ if (d->xi2) {
+ unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0};
+ XISetMask(bits, XI_Motion);
+ XISetMask(bits, XI_ButtonPress);
+ XISetMask(bits, XI_ButtonRelease);
+ XISetMask(bits, XI_Enter);
+ XISetMask(bits, XI_Leave);
+ XIEventMask mask = {XIAllMasterDevices, sizeof(bits), bits};
+ XISelectEvents(d->native, s->window, &mask, 1);
+ }
+ XSetWMProtocols(d->native, s->window, &d->delete_window, 1);
+ XClassHint hint = { .res_name = d->app_id, .res_class = d->app_id };
+ XSetClassHint(d->native, s->window, &hint);
+ Atom xdnd_version = 5;
+ XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&xdnd_version, 1);
+ if (d->im) {
+ s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNClientWindow, s->window, XNFocusWindow, s->window, NULL);
+ }
+ s->next = d->surfaces;
+ d->surfaces = s;
+ set_title(&s->base, spec->title ? spec->title : "YASL");
+ if (spec->fullscreen) {
+ XChangeProperty(d->native, s->window, d->net_state, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&d->fullscreen, 1);
+ s->state |= YASL_STATE_FULLSCREEN;
+ }
+ XMapWindow(d->native, s->window);
+ XFlush(d->native);
+ return &s->base;
+}
+
+static bool popup_spec_valid(const struct x11_surface *parent,
+ const struct yasl_popup_spec *spec)
+{
+ if (!parent || !spec) return false;
+ int parent_cx = parent->is_popup ? parent->content_offset.x : 0;
+ int parent_cy = parent->is_popup ? parent->content_offset.y : 0;
+ int64_t x = (int64_t)spec->anchor.x - parent_cx;
+ int64_t y = (int64_t)spec->anchor.y - parent_cy;
+ return x >= -32768 && x <= 32767 && y >= -32768 && y <= 32767 &&
+ spec->anchor.width >= 0 && spec->anchor.height >= 0 &&
+ spec->size.x > 0 && spec->size.y > 0 &&
+ spec->size.x <= 32767 && spec->size.y <= 32767 &&
+ spec->content_offset.x >= 0 && spec->content_offset.y >= 0 &&
+ spec->content_size.x >= 0 && spec->content_size.y >= 0 &&
+ (int64_t)spec->content_offset.x + spec->content_size.x <= spec->size.x &&
+ (int64_t)spec->content_offset.y + spec->content_size.y <= spec->size.y;
+}
+
+static void popup_calculate_position(struct x11_display *d,
+ struct x11_surface *parent,
+ const struct yasl_popup_spec *spec,
+ int *out_x, int *out_y)
+{
+ int parent_rx = 0, parent_ry = 0;
+ if (d->native) {
+ Window child;
+ XTranslateCoordinates(d->native, parent->window,
+ RootWindow(d->native, d->screen),
+ 0, 0, &parent_rx, &parent_ry, &child);
+ }
+ int parent_cx = parent->is_popup ? parent->content_offset.x : 0;
+ int parent_cy = parent->is_popup ? parent->content_offset.y : 0;
+
+ int anchor_x = parent_rx + (spec->anchor.x - parent_cx);
+ int anchor_y = parent_ry + (spec->anchor.y - parent_cy);
+
+ int root_x, root_y;
+ if (spec->beside) {
+ root_x = anchor_x + spec->anchor.width - spec->content_offset.x;
+ root_y = anchor_y - spec->content_offset.y;
+ } else {
+ root_x = anchor_x - spec->content_offset.x;
+ root_y = anchor_y + spec->anchor.height - spec->content_offset.y;
+ }
+
+ int mon_x = 0, mon_y = 0;
+ int mon_w = d->native ? DisplayWidth(d->native, d->screen) : 1920;
+ int mon_h = d->native ? DisplayHeight(d->native, d->screen) : 1080;
+
+#if YASL_HAS_XRANDR
+ if (d->native) {
+ int nmonitors = 0;
+ XRRMonitorInfo *monitors = XRRGetMonitors(d->native, RootWindow(d->native, d->screen), True, &nmonitors);
+ if (monitors && nmonitors > 0) {
+ for (int i = 0; i < nmonitors; i++) {
+ if (anchor_x >= monitors[i].x && anchor_x < monitors[i].x + monitors[i].width &&
+ anchor_y >= monitors[i].y && anchor_y < monitors[i].y + monitors[i].height) {
+ mon_x = monitors[i].x;
+ mon_y = monitors[i].y;
+ mon_w = monitors[i].width;
+ mon_h = monitors[i].height;
+ break;
+ }
+ }
+ XRRFreeMonitors(monitors);
+ }
+ }
+#endif
+
+ if (spec->beside) {
+ if (root_x + spec->size.x > mon_x + mon_w) {
+ int flipped_x = anchor_x - spec->size.x + spec->content_offset.x;
+ if (flipped_x >= mon_x) root_x = flipped_x;
+ }
+ } else {
+ if (root_y + spec->size.y > mon_y + mon_h) {
+ int flipped_y = anchor_y - spec->size.y + spec->content_offset.y;
+ if (flipped_y >= mon_y) root_y = flipped_y;
+ }
+ }
+
+ if (root_x + spec->size.x > mon_x + mon_w) root_x = mon_x + mon_w - spec->size.x;
+ if (root_x < mon_x) root_x = mon_x;
+ if (root_y + spec->size.y > mon_y + mon_h) root_y = mon_y + mon_h - spec->size.y;
+ if (root_y < mon_y) root_y = mon_y;
+
+ *out_x = root_x;
+ *out_y = root_y;
+}
+
+static struct yasl_surface *surface_create_popup(struct yasl_surface *parent_base,
+ const struct yasl_popup_spec *spec)
+{
+ struct x11_surface *parent = (struct x11_surface *)parent_base;
+ if (!parent || parent->destroyed || !popup_spec_valid(parent, spec))
+ return NULL;
+ struct x11_display *d = display_of(parent_base);
+ struct x11_surface *s = calloc(1, sizeof(*s));
+ if (!s) return NULL;
+ s->base.display = parent_base->display;
+ s->parent = parent;
+ s->is_popup = true;
+ s->content_offset = spec->content_offset;
+ s->clock.refresh_ns = parent->clock.refresh_ns;
+ if (!resize(s, spec->size)) { free(s); return NULL; }
+
+ int root_x = 0, root_y = 0;
+ popup_calculate_position(d, parent, spec, &root_x, &root_y);
+
+ if (d->native) {
+ XSetWindowAttributes attrs = {
+ .override_redirect = True,
+ .save_under = True,
+ };
+ s->window = XCreateWindow(d->native, RootWindow(d->native, d->screen),
+ root_x, root_y, (unsigned)spec->size.x, (unsigned)spec->size.y,
+ 0, CopyFromParent, InputOutput, CopyFromParent,
+ CWOverrideRedirect | CWSaveUnder, &attrs);
+ if (!s->window) {
+ surface_destroy(&s->base);
+ return NULL;
+ }
+ s->base.native_handle = (void *)(uintptr_t)s->window;
+ s->base.size = s->size;
+ s->base.scale_120 = YASL_SCALE_UNIT;
+ s->gc = XCreateGC(d->native, s->window, 0, NULL);
+ XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
+ FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
+ (d->xi2 ? 0 : ButtonPressMask | ButtonReleaseMask |
+ PointerMotionMask | LeaveWindowMask | EnterWindowMask));
+ if (d->xi2) {
+ unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0};
+ XISetMask(bits, XI_Motion);
+ XISetMask(bits, XI_ButtonPress);
+ XISetMask(bits, XI_ButtonRelease);
+ XISetMask(bits, XI_Enter);
+ XISetMask(bits, XI_Leave);
+ XIEventMask mask = {XIAllMasterDevices, sizeof(bits), bits};
+ XISelectEvents(d->native, s->window, &mask, 1);
+ }
+ Atom xdnd_version = 5;
+ XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&xdnd_version, 1);
+ if (d->im) {
+ s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNClientWindow, s->window, XNFocusWindow, s->window, NULL);
+ }
+ XMapRaised(d->native, s->window);
+ XFlush(d->native);
+ }
+ s->next = d->surfaces;
+ d->surfaces = s;
+ return &s->base;
+}
+
+static bool surface_popup_reposition(struct yasl_surface *popup_base,
+ const struct yasl_popup_spec *spec)
+{
+ struct x11_surface *s = (struct x11_surface *)popup_base;
+ if (!s || s->destroyed || !s->is_popup || !s->parent || s->parent->destroyed ||
+ !popup_spec_valid(s->parent, spec))
+ return false;
+ struct x11_display *d = display_of(popup_base);
+ if (spec->size.x != s->size.x || spec->size.y != s->size.y) {
+ if (!resize(s, spec->size))
+ return false;
+ }
+ s->content_offset = spec->content_offset;
+ int root_x = 0, root_y = 0;
+ popup_calculate_position(d, s->parent, spec, &root_x, &root_y);
+ if (d->native && s->window) {
+ XMoveResizeWindow(d->native, s->window, root_x, root_y,
+ (unsigned)spec->size.x, (unsigned)spec->size.y);
+ XFlush(d->native);
+ }
+ schedule_redraw(&s->base);
+ return true;
+}
+
+static void update_state(struct x11_surface *s)
+{
+ struct x11_display *d = display_of(&s->base);
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *data = NULL;
+ uint32_t state = s->state & YASL_STATE_ACTIVATED;
+ if (XGetWindowProperty(d->native, s->window, d->net_state, 0, 64, False,
+ XA_ATOM, &actual, &format, &count, &left, &data) == Success && format == 32) {
+ Atom *atoms = (Atom *)data;
+ bool h = false, v = false;
+ for (unsigned long i = 0; i < count; i++) {
+ if (atoms[i] == d->fullscreen) state |= YASL_STATE_FULLSCREEN;
+ if (atoms[i] == d->maximized_h) h = true;
+ if (atoms[i] == d->maximized_v) v = true;
+ }
+ if (h && v) state |= YASL_STATE_MAXIMIZED;
+ }
+ if (data) XFree(data);
+ if (state == s->state) return;
+ s->state = state;
+ if (s->cbs && s->cbs->state) s->cbs->state(&s->base, state, s->ud);
+}
+
+static void selection_request(struct x11_display *d, XSelectionRequestEvent *request)
+{
+ XEvent reply = {0};
+ reply.xselection = (XSelectionEvent){
+ .type = SelectionNotify, .display = d->native,
+ .requestor = request->requestor, .selection = request->selection,
+ .target = request->target, .time = request->time,
+ };
+ Atom property = request->property ? request->property : request->target;
+ int slot = request->selection == XA_PRIMARY ? YASL_PRIMARY : YASL_CLIPBOARD;
+ const char *text = d->selection[slot];
+ size_t latin1_len = 0;
+ char *latin1 = text ? yasl_utf8_to_latin1(text, &latin1_len) : NULL;
+ if (text && request->target == d->targets) {
+ Atom targets[] = {d->targets, d->utf8, XA_STRING};
+ XChangeProperty(d->native, request->requestor, property, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)targets, latin1 ? 3 : 2);
+ reply.xselection.property = property;
+ } else if (text && (request->target == d->utf8 || request->target == XA_STRING)) {
+ const char *payload = request->target == XA_STRING ? latin1 : text;
+ size_t len = request->target == XA_STRING ? latin1_len : strlen(text);
+ long max = XMaxRequestSize(d->native) * 4 - 128;
+ if (payload && len <= (size_t)max) {
+ XChangeProperty(d->native, request->requestor, property, request->target,
+ 8, PropModeReplace, (const unsigned char *)payload, (int)len);
+ reply.xselection.property = property;
+ }
+ }
+ free(latin1);
+ XSendEvent(d->native, request->requestor, False, 0, &reply);
+}
+
+static char *parse_file_uris(const char *data, size_t len, size_t *out_len)
+{
+ if (!data || !len) {
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ char *out = malloc(len * 2 + 2);
+ if (!out) {
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ size_t out_pos = 0;
+ size_t i = 0;
+ while (i < len) {
+ while (i < len && (data[i] == '\r' || data[i] == '\n')) i++;
+ if (i >= len) break;
+ size_t start = i;
+ while (i < len && data[i] != '\r' && data[i] != '\n') i++;
+ size_t end = i;
+ while (end > start && (data[end - 1] == ' ' || data[end - 1] == '\t' || data[end - 1] == '\r'))
+ end--;
+ size_t line_len = end - start;
+ if (line_len >= 7 && !strncmp(data + start, "file://", 7)) {
+ memcpy(out + out_pos, data + start, line_len);
+ out_pos += line_len;
+ out[out_pos++] = '\r';
+ out[out_pos++] = '\n';
+ }
+ }
+ if (!out_pos) {
+ free(out);
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ out[out_pos] = '\0';
+ if (out_len) *out_len = out_pos;
+ return out;
+}
+
+static void xdnd_selection_notify(struct x11_display *d, XSelectionEvent *event)
+{
+ if (!d->pending_drop.active) return;
+ Window source = d->pending_drop.source;
+ struct x11_surface *s = d->pending_drop.surface;
+ d->pending_drop.active = false;
+ d->pending_drop.source = None;
+ d->pending_drop.surface = NULL;
+
+ bool success = false;
+ if (s && !s->destroyed && event->property != None) {
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *data = NULL;
+ if (XGetWindowProperty(d->native, s->window, event->property,
+ 0, TEXT_LIMIT / 4, True, AnyPropertyType,
+ &actual, &format, &count, &left, &data) == Success && data && count > 0) {
+ size_t out_len = 0;
+ char *parsed = parse_file_uris((const char *)data, count, &out_len);
+ if (parsed && out_len > 0) {
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_DROP_FILES}, parsed, out_len);
+ free(parsed);
+ success = true;
+ }
+ }
+ if (data) XFree(data);
+ }
+ if (source) {
+ XEvent finished = {0};
+ finished.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_finished,
+ .format = 32,
+ .data.l = {
+ s ? s->window : None,
+ success ? 1 : 0,
+ success ? d->xdnd_action_copy : None,
+ },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &finished);
+ XFlush(d->native);
+ }
+}
+
+static void selection_notify(struct x11_display *d, XSelectionEvent *event)
+{
+ struct paste_request **p = &d->pastes;
+ while (*p && (*p)->window != event->requestor) p = &(*p)->next;
+ if (!*p) return;
+ struct paste_request *r = *p;
+ *p = r->next;
+ struct x11_surface *s = r->target;
+ if (!s || event->property == None) {
+ XDestroyWindow(d->native, r->window);
+ free(r);
+ return;
+ }
+ Atom actual = None;
+ int format = 0;
+ unsigned long count = 0, left = 0;
+ unsigned char *data = NULL;
+ char *buf = NULL;
+ size_t total = 0;
+ unsigned long offset = 0;
+ bool complete = false;
+
+ /* Read up to TEXT_LIMIT bytes in 64KB chunks to avoid huge single-request limits */
+ while (total < TEXT_LIMIT) {
+ unsigned long chunk = 16384; /* 64 KB (in 32-bit multiples) */
+ if (XGetWindowProperty(d->native, r->window, event->property,
+ (long)offset, (long)chunk, True, AnyPropertyType,
+ &actual, &format, &count, &left, &data) != Success || !actual) {
+ if (data) { XFree(data); data = NULL; }
+ break;
+ }
+ if (actual == d->incr) {
+ /* Unsupported INCR streaming transfer */
+ if (data) { XFree(data); data = NULL; }
+ break;
+ }
+ if ((actual != d->utf8 && actual != XA_STRING) || format != 8) {
+ if (data) { XFree(data); data = NULL; }
+ break;
+ }
+ char *nb = realloc(buf, total + count + 1);
+ if (!nb) {
+ XFree(data);
+ break;
+ }
+ buf = nb;
+ if (count) memcpy(buf + total, data, count);
+ total += count;
+ buf[total] = '\0';
+ offset += count / 4;
+ XFree(data);
+ data = NULL;
+ if (left == 0) { complete = true; break; }
+ }
+ if (complete && actual == XA_STRING) {
+ size_t utf8_len;
+ char *utf8 = yasl_utf8_from_latin1(buf, total, &utf8_len);
+ free(buf);
+ buf = utf8;
+ if (buf) total = utf8_len;
+ }
+ if (complete && buf && yasl_utf8_validate(buf, total))
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, buf, total);
+ free(buf);
+ XDestroyWindow(d->native, r->window);
+ free(r);
+}
+
+static void refresh_keyboard(struct x11_display *d)
+{
+ memset(d->keys, 0, sizeof(d->keys));
+ if (!d->has_xkb) return;
+ XkbDescPtr keyboard = XkbGetKeyboard(d->native, XkbAllComponentsMask, XkbUseCoreKbd);
+ if (!keyboard) return;
+ if (keyboard->names && keyboard->names->keys) {
+ for (unsigned code = keyboard->min_key_code; code <= keyboard->max_key_code; code++)
+ d->keys[code] = yasl_key_from_xkb_name(keyboard->names->keys[code].name);
+ }
+ XkbFreeKeyboard(keyboard, XkbAllComponentsMask, True);
+}
+
+static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
+{
+ struct x11_display *d = display_of(&s->base);
+ d->mods = modifiers(key->state);
+ unsigned clean_state = key->state & ~(Mod2Mask | LockMask);
+ KeySym sym = NoSymbol, base = NoSymbol, shifted = NoSymbol;
+
+ int group = XkbGroupForCoreState(key->state);
+ if (d->has_xkb) {
+ sym = XkbKeycodeToKeysym(d->native, key->keycode, group, (clean_state & ShiftMask) ? 1 : 0);
+ base = XkbKeycodeToKeysym(d->native, key->keycode, group, 0);
+ shifted = XkbKeycodeToKeysym(d->native, key->keycode, group, 1);
+ }
+ if (sym == NoSymbol) {
+ XKeyEvent clean_key = *key;
+ clean_key.state = clean_state;
+ sym = XLookupKeysym(&clean_key, (clean_state & ShiftMask) ? 1 : 0);
+ base = XLookupKeysym(&clean_key, 0);
+ shifted = XLookupKeysym(&clean_key, 1);
+ }
+ char local[128];
+ char *text = local;
+ int len = 0;
+ if (key->type == KeyPress) {
+ if (s->ic) {
+ Status status;
+ len = Xutf8LookupString(s->ic, key, local, sizeof(local), &sym, &status);
+ if (status == XBufferOverflow && len > 0 && len < TEXT_LIMIT) {
+ text = malloc((size_t)len + 1);
+ if (text) len = Xutf8LookupString(s->ic, key, text, len + 1, &sym, &status);
+ else len = 0;
+ }
+ if (status != XLookupChars && status != XLookupBoth) len = 0;
+ } else {
+ /* XLookupString's bytes are Latin-1. Use its modifier-aware keysym
+ * and encode UTF-8 even when no input method is available. */
+ XLookupString(key, local, sizeof(local), &sym, NULL);
+ len = xkb_keysym_to_utf8((xkb_keysym_t)sym, local, sizeof(local));
+ len = len > 0 ? len - 1 : 0; /* xkb includes the terminating NUL. */
+ }
+ }
+ uint32_t cp = xkb_keysym_to_utf32((xkb_keysym_t)base);
+ push(s, (struct yasl_input_event){
+ .kind = YASL_INPUT_KEY,
+ .key = {.key = key->keycode < 256 ? d->keys[key->keycode] : YASL_KEY_UNKNOWN,
+ .codepoint = yasl_key_label(cp), .native_code = key->keycode,
+ .shifted = yasl_key_label(xkb_keysym_to_utf32((xkb_keysym_t)shifted)),
+ .mods = d->mods,
+ .action = key->type == KeyRelease ? YASL_KEY_RELEASE :
+ repeat ? YASL_KEY_REPEAT : YASL_KEY_PRESS},
+ }, NULL, 0);
+ if (len > 0 && text && (unsigned char)text[0] >= 0x20 && text[0] != 0x7f &&
+ !(d->mods & (YASL_MOD_CTRL | YASL_MOD_SUPER | YASL_MOD_ALT)))
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_TEXT}, text, (size_t)len);
+ if (text != local) free(text);
+}
+
+static void handle_client_message(struct x11_display *d, struct x11_surface *s,
+ const XClientMessageEvent *ev)
+{
+ if (ev->message_type == d->protocols &&
+ (Atom)ev->data.l[0] == d->delete_window && s->cbs && s->cbs->close) {
+ s->cbs->close(&s->base, s->ud);
+ } else if (ev->message_type == d->xdnd_enter) {
+ Window source = (Window)ev->data.l[0];
+ int version = (int)((unsigned long)ev->data.l[1] >> 24);
+ bool more_types = (ev->data.l[1] & 1) != 0;
+ bool has_uri = false;
+ if (more_types && d->native) {
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *prop_data = NULL;
+ if (XGetWindowProperty(d->native, source, d->xdnd_type_list,
+ 0, 64, False, XA_ATOM, &actual, &format, &count, &left, &prop_data) == Success &&
+ actual == XA_ATOM && format == 32 && prop_data) {
+ Atom *types = (Atom *)prop_data;
+ for (unsigned long i = 0; i < count; i++) {
+ if (types[i] == d->uri_list) { has_uri = true; break; }
+ }
+ }
+ if (prop_data) XFree(prop_data);
+ } else {
+ for (int i = 2; i <= 4; i++) {
+ if ((Atom)ev->data.l[i] == d->uri_list) { has_uri = true; break; }
+ }
+ }
+ d->xdnd.source = source;
+ d->xdnd.version = version;
+ d->xdnd.has_uri_list = has_uri;
+ d->xdnd.accepts = has_uri;
+ } else if (ev->message_type == d->xdnd_position) {
+ Window source = (Window)ev->data.l[0];
+ int rx = (int)(int16_t)((ev->data.l[2] >> 16) & 0xFFFF);
+ int ry = (int)(int16_t)(ev->data.l[2] & 0xFFFF);
+ Time t = (Time)ev->data.l[3];
+ int wx = rx, wy = ry;
+ if (d->native) {
+ Window child;
+ XTranslateCoordinates(d->native, RootWindow(d->native, d->screen), s->window,
+ rx, ry, &wx, &wy, &child);
+ }
+ struct yasl_vector2i pos = {wx, wy};
+ if (d->xdnd.surface && d->xdnd.surface != s) {
+ if (d->xdnd.accepts && d->xdnd.surface->cbs && d->xdnd.surface->cbs->drop)
+ d->xdnd.surface->cbs->drop(&d->xdnd.surface->base, YASL_DROP_LEAVE, d->xdnd.pos, d->xdnd.surface->ud);
+ }
+ d->xdnd.surface = s;
+ d->xdnd.pos = pos;
+ d->xdnd.time = t;
+ if (d->xdnd.accepts && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_OVER, pos, s->ud);
+
+ if (d->native) {
+ XEvent reply = {0};
+ reply.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_status,
+ .format = 32,
+ .data.l = {
+ s->window,
+ d->xdnd.accepts ? 1 : 0,
+ 0,
+ 0,
+ d->xdnd.accepts ? d->xdnd_action_copy : None,
+ },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &reply);
+ }
+ } else if (ev->message_type == d->xdnd_leave) {
+ if (d->xdnd.surface == s) {
+ if (d->xdnd.accepts && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud);
+ }
+ d->xdnd = (typeof(d->xdnd)){0};
+ } else if (ev->message_type == d->xdnd_drop) {
+ Window source = (Window)ev->data.l[0];
+ Time drop_time = (Time)ev->data.l[2];
+ bool was_accepting = d->xdnd.accepts && d->xdnd.has_uri_list && (d->xdnd.source == source);
+ if (d->xdnd.surface == s) {
+ if (was_accepting && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud);
+ }
+ if (was_accepting) {
+ d->pending_drop.active = true;
+ d->pending_drop.source = source;
+ d->pending_drop.surface = s;
+ d->pending_drop.time = drop_time;
+ if (d->native) {
+ XConvertSelection(d->native, d->xdnd_selection, d->uri_list,
+ d->paste_property, s->window, drop_time);
+ }
+ } else if (d->native) {
+ XEvent reply = {0};
+ reply.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_finished,
+ .format = 32,
+ .data.l = { s->window, 0, None },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &reply);
+ }
+ d->xdnd = (typeof(d->xdnd)){0};
+ }
+}
+
+static void dispatch(struct yasl_display *base)
+{
+ struct x11_display *d = (struct x11_display *)base;
+ d->depth++;
+ while (XPending(d->native)) {
+ XEvent event;
+ XNextEvent(d->native, &event);
+ if (event.type == GenericEvent && d->xi2 &&
+ event.xcookie.extension == d->xi_opcode && XGetEventData(d->native, &event.xcookie)) {
+ xi_event(d, &event.xcookie);
+ XFreeEventData(d->native, &event.xcookie);
+ continue;
+ }
+ if (event.type == MappingNotify) {
+ XRefreshKeyboardMapping(&event.xmapping);
+ refresh_keyboard(d);
+ continue;
+ }
+ if (d->has_xkb && event.type == d->xkb_event) {
+ XkbEvent *xkb = (XkbEvent *)&event;
+ if (xkb->any.xkb_type == XkbMapNotify) XkbRefreshKeyboardMapping(&xkb->map);
+ refresh_keyboard(d);
+ continue;
+ }
+ if (XFilterEvent(&event, None)) continue;
+ if (event.type == SelectionRequest) {
+ selection_request(d, &event.xselectionrequest);
+ continue;
+ }
+ if (event.type == SelectionNotify) {
+ if (event.xselection.selection == d->xdnd_selection) {
+ xdnd_selection_notify(d, &event.xselection);
+ continue;
+ }
+ selection_notify(d, &event.xselection);
+ continue;
+ }
+ struct x11_surface *s = d->surfaces;
+ while (s && s->window != event.xany.window) s = s->next;
+ if (!s) continue;
+ switch (event.type) {
+ case Expose: s->want_draw = true; break;
+ case ConfigureNotify: {
+ struct yasl_vector2i size = {event.xconfigure.width, event.xconfigure.height};
+ if (size.x == s->size.x && size.y == s->size.y) break;
+ if (resize(s, size) && s->cbs && s->cbs->configure)
+ s->cbs->configure(&s->base, (struct yasl_geometry){size, YASL_SCALE_UNIT}, s->ud);
+ break;
+ }
+ case ClientMessage:
+ handle_client_message(d, s, &event.xclient);
+ break;
+ case FocusIn: case FocusOut: {
+ bool focused = event.type == FocusIn;
+ if (s->ic) { if (focused) XSetICFocus(s->ic); else XUnsetICFocus(s->ic); }
+ if (focused) s->state |= YASL_STATE_ACTIVATED;
+ else s->state &= ~YASL_STATE_ACTIVATED;
+ push(s, (struct yasl_input_event){
+ .kind = YASL_INPUT_FOCUS, .focus.focused = focused}, NULL, 0);
+ if (s->cbs && s->cbs->state) s->cbs->state(&s->base, s->state, s->ud);
+ break;
+ }
+ case PropertyNotify:
+ if (event.xproperty.atom == d->net_state) update_state(s);
+ break;
+ case KeyPress: case KeyRelease: {
+ bool repeat = false;
+ if (event.type == KeyRelease && XPending(d->native)) {
+ XEvent next;
+ XPeekEvent(d->native, &next);
+ if (next.type == KeyPress && next.xkey.window == event.xkey.window &&
+ next.xkey.keycode == event.xkey.keycode && next.xkey.time == event.xkey.time) {
+ XNextEvent(d->native, &event);
+ repeat = true;
+ }
+ }
+ key_event(s, &event.xkey, repeat);
+ break;
+ }
+ case MotionNotify: case LeaveNotify: case EnterNotify: case ButtonPress: case ButtonRelease: {
+ struct yasl_mouse_event mouse = {.device = YASL_PTR_DEV_MOUSE};
+ unsigned state;
+ if (event.type == MotionNotify) {
+ mouse.pos = (struct yasl_vector2i){event.xmotion.x, event.xmotion.y};
+ mouse.kind = YASL_PTR_MOTION;
+ state = event.xmotion.state;
+ if (d->drag.active) {
+ if (d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
+ else if (d->drag.icon_window && d->native)
+ XMoveWindow(d->native, d->drag.icon_window,
+ event.xmotion.x_root - d->drag.hotspot.x,
+ event.xmotion.y_root - d->drag.hotspot.y);
+ struct yasl_vector2i hpos = mouse.pos;
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, event.xmotion.x_root, event.xmotion.y_root, &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ }
+ } else if (event.type == EnterNotify) {
+ mouse.pos = (struct yasl_vector2i){event.xcrossing.x, event.xcrossing.y};
+ mouse.kind = YASL_PTR_MOTION;
+ state = event.xcrossing.state;
+ if (d->drag.active) {
+ struct yasl_vector2i hpos = mouse.pos;
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, event.xcrossing.x_root, event.xcrossing.y_root, &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ }
+ } else if (event.type == LeaveNotify) {
+ mouse.pos = (struct yasl_vector2i){event.xcrossing.x, event.xcrossing.y};
+ mouse.kind = YASL_PTR_LEAVE;
+ state = event.xcrossing.state;
+ if (d->drag.active)
+ drag_leave_hover(d, s, mouse.pos);
+ } else {
+ mouse.pos = (struct yasl_vector2i){event.xbutton.x, event.xbutton.y};
+ state = event.xbutton.state;
+ unsigned button = event.xbutton.button;
+ if (button >= 4 && button <= 7) {
+ if (event.type == ButtonRelease) break;
+ mouse.kind = YASL_PTR_SCROLL;
+ if (button <= 5) mouse.scroll.y = button == 4 ? -1 : 1;
+ else mouse.scroll.x = button == 6 ? -1 : 1;
+ } else {
+ mouse.kind = event.type == ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
+ mouse.button = pointer_button(button);
+ if (event.type == ButtonRelease && d->drag.active)
+ drag_drop_hover(d);
+ }
+ }
+ d->mods = mouse.mods = modifiers(state);
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_MOUSE, .mouse = mouse}, NULL, 0);
+ break;
+ }
+ }
+ }
+ leave(d);
+}
+
+static bool present(struct yasl_surface *base, const struct yasl_framebuffer *fb,
+ const struct yasl_damage_list *damage)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (s->destroyed || !s->drawing || s->presented || !fb || !damage ||
+ fb->pixels != s->pixels || fb->size.x != s->size.x || fb->size.y != s->size.y ||
+ damage->n < -1 || damage->n > YASL_DAMAGE_MAX) return false;
+ struct x11_display *d = display_of(base);
+ XImage *im = s->image;
+ if (s->shm_active) {
+ if (damage->n == -1)
+ XShmPutImage(d->native, s->window, s->gc, im, 0, 0, 0, 0,
+ (unsigned)s->size.x, (unsigned)s->size.y, False);
+ else for (int i = 0; i < damage->n; i++) {
+ struct yasl_rect2i r = damage->rects[i];
+ int64_t right = (int64_t)r.x + r.width, bottom = (int64_t)r.y + r.height;
+ int x = r.x > 0 ? r.x : 0, y = r.y > 0 ? r.y : 0;
+ int w = (int)(right < s->size.x ? right : s->size.x) - x;
+ int h = (int)(bottom < s->size.y ? bottom : s->size.y) - y;
+ if (w > 0 && h > 0)
+ XShmPutImage(d->native, s->window, s->gc, im, x, y, x, y, (unsigned)w, (unsigned)h, False);
+ }
+ s->presented = true;
+ yasl_clock_presented(&s->clock, yasl_loop_now_ns(), false);
+ return true;
+ }
+ bool direct = im->bits_per_pixel == 32 && im->red_mask == 0xff0000 &&
+ im->green_mask == 0xff00 && im->blue_mask == 0xff;
+ int y_start = 0, y_end = s->size.y;
+ int x_start = 0, x_end = s->size.x;
+ if (damage->n >= 0) {
+ y_start = s->size.y; y_end = 0;
+ x_start = s->size.x; x_end = 0;
+ for (int i = 0; i < damage->n; i++) {
+ struct yasl_rect2i r = damage->rects[i];
+ int y0 = r.y > 0 ? r.y : 0;
+ int y1 = (r.y + r.height < s->size.y) ? r.y + r.height : s->size.y;
+ int x0 = r.x > 0 ? r.x : 0;
+ int x1 = (r.x + r.width < s->size.x) ? r.x + r.width : s->size.x;
+ if (y0 < y_start) y_start = y0;
+ if (y1 > y_end) y_end = y1;
+ if (x0 < x_start) x_start = x0;
+ if (x1 > x_end) x_end = x1;
+ }
+ if (y_start >= y_end || x_start >= x_end) {
+ y_start = y_end = x_start = x_end = 0;
+ }
+ }
+ size_t copy_bytes = (size_t)(x_end - x_start) * 4;
+ for (int y = y_start; y < y_end; y++) {
+ const uint32_t *row = s->pixels + (size_t)y * s->size.x;
+ if (direct) memcpy(im->data + (size_t)y * im->bytes_per_line + (size_t)x_start * 4,
+ row + x_start, copy_bytes);
+ else for (int x = x_start; x < x_end; x++) {
+ uint32_t p = row[x];
+ XPutPixel(im, x, y, component((p >> 16) & 255, im->red_mask) |
+ component((p >> 8) & 255, im->green_mask) | component(p & 255, im->blue_mask));
+ }
+ }
+ if (damage->n == -1)
+ XPutImage(d->native, s->window, s->gc, im, 0, 0, 0, 0,
+ (unsigned)s->size.x, (unsigned)s->size.y);
+ else for (int i = 0; i < damage->n; i++) {
+ struct yasl_rect2i r = damage->rects[i];
+ int64_t right = (int64_t)r.x + r.width, bottom = (int64_t)r.y + r.height;
+ int x = r.x > 0 ? r.x : 0, y = r.y > 0 ? r.y : 0;
+ int w = (int)(right < s->size.x ? right : s->size.x) - x;
+ int h = (int)(bottom < s->size.y ? bottom : s->size.y) - y;
+ if (w > 0 && h > 0)
+ XPutImage(d->native, s->window, s->gc, im, x, y, x, y, (unsigned)w, (unsigned)h);
+ }
+ s->presented = true;
+ yasl_clock_presented(&s->clock, yasl_loop_now_ns(), false);
+ return true;
+}
+
+static void flush(struct yasl_display *base)
+{
+ struct x11_display *d = (struct x11_display *)base;
+ d->depth++;
+ for (struct x11_surface *s = d->surfaces, *next; s; s = next) {
+ next = s->next;
+ if (s->destroyed || !s->want_draw || !s->cbs || !s->cbs->draw || s->drawing) continue;
+ s->want_draw = false;
+ s->drawing = true;
+ s->presented = false;
+ s->damage = (struct yasl_damage_list){0};
+ s->frame = (struct yasl_frame){
+ .surface = &s->base,
+ .target = {.generation = yasl_frame_begin(&s->base),
+ .pixels = s->pixels, .size = s->size},
+ };
+ s->cbs->draw(&s->base, &s->frame, s->ud);
+ s->drawing = false;
+ }
+ XFlush(d->native);
+ leave(d);
+}
+
+static int ready(int fd, short events, void *ud)
+{
+ (void)fd;
+ struct yasl_display *base = ud;
+ if (events & (POLLIN | POLLERR | POLLHUP)) {
+ dispatch(base);
+ flush(base);
+ }
+ return 0;
+}
+
+static int dispatch_timeout(struct yasl_display *base, int timeout_ms)
+{
+ struct x11_display *d = (struct x11_display *)base;
+ if (unlikely(!d || !d->native)) return -1;
+ dispatch(base);
+ for (struct x11_surface *s = d->surfaces; s; s = s->next) {
+ if (s->want_draw || s->input.count || s->input.reset_pending) {
+ timeout_ms = 0;
+ break;
+ }
+ }
+ if (d->loop) {
+ XFlush(d->native);
+ if (XEventsQueued(d->native, QueuedAlready) > 0)
+ timeout_ms = 0;
+ if (!XPending(d->native) && timeout_ms != 0) {
+ yasl_loop_step(d->loop, timeout_ms);
+ dispatch(base);
+ } else {
+ yasl_loop_step(d->loop, 0);
+ dispatch(base);
+ }
+ }
+ return 0;
+}
+
+static struct yasl_display *display_open(struct yasl_loop *loop,
+ const struct yasl_display_spec *spec)
+{
+ struct x11_display *d = calloc(1, sizeof(*d));
+ if (!d) return NULL;
+ d->base.ops = &yasl_x11_backend;
+ d->base.callbacks = spec && spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0};
+ d->native = XOpenDisplay(NULL);
+ if (!d->native) { free(d); errno = ENODEV; return NULL; }
+ XSetErrorHandler(yasl_x11_error_handler);
+ d->base.native_handle = d->native;
+ d->screen = DefaultScreen(d->native);
+ d->has_shm = XShmQueryExtension(d->native);
+ int xkb_opcode = 0, xkb_event = 0, xkb_error = 0;
+ int xkb_major = XkbMajorVersion, xkb_minor = XkbMinorVersion;
+ d->has_xkb = XkbQueryExtension(d->native, &xkb_opcode, &xkb_event, &xkb_error, &xkb_major, &xkb_minor);
+ d->xkb_event = xkb_event;
+ if (d->has_xkb) {
+ unsigned events = XkbNewKeyboardNotifyMask | XkbMapNotifyMask | XkbNamesNotifyMask;
+ XkbSelectEvents(d->native, XkbUseCoreKbd, events, events);
+ refresh_keyboard(d);
+ }
+ int xi_event_base, xi_error_base;
+ int major = 2, minor = 0;
+ d->xi2 = XQueryExtension(d->native, "XInputExtension", &d->xi_opcode,
+ &xi_event_base, &xi_error_base) &&
+ XIQueryVersion(d->native, &major, &minor) == Success && major >= 2;
+ if (d->xi2) {
+ d->abs_pressure = XInternAtom(d->native, "Abs Pressure", False);
+ d->abs_tilt_x = XInternAtom(d->native, "Abs Tilt X", False);
+ d->abs_tilt_y = XInternAtom(d->native, "Abs Tilt Y", False);
+ d->tool_type = XInternAtom(d->native, "Wacom Tool Type", False);
+ d->eraser = XInternAtom(d->native, "ERASER", False);
+ unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0};
+ XISetMask(bits, XI_HierarchyChanged);
+ XISetMask(bits, XI_DeviceChanged);
+ XIEventMask mask = {XIAllDevices, sizeof(bits), bits};
+ XISelectEvents(d->native, RootWindow(d->native, d->screen), &mask, 1);
+ }
+ if (!loop) {
+ d->loop = yasl_loop_create();
+ d->owns_loop = true;
+ } else {
+ d->loop = loop;
+ }
+ d->base.loop = d->loop;
+ d->app_id = strdup(spec && spec->app_id ? spec->app_id : "YASL");
+ if (!d->loop || !d->app_id) {
+ if (d->owns_loop) yasl_loop_destroy(d->loop);
+ free(d->app_id);
+ XCloseDisplay(d->native);
+ free(d);
+ return NULL;
+ }
+ d->im = XOpenIM(d->native, NULL, NULL, NULL);
+ d->selection_window = XCreateSimpleWindow(d->native,
+ RootWindow(d->native, d->screen), 0, 0, 1, 1, 0, 0, 0);
+#define ATOM(field, name) d->field = XInternAtom(d->native, name, False)
+ ATOM(protocols, "WM_PROTOCOLS");
+ ATOM(delete_window, "WM_DELETE_WINDOW");
+ ATOM(utf8, "UTF8_STRING");
+ ATOM(clipboard, "CLIPBOARD");
+ ATOM(targets, "TARGETS");
+ ATOM(paste_property, "YASL_SELECTION");
+ ATOM(incr, "INCR");
+ ATOM(net_name, "_NET_WM_NAME");
+ ATOM(net_state, "_NET_WM_STATE");
+ ATOM(fullscreen, "_NET_WM_STATE_FULLSCREEN");
+ ATOM(maximized_h, "_NET_WM_STATE_MAXIMIZED_HORZ");
+ ATOM(maximized_v, "_NET_WM_STATE_MAXIMIZED_VERT");
+ ATOM(xdnd_aware, "XdndAware");
+ ATOM(xdnd_enter, "XdndEnter");
+ ATOM(xdnd_position, "XdndPosition");
+ ATOM(xdnd_status, "XdndStatus");
+ ATOM(xdnd_leave, "XdndLeave");
+ ATOM(xdnd_drop, "XdndDrop");
+ ATOM(xdnd_finished, "XdndFinished");
+ ATOM(xdnd_selection, "XdndSelection");
+ ATOM(xdnd_action_copy, "XdndActionCopy");
+ ATOM(xdnd_type_list, "XdndTypeList");
+ ATOM(uri_list, "text/uri-list");
+#undef ATOM
+ if (d->loop) {
+ d->watch = yasl_loop_add_fd(d->loop, ConnectionNumber(d->native), POLLIN, ready, &d->base);
+ if (d->watch < 0) {
+ if (d->im) XCloseIM(d->im);
+ XDestroyWindow(d->native, d->selection_window);
+ XCloseDisplay(d->native);
+ free(d->app_id);
+ if (d->owns_loop) yasl_loop_destroy(d->loop);
+ free(d);
+ return NULL;
+ }
+ }
+ return &d->base;
+}
+
+static void display_close(struct yasl_display *base)
+{
+ struct x11_display *d = (struct x11_display *)base;
+ assert(!d->surfaces && "destroy every surface before its display");
+ drag_cancel(base);
+ cancel_pastes(d, NULL);
+ d->pending_drop.active = false;
+ if (d->loop && d->watch > 0) yasl_loop_del_fd(d->loop, d->watch);
+ if (d->owns_loop && d->loop) {
+ yasl_loop_destroy(d->loop);
+ d->loop = NULL;
+ d->base.loop = NULL;
+ }
+ if (d->im) XCloseIM(d->im);
+ clear_pointers(d);
+ for (size_t i = 0; i < sizeof(d->cursors) / sizeof(d->cursors[0]); i++) {
+ if (d->cursors[i]) {
+ XFreeCursor(d->native, d->cursors[i]);
+ d->cursors[i] = None;
+ }
+ }
+ XDestroyWindow(d->native, d->selection_window);
+ XCloseDisplay(d->native);
+ free(d->selection[0]);
+ free(d->selection[1]);
+ free(d->app_id);
+ free(d);
+}
+
+static uint32_t caps(const struct yasl_display *d)
+{
+ return YASL_CAP_POPUPS | YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
+ YASL_CAP_PRIMARY_SELECTION | YASL_CAP_CSD |
+ (((const struct x11_display *)d)->xi2 ? YASL_CAP_TABLET : 0);
+}
+
+static void copy(struct yasl_display *base, enum yasl_selection sel, const char *text)
+{
+ if (!text || (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY)) return;
+ struct x11_display *d = (struct x11_display *)base;
+ char *value = strdup(text);
+ if (!value) return;
+ free(d->selection[sel]);
+ d->selection[sel] = value;
+ XSetSelectionOwner(d->native, sel == YASL_PRIMARY ? XA_PRIMARY : d->clipboard,
+ d->selection_window, CurrentTime);
+ XFlush(d->native);
+}
+
+static void paste(struct yasl_display *base, enum yasl_selection sel, struct yasl_surface *target)
+{
+ if (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) return;
+ struct x11_display *d = (struct x11_display *)base;
+ struct x11_surface *s = (struct x11_surface *)target;
+ Atom selection = sel == YASL_PRIMARY ? XA_PRIMARY : d->clipboard;
+ if (XGetSelectionOwner(d->native, selection) == d->selection_window) {
+ const char *text = d->selection[sel];
+ if (text) push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, text, strlen(text));
+ } else {
+ struct paste_request *r = calloc(1, sizeof(*r));
+ if (!r) return;
+ r->window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen),
+ 0, 0, 1, 1, 0, 0, 0);
+ r->target = s;
+ r->next = d->pastes;
+ d->pastes = r;
+ XConvertSelection(d->native, selection, d->utf8, d->paste_property,
+ r->window, CurrentTime);
+ XFlush(d->native);
+ }
+}
+
+static bool input_poll(struct yasl_surface *base, struct yasl_input_event *event)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ return !s->destroyed && input_queue_pop(&s->input, event);
+}
+
+static void schedule_redraw(struct yasl_surface *base)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (!s->destroyed) {
+ s->want_draw = true;
+ struct x11_display *d = display_of(base);
+ if (d && d->loop)
+ yasl_loop_kick(d->loop);
+ }
+}
+
+static struct yasl_display *surface_display(const struct yasl_surface *s) { return s->display; }
+static struct yasl_geometry geometry(const struct yasl_surface *s)
+{
+ return (struct yasl_geometry){((const struct x11_surface *)s)->size, YASL_SCALE_UNIT};
+}
+static uint32_t state(const struct yasl_surface *s) { return ((const struct x11_surface *)s)->state; }
+static int mods(const struct yasl_display *d) { return (int)((const struct x11_display *)d)->mods; }
+static void *native_display(struct yasl_display *d) { return ((struct x11_display *)d)->native; }
+static void *native_surface(struct yasl_surface *s) { return (void *)(uintptr_t)((struct x11_surface *)s)->window; }
+
+static struct yasl_timing timing(const struct yasl_surface *base)
+{
+ const struct x11_surface *s = (const struct x11_surface *)base;
+ struct yasl_timing t = {.refresh_ns = s->clock.refresh_ns,
+ .last_present_ns = s->clock.last_present_ns,
+ .next_present_ns = yasl_clock_next_present_ns(&s->clock),
+ .source = s->clock.source};
+ yasl_clock_resize_stats(&s->clock, &t.resize);
+ return t;
+}
+
+static uint64_t frame_time(struct yasl_surface *s, uint64_t now)
+{
+ return yasl_clock_frame_timestamp_ns(&((struct x11_surface *)s)->clock, now);
+}
+
+static void cursor(struct yasl_surface *base, enum yasl_cursor cursor)
+{
+ static const unsigned shapes[] = {XC_left_ptr, XC_xterm, XC_hand2, XC_crosshair,
+ XC_fleur, XC_bottom_right_corner, XC_bottom_left_corner,
+ XC_sb_h_double_arrow, XC_sb_v_double_arrow};
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (s->destroyed || (unsigned)cursor > YASL_CURSOR_HIDDEN) return;
+ struct x11_display *d = display_of(base);
+ if (!d->native) return;
+
+ if (!d->cursors[cursor]) {
+ if (cursor == YASL_CURSOR_HIDDEN) {
+ char bits[] = {0};
+ Pixmap pixmap = XCreateBitmapFromData(d->native, s->window, bits, 1, 1);
+ XColor color = {0};
+ d->cursors[cursor] = XCreatePixmapCursor(d->native, pixmap, pixmap, &color, &color, 0, 0);
+ XFreePixmap(d->native, pixmap);
+ } else if ((unsigned)cursor < sizeof(shapes) / sizeof(shapes[0])) {
+ d->cursors[cursor] = XCreateFontCursor(d->native, shapes[cursor]);
+ }
+ }
+ if (d->cursors[cursor])
+ XDefineCursor(d->native, s->window, d->cursors[cursor]);
+}
+
+static bool place(struct yasl_surface *base, struct yasl_vector2i at)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ struct x11_display *d = display_of(base);
+ Window root, child;
+ int root_x, root_y, x, y;
+ unsigned mask;
+ if (s->destroyed || !d->native || !XQueryPointer(d->native, s->window,
+ &root, &child, &root_x, &root_y, &x, &y, &mask)) return false;
+ XMoveWindow(d->native, s->window, root_x - at.x, root_y - at.y);
+ return true;
+}
+
+static void drag_end(struct x11_display *d, enum yasl_drag_end how)
+{
+ if (!d || !d->drag.active) return;
+ struct yasl_drag_listener l = d->drag.listener;
+ void *ud = d->drag.ud;
+ struct x11_surface *hover = d->drag.hover;
+ struct yasl_vector2i hover_pos = d->drag.hover_pos;
+
+ d->drag.active = false;
+ d->drag.listener = (struct yasl_drag_listener){0};
+ d->drag.hover = NULL;
+ d->drag.origin = NULL;
+ d->drag.carried = NULL;
+
+ if (d->drag.icon_window && d->native) {
+ XDestroyWindow(d->native, d->drag.icon_window);
+ d->drag.icon_window = None;
+ }
+ if (d->drag.icon_pixmap && d->native) {
+ XFreePixmap(d->native, d->drag.icon_pixmap);
+ d->drag.icon_pixmap = None;
+ }
+
+ if (hover && !hover->destroyed && hover->cbs && hover->cbs->drag) {
+ if (how == YASL_DRAG_DROPPED)
+ hover->cbs->drag(&hover->base, YASL_DRAG_DROP, hover_pos, hover->ud);
+ else
+ hover->cbs->drag(&hover->base, YASL_DRAG_LEAVE, hover_pos, hover->ud);
+ }
+
+ if (l.end)
+ l.end(ud, how);
+}
+
+static bool drag_start(struct yasl_display *base, struct yasl_surface *origin,
+ const struct yasl_drag_image *image,
+ const struct yasl_drag_listener *l, void *ud)
+{
+ struct x11_display *d = (struct x11_display *)base;
+ if (!d || !origin || d->drag.active || !l)
+ return false;
+ d->drag.active = true;
+ d->drag.origin = (struct x11_surface *)origin;
+ d->drag.carried = NULL;
+ d->drag.hover = NULL;
+ d->drag.listener = *l;
+ d->drag.ud = ud;
+ d->drag.icon_window = None;
+ d->drag.icon_pixmap = None;
+
+ if (image && image->pixels && image->size.x > 0 && image->size.y > 0) {
+ d->drag.hotspot = image->hotspot;
+ int rx = 0, ry = 0;
+ if (d->native) {
+ Window root, child;
+ int wx, wy;
+ unsigned mask;
+ XQueryPointer(d->native, RootWindow(d->native, d->screen),
+ &root, &child, &rx, &ry, &wx, &wy, &mask);
+ int x = rx - image->hotspot.x;
+ int y = ry - image->hotspot.y;
+ XSetWindowAttributes attrs = {
+ .override_redirect = True,
+ .save_under = True,
+ };
+ d->drag.icon_window = XCreateWindow(d->native, RootWindow(d->native, d->screen),
+ x, y, (unsigned)image->size.x, (unsigned)image->size.y, 0,
+ CopyFromParent, InputOutput, CopyFromParent,
+ CWOverrideRedirect | CWSaveUnder, &attrs);
+ if (d->drag.icon_window) {
+ GC gc = XCreateGC(d->native, d->drag.icon_window, 0, NULL);
+ XImage *im = XCreateImage(d->native, DefaultVisual(d->native, d->screen),
+ (unsigned)DefaultDepth(d->native, d->screen), ZPixmap, 0, NULL,
+ (unsigned)image->size.x, (unsigned)image->size.y, 32, 0);
+ if (gc && im) {
+ im->data = malloc((size_t)im->bytes_per_line * (size_t)image->size.y);
+ if (im->data) {
+ bool direct = im->bits_per_pixel == 32 && im->red_mask == 0xff0000 &&
+ im->green_mask == 0xff00 && im->blue_mask == 0xff;
+ for (int r = 0; r < image->size.y; r++) {
+ const uint32_t *row = image->pixels + (size_t)r * image->size.x;
+ if (direct) memcpy(im->data + (size_t)r * im->bytes_per_line, row, (size_t)image->size.x * 4);
+ else for (int c = 0; c < image->size.x; c++) {
+ uint32_t p = row[c];
+ XPutPixel(im, c, r, component((p >> 16) & 255, im->red_mask) |
+ component((p >> 8) & 255, im->green_mask) | component(p & 255, im->blue_mask));
+ }
+ }
+ d->drag.icon_pixmap = XCreatePixmap(d->native, RootWindow(d->native, d->screen),
+ (unsigned)image->size.x, (unsigned)image->size.y,
+ (unsigned)DefaultDepth(d->native, d->screen));
+ if (d->drag.icon_pixmap) {
+ XPutImage(d->native, d->drag.icon_pixmap, gc, im, 0, 0, 0, 0,
+ (unsigned)image->size.x, (unsigned)image->size.y);
+ XSetWindowBackgroundPixmap(d->native, d->drag.icon_window, d->drag.icon_pixmap);
+ }
+ }
+ }
+ if (im) XDestroyImage(im);
+ if (gc) XFreeGC(d->native, gc);
+ XMapRaised(d->native, d->drag.icon_window);
+ XFlush(d->native);
+ }
+ }
+ }
+ return true;
+}
+
+static void drag_cancel(struct yasl_display *base)
+{
+ drag_end((struct x11_display *)base, YASL_DRAG_CANCELLED);
+}
+
+static bool drag_carry(struct yasl_surface *base, struct yasl_vector2i offset)
+{
+ struct x11_display *d = display_of(base);
+ if (!d || !d->drag.active)
+ return false;
+ if (d->drag.icon_window && d->native) {
+ XDestroyWindow(d->native, d->drag.icon_window);
+ d->drag.icon_window = None;
+ }
+ if (d->drag.icon_pixmap && d->native) {
+ XFreePixmap(d->native, d->drag.icon_pixmap);
+ d->drag.icon_pixmap = None;
+ }
+ d->drag.carried = (struct x11_surface *)base;
+ d->drag.carry_offset = offset;
+ return d->native ? place(base, offset) : true;
+}
+
+static bool interactive_move(struct yasl_surface *base)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ struct x11_display *d = display_of(base);
+ Window root, child;
+ int rx, ry, x, y;
+ unsigned mask;
+ if (s->destroyed || !d->native || !XQueryPointer(d->native, s->window,
+ &root, &child, &rx, &ry, &x, &y, &mask)) return false;
+ XUngrabPointer(d->native, CurrentTime);
+ net_message(s, XInternAtom(d->native, "_NET_WM_MOVERESIZE", False), rx, ry, 8, 1, 1);
+ return true;
+}
+
+static bool interactive_resize(struct yasl_surface *base, enum yasl_resize_edge edge)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ struct x11_display *d = display_of(base);
+ Window root, child;
+ int rx, ry, x, y;
+ unsigned mask;
+ if (s->destroyed || !d->native || !XQueryPointer(d->native, s->window,
+ &root, &child, &rx, &ry, &x, &y, &mask)) return false;
+
+ int dir;
+ switch (edge) {
+ case YASL_EDGE_TOP_LEFT: dir = 0; break;
+ case YASL_EDGE_TOP: dir = 1; break;
+ case YASL_EDGE_TOP_RIGHT: dir = 2; break;
+ case YASL_EDGE_RIGHT: dir = 3; break;
+ case YASL_EDGE_BOTTOM_RIGHT: dir = 4; break;
+ case YASL_EDGE_BOTTOM: dir = 5; break;
+ case YASL_EDGE_BOTTOM_LEFT: dir = 6; break;
+ case YASL_EDGE_LEFT: dir = 7; break;
+ default: return false;
+ }
+
+ XUngrabPointer(d->native, CurrentTime);
+ net_message(s, XInternAtom(d->native, "_NET_WM_MOVERESIZE", False), rx, ry, dir, 1, 1);
+ return true;
+}
+
+static bool show_window_menu(struct yasl_surface *base, struct yasl_vector2i pos)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ struct x11_display *d = display_of(base);
+ if (s->destroyed || !d->native) return false;
+ Atom atom = XInternAtom(d->native, "_GTK_SHOW_WINDOW_MENU", False);
+ net_message(s, atom, s->window, pos.x, pos.y, 0, 0);
+ return true;
+}
+
+static void set_ime_caret(struct yasl_surface *base, struct yasl_rect2i caret)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ if (!s) return;
+ s->ime_caret = caret;
+ if (s->ic) {
+ XPoint spot = { (short)caret.x, (short)(caret.y + caret.height) };
+ XVaNestedList preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL);
+ if (preedit_attr) {
+ XSetICValues(s->ic, XNPreeditAttributes, preedit_attr, NULL);
+ XFree(preedit_attr);
+ }
+ }
+}
+
+static const char *ime_preedit(const struct yasl_surface *base)
+{
+ const struct x11_surface *s = (const struct x11_surface *)base;
+ return s ? s->ime_preedit : NULL;
+}
+
+static bool ime_preedit_cursor(const struct yasl_surface *base, int *begin, int *end)
+{
+ const struct x11_surface *s = (const struct x11_surface *)base;
+ if (!s || !s->ime_preedit || s->ime_cursor_begin < 0)
+ return false;
+ if (begin) *begin = s->ime_cursor_begin;
+ if (end) *end = s->ime_cursor_end;
+ return true;
+}
+
+const struct yasl_backend_ops yasl_x11_backend = {
+ .type = YASL_BACKEND_X11,
+ .display_open = display_open,
+ .display_close = display_close,
+ .display_caps = caps,
+ .display_dispatch_timeout = dispatch_timeout,
+ .display_flush = flush,
+ .display_modifiers = mods,
+ .display_copy = copy,
+ .display_paste = paste,
+ .display_drag_start = drag_start,
+ .display_drag_cancel = drag_cancel,
+ .surface_create_toplevel = create_toplevel,
+ .surface_create_popup = surface_create_popup,
+ .surface_popup_reposition = surface_popup_reposition,
+ .surface_destroy = surface_destroy,
+ .surface_display = surface_display,
+ .surface_set_callbacks = set_callbacks,
+ .surface_set_title = set_title,
+ .surface_schedule_redraw = schedule_redraw,
+ .surface_present = present,
+ .surface_input_poll = input_poll,
+ .surface_geometry = geometry,
+ .surface_state = state,
+ .surface_timing = timing,
+ .surface_frame_time_ns = frame_time,
+ .surface_set_cursor = cursor,
+ .surface_drag_carry = drag_carry,
+ .surface_place_under_pointer = place,
+ .surface_start_interactive_move = interactive_move,
+ .surface_start_interactive_resize = interactive_resize,
+ .surface_show_window_menu = show_window_menu,
+ .surface_set_ime_caret = set_ime_caret,
+ .surface_ime_preedit = ime_preedit,
+ .surface_ime_preedit_cursor = ime_preedit_cursor,
+ .display_get_native_handle = native_display,
+ .surface_get_native_handle = native_surface,
+};