summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 00:09:38 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 00:09:38 +0300
commit624d1371304a5ae4b3429ad5e4267aac7cd03dfd (patch)
tree32de069f6eec83686521fc4bf39cc9874b123e93
parente813252e685d761235ad45e170d97c9375f51958 (diff)
missing impls and hardening
-rw-r--r--README.md47
-rw-r--r--include/yasl/timing.h10
-rw-r--r--include/yasl/yasl.h64
-rw-r--r--src/cocoa/window.m67
-rw-r--r--src/common/backend.c57
-rw-r--r--src/common/backend.h9
-rw-r--r--src/common/input.c103
-rw-r--r--src/common/input.h19
-rw-r--r--src/common/meson.build2
-rw-r--r--src/common/timing.c1
-rw-r--r--src/wayland/buffer.c9
-rw-r--r--src/wayland/clipboard.c32
-rw-r--r--src/wayland/clipboard.h5
-rw-r--r--src/wayland/drag.c12
-rw-r--r--src/wayland/internal.h34
-rw-r--r--src/wayland/keyboard.c6
-rw-r--r--src/wayland/pointer.c8
-rw-r--r--src/wayland/surface.c13
-rw-r--r--src/x11/window.c109
-rw-r--r--tests/cocoa/test_native.c14
-rw-r--r--tests/cocoa/test_ownership.m45
-rw-r--r--tests/common/test_backends.c92
-rw-r--r--tests/common/test_contracts.c175
-rw-r--r--tests/common/test_input_alloc.c48
-rw-r--r--tests/meson.build28
-rw-r--r--tests/wayland/test_input.c26
26 files changed, 855 insertions, 180 deletions
diff --git a/README.md b/README.md
index 7a5f06d..6fc8848 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,47 @@ The native surface handle is a `wl_surface *` on Wayland.
On X11, convert it to `Window` through `uintptr_t`.
On Cocoa, the native surface handle is the content `NSView *`.
+## Ownership and backend contracts
+
+- Calls and callbacks use the thread that opened the display (the main thread
+ on Cocoa). Supplied event loops are borrowed. Destroy popups before parents,
+ surfaces before displays, and displays before supplied loops. Surface
+ destruction inside surface callbacks is supported; display or loop destruction
+ during callbacks/dispatch is not.
+- Callback tables, drag listeners, input strings, titles, clipboard text, and
+ drag pixels are copied. Userdata is borrowed. Cocoa retains a supplied Metal
+ device. Native handles are borrowed, not transferred to the caller.
+- Text, paste, and file-drop event payloads remain valid until the next
+ **successful** input poll on that surface or surface destruction. Dispatch,
+ flush, selection replacement, and failed polls do not invalidate them.
+ Copy a payload if it must live longer.
+- Input queues grow rather than silently dropping events at a fixed limit.
+ If allocation fails, pending input is discarded and `YASL_INPUT_RESET` is
+ delivered before later events. Apps must clear held keys/buttons and input
+ composition state on this event.
+- Clipboard requests retain their original target. Destroying that target
+ cancels delivery, rather than redirecting the paste to a focused window.
+ Unsupported primary selections are no-ops, never aliases for the clipboard.
+- CPU pixels are tightly packed premultiplied `0xAARRGGBB`. Creation sizes are
+ logical pixels; framebuffer sizes, pointer coordinates, caret and damage are
+ device pixels. Every draw target needs complete valid pixel contents; do not
+ assume a recycled framebuffer contains the previous frame.
+- Present only the current draw callback's target, at most once. Targets expire
+ when the callback returns; preserve the opaque `generation` when copying a
+ target. Damage is required, clipped to the target, and validated uniformly. Backends may upload the whole frame despite damage hints.
+- A non-NULL `graphics_device` requires `YASL_CAP_NATIVE_GRAPHICS` (currently
+ Cocoa); other backends reject it with `ENOTSUP`. In native Metal mode the app
+ submits and presents the drawable, then calls `yasl_surface_present()` to
+ record that submission.
+- `YASL_CAP_FRAME_PACING` distinguishes compositor/display-driven draws from
+ X11's unpaced flush-driven draws. `yasl_timing.source` distinguishes submission
+ estimates, frame callbacks, and confirmed presentation feedback. Refresh and
+ next-presentation times are estimates, not universal vsync guarantees.
+
+These are pre-release API changes. Applications must handle the new reset input
+kind and should inspect timing provenance instead of assuming all timestamps
+confirm scanout. See `include/yasl/yasl.h` for the complete contracts.
+
## Building
Use a C23 compiler and Meson 1.6 or later.
@@ -104,7 +145,11 @@ meson test -C build-test --print-errorlogs
The tests start isolated display servers.
They test explicit requests, automatic selection, and fallback.
-They also test both backends in one process.
+They also test both backends in one process, callback-table copying, input
+payload ownership, queue growth, concurrent clipboard targets, and consistent
+frame/damage validation. Wayland IME ownership tests need no display server.
+For memory and undefined-behavior checks, configure a separate build with
+`-Db_sanitize=address,undefined`.
On macOS, the same option runs native Cocoa window tests.
These tests need a desktop session.
diff --git a/include/yasl/timing.h b/include/yasl/timing.h
index 7c66967..85f305d 100644
--- a/include/yasl/timing.h
+++ b/include/yasl/timing.h
@@ -15,11 +15,20 @@ struct yasl_resize_stats {
long minflt;
};
+/* Describes last_present_ns, not merely availability of an extension. */
+enum yasl_timing_source {
+ YASL_TIMING_UNKNOWN = 0,
+ YASL_TIMING_SUBMISSION, /* CPU submission time; not scanout confirmation */
+ YASL_TIMING_FRAME_CALLBACK, /* compositor pacing callback, not scanout */
+ YASL_TIMING_PRESENTATION, /* compositor-confirmed presentation */
+};
+
struct yasl_timing {
double refresh_ns;
uint64_t last_present_ns;
uint64_t next_present_ns;
uint64_t frames_skipped;
+ enum yasl_timing_source source;
struct yasl_resize_stats resize;
};
@@ -33,6 +42,7 @@ struct yasl_frame_clock {
long resize_minflt;
uint64_t frame_time_ns;
double refresh_ns;
+ enum yasl_timing_source source;
};
uint64_t yasl_clock_extend_ms(uint32_t ms, uint64_t now_ns);
diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h
index 6fea487..2852112 100644
--- a/include/yasl/yasl.h
+++ b/include/yasl/yasl.h
@@ -4,6 +4,8 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
+#include <limits.h>
+#include <float.h>
#include "yasl/timing.h"
@@ -45,14 +47,25 @@ struct yasl_rect2i {
#define YASL_SCALE_UNIT 120
#define YASL_MIN_LOGICAL_PX 64
+/* Nearest integer, ties away from zero. Zero scale returns zero; finite
+ * out-of-range results saturate. Non-finite logical coordinates return zero. */
static inline int yasl_scale_to_device(double logical, uint32_t scale_120)
{
- return (int)((logical * (double)scale_120 + YASL_SCALE_UNIT / 2) / YASL_SCALE_UNIT);
+ if (!scale_120 || !(logical >= -DBL_MAX && logical <= DBL_MAX)) return 0;
+ double px = logical * ((double)scale_120 / YASL_SCALE_UNIT);
+ if (px >= INT_MAX) return INT_MAX;
+ if (px <= INT_MIN) return INT_MIN;
+ return (int)(px < 0 ? px - 0.5 : px + 0.5);
}
static inline int yasl_scale_to_logical(int device, uint32_t scale_120)
{
- return (int)(((int64_t)device * YASL_SCALE_UNIT + scale_120 / 2) / scale_120);
+ if (!scale_120) return 0;
+ int64_t px = (int64_t)device * YASL_SCALE_UNIT;
+ px = (px + (px < 0 ? -(int64_t)(scale_120 / 2) : scale_120 / 2)) / scale_120;
+ if (px > INT_MAX) return INT_MAX;
+ if (px < INT_MIN) return INT_MIN;
+ return (int)px;
}
/* Capabilities */
@@ -63,6 +76,8 @@ enum yasl_display_cap : uint32_t {
YASL_CAP_PLACE_WINDOW = 1 << 3,
YASL_CAP_PRIMARY_SELECTION = 1 << 4,
YASL_CAP_CSD = 1 << 5,
+ YASL_CAP_NATIVE_GRAPHICS = 1 << 6,
+ YASL_CAP_FRAME_PACING = 1 << 7, /* compositor/display-driven draws */
};
enum yasl_selection : uint8_t {
@@ -103,7 +118,12 @@ enum yasl_resize_edge : uint32_t {
YASL_EDGE_BOTTOM_RIGHT = 10,
};
-/* Damage & Framebuffer */
+/* Damage & Framebuffer
+ * CPU pixels are tightly packed premultiplied ARGB8888 (0xAARRGGBB).
+ * Size, pointer positions, IME caret and damage use device pixels; surface
+ * creation sizes use logical pixels. Damage is a hint: backends may upload
+ * the whole frame. Every pixel must contain valid content on each draw.
+ */
#define YASL_DAMAGE_MAX 64
struct yasl_damage_list {
@@ -113,6 +133,7 @@ struct yasl_damage_list {
};
struct yasl_framebuffer {
+ uint64_t generation; /* Opaque draw identity; preserve when copying target. */
uint32_t *pixels;
struct yasl_vector2i size;
int buffer;
@@ -224,6 +245,9 @@ enum yasl_input_kind {
YASL_INPUT_PASTE,
YASL_INPUT_DROP_FILES,
YASL_INPUT_TEXT,
+ /* Allocation failure broke the input stream. Clear all held keys/buttons
+ * and composition state before processing subsequent events. */
+ YASL_INPUT_RESET,
};
struct yasl_input_event {
@@ -240,6 +264,17 @@ struct yasl_input_event {
};
};
+/* Ownership and threading
+ * All calls and callbacks run on the display's opening thread (main thread on
+ * Cocoa). Displays borrow supplied loops; destroy surfaces before displays,
+ * popups before parents, and displays before their supplied loop.
+ * Specs, strings, callback tables and drag listeners are copied during calls.
+ * Userdata is borrowed until callbacks are removed or the object/drag ends.
+ * Native handles are borrowed and expire when their owning object is destroyed.
+ * Surface destruction is allowed from its callbacks; display/loop destruction
+ * during dispatch or callbacks is not supported.
+ */
+
/* Callbacks */
struct yasl_display_cbs {
void (*quit)(void *ud);
@@ -284,9 +319,12 @@ YASL_API void yasl_display_dispatch(struct yasl_display *d);
YASL_API int yasl_display_dispatch_timeout(struct yasl_display *d, int timeout_ms);
YASL_API void yasl_display_flush(struct yasl_display *d);
YASL_API int yasl_display_modifiers(const struct yasl_display *d);
+/* Unsupported/invalid selections are no-ops, never clipboard aliases. */
YASL_API void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text);
YASL_API void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target);
+/* A successful drag invokes the copied listener's non-NULL end once, including
+ * cancel and display close. Failed starts do not invoke it. */
YASL_API bool yasl_display_drag_start(struct yasl_display *d, struct yasl_surface *origin,
const struct yasl_drag_image *image,
const struct yasl_drag_listener *l, void *ud);
@@ -298,7 +336,9 @@ struct yasl_surface_spec {
struct yasl_vector2i size;
bool fullscreen;
bool tearing;
- /* Optional native graphics device. NULL selects CPU pixel presentation. */
+ /* Optional native graphics device (retained on Cocoa). Requires
+ * YASL_CAP_NATIVE_GRAPHICS; other backends reject it with ENOTSUP.
+ * NULL selects CPU pixel presentation. */
void *graphics_device;
};
@@ -323,19 +363,33 @@ YASL_API void yasl_surface_set_callbacks(struct yasl_surface *s,
const struct yasl_surface_cbs *cbs, void *ud);
YASL_API void yasl_surface_set_title(struct yasl_surface *s, const char *title);
YASL_API void yasl_surface_schedule_redraw(struct yasl_surface *s);
+/* Present exactly the target supplied to the current draw callback, at most
+ * once. Targets and native drawables expire when that callback returns or
+ * the surface is destroyed.
+ * damage is required: n must be -1 or 0..YASL_DAMAGE_MAX. Rectangles must have
+ * nonnegative extents; they are clipped to the target. Invalid input returns
+ * false without consuming the frame. Native renderers submit/present their
+ * drawable themselves, then call this function to record the submission. */
YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_framebuffer *fb,
const struct yasl_damage_list *damage);
+/* Event payload strings are library-owned, NUL-terminated, and valid until
+ * the next SUCCESSFUL poll on this surface or surface destruction. Dispatch,
+ * flush and unsuccessful polls do not invalidate them. Copy to keep longer. */
YASL_API bool yasl_surface_input_poll(struct yasl_surface *s, struct yasl_input_event *ev);
YASL_API struct yasl_vector2i yasl_surface_size(const struct yasl_surface *s);
YASL_API uint32_t yasl_surface_scale_120(const struct yasl_surface *s);
YASL_API uint32_t yasl_surface_state(const struct yasl_surface *s);
+/* Timing uses monotonic nanoseconds. Check source before treating timestamps
+ * as presentation confirmation. refresh/next_present are estimates. Without
+ * YASL_CAP_FRAME_PACING, redraws run on flush without display synchronization.
+ * Tearing may bypass pacing even when the capability is present. */
YASL_API struct yasl_timing yasl_surface_timing(const struct yasl_surface *s);
YASL_API uint64_t yasl_surface_frame_time_ns(struct yasl_surface *s, uint64_t now);
YASL_API void yasl_surface_set_cursor(struct yasl_surface *s, enum yasl_cursor cursor);
YASL_API void yasl_surface_set_background_color(struct yasl_surface *s, uint32_t bg);
-/* IME Support */
+/* IME Support: preedit is borrowed until the next dispatch or destruction. */
YASL_API const char *yasl_surface_ime_preedit(const struct yasl_surface *s);
YASL_API bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *s, int *begin, int *end);
YASL_API void yasl_surface_set_ime_caret(struct yasl_surface *s, struct yasl_rect2i caret);
diff --git a/src/cocoa/window.m b/src/cocoa/window.m
index 8712ce8..4454be3 100644
--- a/src/cocoa/window.m
+++ b/src/cocoa/window.m
@@ -42,35 +42,7 @@ enum {
@property (nonatomic, assign) BOOL fullscreenTransition;
@end
-#define YASL_QUEUE_CAP 256
-struct input_queue {
- struct yasl_input_event events[YASL_QUEUE_CAP];
- int head;
- int tail;
- int count;
-};
-
-static inline void input_queue_push(struct input_queue *q, const struct yasl_input_event *ev)
-{
- if (q->count >= YASL_QUEUE_CAP) return;
- q->events[q->tail] = *ev;
- q->tail = (q->tail + 1) % YASL_QUEUE_CAP;
- q->count++;
-}
-
-static inline bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev)
-{
- if (q->count <= 0) return false;
- *ev = q->events[q->head];
- q->head = (q->head + 1) % YASL_QUEUE_CAP;
- q->count--;
- return true;
-}
-
-static inline void input_queue_clear(struct input_queue *q)
-{
- q->head = q->tail = q->count = 0;
-}
+#include "common/input.h"
struct cocoa_surface {
struct yasl_surface base;
@@ -145,15 +117,6 @@ static struct cocoa_display darwin_ctx;
static void render_and_commit(struct cocoa_surface *w);
static void reap_dead(struct cocoa_display *d);
-static uint64_t media_to_loop_ns(CFTimeInterval t)
-{
- double delta = (t - CACurrentMediaTime()) * 1e9;
- uint64_t now = yasl_loop_now_ns();
- if (delta < 0.0 && -delta >= (double)now)
- return 0;
- return (uint64_t)((double)now + delta);
-}
-
static void set_state(struct cocoa_surface *w, uint32_t bit, bool on)
{
if (!w || w->closed)
@@ -502,7 +465,6 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor)
CFTimeInterval period = link.targetTimestamp - link.timestamp;
if (period > 0.0 && period < 0.25)
w->clock.refresh_ns = period * 1e9;
- w->clock.last_present_ns = media_to_loop_ns(link.timestamp);
w->frame_due = true;
if (!w->want_draw || (w->state & YASL_STATE_SUSPENDED)) {
if (++w->idle_frames >= LINK_IDLE_FRAMES)
@@ -810,8 +772,10 @@ static NSString *plain_string(id string)
bool cocoa_surface_present(struct cocoa_surface *w, const struct yasl_framebuffer *fb,
const struct yasl_damage_list *damage)
{
- (void)damage;
- if (!w || w->closed || !w->rendering || w->did_present || !fb ||
+ if (!w || w->closed || !w->rendering || w->did_present || !fb || !damage ||
+ fb->pixels != w->frame.target.pixels || fb->size.x != w->size.x ||
+ fb->size.y != w->size.y || fb->buffer != w->frame.target.buffer ||
+ fb->transaction != w->frame.target.transaction ||
!fb->native_drawable || fb->native_drawable != w->frame.target.native_drawable)
return false;
if (w->software_queue) {
@@ -850,7 +814,7 @@ static void render_and_commit(struct cocoa_surface *w)
return;
size_t count = (size_t)w->size.x * (size_t)w->size.y;
- if (count > w->pixel_capacity) {
+ if (w->software_queue && count > w->pixel_capacity) {
uint32_t *pixels = realloc(w->pixels, count * sizeof(*pixels));
if (!pixels) return;
w->pixels = pixels;
@@ -866,7 +830,8 @@ static void render_and_commit(struct cocoa_surface *w)
w->frame = (struct yasl_frame){
.surface = &w->base,
.target = {
- .pixels = w->pixels,
+ .generation = yasl_frame_begin(&w->base),
+ .pixels = w->software_queue ? w->pixels : NULL,
.size = w->size,
.damage = &w->damage,
.native_drawable = drawable,
@@ -943,7 +908,8 @@ void cocoa_display_dispatch(struct cocoa_display *d)
bool immediate = false;
for (int i = 0; i < d->count; i++) {
struct cocoa_surface *w = d->windows[i];
- if (w->input.count > 0 || (w->want_draw && (!w->link || w->frame_due || w->unpaced))) {
+ if (w->input.count > 0 || w->input.reset_pending ||
+ (w->want_draw && (!w->link || w->frame_due || w->unpaced))) {
immediate = true;
break;
}
@@ -1039,7 +1005,8 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya
darwin_ctx.open = true;
darwin_ctx.loop = loop;
if (spec) {
- darwin_ctx.cbs = spec->cbs;
+ darwin_ctx.base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0};
+ darwin_ctx.cbs = &darwin_ctx.base.callbacks;
darwin_ctx.ud = spec->ud;
if (spec->app_id) darwin_ctx.app_id = strdup(spec->app_id);
}
@@ -1065,7 +1032,8 @@ void cocoa_display_close(struct cocoa_display *d)
uint32_t cocoa_display_caps(const struct cocoa_display *d)
{
- return d ? (YASL_CAP_PLACE_WINDOW | YASL_CAP_CSD) : 0;
+ return d ? (YASL_CAP_PLACE_WINDOW | YASL_CAP_CSD | YASL_CAP_NATIVE_GRAPHICS |
+ YASL_CAP_FRAME_PACING) : 0;
}
struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d,
@@ -1302,7 +1270,7 @@ void cocoa_display_paste(struct cocoa_display *d, enum yasl_selection sel, struc
(void)d; (void)sel;
if (!target) return;
NSString *s = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString];
- const char *utf8 = s.length ? s.UTF8String : NULL;
+ const char *utf8 = s ? s.UTF8String : NULL;
if (utf8) {
struct yasl_input_event ev = {
.kind = YASL_INPUT_PASTE,
@@ -1388,9 +1356,9 @@ void *cocoa_surface_get_native_handle(struct cocoa_surface *s)
void cocoa_surface_set_callbacks(struct cocoa_surface *w,
const struct yasl_surface_cbs *cbs, void *ud)
{
- static const struct yasl_surface_cbs empty = {0};
if (!w || w->closed) return;
- w->cbs = cbs ? cbs : &empty;
+ w->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0};
+ w->cbs = &w->base.callbacks;
w->ud = ud;
}
@@ -1412,6 +1380,7 @@ struct yasl_timing cocoa_surface_timing(const struct cocoa_surface *w)
timing.last_present_ns = w->clock.last_present_ns;
timing.next_present_ns = yasl_clock_next_present_ns(&w->clock);
timing.frames_skipped = w->frames_skipped;
+ timing.source = w->clock.source;
yasl_clock_resize_stats(&w->clock, &timing.resize);
}
return timing;
diff --git a/src/common/backend.c b/src/common/backend.c
index 2220f1f..214854b 100644
--- a/src/common/backend.c
+++ b/src/common/backend.c
@@ -113,14 +113,18 @@ int yasl_display_modifiers(const struct yasl_display *d)
void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text)
{
- if (!d || !d->ops->display_copy)
+ if (!d || !d->ops->display_copy || !text ||
+ (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) ||
+ (sel == YASL_PRIMARY && !(yasl_display_caps(d) & YASL_CAP_PRIMARY_SELECTION)))
return;
d->ops->display_copy(d, sel, text);
}
void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target)
{
- if (!d || !d->ops->display_paste)
+ if (!d || !d->ops->display_paste ||
+ (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) ||
+ (sel == YASL_PRIMARY && !(yasl_display_caps(d) & YASL_CAP_PRIMARY_SELECTION)))
return;
if (!target || target->display != d)
return;
@@ -145,8 +149,17 @@ void yasl_display_drag_cancel(struct yasl_display *d)
struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d, const struct yasl_surface_spec *spec)
{
- if (!d || !d->ops->surface_create_toplevel)
- return 0;
+ if (!d || !d->ops->surface_create_toplevel || !spec ||
+ spec->size.x <= 0 || spec->size.y <= 0 ||
+ spec->size.x > 32767 || spec->size.y > 32767 ||
+ (int64_t)spec->size.x * spec->size.y * 4 > INT32_MAX) {
+ errno = EINVAL;
+ return NULL;
+ }
+ if (spec->graphics_device && !(yasl_display_caps(d) & YASL_CAP_NATIVE_GRAPHICS)) {
+ errno = ENOTSUP;
+ return NULL;
+ }
return d->ops->surface_create_toplevel(d, spec);
}
@@ -201,14 +214,34 @@ void yasl_surface_schedule_redraw(struct yasl_surface *s)
bool yasl_surface_present(struct yasl_surface *s, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage)
{
- if (!s || !s->display->ops->surface_present)
- return 0;
- return s->display->ops->surface_present(s, fb, damage);
+ if (!s || !s->display->ops->surface_present || !fb || !damage ||
+ !fb->generation || fb->generation != s->frame_generation ||
+ fb->size.x <= 0 || fb->size.y <= 0 ||
+ damage->n < -1 || damage->n > YASL_DAMAGE_MAX)
+ return false;
+ struct yasl_damage_list clipped = {.n = damage->n};
+ if (damage->n >= 0) {
+ clipped.n = 0;
+ for (int i = 0; i < damage->n; i++) {
+ struct yasl_rect2i r = damage->rects[i];
+ if (r.width < 0 || r.height < 0) return false;
+ int64_t right = (int64_t)r.x + r.width;
+ int64_t bottom = (int64_t)r.y + r.height;
+ int64_t x = r.x > 0 ? r.x : 0, y = r.y > 0 ? r.y : 0;
+ if (right > fb->size.x) right = fb->size.x;
+ if (bottom > fb->size.y) bottom = fb->size.y;
+ if (right > x && bottom > y)
+ clipped.rects[clipped.n++] = (struct yasl_rect2i){
+ (int)x, (int)y, (int)(right - x), (int)(bottom - y),
+ };
+ }
+ }
+ return s->display->ops->surface_present(s, fb, &clipped);
}
bool yasl_surface_input_poll(struct yasl_surface *s, struct yasl_input_event *ev)
{
- if (!s || !s->display->ops->surface_input_poll)
+ if (!s || !ev || !s->display->ops->surface_input_poll)
return 0;
return s->display->ops->surface_input_poll(s, ev);
}
@@ -271,7 +304,7 @@ const char *yasl_surface_ime_preedit(const struct yasl_surface *s)
bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *s, int *begin, int *end)
{
- if (!s || !s->display->ops->surface_ime_preedit_cursor)
+ if (!s || !begin || !end || !s->display->ops->surface_ime_preedit_cursor)
return 0;
return s->display->ops->surface_ime_preedit_cursor(s, begin, end);
}
@@ -292,7 +325,11 @@ bool yasl_surface_start_interactive_move(struct yasl_surface *s)
bool yasl_surface_start_interactive_resize(struct yasl_surface *s, enum yasl_resize_edge edge)
{
- if (!s || !s->display->ops->surface_start_interactive_resize)
+ if (!s || !s->display->ops->surface_start_interactive_resize ||
+ (edge != YASL_EDGE_TOP && edge != YASL_EDGE_BOTTOM &&
+ edge != YASL_EDGE_LEFT && edge != YASL_EDGE_RIGHT &&
+ edge != YASL_EDGE_TOP_LEFT && edge != YASL_EDGE_TOP_RIGHT &&
+ edge != YASL_EDGE_BOTTOM_LEFT && edge != YASL_EDGE_BOTTOM_RIGHT))
return 0;
return s->display->ops->surface_start_interactive_resize(s, edge);
}
diff --git a/src/common/backend.h b/src/common/backend.h
index c1d878b..c6691eb 100644
--- a/src/common/backend.h
+++ b/src/common/backend.h
@@ -7,12 +7,21 @@ struct yasl_backend_ops;
struct yasl_display {
const struct yasl_backend_ops *ops;
+ struct yasl_display_cbs callbacks;
};
struct yasl_surface {
struct yasl_display *display;
+ struct yasl_surface_cbs callbacks;
+ uint64_t frame_generation;
};
+static inline uint64_t yasl_frame_begin(struct yasl_surface *s)
+{
+ if (++s->frame_generation == 0) ++s->frame_generation;
+ return s->frame_generation;
+}
+
struct yasl_backend_ops {
enum yasl_backend type;
struct yasl_display *(*display_open)(struct yasl_loop *loop, const struct yasl_display_spec *spec);
diff --git a/src/common/input.c b/src/common/input.c
new file mode 100644
index 0000000..eac4d1a
--- /dev/null
+++ b/src/common/input.c
@@ -0,0 +1,103 @@
+#include "input.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+static const char *event_text(const struct yasl_input_event *ev, size_t *len)
+{
+ switch (ev->kind) {
+ case YASL_INPUT_TEXT: *len = ev->text.len; return ev->text.text;
+ case YASL_INPUT_PASTE: *len = ev->paste.len; return ev->paste.text;
+ case YASL_INPUT_DROP_FILES: *len = ev->drop_files.len; return ev->drop_files.files;
+ default: *len = 0; return NULL;
+ }
+}
+
+static void set_text(struct yasl_input_event *ev, const char *text)
+{
+ switch (ev->kind) {
+ case YASL_INPUT_TEXT: ev->text.text = text; break;
+ case YASL_INPUT_PASTE: ev->paste.text = text; break;
+ case YASL_INPUT_DROP_FILES: ev->drop_files.files = text; break;
+ default: break;
+ }
+}
+
+static void discard_pending(struct input_queue *q)
+{
+ for (size_t i = 0; i < q->count; i++) {
+ size_t len;
+ free((void *)event_text(&q->events[(q->head + i) % q->capacity], &len));
+ }
+ q->head = q->count = 0;
+}
+
+bool input_queue_push(struct input_queue *q, const struct yasl_input_event *ev)
+{
+ struct yasl_input_event copy = *ev;
+ size_t len;
+ const char *text = event_text(ev, &len);
+ char *owned = NULL;
+ if (ev->kind == YASL_INPUT_TEXT || ev->kind == YASL_INPUT_PASTE ||
+ ev->kind == YASL_INPUT_DROP_FILES) {
+ if ((!text && len) || len == SIZE_MAX) goto failed;
+ owned = malloc(len + 1);
+ if (!owned) goto failed;
+ if (len) memcpy(owned, text, len);
+ owned[len] = '\0';
+ set_text(&copy, owned);
+ }
+ if (q->count == q->capacity) {
+ size_t capacity = q->capacity ? q->capacity * 2 : 32;
+ if (capacity < q->capacity || capacity > SIZE_MAX / sizeof(*q->events))
+ goto failed;
+ struct yasl_input_event *events = malloc(capacity * sizeof(*events));
+ if (!events) goto failed;
+ for (size_t i = 0; i < q->count; i++)
+ events[i] = q->events[(q->head + i) % q->capacity];
+ free(q->events);
+ q->events = events;
+ q->capacity = capacity;
+ q->head = 0;
+ }
+ q->events[(q->head + q->count) % q->capacity] = copy;
+ q->count++;
+ return true;
+
+failed:
+ free(owned);
+ /* A missing release must never silently leave app input state stuck.
+ * Discard the old stream and deliver an allocation-free reset first. */
+ discard_pending(q);
+ q->reset_surface = ev->surface;
+ q->reset_pending = true;
+ return false;
+}
+
+bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev)
+{
+ if (!ev || (!q->count && !q->reset_pending)) return false;
+ free(q->last_text);
+ q->last_text = NULL;
+ if (q->reset_pending) {
+ *ev = (struct yasl_input_event){
+ .surface = q->reset_surface, .kind = YASL_INPUT_RESET,
+ };
+ q->reset_pending = false;
+ return true;
+ }
+ *ev = q->events[q->head];
+ size_t len;
+ q->last_text = (char *)event_text(ev, &len);
+ q->head = (q->head + 1) % q->capacity;
+ q->count--;
+ return true;
+}
+
+void input_queue_clear(struct input_queue *q)
+{
+ discard_pending(q);
+ free(q->last_text);
+ free(q->events);
+ *q = (struct input_queue){0};
+}
diff --git a/src/common/input.h b/src/common/input.h
new file mode 100644
index 0000000..d7264f6
--- /dev/null
+++ b/src/common/input.h
@@ -0,0 +1,19 @@
+#ifndef YASL_INPUT_H
+#define YASL_INPUT_H
+
+#include "yasl/yasl.h"
+
+/* Each queued string and the last successfully polled string are owned here. */
+struct input_queue {
+ struct yasl_input_event *events;
+ size_t head, count, capacity;
+ char *last_text;
+ struct yasl_surface *reset_surface;
+ bool reset_pending;
+};
+
+bool input_queue_push(struct input_queue *q, const struct yasl_input_event *ev);
+bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev);
+void input_queue_clear(struct input_queue *q);
+
+#endif
diff --git a/src/common/meson.build b/src/common/meson.build
index 61653df..5cfb187 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -1 +1 @@
-sources += files('backend.c', 'loop.c', 'timing.c', 'utf8.c')
+sources += files('backend.c', 'input.c', 'loop.c', 'timing.c', 'utf8.c')
diff --git a/src/common/timing.c b/src/common/timing.c
index 3e3a705..17f71b5 100644
--- a/src/common/timing.c
+++ b/src/common/timing.c
@@ -17,6 +17,7 @@ void yasl_clock_presented(struct yasl_frame_clock *c, uint64_t now, bool sample)
c->refresh_ns += 0.125 * (dt - c->refresh_ns);
}
c->last_present_ns = now;
+ c->source = YASL_TIMING_SUBMISSION;
}
void yasl_clock_note_resize(struct yasl_frame_clock *c, uint64_t dt_ns)
diff --git a/src/wayland/buffer.c b/src/wayland/buffer.c
index 76f6c0e..3f9cf84 100644
--- a/src/wayland/buffer.c
+++ b/src/wayland/buffer.c
@@ -278,8 +278,10 @@ static void frame_done(void *data, struct wl_callback *cb, uint32_t ms)
w->frame_cb = NULL;
uint64_t now = yasl_loop_now_ns();
- if (!w->conn->pres_clock_known)
+ if (!w->conn->pres_clock_known) {
yasl_clock_presented(&w->clock, yasl_clock_extend_ms(ms, now), w->sample_refresh);
+ w->clock.source = YASL_TIMING_FRAME_CALLBACK;
+ }
if (w->want_draw)
wayland_surface_schedule_redraw(w);
@@ -358,6 +360,7 @@ static void presented(void *data, struct wp_presentation_feedback *fb,
(((uint64_t)tv_sec_hi << 32) | tv_sec_lo) * 1000000000ull + tv_nsec);
if (refresh) w->clock.refresh_ns = refresh;
w->clock.last_present_ns = ns;
+ w->clock.source = YASL_TIMING_PRESENTATION;
}
(void)fb;
feedback_destroy(feedback);
@@ -444,7 +447,7 @@ bool wayland_surface_present(struct wayland_surface *w, const struct yasl_frameb
if (!w || w->closed || !w->frame_active || w->frame_presented || !fb ||
!damage || fb->pixels != w->frame.target.pixels ||
fb->size.x != w->width || fb->size.y != w->height ||
- fb->buffer != w->cur)
+ fb->buffer != w->cur || fb->native_drawable || fb->transaction)
return false;
w->damage = *damage;
@@ -485,6 +488,7 @@ void surface_draw_frame(struct wayland_surface *w)
w->frame = (struct yasl_frame){
.surface = &w->base,
.target = {
+ .generation = yasl_frame_begin(&w->base),
.pixels = w->buffers[idx].pixels,
.size = { .x = w->width, .y = w->height },
.buffer = idx,
@@ -544,6 +548,7 @@ struct yasl_timing wayland_surface_timing(const struct wayland_surface *w)
.last_present_ns = w->clock.last_present_ns,
.next_present_ns = yasl_clock_next_present_ns(&w->clock),
.frames_skipped = w->frames_skipped,
+ .source = w->clock.source,
};
yasl_clock_resize_stats(&w->clock, &t.resize);
return t;
diff --git a/src/wayland/clipboard.c b/src/wayland/clipboard.c
index 0e6ce33..7df1a70 100644
--- a/src/wayland/clipboard.c
+++ b/src/wayland/clipboard.c
@@ -134,10 +134,8 @@ static int transfer_io(int fd, short events, void *ud)
memcpy(t->text + t->len, buf, (size_t)n);
t->len += n;
} else if (n == 0) {
- if (t->len) {
- t->owner->receive(t->text, t->len, t->uri_list,
- t->drop_surface, t->owner->ud);
- }
+ t->owner->receive(t->text ? t->text : "", t->len, t->uri_list,
+ t->drop_surface, t->owner->ud);
transfer_end(t, true);
} else if (errno != EAGAIN && errno != EINTR) transfer_destroy(t);
}
@@ -556,24 +554,22 @@ void clipboard_copy(struct clipboard *c, const char *text, uint32_t serial)
wl_data_device_set_selection(c->device, c->source, serial);
}
-void clipboard_paste(struct clipboard *c)
+void clipboard_paste(struct clipboard *c, struct wl_surface *target)
{
if (!c || !c->device || !c->selection || !c->selection->mime) return;
int fds[2];
if (pipe2(fds, O_CLOEXEC) < 0) return;
wl_data_offer_receive(c->selection->proxy, c->selection->mime, fds[1]);
close(fds[1]);
- start_transfer(c, fds[0], false, false);
+ struct transfer *t = start_transfer(c, fds[0], false, false);
+ if (t) t->drop_surface = target;
wl_display_flush(c->display);
}
void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t serial)
{
if (!c || !text || !serial) return;
- if (!c->primary_device || !c->primary_manager) {
- clipboard_copy(c, text, serial);
- return;
- }
+ if (!c->primary_device || !c->primary_manager) return;
char *copy = strdup(text);
if (!copy) return;
if (c->primary_source) zwp_primary_selection_source_v1_destroy(c->primary_source);
@@ -589,7 +585,7 @@ void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t seri
zwp_primary_selection_device_v1_set_selection(c->primary_device, c->primary_source, serial);
}
-void clipboard_paste_primary(struct clipboard *c)
+void clipboard_paste_primary(struct clipboard *c, struct wl_surface *target)
{
if (!c) return;
if (c->primary_device && c->primary_selection && c->primary_selection->mime) {
@@ -598,11 +594,21 @@ void clipboard_paste_primary(struct clipboard *c)
zwp_primary_selection_offer_v1_receive(c->primary_selection->proxy,
c->primary_selection->mime, fds[1]);
close(fds[1]);
- start_transfer(c, fds[0], false, false);
+ struct transfer *t = start_transfer(c, fds[0], false, false);
+ if (t) t->drop_surface = target;
wl_display_flush(c->display);
return;
}
- clipboard_paste(c);
+}
+
+void clipboard_forget_surface(struct clipboard *c, struct wl_surface *surface)
+{
+ if (!c || !surface) return;
+ for (struct transfer *t = c->transfers, *next; t; t = next) {
+ next = t->next;
+ if (t->drop_surface == surface) transfer_destroy(t);
+ }
+ if (c->drag_surface == surface) c->drag_surface = NULL;
}
void clipboard_destroy(struct clipboard *c)
diff --git a/src/wayland/clipboard.h b/src/wayland/clipboard.h
index 6ea75c6..9fefa8e 100644
--- a/src/wayland/clipboard.h
+++ b/src/wayland/clipboard.h
@@ -24,9 +24,10 @@ void clipboard_destroy(struct clipboard *);
bool clipboard_set_seat(struct clipboard *, struct wl_seat *);
void clipboard_set_primary_manager(struct clipboard *, struct zwp_primary_selection_device_manager_v1 *);
void clipboard_copy(struct clipboard *, const char *, uint32_t);
-void clipboard_paste(struct clipboard *);
+void clipboard_paste(struct clipboard *, struct wl_surface *target);
+void clipboard_forget_surface(struct clipboard *, struct wl_surface *);
void clipboard_copy_primary(struct clipboard *, const char *, uint32_t);
-void clipboard_paste_primary(struct clipboard *);
+void clipboard_paste_primary(struct clipboard *, struct wl_surface *target);
typedef void (*clipboard_drag_fn)(enum yasl_drag_event, struct wl_surface *on,
double x, double y, void *);
diff --git a/src/wayland/drag.c b/src/wayland/drag.c
index 7c15002..989dbb6 100644
--- a/src/wayland/drag.c
+++ b/src/wayland/drag.c
@@ -90,11 +90,11 @@ static void drag_clear(struct wayland_display *c, bool ended)
static void drag_finish(struct wayland_display *c, enum yasl_drag_end how, bool ended)
{
- const struct yasl_drag_listener *l = c->drag.listener;
+ struct yasl_drag_listener l = c->drag.listener;
void *ud = c->drag.ud;
drag_clear(c, ended);
- if (l && l->end)
- l->end(ud, how);
+ if (l.end)
+ l.end(ud, how);
}
void conn_drag_end(struct wayland_display *c)
@@ -111,7 +111,7 @@ static void source_cancelled(void *ud, struct wl_data_source *s)
{
(void)s;
struct wayland_display *c = ud;
- bool released = c->drag.dropped || !c->drag.carrying;
+ bool released = c->drag.dropped;
drag_finish(c, released ? YASL_DRAG_DROPPED : YASL_DRAG_CANCELLED, true);
}
@@ -171,7 +171,7 @@ bool wayland_display_drag_start(struct wayland_display *c, struct wayland_surfac
}
c->drag.source = s;
c->drag.seat = seat;
- c->drag.listener = l;
+ c->drag.listener = *l;
c->drag.ud = ud;
wl_data_source_add_listener(s, &source_listener, c);
wl_data_source_offer(s, clipboard_drag_mime());
@@ -190,7 +190,7 @@ bool wayland_display_drag_start(struct wayland_display *c, struct wayland_surfac
void wayland_display_drag_cancel(struct wayland_display *c)
{
if (c && c->drag.source)
- drag_clear(c, false);
+ drag_finish(c, YASL_DRAG_CANCELLED, false);
}
bool wayland_surface_drag_carry(struct wayland_surface *w, struct yasl_vector2i offset)
diff --git a/src/wayland/internal.h b/src/wayland/internal.h
index cba5096..ccfd2dc 100644
--- a/src/wayland/internal.h
+++ b/src/wayland/internal.h
@@ -100,7 +100,7 @@ struct conn_drag {
struct wl_data_source *source;
struct xdg_toplevel_drag_v1 *toplevel;
struct drag_icon icon;
- const struct yasl_drag_listener *listener;
+ struct yasl_drag_listener listener;
void *ud;
struct seat *seat;
uint32_t scale_120;
@@ -192,36 +192,7 @@ struct seat {
bool touch_scrolling;
};
-#define YASL_QUEUE_CAP 256
-#define YASL_QUEUE_MASK (YASL_QUEUE_CAP - 1)
-struct input_queue {
- struct yasl_input_event events[YASL_QUEUE_CAP];
- int head;
- int tail;
- int count;
-};
-
-static inline void input_queue_push(struct input_queue *q, const struct yasl_input_event *ev)
-{
- if (unlikely(q->count >= YASL_QUEUE_CAP)) return;
- q->events[q->tail] = *ev;
- q->tail = (q->tail + 1) & YASL_QUEUE_MASK;
- q->count++;
-}
-
-static inline bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev)
-{
- if (unlikely(q->count <= 0)) return false;
- *ev = q->events[q->head];
- q->head = (q->head + 1) & YASL_QUEUE_MASK;
- q->count--;
- return true;
-}
-
-static inline void input_queue_clear(struct input_queue *q)
-{
- q->head = q->tail = q->count = 0;
-}
+#include "common/input.h"
struct wayland_display {
struct yasl_display base;
@@ -257,7 +228,6 @@ struct wayland_display {
struct xdg_toplevel_drag_manager_v1 *toplevel_drag_mgr;
struct conn_drag drag;
struct wayland_surface *surfaces;
- struct wayland_surface *paste_target;
struct wayland_surface *dead;
/* 4-byte integers */
diff --git a/src/wayland/keyboard.c b/src/wayland/keyboard.c
index 6bdd58c..f7e2691 100644
--- a/src/wayland/keyboard.c
+++ b/src/wayland/keyboard.c
@@ -374,11 +374,11 @@ void wayland_display_paste(struct wayland_display *c, enum yasl_selection sel, s
struct seat *s = c ? conn_active_seat(c) : NULL;
if (!s || !s->clipboard)
return;
- c->paste_target = target;
+ if (!target || target->closed) return;
if (sel == YASL_PRIMARY)
- clipboard_paste_primary(s->clipboard);
+ clipboard_paste_primary(s->clipboard, target->surface);
else
- clipboard_paste(s->clipboard);
+ clipboard_paste(s->clipboard, target->surface);
}
bool wayland_surface_input_poll(struct wayland_surface *w, struct yasl_input_event *ev)
diff --git a/src/wayland/pointer.c b/src/wayland/pointer.c
index d434047..3b3ad73 100644
--- a/src/wayland/pointer.c
+++ b/src/wayland/pointer.c
@@ -1032,10 +1032,9 @@ static void clipboard_received(const char *text, size_t len, bool uri_list,
{
struct seat *s = ud;
struct wayland_display *c = s->conn;
- struct wayland_surface *w = on ? surface_root(conn_surface_for(c, on)) : NULL;
- if (!w)
- w = c->paste_target ? c->paste_target : s->keyboard_focus;
- if (!len || !w) return;
+ struct wayland_surface *w = conn_surface_for(c, on);
+ if (uri_list) w = surface_root(w);
+ if (!w || w->closed) return;
struct yasl_input_event ev = {
.surface = &w->base,
.kind = uri_list ? YASL_INPUT_DROP_FILES : YASL_INPUT_PASTE,
@@ -1216,6 +1215,7 @@ void seat_bind(struct wayland_display *c, struct wl_registry *registry,
void seat_forget_surface(struct seat *s, struct wayland_surface *w)
{
+ clipboard_forget_surface(s->clipboard, w->surface);
struct wayland_surface **refs[] = {
&s->pointer_focus, &s->keyboard_focus,
&s->tablet_focus, &s->ime_focus, &s->focus_leaving, &s->ptr_left,
diff --git a/src/wayland/surface.c b/src/wayland/surface.c
index 8de4992..ef355ab 100644
--- a/src/wayland/surface.c
+++ b/src/wayland/surface.c
@@ -128,7 +128,7 @@ static inline bool display_has_immediate_work(const struct wayland_display *c)
if (unlikely(!c || c->lost)) return false;
for (struct wayland_surface *w = c->surfaces; w; w = w->conn_next) {
bool ready_draw = w->want_draw & (w->frame_cb == NULL) & w->configured;
- bool has_input = (w->input.count > 0);
+ bool has_input = (w->input.count > 0 || w->input.reset_pending);
if (unlikely((ready_draw | has_input) & !w->closed))
return true;
}
@@ -250,7 +250,8 @@ struct wayland_display *wayland_display_open(struct yasl_loop *loop, const struc
}
if (!c->loop) goto err;
if (spec) {
- c->cbs = spec->cbs;
+ c->base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0};
+ c->cbs = &c->base.callbacks;
c->ud = spec->ud;
if (spec->app_id) c->app_id = strdup(spec->app_id);
}
@@ -288,7 +289,7 @@ void wayland_display_close(struct wayland_display *c)
uint32_t wayland_display_caps(const struct wayland_display *c)
{
if (!c) return 0;
- uint32_t caps = YASL_CAP_POPUPS | YASL_CAP_CSD;
+ uint32_t caps = YASL_CAP_POPUPS | YASL_CAP_CSD | YASL_CAP_FRAME_PACING;
struct seat *s = conn_active_seat(c);
const struct wl_data_device *device = s ? clipboard_data_device(s->clipboard) : NULL;
if (device && wl_data_device_get_version((struct wl_data_device *)device) >= 3)
@@ -318,8 +319,6 @@ static void conn_unlink(struct wayland_surface *w)
break;
}
}
- if (c->paste_target == w)
- c->paste_target = NULL;
for (struct seat *s = c->seats; s; s = s->next)
seat_forget_surface(s, w);
w->conn = NULL;
@@ -397,8 +396,8 @@ err:
void wayland_surface_set_callbacks(struct wayland_surface *w, const struct yasl_surface_cbs *cbs, void *ud)
{
if (w && !w->closed) {
- static const struct yasl_surface_cbs no_callbacks = {0};
- w->cbs = cbs ? cbs : &no_callbacks;
+ w->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0};
+ w->cbs = &w->base.callbacks;
w->ud = ud;
}
}
diff --git a/src/x11/window.c b/src/x11/window.c
index 4680d3f..e40db6e 100644
--- a/src/x11/window.c
+++ b/src/x11/window.c
@@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "common/backend.h"
+#include "common/input.h"
#include <X11/Xatom.h>
#include <X11/XKBlib.h>
@@ -16,7 +17,6 @@
#include <stdlib.h>
#include <string.h>
-#define INPUT_CAP 256
#define TEXT_LIMIT (16 * 1024 * 1024)
#ifndef likely
@@ -38,10 +38,7 @@ struct x11_surface {
struct yasl_vector2i size;
const struct yasl_surface_cbs *cbs;
void *ud;
- struct yasl_input_event events[INPUT_CAP];
- char *event_text[INPUT_CAP];
- char *last_text;
- unsigned head, count;
+ struct input_queue input;
struct yasl_frame frame;
struct yasl_damage_list damage;
struct yasl_frame_clock clock;
@@ -49,6 +46,12 @@ struct x11_surface {
bool want_draw, drawing, presented, destroyed;
};
+struct paste_request {
+ struct paste_request *next;
+ struct x11_surface *target;
+ Window window;
+};
+
struct x11_display {
struct yasl_display base;
Display *native;
@@ -62,7 +65,7 @@ struct x11_display {
Atom protocols, delete_window, utf8, clipboard, targets, paste_property;
Atom net_name, net_state, fullscreen, maximized_h, maximized_v;
char *selection[2];
- struct x11_surface *paste_target;
+ struct paste_request *pastes;
uint32_t mods;
char *app_id;
bool owns_loop;
@@ -86,25 +89,18 @@ static void leave(struct x11_display *d)
static void push(struct x11_surface *s, struct yasl_input_event event,
const char *text, size_t len)
{
- if (s->destroyed || s->count == INPUT_CAP) return;
- unsigned slot = (s->head + s->count) % INPUT_CAP;
+ if (s->destroyed) return;
if (text) {
- char *copy = malloc(len + 1);
- if (!copy) return;
- memcpy(copy, text, len);
- copy[len] = 0;
- s->event_text[slot] = copy;
if (event.kind == YASL_INPUT_TEXT) {
- event.text.text = copy;
+ event.text.text = text;
event.text.len = len;
} else {
- event.paste.text = copy;
+ event.paste.text = text;
event.paste.len = len;
}
}
event.surface = &s->base;
- s->events[slot] = event;
- s->count++;
+ input_queue_push(&s->input, &event);
}
static uint32_t modifiers(unsigned state)
@@ -149,6 +145,23 @@ static bool resize(struct x11_surface *s, struct yasl_vector2i size)
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;
@@ -158,14 +171,13 @@ static void surface_destroy(struct yasl_surface *base)
for (struct x11_surface **p = &d->surfaces; *p; p = &(*p)->next) {
if (*p == s) { *p = s->next; break; }
}
- if (d->paste_target == s) d->paste_target = NULL;
+ cancel_pastes(d, s);
if (s->ic) XDestroyIC(s->ic);
if (s->image) XDestroyImage(s->image);
free(s->pixels);
if (s->gc) XFreeGC(d->native, s->gc);
if (s->window) XDestroyWindow(d->native, s->window);
- for (unsigned i = 0; i < INPUT_CAP; i++) free(s->event_text[i]);
- free(s->last_text);
+ input_queue_clear(&s->input);
if (d->depth) {
s->dead_next = d->dead;
d->dead = s;
@@ -177,7 +189,8 @@ static void set_callbacks(struct yasl_surface *base,
{
struct x11_surface *s = (struct x11_surface *)base;
if (s->destroyed) return;
- s->cbs = cbs;
+ s->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0};
+ s->cbs = &s->base.callbacks;
s->ud = ud;
}
@@ -295,14 +308,22 @@ static void selection_request(struct x11_display *d, XSelectionRequestEvent *req
static void selection_notify(struct x11_display *d, XSelectionEvent *event)
{
- struct x11_surface *s = d->paste_target;
- d->paste_target = NULL;
- if (!s || event->property == None) return;
+ 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;
int format;
unsigned long count, left;
unsigned char *data = NULL;
- if (XGetWindowProperty(d->native, d->selection_window, event->property,
+ if (XGetWindowProperty(d->native, r->window, event->property,
0, TEXT_LIMIT / 4, True, AnyPropertyType,
&actual, &format, &count, &left, &data) == Success &&
(actual == d->utf8 || actual == XA_STRING) && format == 8 && !left) {
@@ -310,6 +331,8 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
(const char *)data, count);
}
if (data) XFree(data);
+ XDestroyWindow(d->native, r->window);
+ free(r);
}
static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
@@ -458,6 +481,7 @@ static bool present(struct yasl_surface *base, const struct yasl_framebuffer *fb
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 ||
+ fb->buffer != s->frame.target.buffer || fb->native_drawable || fb->transaction ||
damage->n < -1 || damage->n > YASL_DAMAGE_MAX) return false;
struct x11_display *d = display_of(base);
XImage *im = s->image;
@@ -502,7 +526,8 @@ static void flush(struct yasl_display *base)
s->damage = (struct yasl_damage_list){0};
s->frame = (struct yasl_frame){
.surface = &s->base,
- .target = {.pixels = s->pixels, .size = s->size, .damage = &s->damage},
+ .target = {.generation = yasl_frame_begin(&s->base),
+ .pixels = s->pixels, .size = s->size, .damage = &s->damage},
};
s->cbs->draw(&s->base, &s->frame, s->ud);
s->drawing = false;
@@ -537,6 +562,7 @@ static struct yasl_display *display_open(struct yasl_loop *loop,
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; }
d->screen = DefaultScreen(d->native);
@@ -547,7 +573,13 @@ static struct yasl_display *display_open(struct yasl_loop *loop,
d->loop = loop;
}
d->app_id = strdup(spec && spec->app_id ? spec->app_id : "YASL");
- if (!d->app_id) { XCloseDisplay(d->native); free(d); return NULL; }
+ 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);
@@ -583,6 +615,7 @@ 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");
+ cancel_pastes(d, NULL);
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);
if (d->im) XCloseIM(d->im);
@@ -623,9 +656,15 @@ static void paste(struct yasl_display *base, enum yasl_selection sel, struct yas
const char *text = d->selection[sel];
if (text) push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, text, strlen(text));
} else {
- d->paste_target = s;
+ 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,
- d->selection_window, CurrentTime);
+ r->window, CurrentTime);
XFlush(d->native);
}
}
@@ -633,14 +672,7 @@ static void paste(struct yasl_display *base, enum yasl_selection sel, struct yas
static bool input_poll(struct yasl_surface *base, struct yasl_input_event *event)
{
struct x11_surface *s = (struct x11_surface *)base;
- if (!event || s->destroyed || !s->count) return false;
- free(s->last_text);
- s->last_text = s->event_text[s->head];
- s->event_text[s->head] = NULL;
- *event = s->events[s->head];
- s->head = (s->head + 1) % INPUT_CAP;
- s->count--;
- return true;
+ return !s->destroyed && input_queue_pop(&s->input, event);
}
static void schedule_redraw(struct yasl_surface *base)
@@ -667,7 +699,8 @@ 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)};
+ .next_present_ns = yasl_clock_next_present_ns(&s->clock),
+ .source = s->clock.source};
yasl_clock_resize_stats(&s->clock, &t.resize);
return t;
}
diff --git a/tests/cocoa/test_native.c b/tests/cocoa/test_native.c
index 03fde88..41df79a 100644
--- a/tests/cocoa/test_native.c
+++ b/tests/cocoa/test_native.c
@@ -19,7 +19,16 @@ static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *u
struct yasl_vector2i size = yasl_surface_size(surface);
assert(size.x == frame->target.size.x && size.y == frame->target.size.y);
for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0;
- struct yasl_damage_list damage = {.n = -1};
+ struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1};
+ assert(!yasl_surface_present(surface, &frame->target, &damage));
+ damage.n = -1;
+ struct yasl_framebuffer invalid = frame->target;
+ invalid.buffer++;
+ assert(!yasl_surface_present(surface, &invalid, &damage));
+ invalid = frame->target;
+ invalid.generation--;
+ assert(!yasl_surface_present(surface, &invalid, &damage));
+ assert(!yasl_surface_present(surface, &frame->target, NULL));
assert(yasl_surface_present(surface, &frame->target, &damage));
assert(!yasl_surface_present(surface, &frame->target, &damage));
state->draws++;
@@ -36,8 +45,9 @@ static void test_window(enum yasl_backend request, bool null_spec)
struct yasl_surface_spec surface = {.title = "Native backend test", .size = {160, 100}};
state.surface = yasl_surface_create_toplevel(state.display, &surface);
assert(state.surface);
- static const struct yasl_surface_cbs callbacks = {.draw = draw};
+ struct yasl_surface_cbs callbacks = {.draw = draw};
yasl_surface_set_callbacks(state.surface, &callbacks, &state);
+ callbacks.draw = NULL;
yasl_surface_schedule_redraw(state.surface);
uint64_t deadline = yasl_loop_now_ns() + 3000000000ull;
while (!state.draws && yasl_loop_now_ns() < deadline) {
diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m
new file mode 100644
index 0000000..aa75b1c
--- /dev/null
+++ b/tests/cocoa/test_ownership.m
@@ -0,0 +1,45 @@
+#import <AppKit/AppKit.h>
+#include "yasl/yasl.h"
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+int main(void)
+{
+ struct yasl_display *d = yasl_display_open(NULL, NULL);
+ assert(d);
+ struct yasl_surface_spec spec = {.title = "Ownership test", .size = {160, 100}};
+ struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec);
+ assert(s);
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(s, &event)) {}
+ @autoreleasepool {
+ for (int i = 0; i < 600; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "paste-%d", i);
+ yasl_display_copy(d, YASL_CLIPBOARD, text);
+ yasl_display_paste(d, YASL_CLIPBOARD, s);
+ memset(text, 'x', sizeof(text));
+ }
+ }
+ /* All NSString/UTF8String temporaries have now expired. */
+ for (int i = 0; i < 600; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "paste-%d", i);
+ assert(yasl_surface_input_poll(s, &event));
+ assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0);
+ }
+ const char *last = event.paste.text;
+ assert(!yasl_surface_input_poll(s, &event));
+ @autoreleasepool { yasl_display_copy(d, YASL_CLIPBOARD, "new selection"); }
+ assert(strcmp(last, "paste-599") == 0);
+ @autoreleasepool {
+ yasl_display_copy(d, YASL_CLIPBOARD, "");
+ yasl_display_paste(d, YASL_CLIPBOARD, s);
+ }
+ assert(yasl_surface_input_poll(s, &event));
+ assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]);
+ yasl_surface_destroy(s);
+ yasl_display_close(d);
+ return 0;
+}
diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c
index 48a6b68..3372290 100644
--- a/tests/common/test_backends.c
+++ b/tests/common/test_backends.c
@@ -67,7 +67,24 @@ static void draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud)
assert(size.x == frame->target.size.x && size.y == frame->target.size.y);
assert(frame->target.pixels);
for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0;
- struct yasl_damage_list damage = {.n = -1};
+ struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1};
+ assert(!yasl_surface_present(s, &frame->target, &damage));
+ damage.n = -2;
+ assert(!yasl_surface_present(s, &frame->target, &damage));
+ damage.n = 1;
+ damage.rects[0].width = -1;
+ assert(!yasl_surface_present(s, &frame->target, &damage));
+ damage.n = -1;
+ struct yasl_framebuffer invalid = frame->target;
+ invalid.buffer++;
+ assert(!yasl_surface_present(s, &invalid, &damage));
+ invalid = frame->target;
+ invalid.generation--;
+ assert(!yasl_surface_present(s, &invalid, &damage));
+ invalid = frame->target;
+ invalid.native_drawable = (void *)(uintptr_t)1;
+ assert(!yasl_surface_present(s, &invalid, &damage));
+ assert(!yasl_surface_present(s, &frame->target, NULL));
assert(yasl_surface_present(s, &frame->target, &damage));
assert(!yasl_surface_present(s, &frame->target, &damage));
w->draws++;
@@ -98,12 +115,22 @@ static const struct yasl_surface_cbs callbacks = {
.draw = draw, .configure = configure, .close = close_window,
};
+static void attach(struct window *w)
+{
+ struct yasl_surface_cbs temporary = callbacks;
+ yasl_surface_set_callbacks(w->surface, &temporary, w);
+ temporary.draw = NULL;
+ temporary.configure = NULL;
+ temporary.close = NULL;
+}
+
static struct window create(struct yasl_display *d)
{
struct yasl_surface_spec spec = {.title = "Backend test", .size = {160, 100}};
struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec);
assert(s);
assert(yasl_surface_display(s) == d);
+ assert(yasl_surface_timing(s).source == YASL_TIMING_UNKNOWN);
return (struct window){.surface = s, .display = d};
}
@@ -153,6 +180,61 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct
}
}
assert(saw_key && saw_paste);
+ assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION);
+ assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING));
+
+ /* Reuse caller storage, replace the selection, and exceed the old queue
+ * cap before polling: each paste must retain its own original contents. */
+ for (int i = 0; i < 300; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "owned-%d", i);
+ yasl_display_copy(d, YASL_CLIPBOARD, text);
+ yasl_display_paste(d, YASL_CLIPBOARD, w->surface);
+ memset(text, 'x', sizeof(text));
+ }
+ for (int i = 0; i < 300; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "owned-%d", i);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0);
+ }
+ const char *last = event.paste.text;
+ assert(!yasl_surface_input_poll(w->surface, &event));
+ yasl_display_copy(d, YASL_CLIPBOARD, "changed");
+ pump(loop, d, NULL);
+ assert(strcmp(last, "owned-299") == 0);
+ yasl_display_copy(d, YASL_CLIPBOARD, "");
+ yasl_display_paste(d, YASL_CLIPBOARD, w->surface);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]);
+
+ /* Concurrent external conversions must not share a mutable target slot. */
+ struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11};
+ struct yasl_display *owner = yasl_display_open(loop, &ds);
+ assert(owner);
+ yasl_display_copy(owner, YASL_CLIPBOARD, "external");
+ struct window other = create(d), cancelled = create(d);
+ yasl_display_paste(d, YASL_CLIPBOARD, w->surface);
+ yasl_display_paste(d, YASL_CLIPBOARD, other.surface);
+ yasl_display_paste(d, YASL_CLIPBOARD, cancelled.surface);
+ yasl_surface_destroy(cancelled.surface);
+ pump(loop, d, owner);
+ int first_pastes = 0, second_pastes = 0;
+ while (yasl_surface_input_poll(w->surface, &event)) {
+ if (event.kind == YASL_INPUT_PASTE) {
+ assert(strcmp(event.paste.text, "external") == 0);
+ first_pastes++;
+ }
+ }
+ while (yasl_surface_input_poll(other.surface, &event)) {
+ if (event.kind == YASL_INPUT_PASTE) {
+ assert(strcmp(event.paste.text, "external") == 0);
+ second_pastes++;
+ }
+ }
+ assert(first_pastes == 1 && second_pastes == 1);
+ yasl_surface_destroy(other.surface);
+ yasl_display_close(owner);
key.xclient = (XClientMessageEvent){.type = ClientMessage, .display = native,
.window = window, .message_type = XInternAtom(native, "WM_PROTOCOLS", False),
.format = 32, .data.l = {(long)XInternAtom(native, "WM_DELETE_WINDOW", False)}};
@@ -177,8 +259,8 @@ int main(int argc, char **argv)
struct yasl_display *b = yasl_display_open(loop, &spec);
assert(a && b);
struct window wa = create(a), wb = create(b);
- yasl_surface_set_callbacks(wa.surface, &callbacks, &wa);
- yasl_surface_set_callbacks(wb.surface, &callbacks, &wb);
+ attach(&wa);
+ attach(&wb);
pump(loop, a, b);
assert(wa.draws && wb.draws);
yasl_surface_destroy(wa.surface);
@@ -195,7 +277,7 @@ int main(int argc, char **argv)
struct window w = create(d);
pump(loop, d, NULL);
assert(w.draws == 0);
- yasl_surface_set_callbacks(w.surface, &callbacks, &w);
+ attach(&w);
yasl_surface_schedule_redraw(w.surface);
pump(loop, d, NULL);
assert(w.draws > 0);
@@ -204,7 +286,7 @@ int main(int argc, char **argv)
if (w.surface) yasl_surface_destroy(w.surface);
w = create(d);
w.destroy_on_draw = true;
- yasl_surface_set_callbacks(w.surface, &callbacks, &w);
+ attach(&w);
pump(loop, d, NULL);
assert(w.draws && !w.surface);
yasl_display_close(d);
diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c
new file mode 100644
index 0000000..65eebcd
--- /dev/null
+++ b/tests/common/test_contracts.c
@@ -0,0 +1,175 @@
+#include "common/backend.h"
+#include "common/input.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void test_queue(void)
+{
+ struct input_queue q = {0};
+ struct yasl_surface surface = {0};
+ const enum yasl_input_kind kinds[] = {
+ YASL_INPUT_TEXT, YASL_INPUT_PASTE, YASL_INPUT_DROP_FILES,
+ };
+ for (size_t k = 0; k < sizeof(kinds) / sizeof(kinds[0]); k++) {
+ char *source = malloc(4);
+ assert(source);
+ memcpy(source, "a\0b", 4);
+ struct yasl_input_event in = {.surface = &surface, .kind = kinds[k]};
+ if (in.kind == YASL_INPUT_TEXT) { in.text.text = source; in.text.len = 3; }
+ if (in.kind == YASL_INPUT_PASTE) { in.paste.text = source; in.paste.len = 3; }
+ if (in.kind == YASL_INPUT_DROP_FILES) { in.drop_files.files = source; in.drop_files.len = 3; }
+ assert(input_queue_push(&q, &in));
+ memset(source, 'x', 4);
+ free(source);
+ struct yasl_input_event out;
+ assert(input_queue_pop(&q, &out));
+ const char *payload = out.kind == YASL_INPUT_TEXT ? out.text.text :
+ out.kind == YASL_INPUT_PASTE ? out.paste.text : out.drop_files.files;
+ assert(out.surface == &surface && out.kind == kinds[k]);
+ assert(memcmp(payload, "a\0b", 4) == 0);
+ assert(!input_queue_pop(&q, &out));
+ assert(memcmp(payload, "a\0b", 4) == 0);
+ /* Growth/wraparound must not invalidate a previously polled payload. */
+ for (unsigned i = 0; i < 4096; i++) {
+ in = (struct yasl_input_event){.kind = YASL_INPUT_KEY, .key.key = i};
+ assert(input_queue_push(&q, &in));
+ }
+ assert(memcmp(payload, "a\0b", 4) == 0);
+ for (unsigned i = 0; i < 2048; i++) {
+ assert(input_queue_pop(&q, &out));
+ assert(out.key.key == i);
+ }
+ for (unsigned i = 4096; i < 6144; i++) {
+ in.key.key = i;
+ assert(input_queue_push(&q, &in));
+ }
+ for (unsigned i = 2048; i < 6144; i++) {
+ assert(input_queue_pop(&q, &out));
+ assert(out.key.key == i);
+ }
+ assert(!input_queue_pop(&q, &out));
+ }
+ struct yasl_input_event empty = {.surface = &surface, .kind = YASL_INPUT_PASTE};
+ struct yasl_input_event out;
+ assert(input_queue_push(&q, &empty));
+ assert(input_queue_pop(&q, &out));
+ assert(out.paste.text && !out.paste.len && out.paste.text[0] == '\0');
+ struct yasl_input_event in = {.surface = &surface, .kind = YASL_INPUT_TEXT,
+ .text = {"owned", 5}};
+ assert(input_queue_push(&q, &in));
+ assert(input_queue_pop(&q, &out));
+ const char *last = out.text.text;
+ assert(input_queue_push(&q, &in));
+ /* Exercise the allocation-failure reset path without allocating huge RAM. */
+ in.text.len = SIZE_MAX;
+ assert(!input_queue_push(&q, &in));
+ assert(strcmp(last, "owned") == 0);
+ in = (struct yasl_input_event){.surface = &surface, .kind = YASL_INPUT_KEY};
+ assert(input_queue_push(&q, &in));
+ assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET);
+ assert(out.surface == &surface);
+ assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_KEY);
+ assert(!input_queue_pop(&q, &out));
+ in = (struct yasl_input_event){.kind = YASL_INPUT_PASTE, .paste = {"pending", 7}};
+ assert(input_queue_push(&q, &in));
+ input_queue_clear(&q);
+ input_queue_clear(&q);
+ assert(!q.events && !q.last_text && !q.count && !q.reset_pending);
+}
+
+static int presents, copies, pastes, creates;
+static struct yasl_damage_list received;
+static bool present(struct yasl_surface *s, const struct yasl_framebuffer *fb,
+ const struct yasl_damage_list *damage)
+{
+ (void)s; (void)fb;
+ presents++;
+ received = *damage;
+ return true;
+}
+static uint32_t caps(const struct yasl_display *d) { (void)d; return 0; }
+static void copy(struct yasl_display *d, enum yasl_selection sel, const char *text)
+{ (void)d; (void)sel; (void)text; copies++; }
+static void paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *s)
+{ (void)d; (void)sel; (void)s; pastes++; }
+static struct yasl_surface *create(struct yasl_display *d, const struct yasl_surface_spec *spec)
+{ (void)d; (void)spec; creates++; return NULL; }
+
+static void test_validation(void)
+{
+ const struct yasl_backend_ops ops = {.surface_present = present,
+ .display_caps = caps, .display_copy = copy, .display_paste = paste,
+ .surface_create_toplevel = create};
+ struct yasl_display d = {.ops = &ops};
+ struct yasl_surface s = {.display = &d, .frame_generation = 1};
+ struct yasl_framebuffer fb = {.generation = 1, .size = {100, 80}};
+ struct yasl_damage_list damage = {.n = -2};
+ assert(!yasl_surface_present(&s, &fb, &damage));
+ damage.n = YASL_DAMAGE_MAX + 1;
+ assert(!yasl_surface_present(&s, &fb, &damage));
+ damage.n = 1;
+ damage.rects[0] = (struct yasl_rect2i){0, 0, -1, 1};
+ assert(!yasl_surface_present(&s, &fb, &damage));
+ assert(!yasl_surface_present(&s, &fb, NULL));
+ assert(!presents);
+ damage.n = -1;
+ fb.generation = 0;
+ assert(!yasl_surface_present(&s, &fb, &damage));
+ fb.generation = 2;
+ assert(!yasl_surface_present(&s, &fb, &damage));
+ fb.generation = 1;
+ damage.n = 4;
+ damage.rects[0] = (struct yasl_rect2i){-10, -10, 30, 30};
+ damage.rects[1] = (struct yasl_rect2i){90, 70, INT_MAX, INT_MAX};
+ damage.rects[2] = (struct yasl_rect2i){INT_MAX, INT_MAX, INT_MAX, INT_MAX};
+ damage.rects[3] = (struct yasl_rect2i){INT_MIN, INT_MIN, INT_MAX, INT_MAX};
+ assert(yasl_surface_present(&s, &fb, &damage));
+ assert(received.n == 2 && received.rects[0].x == 0 && received.rects[0].width == 20);
+ assert(received.rects[1].width == 10 && received.rects[1].height == 10);
+ assert(damage.n == 4 && damage.rects[0].x == -10);
+ damage.n = -1;
+ assert(yasl_surface_present(&s, &fb, &damage) && received.n == -1);
+ damage.n = 0;
+ assert(yasl_surface_present(&s, &fb, &damage) && received.n == 0);
+ yasl_display_copy(&d, YASL_PRIMARY, "no alias");
+ yasl_display_copy(&d, (enum yasl_selection)255, "invalid");
+ yasl_display_paste(&d, YASL_PRIMARY, &s);
+ yasl_display_paste(&d, (enum yasl_selection)255, &s);
+ assert(!copies && !pastes);
+ yasl_display_copy(&d, YASL_CLIPBOARD, "yes");
+ yasl_display_paste(&d, YASL_CLIPBOARD, &s);
+ assert(copies == 1 && pastes == 1);
+ struct yasl_surface_spec spec = {.size = {100, 80}, .graphics_device = &d};
+ assert(!yasl_surface_create_toplevel(&d, &spec) && errno == ENOTSUP);
+ spec.graphics_device = NULL;
+ spec.size.x = 0;
+ assert(!yasl_surface_create_toplevel(&d, &spec) && errno == EINVAL);
+ assert(!creates);
+}
+
+static void test_scale(void)
+{
+ assert(yasl_scale_to_device(-10, 120) == -10);
+ assert(yasl_scale_to_device(-0.5, 120) == -1);
+ assert(yasl_scale_to_device(0.5, 120) == 1);
+ assert(yasl_scale_to_device(NAN, 120) == 0);
+ assert(yasl_scale_to_device(INFINITY, 120) == 0);
+ assert(yasl_scale_to_device(DBL_MAX, 240) == INT_MAX);
+ assert(yasl_scale_to_logical(-1, 240) == -1);
+ assert(yasl_scale_to_logical(1, 240) == 1);
+ assert(yasl_scale_to_logical(INT_MAX, 1) == INT_MAX);
+ assert(yasl_scale_to_logical(INT_MIN, 1) == INT_MIN);
+ assert(yasl_scale_to_logical(1, 0) == 0);
+}
+
+int main(void)
+{
+ test_queue();
+ test_validation();
+ test_scale();
+ return 0;
+}
diff --git a/tests/common/test_input_alloc.c b/tests/common/test_input_alloc.c
new file mode 100644
index 0000000..e311dc0
--- /dev/null
+++ b/tests/common/test_input_alloc.c
@@ -0,0 +1,48 @@
+#include "common/input.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* This executable compiles input.c with malloc redirected here. */
+static int allocations = -1;
+void *yasl_test_malloc(size_t bytes)
+{
+ if (allocations == 0) return NULL;
+ if (allocations > 0) allocations--;
+ return calloc(1, bytes);
+}
+
+int main(void)
+{
+ struct yasl_surface *surface = (struct yasl_surface *)(uintptr_t)1;
+ for (int failure = 0; failure < 2; failure++) {
+ struct input_queue q = {0};
+ struct yasl_input_event in = {.surface = surface, .kind = YASL_INPUT_PASTE,
+ .paste = {"payload", 7}}, out;
+ /* Fail either payload allocation or the initial event-array allocation. */
+ allocations = failure;
+ assert(!input_queue_push(&q, &in));
+ assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET);
+ assert(out.surface == surface);
+ assert(!input_queue_pop(&q, &out));
+ input_queue_clear(&q);
+
+ allocations = -1;
+ assert(input_queue_push(&q, &in));
+ assert(input_queue_pop(&q, &out));
+ const char *last = out.paste.text;
+ for (size_t i = 0; i < q.capacity; i++) assert(input_queue_push(&q, &in));
+ allocations = failure;
+ assert(!input_queue_push(&q, &in));
+ assert(strcmp(last, "payload") == 0);
+ assert(!q.count && q.reset_pending);
+ allocations = -1;
+ in = (struct yasl_input_event){.surface = surface, .kind = YASL_INPUT_KEY,
+ .key.action = YASL_KEY_RELEASE};
+ assert(input_queue_push(&q, &in));
+ assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET);
+ assert(input_queue_pop(&q, &out) && out.key.action == YASL_KEY_RELEASE);
+ input_queue_clear(&q);
+ }
+ return 0;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 950f5f5..1fcac3e 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -12,6 +12,22 @@ add_test_setup('msan')
test('basic', test_exe)
+contracts_test = executable(
+ 'yasl-contracts-test',
+ 'common/test_contracts.c',
+ include_directories: inc,
+ dependencies: yasl_dep,
+)
+test('contracts', contracts_test)
+
+input_alloc_test = executable(
+ 'yasl-input-alloc-test',
+ ['common/test_input_alloc.c', '../src/common/input.c'],
+ include_directories: inc,
+ c_args: ['-Dmalloc=yasl_test_malloc'],
+)
+test('input-allocation-failure', input_alloc_test)
+
selection_test = executable(
'yasl-selection-test',
'common/test_selection.c',
@@ -19,12 +35,24 @@ selection_test = executable(
)
test('selection', selection_test)
+if has_wayland
+ ime_test = executable('yasl-ime-test', 'wayland/test_input.c',
+ include_directories: inc,
+ dependencies: yasl_dep,
+ )
+ test('ime-ownership', ime_test)
+endif
+
if get_option('integration_tests')
if has_cocoa
native_test = executable('yasl-native-test', 'cocoa/test_native.c',
dependencies: yasl_dep,
)
test('native', native_test, timeout: 20)
+ ownership_test = executable('yasl-ownership-test', 'cocoa/test_ownership.m',
+ dependencies: yasl_dep,
+ )
+ test('native-ownership', ownership_test, timeout: 20)
else
if not has_wayland or not has_x11
error('Linux integration tests require Wayland and X11.')
diff --git a/tests/wayland/test_input.c b/tests/wayland/test_input.c
new file mode 100644
index 0000000..f421e6c
--- /dev/null
+++ b/tests/wayland/test_input.c
@@ -0,0 +1,26 @@
+#include "wayland/internal.h"
+#include <assert.h>
+
+int main(void)
+{
+ struct wayland_surface w = {0};
+ struct seat seat = {.ime_focus = &w};
+ wayland_surface_set_callbacks(&w, NULL, NULL);
+ for (int i = 0; i < 600; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "commit-%d", i);
+ text_input_listener.commit_string(&seat, NULL, text);
+ text_input_listener.done(&seat, NULL, (uint32_t)i);
+ assert(!w.ime_commit);
+ }
+ for (int i = 0; i < 600; i++) {
+ char text[32];
+ snprintf(text, sizeof(text), "commit-%d", i);
+ struct yasl_input_event event;
+ assert(wayland_surface_input_poll(&w, &event));
+ assert(event.kind == YASL_INPUT_TEXT && event.surface == &w.base);
+ assert(strcmp(event.text.text, text) == 0);
+ }
+ input_queue_clear(&w.input);
+ return 0;
+}