summaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/x11/window.c483
1 files changed, 427 insertions, 56 deletions
diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c
index 43e2109..e87f682 100644
--- a/src/platform/x11/window.c
+++ b/src/platform/x11/window.c
@@ -55,6 +55,8 @@ struct x11_surface {
Window window;
GC gc;
Colormap colormap;
+ Visual *visual;
+ int visual_depth;
XIC ic;
#if YASL_HAS_EGL
EGLContext gl_context;
@@ -91,6 +93,24 @@ struct paste_request {
Atom selection;
uint64_t id;
yasl_private_timer_id timeout;
+ /* INCR receive: chunks accumulate in buf until a zero-length chunk arrives. */
+ bool incr;
+ Atom incr_type;
+ char *buf;
+ size_t buf_len;
+};
+
+/* An INCR transfer this backend sends to a requestor. data is a private copy, so
+ * clipboard changes do not affect a transfer in progress. */
+struct incr_send {
+ struct incr_send *next;
+ struct x11_display *display;
+ Window requestor;
+ Atom property, target;
+ char *data;
+ size_t length, offset;
+ bool finishing;
+ yasl_private_timer_id timeout;
};
struct tablet_axis {
@@ -125,9 +145,18 @@ struct x11_display {
int watch;
struct x11_surface *surfaces, *dead;
int depth;
+ /* 32-bit TrueColor visual for CPU windows, when the server has one. */
+ Visual *argb_visual;
+ int argb_depth;
+ /* Device scale numerator from Xft.dpi, with 120 representing 1x. */
+ uint32_t scale_120;
XIM im;
Window selection_window;
+ /* Server time of the latest key or button event, and the time each selection was taken. */
+ Time last_time, selection_time[2];
+ struct incr_send *incr_sends;
Atom protocols, delete_window, utf8, clipboard, targets, paste_property, incr;
+ Atom timestamp, multiple, pair;
Atom net_name, net_state, fullscreen, maximized_h, maximized_v, motif_hints;
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;
@@ -210,6 +239,24 @@ static void push(struct x11_surface *s, struct yasl_private_event event,
input_queue_push(&s->input, &event);
}
+static void note_time(struct x11_display *d, Time time)
+{
+ if (time != CurrentTime) d->last_time = time;
+}
+
+/* Xft.dpi of 96 is 1x. The scale is clamped to 1x through 4x; unset is 1x. */
+static uint32_t scale_from_xft_dpi(Display *native)
+{
+ const char *value = XGetDefault(native, "Xft", "dpi");
+ char *end = NULL;
+ double dpi = value ? strtod(value, &end) : 0;
+ if (!value || end == value || !isfinite(dpi) || dpi <= 0) return YASL_PRIVATE_SCALE_UNIT;
+ double scale = dpi / 96.0 * YASL_PRIVATE_SCALE_UNIT;
+ if (scale < YASL_PRIVATE_SCALE_UNIT) scale = YASL_PRIVATE_SCALE_UNIT;
+ if (scale > 4 * YASL_PRIVATE_SCALE_UNIT) scale = 4 * YASL_PRIVATE_SCALE_UNIT;
+ return (uint32_t)lround(scale);
+}
+
static unsigned long component(uint32_t color, unsigned long mask)
{
if (unlikely(!mask)) return 0;
@@ -581,6 +628,8 @@ static void xi_pointer_event(struct x11_surface *s, const XIDeviceEvent *ev)
static void xi_event(struct x11_display *d, XGenericEventCookie *cookie)
{
+ if (cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease)
+ note_time(d, ((const XIDeviceEvent *)cookie->data)->time);
if (cookie->evtype == XI_HierarchyChanged || cookie->evtype == XI_DeviceChanged) {
if (cookie->evtype == XI_DeviceChanged &&
((XIDeviceChangedEvent *)cookie->data)->reason == XISlaveSwitch) return;
@@ -705,8 +754,8 @@ static bool resize(struct x11_surface *s, struct yasl_private_vector2i size)
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,
+ image = XShmCreateImage(d->native, s->visual,
+ (unsigned)s->visual_depth, ZPixmap,
s->shm_info.shmaddr, &s->shm_info,
(unsigned)size.x, (unsigned)size.y);
if (image) {
@@ -733,8 +782,8 @@ static bool resize(struct x11_surface *s, struct yasl_private_vector2i size)
}
}
if (!shm_ok) {
- image = XCreateImage(d->native, DefaultVisual(d->native, d->screen),
- (unsigned)DefaultDepth(d->native, d->screen), ZPixmap, 0, NULL,
+ image = XCreateImage(d->native, s->visual,
+ (unsigned)s->visual_depth, 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) {
@@ -754,7 +803,7 @@ static bool resize(struct x11_surface *s, struct yasl_private_vector2i size)
s->pixels = pixels;
s->size = size;
s->base.size = size;
- s->base.scale_120 = YASL_PRIVATE_SCALE_UNIT;
+ s->base.scale_120 = d->scale_120;
s->want_draw = true;
yasl_private_clock_note_resize(&s->clock, yasl_private_loop_now_ns() - start);
return true;
@@ -764,16 +813,18 @@ 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 && r->target == target) {
+ if (target && r->target == target && !r->incr) {
/* Keep the requestor window alive until the selection owner replies.
* Some INCR owners write to it after the original surface is gone. */
r->target = NULL;
r->id = 0;
p = &r->next;
- } else if (!target) {
+ } else if (!target || r->target == target) {
+ /* An INCR transfer in progress is abandoned; its owner times out. */
*p = r->next;
if (r->timeout) yasl_private_loop_del_timer(d->loop, r->timeout);
XDestroyWindow(d->native, r->window);
+ free(r->buf);
free(r);
} else {
p = &r->next;
@@ -795,10 +846,33 @@ static uint64_t paste_timeout(void *ud, yasl_private_timer_id id, uint64_t inter
XDestroyWindow(d->native, r->window);
if (s && !s->destroyed)
yasl_private_surface_complete_paste(&s->base, r->id, YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
+ free(r->buf);
free(r);
return 0;
}
+/* Restart the stall timeout; an INCR transfer gets a fresh allowance per chunk. */
+static void paste_rearm(struct paste_request *r)
+{
+ struct x11_display *d = r->display;
+ if (r->timeout) yasl_private_loop_del_timer(d->loop, r->timeout);
+ r->timeout = yasl_private_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, paste_timeout, r);
+}
+
+/* Unlink a paste request at *link and report its outcome to the surface, if alive. */
+static void paste_complete(struct x11_display *d, struct paste_request **link,
+ enum yasl_private_transfer_status status, const char *text, size_t len)
+{
+ struct paste_request *r = *link;
+ *link = r->next;
+ if (r->timeout) yasl_private_loop_del_timer(d->loop, r->timeout);
+ XDestroyWindow(d->native, r->window);
+ if (r->target && !r->target->destroyed)
+ yasl_private_surface_complete_paste(&r->target->base, r->id, status, text, len);
+ free(r->buf);
+ free(r);
+}
+
static void surface_destroy(struct yasl_surface *base)
{
struct x11_surface *s = (struct x11_surface *)base;
@@ -891,13 +965,16 @@ static bool surface_set_size(struct yasl_surface *base, struct yasl_private_vect
{
struct x11_surface *s = (struct x11_surface *)base;
struct x11_display *d = (struct x11_display *)base->display;
- if (s->destroyed || s->is_popup || size.x <= 0 || size.y <= 0 ||
- size.x > 32767 || size.y > 32767) {
+ /* The request is logical; the window is sized in device pixels. */
+ struct yasl_private_vector2i px = {yasl_private_scale_to_device(size.x, s->base.scale_120),
+ yasl_private_scale_to_device(size.y, s->base.scale_120)};
+ if (s->destroyed || s->is_popup || px.x <= 0 || px.y <= 0 ||
+ px.x > 32767 || px.y > 32767) {
errno = EINVAL;
return false;
}
if (!d->native || !s->window) { errno = ENODEV; return false; }
- XResizeWindow(d->native, s->window, (unsigned)size.x, (unsigned)size.y);
+ XResizeWindow(d->native, s->window, (unsigned)px.x, (unsigned)px.y);
XFlush(d->native);
return true;
}
@@ -1137,13 +1214,18 @@ static struct yasl_surface *create_toplevel_visual(struct yasl_private_display *
s->base.creation_flags = spec->flags | (spec->external_gpu ? 0u : YASL_PRIVATE_SURFACE_CPU);
s->base.presenter = presenter;
s->clock.refresh_ns = YASL_PRIVATE_REFRESH_DEFAULT_NS;
- if (!resize(s, spec->size)) { free(s); return NULL; }
+ /* Logical creation sizes become device pixels. */
+ struct yasl_private_vector2i px = {yasl_private_scale_to_device(spec->size.x, d->scale_120),
+ yasl_private_scale_to_device(spec->size.y, d->scale_120)};
if (!visual) visual = DefaultVisual(d->native, d->screen);
if (!depth) depth = DefaultDepth(d->native, d->screen);
+ s->visual = visual;
+ s->visual_depth = depth;
+ if (!resize(s, px)) { free(s); return NULL; }
s->colormap = XCreateColormap(d->native, RootWindow(d->native, d->screen), visual, AllocNone);
XSetWindowAttributes attrs = {.colormap = s->colormap, .border_pixel = 0, .background_pixel = 0};
s->window = XCreateWindow(d->native, RootWindow(d->native, d->screen), 0, 0,
- (unsigned)spec->size.x, (unsigned)spec->size.y, 0, depth, InputOutput,
+ (unsigned)px.x, (unsigned)px.y, 0, depth, InputOutput,
visual, CWBorderPixel | CWBackPixel | CWColormap, &attrs);
if (!s->window) {
XFreeColormap(d->native, s->colormap);
@@ -1153,7 +1235,7 @@ static struct yasl_surface *create_toplevel_visual(struct yasl_private_display *
}
s->base.native_handle = (void *)(uintptr_t)s->window;
s->base.size = s->size;
- s->base.scale_120 = YASL_PRIVATE_SCALE_UNIT;
+ s->base.scale_120 = d->scale_120;
s->gc = XCreateGC(d->native, s->window, 0, NULL);
XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
@@ -1179,8 +1261,8 @@ static struct yasl_surface *create_toplevel_visual(struct yasl_private_display *
}
if (spec->flags & YASL_PRIVATE_SURFACE_FIXED_SIZE) {
XSizeHints size_hints = {.flags = PMinSize | PMaxSize,
- .min_width = spec->size.x, .max_width = spec->size.x,
- .min_height = spec->size.y, .max_height = spec->size.y};
+ .min_width = px.x, .max_width = px.x,
+ .min_height = px.y, .max_height = px.y};
XSetWMNormalHints(d->native, s->window, &size_hints);
}
XClassHint hint = { .res_name = d->app_id, .res_class = d->app_id };
@@ -1207,7 +1289,11 @@ static struct yasl_surface *create_toplevel(struct yasl_private_display *base,
{
if (!spec) return NULL;
if (graphics_device) { errno = ENOTSUP; return NULL; }
- return create_toplevel_visual(base, spec, NULL, 0,
+ struct x11_display *d = (struct x11_display *)base;
+ /* CPU windows keep alpha when the server has a 32-bit visual. */
+ Visual *visual = !spec->external_gpu && d->argb_visual ? d->argb_visual : NULL;
+ int depth = visual ? d->argb_depth : 0;
+ return create_toplevel_visual(base, spec, visual, depth,
spec->external_gpu ? YASL_PRIVATE_PRESENTER_NONE : YASL_PRIVATE_PRESENTER_CPU);
}
@@ -1581,10 +1667,19 @@ static struct yasl_surface *surface_create_popup(struct yasl_surface *parent_bas
s->base.creation_flags = parent_base->creation_flags;
s->base.presenter = (parent_base->creation_flags & YASL_PRIVATE_SURFACE_CPU) ?
YASL_PRIVATE_PRESENTER_CPU : YASL_PRIVATE_PRESENTER_NONE;
+ s->base.scale_120 = d->scale_120;
s->parent = parent;
s->is_popup = true;
s->content_offset = spec->content_offset;
s->clock.refresh_ns = parent->clock.refresh_ns;
+ bool argb = s->base.presenter == YASL_PRIVATE_PRESENTER_CPU && d->argb_visual;
+ if (argb) {
+ s->visual = d->argb_visual;
+ s->visual_depth = d->argb_depth;
+ } else if (d->native) {
+ s->visual = DefaultVisual(d->native, d->screen);
+ s->visual_depth = DefaultDepth(d->native, d->screen);
+ }
int root_x = 0, root_y = 0;
struct yasl_private_vector2i actual_size;
popup_calculate_position(d, parent, spec, &root_x, &root_y, &actual_size);
@@ -1595,17 +1690,28 @@ static struct yasl_surface *surface_create_popup(struct yasl_surface *parent_bas
.override_redirect = True,
.save_under = True,
};
+ unsigned long mask = CWOverrideRedirect | CWSaveUnder;
+ Visual *visual = CopyFromParent;
+ int depth = CopyFromParent;
+ if (argb) {
+ s->colormap = XCreateColormap(d->native, RootWindow(d->native, d->screen),
+ s->visual, AllocNone);
+ attrs.colormap = s->colormap;
+ attrs.border_pixel = 0;
+ attrs.background_pixel = 0;
+ mask |= CWColormap | CWBorderPixel | CWBackPixel;
+ visual = s->visual;
+ depth = s->visual_depth;
+ }
s->window = XCreateWindow(d->native, RootWindow(d->native, d->screen),
root_x, root_y, (unsigned)actual_size.x, (unsigned)actual_size.y,
- 0, CopyFromParent, InputOutput, CopyFromParent,
- CWOverrideRedirect | CWSaveUnder, &attrs);
+ 0, depth, InputOutput, visual, mask, &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_PRIVATE_SCALE_UNIT;
s->gc = XCreateGC(d->native, s->window, 0, NULL);
XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
@@ -1654,7 +1760,7 @@ static bool surface_popup_reposition(struct yasl_surface *popup_base,
}
if (changed_size)
yasl_private_surface_notify_configure(&s->base,
- (struct yasl_private_geometry){actual_size, YASL_PRIVATE_SCALE_UNIT});
+ (struct yasl_private_geometry){actual_size, s->base.scale_120});
schedule_redraw(&s->base);
return true;
}
@@ -1684,36 +1790,221 @@ static void update_state(struct x11_surface *s)
yasl_private_surface_notify_state(&s->base, state);
}
+/* Largest payload written in one property; bigger ones go out as INCR chunks. */
+static size_t incr_chunk_limit(const struct x11_display *d)
+{
+ return (size_t)XMaxRequestSize(d->native) * 4 - 128;
+}
+
+enum convert_status { CONVERT_REFUSED, CONVERT_WRITTEN, CONVERT_INCR };
+
+/* Unlink and free an INCR transfer. Cancel its timer unless the timer itself is calling. */
+static void incr_send_retire(struct x11_display *d, struct incr_send *st, bool cancel_timer)
+{
+ for (struct incr_send **p = &d->incr_sends; *p; p = &(*p)->next) {
+ if (*p != st) continue;
+ *p = st->next;
+ break;
+ }
+ if (cancel_timer && st->timeout) yasl_private_loop_del_timer(d->loop, st->timeout);
+ free(st->data);
+ free(st);
+}
+
+static uint64_t incr_send_timeout(void *ud, yasl_private_timer_id id, uint64_t interval_ns)
+{
+ (void)id;
+ (void)interval_ns;
+ struct incr_send *st = ud;
+ incr_send_retire(st->display, st, false);
+ return 0;
+}
+
+/* Restart the stall timeout; each chunk gets a fresh allowance. */
+static void incr_send_arm(struct incr_send *st)
+{
+ struct x11_display *d = st->display;
+ if (st->timeout) yasl_private_loop_del_timer(d->loop, st->timeout);
+ st->timeout = yasl_private_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, incr_send_timeout, st);
+}
+
+/* Start an INCR send: publish the total size, then wait for the requestor to
+ * delete the property before each chunk. On failure data stays with the caller. */
+static bool incr_send_start(struct x11_display *d, Window requestor, Atom property,
+ Atom target, char *data, size_t length)
+{
+ struct incr_send *st = calloc(1, sizeof(*st));
+ if (!st) return false;
+ st->display = d;
+ st->requestor = requestor;
+ st->property = property;
+ st->target = target;
+ st->data = data;
+ st->length = length;
+ st->timeout = yasl_private_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, incr_send_timeout, st);
+ if (!st->timeout) {
+ free(st);
+ return false;
+ }
+ /* The requestor's property deletions arrive as PropertyNotify events. */
+ XSelectInput(d->native, requestor, PropertyChangeMask);
+ unsigned long total = length;
+ XChangeProperty(d->native, requestor, property, d->incr, 32, PropModeReplace,
+ (const unsigned char *)&total, 1);
+ st->next = d->incr_sends;
+ d->incr_sends = st;
+ return true;
+}
+
+/* Each requestor deletion asks for the next chunk. After the last data chunk, a
+ * zero-length chunk ends the transfer, and its deletion finishes it. */
+static void incr_send_step(struct incr_send *st)
+{
+ struct x11_display *d = st->display;
+ if (st->finishing) {
+ incr_send_retire(d, st, true);
+ return;
+ }
+ if (st->offset >= st->length) {
+ XChangeProperty(d->native, st->requestor, st->property, st->target, 8,
+ PropModeReplace, (const unsigned char *)"", 0);
+ st->finishing = true;
+ } else {
+ size_t n = st->length - st->offset;
+ if (n > incr_chunk_limit(d)) n = incr_chunk_limit(d);
+ XChangeProperty(d->native, st->requestor, st->property, st->target, 8,
+ PropModeReplace, (const unsigned char *)st->data + st->offset, (int)n);
+ st->offset += n;
+ }
+ incr_send_arm(st);
+ XFlush(d->native);
+}
+
+/* Returns true when a deletion belongs to an INCR transfer. */
+static bool incr_send_property_deleted(struct x11_display *d, const XPropertyEvent *ev)
+{
+ for (struct incr_send *st = d->incr_sends; st; st = st->next) {
+ if (st->requestor != ev->window || st->property != ev->atom) continue;
+ incr_send_step(st);
+ return true;
+ }
+ return false;
+}
+
+static void incr_cancel_all(struct x11_display *d)
+{
+ while (d->incr_sends) incr_send_retire(d, d->incr_sends, true);
+}
+
+/* Write the selection's value for target to property on requestor. allow_incr is
+ * false inside MULTIPLE, where a payload that needs INCR is refused. */
+static enum convert_status convert_selection(struct x11_display *d, int slot, Atom target,
+ Window requestor, Atom property, bool allow_incr)
+{
+ const char *text = d->selection[slot];
+ if (!text) return CONVERT_REFUSED;
+ if (target == d->targets) {
+ size_t latin1_len = 0;
+ char *latin1 = yasl_private_utf8_to_latin1(text, &latin1_len);
+ Atom list[5];
+ int count = 0;
+ list[count++] = d->targets;
+ list[count++] = d->utf8;
+ if (latin1) list[count++] = XA_STRING;
+ list[count++] = d->timestamp;
+ list[count++] = d->multiple;
+ free(latin1);
+ XChangeProperty(d->native, requestor, property, XA_ATOM, 32, PropModeReplace,
+ (const unsigned char *)list, count);
+ return CONVERT_WRITTEN;
+ }
+ if (target == d->timestamp) {
+ Time when = d->selection_time[slot];
+ XChangeProperty(d->native, requestor, property, XA_INTEGER, 32, PropModeReplace,
+ (const unsigned char *)&when, 1);
+ return CONVERT_WRITTEN;
+ }
+ if (target == d->multiple) {
+ Atom actual = None;
+ int format = 0;
+ unsigned long count = 0, left = 0;
+ unsigned char *pairs = NULL;
+ if (XGetWindowProperty(d->native, requestor, property, 0, 0x10000, False, d->pair,
+ &actual, &format, &count, &left, &pairs) != Success || !pairs ||
+ actual != d->pair || format != 32 || count < 2 || (count & 1) || left) {
+ if (pairs) XFree(pairs);
+ return CONVERT_REFUSED;
+ }
+ Atom *entries = (Atom *)pairs;
+ for (unsigned long i = 0; i + 1 < count; i += 2) {
+ Atom sub_target = entries[i];
+ Atom sub_property = entries[i + 1];
+ bool ok = sub_target != d->multiple && sub_property != None && sub_property != property &&
+ convert_selection(d, slot, sub_target, requestor, sub_property, false) == CONVERT_WRITTEN;
+ if (!ok) entries[i + 1] = None;
+ }
+ XChangeProperty(d->native, requestor, property, d->pair, 32, PropModeReplace,
+ pairs, (int)count);
+ XFree(pairs);
+ return CONVERT_WRITTEN;
+ }
+ if (target != d->utf8 && target != XA_STRING) return CONVERT_REFUSED;
+ const char *payload = text;
+ size_t len = strlen(text);
+ char *data = NULL;
+ if (target == XA_STRING) {
+ data = yasl_private_utf8_to_latin1(text, &len);
+ if (!data) return CONVERT_REFUSED;
+ payload = data;
+ }
+ if (len <= incr_chunk_limit(d)) {
+ XChangeProperty(d->native, requestor, property, target, 8, PropModeReplace,
+ (const unsigned char *)payload, (int)len);
+ free(data);
+ return CONVERT_WRITTEN;
+ }
+ if (!allow_incr) {
+ free(data);
+ return CONVERT_REFUSED;
+ }
+ if (!data) {
+ data = malloc(len);
+ if (!data) return CONVERT_REFUSED;
+ memcpy(data, text, len);
+ }
+ if (incr_send_start(d, requestor, property, target, data, len)) return CONVERT_INCR;
+ free(data);
+ return CONVERT_REFUSED;
+}
+
static void selection_request(struct x11_display *d, XSelectionRequestEvent *request)
{
+ int slot = request->selection == XA_PRIMARY ? YASL_PRIVATE_PRIMARY :
+ request->selection == d->clipboard ? YASL_PRIVATE_CLIPBOARD : -1;
+ /* Obsolete requestors pass property None and expect the target name. */
+ Atom property = request->property ? request->property : request->target;
+ enum convert_status status = slot < 0 ? CONVERT_REFUSED :
+ convert_selection(d, slot, request->target, request->requestor, property, true);
XEvent reply = {0};
reply.xselection = (XSelectionEvent){
.type = SelectionNotify, .display = d->native,
.requestor = request->requestor, .selection = request->selection,
.target = request->target, .time = request->time,
+ .property = status == CONVERT_REFUSED ? None : property,
};
- Atom property = request->property ? request->property : request->target;
- int slot = request->selection == XA_PRIMARY ? YASL_PRIVATE_PRIMARY : YASL_PRIVATE_CLIPBOARD;
- const char *text = d->selection[slot];
- size_t latin1_len = 0;
- char *latin1 = text ? yasl_private_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);
+ XFlush(d->native);
+}
+
+/* Another client took a selection this display owned. */
+static void selection_clear(struct x11_display *d, const XSelectionClearEvent *event)
+{
+ if (event->window != d->selection_window) return;
+ int slot = event->selection == XA_PRIMARY ? YASL_PRIVATE_PRIMARY :
+ event->selection == d->clipboard ? YASL_PRIVATE_CLIPBOARD : -1;
+ if (slot < 0) return;
+ free(d->selection[slot]);
+ d->selection[slot] = NULL;
}
static char *parse_file_uris(const char *data, size_t len, size_t *out_len)
@@ -1830,9 +2121,7 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
free(r);
return;
}
- /* Probe without deleting first. Deleting an INCR marker acknowledges it
- * and invites the owner to write more properties; this backend rejects
- * streaming, so leave it intact and retain a tombstone until timeout. */
+ /* Probe without deleting first; the type tells an INCR marker apart. */
Atom probe_type = None;
int probe_format = 0;
unsigned long probe_count = 0, probe_left = 0;
@@ -1842,10 +2131,12 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
&probe_count, &probe_left, &probe_data);
if (probe_data) XFree(probe_data);
if (probe_result == Success && probe_type == d->incr) {
- yasl_private_surface_complete_paste(&s->base, r->id,
- YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
- r->target = NULL;
- r->id = 0;
+ /* Deleting the marker tells the owner to send the first chunk. The
+ * request stays queued until a zero-length chunk ends the transfer. */
+ XDeleteProperty(d->native, r->window, event->property);
+ r->incr = true;
+ paste_rearm(r);
+ XFlush(d->native);
return;
}
*p = r->next;
@@ -1868,11 +2159,6 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
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;
@@ -1914,6 +2200,63 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
free(r);
}
+/* Consume a PropertyNotify on an INCR paste window. Returns false for other windows. */
+static bool paste_incr_property(struct x11_display *d, const XPropertyEvent *ev)
+{
+ struct paste_request **link = &d->pastes;
+ while (*link && !((*link)->incr && (*link)->window == ev->window)) link = &(*link)->next;
+ if (!*link) return false;
+ if (ev->state != PropertyNewValue || ev->atom != d->paste_property) return true;
+ struct paste_request *r = *link;
+ Atom actual = None;
+ int format = 0;
+ unsigned long count = 0, left = 0;
+ unsigned char *data = NULL;
+ /* Reading and deleting the chunk lets the owner write the next one. */
+ int result = XGetWindowProperty(d->native, r->window, ev->atom, 0, TEXT_LIMIT / 4, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data);
+ bool valid = result == Success && actual != None && format == 8 && left == 0 &&
+ (actual == d->utf8 || actual == XA_STRING) && count <= TEXT_LIMIT - r->buf_len;
+ if (!valid) {
+ if (data) XFree(data);
+ paste_complete(d, link, YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
+ return true;
+ }
+ if (count == 0) {
+ /* A zero-length chunk ends the transfer. */
+ if (data) XFree(data);
+ if (r->buf && r->incr_type == XA_STRING) {
+ size_t utf8_len = 0;
+ char *utf8 = yasl_private_utf8_from_latin1(r->buf, r->buf_len, &utf8_len);
+ free(r->buf);
+ r->buf = utf8;
+ r->buf_len = utf8_len;
+ if (!utf8) {
+ paste_complete(d, link, YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
+ return true;
+ }
+ }
+ bool text_ok = !r->buf || yasl_private_utf8_validate(r->buf, r->buf_len);
+ if (text_ok) paste_complete(d, link, YASL_PRIVATE_TRANSFER_OK, r->buf ? r->buf : "", r->buf_len);
+ else paste_complete(d, link, YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
+ return true;
+ }
+ if (!r->incr_type) r->incr_type = actual;
+ char *grown = realloc(r->buf, r->buf_len + count + 1);
+ if (!grown) {
+ XFree(data);
+ paste_complete(d, link, YASL_PRIVATE_TRANSFER_FAILED, NULL, 0);
+ return true;
+ }
+ r->buf = grown;
+ memcpy(r->buf + r->buf_len, data, count);
+ r->buf_len += count;
+ r->buf[r->buf_len] = '\0';
+ XFree(data);
+ paste_rearm(r);
+ return true;
+}
+
static void refresh_keyboard(struct x11_display *d)
{
memset(d->keys, 0, sizeof(d->keys));
@@ -2126,11 +2469,24 @@ static void dispatch(struct yasl_private_display *base)
refresh_keyboard(d);
continue;
}
+ switch (event.type) {
+ case KeyPress: case KeyRelease: note_time(d, event.xkey.time); break;
+ case ButtonPress: case ButtonRelease: note_time(d, event.xbutton.time); break;
+ }
if (XFilterEvent(&event, None)) continue;
if (event.type == SelectionRequest) {
selection_request(d, &event.xselectionrequest);
continue;
}
+ if (event.type == SelectionClear) {
+ selection_clear(d, &event.xselectionclear);
+ continue;
+ }
+ if (event.type == PropertyNotify) {
+ if (event.xproperty.state == PropertyDelete && incr_send_property_deleted(d, &event.xproperty))
+ continue;
+ if (paste_incr_property(d, &event.xproperty)) continue;
+ }
if (event.type == SelectionNotify) {
if (event.xselection.selection == d->xdnd_selection) {
xdnd_selection_notify(d, &event.xselection);
@@ -2149,7 +2505,7 @@ static void dispatch(struct yasl_private_display *base)
if (size.x == s->size.x && size.y == s->size.y) break;
if (resize(s, size))
yasl_private_surface_notify_configure(&s->base,
- (struct yasl_private_geometry){size, YASL_PRIVATE_SCALE_UNIT});
+ (struct yasl_private_geometry){size, s->base.scale_120});
break;
}
case ClientMessage:
@@ -2411,6 +2767,12 @@ static struct yasl_private_display *display_open(struct yasl_private_loop *loop,
XSetErrorHandler(yasl_private_x11_error_handler);
d->base.native_handle = d->native;
d->screen = DefaultScreen(d->native);
+ d->scale_120 = scale_from_xft_dpi(d->native);
+ XVisualInfo argb;
+ if (XMatchVisualInfo(d->native, d->screen, 32, TrueColor, &argb)) {
+ d->argb_visual = argb.visual;
+ d->argb_depth = argb.depth;
+ }
d->has_shm = XShmQueryExtension(d->native);
int xkb_opcode = 0, xkb_event = 0, xkb_error = 0;
int xkb_major = XkbMajorVersion, xkb_minor = XkbMinorVersion;
@@ -2472,6 +2834,9 @@ static struct yasl_private_display *display_open(struct yasl_private_loop *loop,
ATOM(targets, "TARGETS");
ATOM(paste_property, "YASL_PRIVATE_SELECTION");
ATOM(incr, "INCR");
+ ATOM(timestamp, "TIMESTAMP");
+ ATOM(multiple, "MULTIPLE");
+ ATOM(pair, "ATOM_PAIR");
ATOM(net_name, "_NET_WM_NAME");
ATOM(net_state, "_NET_WM_STATE");
ATOM(motif_hints, "_MOTIF_WM_HINTS");
@@ -2511,6 +2876,7 @@ static void display_close(struct yasl_private_display *base)
assert(!d->surfaces && "destroy every surface before its display");
drag_cancel(base);
cancel_pastes(d, NULL);
+ incr_cancel_all(d);
d->pending_drop.active = false;
if (d->loop && d->watch > 0) yasl_private_loop_del_fd(d->loop, d->watch);
if (d->owns_loop && d->loop) {
@@ -2555,8 +2921,10 @@ static bool copy_text(struct yasl_private_display *base, enum yasl_private_selec
if (!value) { errno = ENOMEM; return false; }
free(d->selection[sel]);
d->selection[sel] = value;
+ /* Take ownership at the latest key or button time; TIMESTAMP reports it. */
+ d->selection_time[sel] = d->last_time;
XSetSelectionOwner(d->native, sel == YASL_PRIVATE_PRIMARY ? XA_PRIMARY : d->clipboard,
- d->selection_window, CurrentTime);
+ d->selection_window, d->last_time);
XFlush(d->native);
return true;
}
@@ -2590,6 +2958,8 @@ static bool request_paste(struct yasl_private_display *base, enum yasl_private_s
r->window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen),
0, 0, 1, 1, 0, 0, 0);
if (!r->window) { free(r); errno = EIO; return false; }
+ /* INCR chunks arrive as PropertyNotify events on this window. */
+ XSelectInput(d->native, r->window, PropertyChangeMask);
r->display = d;
r->target = s;
r->selection = selection;
@@ -2631,7 +3001,8 @@ static void schedule_redraw(struct yasl_surface *base)
static struct yasl_private_display *surface_display(const struct yasl_surface *s) { return s->display; }
static struct yasl_private_geometry geometry(const struct yasl_surface *s)
{
- return (struct yasl_private_geometry){((const struct x11_surface *)s)->size, YASL_PRIVATE_SCALE_UNIT};
+ const struct x11_surface *x = (const struct x11_surface *)s;
+ return (struct yasl_private_geometry){x->size, x->base.scale_120};
}
static int mods(const struct yasl_private_display *d) { return (int)((const struct x11_display *)d)->mods; }
static void *native_display(struct yasl_private_display *d) { return ((struct x11_display *)d)->native; }