summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/tablet.c355
-rw-r--r--include/yasl/native/cocoa.h30
-rw-r--r--include/yasl/native/wayland.h22
-rw-r--r--include/yasl/native/x11.h22
-rw-r--r--meson.build10
-rw-r--r--src/cocoa/window.m84
-rw-r--r--src/common/native.c36
-rw-r--r--src/wayland/api.h1
-rw-r--r--src/x11/window.c72
-rw-r--r--tests/cocoa/test_cursor.m53
-rw-r--r--tests/cocoa/test_ownership.m4
-rw-r--r--tests/cocoa/test_tablet.m123
-rw-r--r--tests/common/test_backends.c18
-rw-r--r--tests/common/test_tablet_example.c53
-rw-r--r--tests/helpers/run_x11_tablet.py124
-rw-r--r--tests/meson.build16
-rw-r--r--tests/test_api.cpp19
-rw-r--r--tests/wayland/test_cursor.c118
-rw-r--r--tests/wayland/test_tablet.c62
-rw-r--r--tests/x11/inputtest-protocol.h92
-rw-r--r--tests/x11/test_tablet.c103
-rw-r--r--tests/x11/test_tablet_native.c180
22 files changed, 1584 insertions, 13 deletions
diff --git a/examples/tablet.c b/examples/tablet.c
new file mode 100644
index 0000000..4d7a8bd
--- /dev/null
+++ b/examples/tablet.c
@@ -0,0 +1,355 @@
+/* Pressure-sensitive drawing using only YASL's portable API. */
+#include <yasl/yasl.h>
+
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define PAPER 0xfff5f2eau
+#define INK 0xff315fadu
+#define PANEL 0xff172338u
+#define TEXT 0xffe5edf8u
+#define ACCENT 0xff59d5b6u
+#define HEADER 104
+
+struct app {
+ bool running, smoke_test, failed, tablet_supported;
+ bool down, visible, previous;
+ enum yasl_backend backend;
+ struct yasl_mouse_event pointer;
+ double last_x, last_y, last_radius;
+ uint32_t *canvas;
+ struct yasl_geometry geometry;
+};
+
+static const char *backend_name(enum yasl_backend backend)
+{
+ switch (backend) {
+ case YASL_BACKEND_WAYLAND: return "WAYLAND";
+ case YASL_BACKEND_X11: return "X11";
+ case YASL_BACKEND_COCOA: return "COCOA";
+ default: return "AUTO";
+ }
+}
+
+static const char *device_name(enum yasl_pointer_device device)
+{
+ switch (device) {
+ case YASL_PTR_DEV_PEN: return "PEN";
+ case YASL_PTR_DEV_ERASER: return "ERASER";
+ case YASL_PTR_DEV_TOUCH: return "TOUCH";
+ default: return "MOUSE";
+ }
+}
+
+static bool is_pen(enum yasl_pointer_device device)
+{
+ return device == YASL_PTR_DEV_PEN || device == YASL_PTR_DEV_ERASER;
+}
+
+static double scale(const struct app *app)
+{
+ return (double)app->geometry.scale_120 / YASL_SCALE_UNIT;
+}
+
+static void cancel_stroke(struct app *app)
+{
+ app->down = app->previous = app->visible = false;
+ app->pointer.pen = (struct yasl_pointer_pen){0};
+}
+
+static bool resize_canvas(struct app *app, struct yasl_geometry geometry)
+{
+ int w = geometry.size.x, h = geometry.size.y;
+ if (w <= 0 || h <= 0 || !geometry.scale_120 ||
+ (size_t)w > SIZE_MAX / sizeof(uint32_t) / (size_t)h) return false;
+ if (app->canvas && w == app->geometry.size.x && h == app->geometry.size.y &&
+ geometry.scale_120 == app->geometry.scale_120) return true;
+ uint32_t *pixels = malloc((size_t)w * h * sizeof(*pixels));
+ if (!pixels) return false;
+ double ratio = (double)app->geometry.scale_120 / geometry.scale_120;
+ for (int y = 0; y < h; y++) {
+ int old_y = (int)(y * ratio);
+ for (int x = 0; x < w; x++) {
+ int old_x = (int)(x * ratio);
+ pixels[(size_t)y * w + x] = app->canvas &&
+ old_x < app->geometry.size.x && old_y < app->geometry.size.y ?
+ app->canvas[(size_t)old_y * app->geometry.size.x + old_x] : PAPER;
+ }
+ }
+ free(app->canvas);
+ app->canvas = pixels;
+ app->geometry = geometry;
+ cancel_stroke(app);
+ return true;
+}
+
+static void clear_canvas(struct app *app)
+{
+ for (size_t i = 0, n = (size_t)app->geometry.size.x * app->geometry.size.y; i < n; i++)
+ app->canvas[i] = PAPER;
+ app->previous = false;
+}
+
+static double brush_radius(const struct app *app)
+{
+ return scale(app) * (is_pen(app->pointer.device) ?
+ 2 + 24 * ((double)app->pointer.pen.pressure / 65535) : 6);
+}
+
+static void stamp(struct app *app, double cx, double cy, double radius)
+{
+ int w = app->geometry.size.x, h = app->geometry.size.y;
+ int top = yasl_scale_to_device(HEADER, app->geometry.scale_120);
+ int x0 = (int)fmax(0, fmin(w, floor(cx - radius)));
+ int y0 = (int)fmax(top, fmin(h, floor(cy - radius)));
+ int x1 = (int)fmax(0, fmin(w, ceil(cx + radius)));
+ int y1 = (int)fmax(0, fmin(h, ceil(cy + radius)));
+ uint32_t color = app->pointer.device == YASL_PTR_DEV_ERASER ? PAPER : INK;
+ for (int y = y0; y < y1; y++)
+ for (int x = x0; x < x1; x++) {
+ double dx = x + 0.5 - cx, dy = y + 0.5 - cy;
+ if (dx * dx + dy * dy <= radius * radius)
+ app->canvas[(size_t)y * w + x] = color;
+ }
+}
+
+static void stroke(struct app *app)
+{
+ double x = app->pointer.pos.x, y = app->pointer.pos.y;
+ double radius = brush_radius(app);
+ /* Leaving the canvas breaks the line so re-entry cannot bridge the UI. */
+ if (x < 0 || x >= app->geometry.size.x ||
+ y < HEADER * scale(app) || y >= app->geometry.size.y) {
+ app->previous = false;
+ return;
+ }
+ if (app->previous) {
+ double dx = x - app->last_x, dy = y - app->last_y;
+ int steps = (int)ceil(hypot(dx, dy));
+ for (int i = 1; i <= steps; i++) {
+ double t = (double)i / steps;
+ stamp(app, app->last_x + dx * t, app->last_y + dy * t,
+ app->last_radius + (radius - app->last_radius) * t);
+ }
+ }
+ /* Also stamp pressure-only motion, even when position has not changed. */
+ stamp(app, x, y, radius);
+ app->last_x = x;
+ app->last_y = y;
+ app->last_radius = radius;
+ app->previous = true;
+}
+
+static void input(struct app *app, const struct yasl_input_event *event)
+{
+ if (event->kind == YASL_INPUT_RESET ||
+ (event->kind == YASL_INPUT_FOCUS && !event->focus.focused)) {
+ cancel_stroke(app);
+ } else if (event->kind == YASL_INPUT_KEY && event->key.action == YASL_KEY_PRESS) {
+ if (event->key.key == 0xff1b) app->running = false;
+ if (event->key.key == 'c' || event->key.key == 'C' || event->key.key == ' ')
+ clear_canvas(app);
+ } else if (event->kind == YASL_INPUT_MOUSE) {
+ const struct yasl_mouse_event *pointer = &event->mouse;
+ if (pointer->kind == YASL_PTR_SCROLL) return;
+ if (app->pointer.device != pointer->device) cancel_stroke(app);
+ app->pointer = *pointer;
+ app->visible = pointer->kind != YASL_PTR_LEAVE;
+ if (pointer->kind == YASL_PTR_LEAVE) cancel_stroke(app);
+ else if (pointer->kind == YASL_PTR_PRESS && pointer->button == 0) {
+ app->down = true;
+ app->previous = false;
+ stroke(app);
+ } else if (pointer->kind == YASL_PTR_RELEASE && pointer->button == 0) {
+ app->down = app->previous = false;
+ app->pointer.pen.pressure = 0;
+ } else if (pointer->kind == YASL_PTR_MOTION && app->down) stroke(app);
+ }
+}
+
+/* Tiny built-in 5x7 font keeps this example independent of a text library. */
+static const uint8_t glyphs[128][7] = {
+ ['A']={14,17,17,31,17,17,17}, ['B']={30,17,17,30,17,17,30},
+ ['C']={14,17,16,16,16,17,14}, ['D']={30,17,17,17,17,17,30},
+ ['E']={31,16,16,30,16,16,31}, ['F']={31,16,16,30,16,16,16},
+ ['G']={14,17,16,23,17,17,14}, ['H']={17,17,17,31,17,17,17},
+ ['I']={14,4,4,4,4,4,14}, ['J']={7,2,2,2,2,18,12},
+ ['K']={17,18,20,24,20,18,17}, ['L']={16,16,16,16,16,16,31},
+ ['M']={17,27,21,21,17,17,17}, ['N']={17,25,21,19,17,17,17},
+ ['O']={14,17,17,17,17,17,14}, ['P']={30,17,17,30,16,16,16},
+ ['Q']={14,17,17,17,21,18,13}, ['R']={30,17,17,30,20,18,17},
+ ['S']={15,16,16,14,1,1,30}, ['T']={31,4,4,4,4,4,4},
+ ['U']={17,17,17,17,17,17,14}, ['V']={17,17,17,17,17,10,4},
+ ['W']={17,17,17,21,21,21,10}, ['X']={17,17,10,4,10,17,17},
+ ['Y']={17,17,10,4,4,4,4}, ['Z']={31,1,2,4,8,16,31},
+ ['0']={14,17,19,21,25,17,14}, ['1']={4,12,4,4,4,4,14},
+ ['2']={14,17,1,2,4,8,31}, ['3']={30,1,1,14,1,1,30},
+ ['4']={2,6,10,18,31,2,2}, ['5']={31,16,16,30,1,1,30},
+ ['6']={14,16,16,30,17,17,14}, ['7']={31,1,2,4,8,8,8},
+ ['8']={14,17,17,14,17,17,14}, ['9']={14,17,17,15,1,1,14},
+ ['-']={0,0,0,31,0,0,0}, ['+']={0,4,4,31,4,4,0},
+ ['/']={1,1,2,4,8,16,16}, [':']={0,4,4,0,4,4,0},
+ ['.']={0,0,0,0,0,4,4}, ['%']={17,2,4,4,8,16,17},
+};
+
+static void rect(struct yasl_framebuffer *fb, int x, int y, int w, int h, uint32_t color)
+{
+ int right = x + w < fb->size.x ? x + w : fb->size.x;
+ int bottom = y + h < fb->size.y ? y + h : fb->size.y;
+ for (int j = y > 0 ? y : 0; j < bottom; j++)
+ for (int i = x > 0 ? x : 0; i < right; i++)
+ fb->pixels[(size_t)j * fb->size.x + i] = color;
+}
+
+static void text(struct yasl_framebuffer *fb, double dpi, int x, int y,
+ const char *str, uint32_t color)
+{
+ for (; *str; str++, x += 12) {
+ unsigned ch = (unsigned char)*str;
+ if (ch >= 128) continue;
+ for (int row = 0; row < 7; row++)
+ for (int col = 0; col < 5; col++) {
+ if (!(glyphs[ch][row] & (1 << (4 - col)))) continue;
+ int left = (int)lround((x + col * 2) * dpi);
+ int top = (int)lround((y + row * 2) * dpi);
+ rect(fb, left, top,
+ (int)lround((x + col * 2 + 2) * dpi) - left,
+ (int)lround((y + row * 2 + 2) * dpi) - top, color);
+ }
+ }
+}
+
+static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud)
+{
+ struct app *app = ud;
+ struct yasl_geometry geometry = yasl_surface_geometry(surface);
+ geometry.size = frame->target.size;
+ if (!resize_canvas(app, geometry)) {
+ fprintf(stderr, "Cannot allocate drawing canvas\n");
+ app->running = false;
+ app->failed = true;
+ return;
+ }
+ struct yasl_framebuffer *fb = &frame->target;
+ memcpy(fb->pixels, app->canvas, (size_t)geometry.size.x * geometry.size.y * sizeof(uint32_t));
+ double dpi = scale(app);
+ rect(fb, 0, 0, fb->size.x, (int)lround(HEADER * dpi), PANEL);
+ char line[128];
+ snprintf(line, sizeof(line), "YASL TABLET / %s / %s / %s", backend_name(app->backend),
+ device_name(app->pointer.device), app->down ? "CONTACT" : "HOVER");
+ text(fb, dpi, 16, 12, line, TEXT);
+ if (app->tablet_supported)
+ snprintf(line, sizeof(line), "PRESSURE %5u / 65535 %5.1f%%", app->pointer.pen.pressure,
+ app->pointer.pen.pressure * 100.0 / 65535);
+ else snprintf(line, sizeof(line), "TABLET PROTOCOL UNAVAILABLE / MOUSE DRAWING");
+ text(fb, dpi, 16, 36, line, ACCENT);
+ int bar_width = (int)lround(220 * dpi);
+ rect(fb, (int)(16 * dpi), (int)(58 * dpi), bar_width, (int)(6 * dpi), 0xff34455eu);
+ rect(fb, (int)(16 * dpi), (int)(58 * dpi),
+ (int)((uint32_t)app->pointer.pen.pressure * (uint64_t)bar_width / 65535), (int)(6 * dpi), ACCENT);
+ snprintf(line, sizeof(line), "TILT X %+d / Y %+d", app->pointer.pen.tilt_x, app->pointer.pen.tilt_y);
+ text(fb, dpi, 260, 54, line, TEXT);
+ text(fb, dpi, 16, 82, "DRAW WITH TIP / FLIP TO ERASE / C CLEAR / ESC QUIT", TEXT);
+ if (app->visible && app->pointer.pos.x >= 0 && app->pointer.pos.x < fb->size.x &&
+ app->pointer.pos.y >= HEADER * dpi && app->pointer.pos.y < fb->size.y) {
+ double radius = brush_radius(app);
+ double cx = app->pointer.pos.x, cy = app->pointer.pos.y;
+ /* Brush outline and tilt vector are overlays, never stored in canvas. */
+ for (int i = 0; i < 180; i++) {
+ double angle = i * (6.283185307179586 / 180);
+ rect(fb, (int)lround(cx + cos(angle) * radius),
+ (int)lround(cy + sin(angle) * radius), 1, 1, PANEL);
+ }
+ double tx = app->pointer.pen.tilt_x * dpi / 2;
+ double ty = app->pointer.pen.tilt_y * dpi / 2;
+ for (int i = 0; i <= 64; i++)
+ rect(fb, (int)lround(cx + tx * i / 64), (int)lround(cy + ty * i / 64), 2, 2, ACCENT);
+ }
+ struct yasl_damage_list damage = {.n = -1};
+ if (!yasl_surface_present(surface, fb, &damage)) {
+ app->failed = true;
+ app->running = false;
+ }
+ if (app->smoke_test) app->running = false;
+}
+
+static void configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud)
+{
+ struct app *app = ud;
+ if (!resize_canvas(app, geometry)) {
+ app->failed = true;
+ app->running = false;
+ }
+ yasl_surface_schedule_redraw(surface);
+}
+
+static void close_window(struct yasl_surface *surface, void *ud)
+{
+ (void)surface;
+ ((struct app *)ud)->running = false;
+}
+
+static void quit(void *ud) { ((struct app *)ud)->running = false; }
+static void lost(void *ud)
+{
+ struct app *app = ud;
+ app->failed = true;
+ app->running = false;
+}
+
+int main(int argc, char **argv)
+{
+ struct app app = {.running = true};
+ enum yasl_backend backend = YASL_BACKEND_AUTO;
+ for (int i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--smoke-test")) app.smoke_test = true;
+ else if (!strcmp(argv[i], "--backend") && i + 1 < argc) {
+ const char *name = argv[++i];
+ if (!strcmp(name, "auto")) backend = YASL_BACKEND_AUTO;
+ else if (!strcmp(name, "wayland")) backend = YASL_BACKEND_WAYLAND;
+ else if (!strcmp(name, "x11")) backend = YASL_BACKEND_X11;
+ else if (!strcmp(name, "cocoa")) backend = YASL_BACKEND_COCOA;
+ else { fprintf(stderr, "Unknown backend: %s\n", name); return 1; }
+ } else {
+ bool help = !strcmp(argv[i], "--help");
+ fprintf(help ? stdout : stderr,
+ "Usage: %s [--backend auto|wayland|x11|cocoa] [--smoke-test]\n", argv[0]);
+ return help ? 0 : 1;
+ }
+ }
+ struct yasl_display_cbs display_cbs = {.quit = quit, .lost = lost};
+ struct yasl_display_spec display_spec = {
+ .app_id = "org.yasl.tablet", .backend = backend, .cbs = &display_cbs, .ud = &app,
+ };
+ struct yasl_display *display = yasl_display_open(NULL, &display_spec);
+ if (!display) { perror("Cannot open display"); return 1; }
+ app.backend = yasl_display_backend(display);
+ app.tablet_supported = (yasl_display_caps(display) & YASL_CAP_TABLET) != 0;
+ struct yasl_surface_spec spec = {.title = "YASL Tablet - pressure and tilt", .size = {900, 640}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ if (!surface) { perror("Cannot create surface"); yasl_display_close(display); return 1; }
+ struct yasl_surface_cbs callbacks = {.draw = draw, .configure = configure, .close = close_window};
+ yasl_surface_set_callbacks(surface, &callbacks, &app);
+ yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN);
+ yasl_surface_schedule_redraw(surface);
+ printf("YASL tablet on %s: pressure changes brush width; eraser removes ink.\n"
+ "Mouse: fixed-width drawing. C/Space: clear. Escape: quit.\n",
+ backend_name(app.backend));
+ if (!app.tablet_supported)
+ printf("This display server does not expose tablet input.\n");
+ while (app.running) {
+ if (yasl_display_dispatch(display, -1) < 0) { app.failed = true; break; }
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {
+ input(&app, &event);
+ yasl_surface_schedule_redraw(surface);
+ }
+ yasl_display_flush(display);
+ }
+ free(app.canvas);
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ return app.failed ? 1 : 0;
+}
diff --git a/include/yasl/native/cocoa.h b/include/yasl/native/cocoa.h
new file mode 100644
index 0000000..9066a19
--- /dev/null
+++ b/include/yasl/native/cocoa.h
@@ -0,0 +1,30 @@
+#ifndef YASL_NATIVE_COCOA_H
+#define YASL_NATIVE_COCOA_H
+
+#include "yasl/yasl.h"
+
+#ifdef __OBJC__
+@class NSApplication;
+@class NSWindow;
+@class NSView;
+#else
+typedef struct objc_object NSApplication;
+typedef struct objc_object NSWindow;
+typedef struct objc_object NSView;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Return borrowed Cocoa handles (expire on display/surface destruction).
+ * Return NULL / nil if display/surface is not a Cocoa object or destroyed. */
+YASL_API NSApplication *yasl_cocoa_display_get_application(struct yasl_display *d);
+YASL_API NSWindow *yasl_cocoa_surface_get_window(struct yasl_surface *s);
+YASL_API NSView *yasl_cocoa_surface_get_view(struct yasl_surface *s);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YASL_NATIVE_COCOA_H */
diff --git a/include/yasl/native/wayland.h b/include/yasl/native/wayland.h
new file mode 100644
index 0000000..4a759d4
--- /dev/null
+++ b/include/yasl/native/wayland.h
@@ -0,0 +1,22 @@
+#ifndef YASL_NATIVE_WAYLAND_H
+#define YASL_NATIVE_WAYLAND_H
+
+#include "yasl/yasl.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct wl_display;
+struct wl_surface;
+
+/* Return borrowed Wayland handles (expire on display/surface destruction).
+ * Return NULL if display/surface is not a Wayland object or destroyed. */
+YASL_API struct wl_display *yasl_wayland_display_get_wl_display(struct yasl_display *d);
+YASL_API struct wl_surface *yasl_wayland_surface_get_wl_surface(struct yasl_surface *s);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YASL_NATIVE_WAYLAND_H */
diff --git a/include/yasl/native/x11.h b/include/yasl/native/x11.h
new file mode 100644
index 0000000..c0105cb
--- /dev/null
+++ b/include/yasl/native/x11.h
@@ -0,0 +1,22 @@
+#ifndef YASL_NATIVE_X11_H
+#define YASL_NATIVE_X11_H
+
+#include "yasl/yasl.h"
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct _XDisplay Display;
+
+/* Return borrowed X11 handles (expire on display/surface destruction).
+ * Return NULL / 0 if display/surface is not an X11 object or destroyed. */
+YASL_API Display *yasl_x11_display_get_display(struct yasl_display *d);
+YASL_API uintptr_t yasl_x11_surface_get_window(struct yasl_surface *s);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YASL_NATIVE_X11_H */
diff --git a/meson.build b/meson.build
index c258765..6673240 100644
--- a/meson.build
+++ b/meson.build
@@ -83,6 +83,16 @@ install_headers(
subdir: 'yasl',
)
+if has_wayland
+ install_headers('include/yasl/native/wayland.h', subdir: 'yasl/native')
+endif
+if has_x11
+ install_headers('include/yasl/native/x11.h', subdir: 'yasl/native')
+endif
+if has_cocoa
+ install_headers('include/yasl/native/cocoa.h', subdir: 'yasl/native')
+endif
+
pkg = import('pkgconfig')
pkg.generate(
yasl_lib.get_shared_lib(),
diff --git a/src/cocoa/window.m b/src/cocoa/window.m
index 6235b77..7b59865 100644
--- a/src/cocoa/window.m
+++ b/src/cocoa/window.m
@@ -3,6 +3,7 @@
#import <QuartzCore/QuartzCore.h>
#include "cocoa/api.h"
+#include "yasl/native/cocoa.h"
#include "common/timing.h"
#include "common/loop.h"
#include "cocoa/keycodes.h"
@@ -112,6 +113,14 @@ struct cocoa_display {
YaslAppDelegate *delegate;
NSPoint cascade;
struct cocoa_surface *dead;
+ struct {
+ bool active;
+ struct cocoa_surface *origin;
+ struct cocoa_surface *carried;
+ struct yasl_vector2i carry_offset;
+ struct yasl_drag_listener listener;
+ void *ud;
+ } drag;
int depth;
bool open;
bool launched;
@@ -121,6 +130,18 @@ struct cocoa_display {
static struct cocoa_display darwin_ctx;
+static void drag_end(struct cocoa_display *d, enum yasl_drag_end how)
+{
+ if (!d || !d->drag.active) return;
+ struct yasl_drag_listener l = d->drag.listener;
+ void *ud = d->drag.ud;
+ d->drag.active = false;
+ d->drag.origin = NULL;
+ d->drag.carried = NULL;
+ if (l.end)
+ l.end(ud, how);
+}
+
static void render_and_commit(struct cocoa_surface *w);
static void reap_dead(struct cocoa_display *d);
@@ -341,6 +362,11 @@ static void push_pointer(struct cocoa_surface *w, NSEvent *ev, enum yasl_pointer
},
};
input_queue_push(&w->input, &input_ev);
+ if (kind == YASL_PTR_MOTION && darwin_ctx.drag.active && darwin_ctx.drag.carried) {
+ cocoa_surface_place_under_pointer(darwin_ctx.drag.carried, darwin_ctx.drag.carry_offset);
+ } else if (kind == YASL_PTR_RELEASE && darwin_ctx.drag.active) {
+ drag_end(&darwin_ctx, YASL_DRAG_DROPPED);
+ }
if (proximity) {
w->tablet_device = YASL_PTR_DEV_MOUSE;
w->tablet_down = false;
@@ -1079,6 +1105,7 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya
void cocoa_display_close(struct cocoa_display *d)
{
if (!d) return;
+ drag_end(d, YASL_DRAG_CANCELLED);
assert(!d->count && "destroy every surface before its display");
reap_dead(d);
if (d->key_up_monitor) {
@@ -1101,7 +1128,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 | YASL_CAP_NATIVE_GRAPHICS |
+ return d ? (YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
+ YASL_CAP_CSD | YASL_CAP_NATIVE_GRAPHICS |
YASL_CAP_FRAME_PACING | YASL_CAP_TABLET) : 0;
}
@@ -1220,6 +1248,8 @@ void cocoa_surface_destroy(struct cocoa_surface *w)
{
assert([NSThread isMainThread]);
if (!w || w->destroyed) return;
+ if (darwin_ctx.drag.active && (darwin_ctx.drag.origin == w || darwin_ctx.drag.carried == w))
+ drag_end(&darwin_ctx, YASL_DRAG_CANCELLED);
darwin_ctx.depth++;
bool native_closed = w->closed;
static const struct yasl_surface_cbs no_callbacks = {0};
@@ -1374,22 +1404,40 @@ bool cocoa_surface_popup_reposition(struct cocoa_surface *popup,
return false;
}
+bool cocoa_surface_start_interactive_move(struct cocoa_surface *s)
+{
+ if (!s || !s->window) return false;
+ [s->window performWindowDragWithEvent:[NSApp currentEvent]];
+ return true;
+}
+
bool cocoa_display_drag_start(struct cocoa_display *d, struct cocoa_surface *origin,
const struct yasl_drag_image *image,
const struct yasl_drag_listener *l, void *ud)
{
- (void)d; (void)origin; (void)image; (void)l; (void)ud;
- return false;
+ (void)image;
+ if (!d || !origin || d->drag.active || !l)
+ return false;
+ d->drag.active = true;
+ d->drag.origin = origin;
+ d->drag.carried = NULL;
+ d->drag.listener = *l;
+ d->drag.ud = ud;
+ return true;
}
-void cocoa_display_drag_cancel(struct cocoa_display *d) { (void)d; }
-bool cocoa_surface_drag_carry(struct cocoa_surface *w, struct yasl_vector2i offset) { (void)w; (void)offset; return false; }
+void cocoa_display_drag_cancel(struct cocoa_display *d)
+{
+ drag_end(d, YASL_DRAG_CANCELLED);
+}
-bool cocoa_surface_start_interactive_move(struct cocoa_surface *s)
+bool cocoa_surface_drag_carry(struct cocoa_surface *w, struct yasl_vector2i offset)
{
- if (!s || !s->window) return false;
- [s->window performWindowDragWithEvent:[NSApp currentEvent]];
- return true;
+ if (!w || w->closed || !darwin_ctx.drag.active)
+ return false;
+ darwin_ctx.drag.carried = w;
+ darwin_ctx.drag.carry_offset = offset;
+ return cocoa_surface_place_under_pointer(w, offset);
}
bool cocoa_surface_start_interactive_resize(struct cocoa_surface *s, enum yasl_resize_edge edge)
@@ -1691,3 +1739,21 @@ const struct yasl_backend_ops yasl_cocoa_backend = {
.surface_get_native_handle = impl_surface_get_native_handle,
.surface_get_native_frame = impl_surface_get_native_frame,
};
+
+NSApplication *yasl_cocoa_display_get_application(struct yasl_display *d)
+{
+ return (d && yasl_display_backend(d) == YASL_BACKEND_COCOA) ?
+ (NSApplication *)yasl_display_get_native_handle(d) : nil;
+}
+
+NSWindow *yasl_cocoa_surface_get_window(struct yasl_surface *s)
+{
+ return (s && s->display && yasl_display_backend(s->display) == YASL_BACKEND_COCOA) ?
+ (NSWindow *)yasl_surface_get_native_handle(s) : nil;
+}
+
+NSView *yasl_cocoa_surface_get_view(struct yasl_surface *s)
+{
+ NSWindow *w = yasl_cocoa_surface_get_window(s);
+ return w ? w.contentView : nil;
+}
diff --git a/src/common/native.c b/src/common/native.c
index 2d9c06f..6e1c69a 100644
--- a/src/common/native.c
+++ b/src/common/native.c
@@ -1,5 +1,13 @@
#include "backend.h"
+#if YASL_HAS_WAYLAND
+#include "yasl/native/wayland.h"
+#endif
+
+#if YASL_HAS_X11
+#include "yasl/native/x11.h"
+#endif
+
#include <errno.h>
void *yasl_display_get_native_handle(struct yasl_display *d)
@@ -14,6 +22,34 @@ void *yasl_surface_get_native_handle(struct yasl_surface *s)
s->display->ops->surface_get_native_handle(s) : NULL;
}
+#if YASL_HAS_WAYLAND
+struct wl_display *yasl_wayland_display_get_wl_display(struct yasl_display *d)
+{
+ return (d && yasl_display_backend(d) == YASL_BACKEND_WAYLAND) ?
+ (struct wl_display *)yasl_display_get_native_handle(d) : NULL;
+}
+
+struct wl_surface *yasl_wayland_surface_get_wl_surface(struct yasl_surface *s)
+{
+ return (s && s->display && yasl_display_backend(s->display) == YASL_BACKEND_WAYLAND) ?
+ (struct wl_surface *)yasl_surface_get_native_handle(s) : NULL;
+}
+#endif
+
+#if YASL_HAS_X11
+Display *yasl_x11_display_get_display(struct yasl_display *d)
+{
+ return (d && yasl_display_backend(d) == YASL_BACKEND_X11) ?
+ (Display *)yasl_display_get_native_handle(d) : NULL;
+}
+
+uintptr_t yasl_x11_surface_get_window(struct yasl_surface *s)
+{
+ return (s && s->display && yasl_display_backend(s->display) == YASL_BACKEND_X11) ?
+ (uintptr_t)yasl_surface_get_native_handle(s) : 0;
+}
+#endif
+
struct yasl_surface *yasl_surface_create_native(struct yasl_display *d,
const struct yasl_surface_spec *spec, void *graphics_device)
{
diff --git a/src/wayland/api.h b/src/wayland/api.h
index 87e896d..e68aca7 100644
--- a/src/wayland/api.h
+++ b/src/wayland/api.h
@@ -2,6 +2,7 @@
#define YASL_WAYLAND_API_H
#include "common/backend.h"
+#include "yasl/native/wayland.h"
struct wayland_display;
struct wayland_surface;
diff --git a/src/x11/window.c b/src/x11/window.c
index 85f1d22..5e3dba9 100644
--- a/src/x11/window.c
+++ b/src/x11/window.c
@@ -89,6 +89,14 @@ struct x11_display {
bool xi2;
Atom abs_pressure, abs_tilt_x, abs_tilt_y, tool_type, eraser;
struct x11_pointer *pointers;
+ struct {
+ bool active;
+ struct x11_surface *origin;
+ struct x11_surface *carried;
+ struct yasl_vector2i carry_offset;
+ struct yasl_drag_listener listener;
+ void *ud;
+ } drag;
};
static struct x11_display *display_of(const struct yasl_surface *s)
@@ -96,6 +104,10 @@ static struct x11_display *display_of(const struct yasl_surface *s)
return (struct x11_display *)s->display;
}
+static bool place(struct yasl_surface *base, struct yasl_vector2i at);
+static void drag_end(struct x11_display *d, enum yasl_drag_end how);
+static void drag_cancel(struct yasl_display *base);
+
static void leave(struct x11_display *d)
{
if (--d->depth) return;
@@ -284,6 +296,10 @@ static void xi_event(struct x11_display *d, XGenericEventCookie *cookie)
cookie->evtype != XI_ButtonRelease) return;
Window window = crossing ? ((XIEnterEvent *)cookie->data)->event :
((XIDeviceEvent *)cookie->data)->event;
+ if (cookie->evtype == XI_Motion && d->drag.active && d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
+ else if (cookie->evtype == XI_ButtonRelease && d->drag.active)
+ drag_end(d, YASL_DRAG_DROPPED);
struct x11_surface *s = d->surfaces;
while (s && s->window != window) s = s->next;
if (!s) return;
@@ -357,6 +373,8 @@ static void surface_destroy(struct yasl_surface *base)
struct x11_surface *s = (struct x11_surface *)base;
if (s->destroyed) return;
struct x11_display *d = display_of(base);
+ if (d->drag.active && (d->drag.origin == s || d->drag.carried == s))
+ drag_end(d, YASL_DRAG_CANCELLED);
s->destroyed = true;
for (struct x11_surface **p = &d->surfaces; *p; p = &(*p)->next) {
if (*p == s) { *p = s->next; break; }
@@ -648,6 +666,8 @@ static void dispatch(struct yasl_display *base)
mouse.pos = (struct yasl_vector2i){event.xmotion.x, event.xmotion.y};
mouse.kind = YASL_PTR_MOTION;
state = event.xmotion.state;
+ if (d->drag.active && d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
} else if (event.type == LeaveNotify) {
mouse.pos = (struct yasl_vector2i){event.xcrossing.x, event.xcrossing.y};
mouse.kind = YASL_PTR_LEAVE;
@@ -663,6 +683,8 @@ static void dispatch(struct yasl_display *base)
} else {
mouse.kind = event.type == ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
mouse.button = pointer_button(button);
+ if (event.type == ButtonRelease && d->drag.active)
+ drag_end(d, YASL_DRAG_DROPPED);
}
}
d->mods = mouse.mods = modifiers(state);
@@ -849,6 +871,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");
+ drag_cancel(base);
cancel_pastes(d, NULL);
if (d->loop && d->watch > 0) yasl_loop_del_fd(d->loop, d->watch);
if (d->owns_loop && d->loop) {
@@ -868,7 +891,8 @@ static void display_close(struct yasl_display *base)
static uint32_t caps(const struct yasl_display *d)
{
- return YASL_CAP_PLACE_WINDOW | YASL_CAP_PRIMARY_SELECTION |
+ return YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
+ YASL_CAP_PRIMARY_SELECTION |
(((const struct x11_display *)d)->xi2 ? YASL_CAP_TABLET : 0);
}
@@ -984,6 +1008,49 @@ static bool place(struct yasl_surface *base, struct yasl_vector2i at)
return true;
}
+static void drag_end(struct x11_display *d, enum yasl_drag_end how)
+{
+ if (!d || !d->drag.active) return;
+ struct yasl_drag_listener l = d->drag.listener;
+ void *ud = d->drag.ud;
+ d->drag.active = false;
+ d->drag.origin = NULL;
+ d->drag.carried = NULL;
+ if (l.end)
+ l.end(ud, how);
+}
+
+static bool drag_start(struct yasl_display *base, struct yasl_surface *origin,
+ const struct yasl_drag_image *image,
+ const struct yasl_drag_listener *l, void *ud)
+{
+ (void)image;
+ struct x11_display *d = (struct x11_display *)base;
+ if (!d || !origin || d->drag.active || !l)
+ return false;
+ d->drag.active = true;
+ d->drag.origin = (struct x11_surface *)origin;
+ d->drag.carried = NULL;
+ d->drag.listener = *l;
+ d->drag.ud = ud;
+ return true;
+}
+
+static void drag_cancel(struct yasl_display *base)
+{
+ drag_end((struct x11_display *)base, YASL_DRAG_CANCELLED);
+}
+
+static bool drag_carry(struct yasl_surface *base, struct yasl_vector2i offset)
+{
+ struct x11_display *d = display_of(base);
+ if (!d || !d->drag.active)
+ return false;
+ d->drag.carried = (struct x11_surface *)base;
+ d->drag.carry_offset = offset;
+ return place(base, offset);
+}
+
static bool interactive_move(struct yasl_surface *base)
{
struct x11_surface *s = (struct x11_surface *)base;
@@ -1008,6 +1075,8 @@ const struct yasl_backend_ops yasl_x11_backend = {
.display_modifiers = mods,
.display_copy = copy,
.display_paste = paste,
+ .display_drag_start = drag_start,
+ .display_drag_cancel = drag_cancel,
.surface_create_toplevel = create_toplevel,
.surface_destroy = surface_destroy,
.surface_display = surface_display,
@@ -1021,6 +1090,7 @@ const struct yasl_backend_ops yasl_x11_backend = {
.surface_timing = timing,
.surface_frame_time_ns = frame_time,
.surface_set_cursor = cursor,
+ .surface_drag_carry = drag_carry,
.surface_place_under_pointer = place,
.surface_start_interactive_move = interactive_move,
.display_get_native_handle = native_display,
diff --git a/tests/cocoa/test_cursor.m b/tests/cocoa/test_cursor.m
new file mode 100644
index 0000000..0f3ba06
--- /dev/null
+++ b/tests/cocoa/test_cursor.m
@@ -0,0 +1,53 @@
+#import <AppKit/AppKit.h>
+#include <yasl/native.h>
+#include <assert.h>
+
+static void assert_transparent(NSCursor *cursor)
+{
+ assert(cursor);
+ NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:cursor.image.TIFFRepresentation];
+ assert(bitmap && bitmap.hasAlpha);
+ for (NSInteger y = 0; y < bitmap.pixelsHigh; y++)
+ for (NSInteger x = 0; x < bitmap.pixelsWide; x++)
+ assert([bitmap colorAtX:x y:y].alphaComponent == 0);
+}
+
+int main(void)
+{
+ @autoreleasepool {
+ struct yasl_display *display = yasl_display_open(NULL, NULL);
+ assert(display);
+ struct yasl_surface_spec spec = {.title = "Cursor test", .size = {320, 240}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ assert(surface);
+ NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
+ NSView *view = window.contentView;
+ yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN);
+ [view cursorUpdate:nil];
+ NSCursor *hidden = [NSCursor currentCursor];
+ assert_transparent(hidden);
+
+ NSEvent *motion = [NSEvent mouseEventWithType:NSEventTypeMouseMoved
+ location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0
+ windowNumber:window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0];
+ [view mouseMoved:motion];
+ [view cursorUpdate:nil];
+ assert([NSCursor currentCursor] == hidden);
+ struct yasl_input_event event;
+ bool moved = false;
+ while (yasl_surface_input_poll(surface, &event))
+ if (event.kind == YASL_INPUT_MOUSE && event.mouse.kind == YASL_PTR_MOTION)
+ moved = true;
+ assert(moved);
+
+ [[NSCursor arrowCursor] set]; /* Simulate leaving and re-entering. */
+ [view cursorUpdate:nil];
+ assert([NSCursor currentCursor] == hidden);
+ yasl_surface_set_cursor(surface, YASL_CURSOR_ARROW);
+ [view cursorUpdate:nil];
+ assert([NSCursor currentCursor] == [NSCursor arrowCursor]);
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ }
+ return 0;
+}
diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m
index 168a47b..2b2f52d 100644
--- a/tests/cocoa/test_ownership.m
+++ b/tests/cocoa/test_ownership.m
@@ -1,4 +1,5 @@
#include "yasl/native.h"
+#include "yasl/native/cocoa.h"
#import <AppKit/AppKit.h>
#include "yasl/yasl.h"
#include <assert.h>
@@ -13,7 +14,10 @@ int main(void)
struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec);
assert(s);
assert(yasl_display_get_native_handle(d) == (void *)NSApp);
+ assert(yasl_cocoa_display_get_application(d) == NSApp);
NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(s);
+ assert(yasl_cocoa_surface_get_window(s) == window);
+ assert(yasl_cocoa_surface_get_view(s) == window.contentView);
assert([window isKindOfClass:[NSWindow class]] && window.contentView);
struct yasl_input_event event;
while (yasl_surface_input_poll(s, &event)) {}
diff --git a/tests/cocoa/test_tablet.m b/tests/cocoa/test_tablet.m
new file mode 100644
index 0000000..f1489a1
--- /dev/null
+++ b/tests/cocoa/test_tablet.m
@@ -0,0 +1,123 @@
+#import <AppKit/AppKit.h>
+#include <yasl/native.h>
+#include <assert.h>
+
+/* A strict event double catches querying tablet-only properties on ordinary
+ * mouse events, and proximity-only properties on tablet point events. */
+@interface TabletSample : NSObject
+@property NSEventType type;
+@property NSEventSubtype subtype;
+@property NSEventModifierFlags modifierFlags;
+@property NSPoint locationInWindow;
+@property NSInteger buttonNumber;
+@property NSUInteger buttonMask;
+@property(getter=isEnteringProximity) BOOL enteringProximity;
+@property NSPointingDeviceType tool;
+@property float samplePressure;
+@property NSPoint sampleTilt;
+@end
+
+@implementation TabletSample
+- (float)pressure
+{
+ assert(self.type == NSEventTypeTabletPoint || self.subtype == NSEventSubtypeTabletPoint);
+ return self.samplePressure;
+}
+- (NSPoint)tilt
+{
+ assert(self.type == NSEventTypeTabletPoint || self.subtype == NSEventSubtypeTabletPoint);
+ return self.sampleTilt;
+}
+- (NSPointingDeviceType)pointingDeviceType
+{
+ assert(self.type == NSEventTypeTabletProximity || self.subtype == NSEventSubtypeTabletProximity);
+ return self.tool;
+}
+@end
+
+static struct yasl_mouse_event next(struct yasl_surface *surface)
+{
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event))
+ if (event.kind == YASL_INPUT_MOUSE) return event.mouse;
+ assert(!"Expected a pointer event");
+ return (struct yasl_mouse_event){0};
+}
+
+int main(void)
+{
+ @autoreleasepool {
+ struct yasl_display *display = yasl_display_open(NULL, NULL);
+ assert(display && (yasl_display_caps(display) & YASL_CAP_TABLET));
+ struct yasl_surface_spec spec = {.title = "Tablet test", .size = {320, 240}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ assert(surface);
+ NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
+ NSView *view = window.contentView;
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {}
+ TabletSample *sample = [TabletSample new];
+ NSEvent *native = (NSEvent *)sample;
+ sample.type = NSEventTypeTabletProximity;
+ sample.enteringProximity = YES;
+ sample.tool = NSPointingDeviceTypePen;
+ [view tabletProximity:native];
+ sample.type = NSEventTypeMouseMoved;
+ sample.subtype = NSEventSubtypeTabletPoint;
+ sample.locationInWindow = NSMakePoint(100, 120);
+ sample.sampleTilt = NSMakePoint(-0.5, 0.5);
+ [view mouseMoved:native];
+ struct yasl_mouse_event ev = next(surface);
+ assert(ev.device == YASL_PTR_DEV_PEN && ev.kind == YASL_PTR_MOTION && !ev.pen.pressure);
+ assert(ev.pen.tilt_x == -45 && ev.pen.tilt_y == -45);
+ sample.type = NSEventTypeLeftMouseDown;
+ sample.samplePressure = 0.5;
+ [view mouseDown:native];
+ ev = next(surface);
+ assert(ev.device == YASL_PTR_DEV_PEN && ev.kind == YASL_PTR_PRESS && ev.button == 0);
+ assert(ev.pen.pressure == 32767);
+ struct yasl_vector2i position = ev.pos;
+ /* Native tablet events cover stationary pressure changes. */
+ sample.type = NSEventTypeTabletPoint;
+ sample.subtype = NSEventSubtypeMouseEvent;
+ sample.buttonMask = NSEventButtonMaskPenTip;
+ sample.samplePressure = 1.5;
+ [view tabletPoint:native];
+ ev = next(surface);
+ assert(ev.kind == YASL_PTR_MOTION && ev.pen.pressure == 65535);
+ assert(ev.pos.x == position.x && ev.pos.y == position.y);
+ sample.samplePressure = 0; /* Zero does not release a held tip button. */
+ [view tabletPoint:native];
+ assert(next(surface).kind == YASL_PTR_MOTION);
+ sample.buttonMask = 0;
+ [view tabletPoint:native];
+ ev = next(surface);
+ assert(ev.kind == YASL_PTR_RELEASE && !ev.pen.pressure && ev.device == YASL_PTR_DEV_PEN);
+ sample.type = NSEventTypeTabletProximity;
+ sample.enteringProximity = NO;
+ [view tabletProximity:native];
+ assert(next(surface).kind == YASL_PTR_LEAVE);
+ sample.enteringProximity = YES;
+ sample.tool = NSPointingDeviceTypeEraser;
+ [view tabletProximity:native];
+ sample.type = NSEventTypeTabletPoint;
+ sample.samplePressure = 0.75;
+ sample.buttonMask = NSEventButtonMaskPenTip;
+ [view tabletPoint:native];
+ ev = next(surface);
+ assert(ev.device == YASL_PTR_DEV_ERASER && ev.kind == YASL_PTR_PRESS && ev.pen.pressure > 0);
+ sample.type = NSEventTypeTabletProximity;
+ sample.enteringProximity = NO;
+ [view tabletProximity:native];
+ ev = next(surface);
+ assert(ev.device == YASL_PTR_DEV_ERASER && ev.kind == YASL_PTR_LEAVE && !ev.pen.pressure);
+ sample.type = NSEventTypeMouseMoved;
+ [view mouseMoved:native];
+ ev = next(surface);
+ assert(ev.device == YASL_PTR_DEV_MOUSE && !ev.pen.pressure);
+ [sample release];
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ }
+ return 0;
+}
diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c
index 605dac0..4c99646 100644
--- a/tests/common/test_backends.c
+++ b/tests/common/test_backends.c
@@ -1,5 +1,11 @@
#define _POSIX_C_SOURCE 200809L
#include "yasl/native.h"
+#if YASL_HAS_WAYLAND
+#include "yasl/native/wayland.h"
+#endif
+#if YASL_HAS_X11
+#include "yasl/native/x11.h"
+#endif
#include "yasl/loop.h"
#include "yasl/yasl.h"
@@ -145,6 +151,8 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct
Display *native = yasl_display_get_native_handle(d);
Window window = (Window)(uintptr_t)yasl_surface_get_native_handle(w->surface);
assert(native && window);
+ assert(yasl_x11_display_get_display(d) == native);
+ assert(yasl_x11_surface_get_window(w->surface) == (uintptr_t)window);
XSync(native, False);
XImage *image = XGetImage(native, window, 0, 0, 1, 1, AllPlanes, ZPixmap);
assert(image && (XGetPixel(image, 0, 0) & 0xffffff) == 0x4080c0);
@@ -282,6 +290,16 @@ 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);
+#if YASL_HAS_WAYLAND
+ assert(yasl_wayland_display_get_wl_display(a) != NULL);
+ assert(yasl_wayland_surface_get_wl_surface(wa.surface) != NULL);
+ assert(!yasl_x11_display_get_display(a));
+#endif
+#if YASL_HAS_X11
+ assert(yasl_x11_display_get_display(b) != NULL);
+ assert(yasl_x11_surface_get_window(wb.surface) != 0);
+ assert(!yasl_wayland_display_get_wl_display(b));
+#endif
attach(&wa);
attach(&wb);
pump(loop, a, b);
diff --git a/tests/common/test_tablet_example.c b/tests/common/test_tablet_example.c
new file mode 100644
index 0000000..1658174
--- /dev/null
+++ b/tests/common/test_tablet_example.c
@@ -0,0 +1,53 @@
+#define main tablet_example_main
+#include "../../examples/tablet.c"
+#undef main
+#include <assert.h>
+
+static void pointer(struct app *app, enum yasl_pointer_kind kind,
+ enum yasl_pointer_device device, int button, int x, int y, uint16_t pressure)
+{
+ struct yasl_input_event event = {.kind = YASL_INPUT_MOUSE, .mouse = {
+ .kind = kind, .device = device, .button = button, .pos = {x, y}, .pen.pressure = pressure}};
+ input(app, &event);
+}
+
+static uint32_t pixel(const struct app *app, int x, int y)
+{
+ return app->canvas[(size_t)y * app->geometry.size.x + x];
+}
+
+int main(void)
+{
+ struct app app = {0};
+ assert(resize_canvas(&app, (struct yasl_geometry){{400, 300}, 120}));
+ pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_PEN, 3, 80, 160, 65535);
+ assert(pixel(&app, 80, 160) == PAPER); /* Hover never paints. */
+ pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_PEN, 2, 80, 160, 65535);
+ assert(!app.down && pixel(&app, 80, 160) == PAPER); /* Side button isn't the tip. */
+ pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_PEN, 0, 80, 160, 0);
+ assert(app.down && pixel(&app, 80, 160) == INK && pixel(&app, 90, 160) == PAPER);
+ pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_PEN, 0, 80, 160, 65535);
+ assert(pixel(&app, 100, 160) == INK); /* Stationary pressure widens brush. */
+ pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_PEN, 0, 180, 160, 20000);
+ assert(pixel(&app, 130, 160) == INK); /* Segments are continuous. */
+ pointer(&app, YASL_PTR_RELEASE, YASL_PTR_DEV_PEN, 0, 180, 160, 0);
+ pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_PEN, 3, 250, 160, 65535);
+ assert(!app.down && pixel(&app, 250, 160) == PAPER);
+ pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_ERASER, 0, 80, 160, 65535);
+ assert(pixel(&app, 80, 160) == PAPER && pixel(&app, 130, 160) == INK);
+ struct yasl_input_event reset = {.kind = YASL_INPUT_RESET};
+ input(&app, &reset);
+ pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_ERASER, 0, 130, 160, 65535);
+ assert(!app.down && pixel(&app, 130, 160) == INK);
+ pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_MOUSE, 0, 300, 200, 0);
+ assert(pixel(&app, 300, 200) == INK);
+ struct yasl_input_event blur = {.kind = YASL_INPUT_FOCUS, .focus.focused = false};
+ input(&app, &blur);
+ assert(!app.down);
+ assert(resize_canvas(&app, (struct yasl_geometry){{800, 600}, 240}));
+ assert(pixel(&app, 600, 400) == INK && !app.previous); /* DPI preserves logical drawing. */
+ clear_canvas(&app);
+ assert(pixel(&app, 600, 400) == PAPER);
+ free(app.canvas);
+ return 0;
+}
diff --git a/tests/helpers/run_x11_tablet.py b/tests/helpers/run_x11_tablet.py
new file mode 100644
index 0000000..cdd36ab
--- /dev/null
+++ b/tests/helpers/run_x11_tablet.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""Run pen pressure through Xorg's virtual input driver and the example app."""
+
+import os
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import time
+
+
+def main():
+ xorg, test = sys.argv[1:]
+ module_path = os.environ.get("YASL_XORG_MODULE_PATH")
+ if not module_path:
+ module_path = subprocess.check_output(
+ [xorg, "-showDefaultModulePath"], text=True, timeout=5).strip()
+ if any(c in module_path for c in ('"', "\n", "\r")):
+ raise ValueError("Invalid Xorg module path")
+ paths = [Path(path) for path in module_path.split(",")]
+ for module in ("drivers/dummy_drv.so", "input/inputtest_drv.so"):
+ if not any((path / module).is_file() for path in paths):
+ raise RuntimeError(f"Missing Xorg module: {module}")
+ with tempfile.TemporaryDirectory(prefix="yasl-tablet-") as directory:
+ root = Path(directory)
+ pen, eraser = root / "pen.sock", root / "eraser.sock"
+ config = root / "xorg.conf"
+ config.write_text(f'''
+Section "ServerFlags"
+ Option "AutoAddDevices" "false"
+ Option "AutoAddGPU" "false"
+ Option "DontVTSwitch" "true"
+EndSection
+Section "Files"
+ ModulePath "{module_path}"
+EndSection
+Section "Module"
+ Disable "glx"
+EndSection
+Section "Device"
+ Identifier "virtual-video"
+ Driver "dummy"
+ VideoRam 16384
+EndSection
+Section "Monitor"
+ Identifier "virtual-monitor"
+ HorizSync 5.0 - 1000.0
+ VertRefresh 5.0 - 200.0
+ Modeline "1200x900" 96.96 1200 1280 1408 1616 900 901 904 932
+EndSection
+Section "Screen"
+ Identifier "virtual-screen"
+ Device "virtual-video"
+ Monitor "virtual-monitor"
+ DefaultDepth 24
+ SubSection "Display"
+ Depth 24
+ Modes "1200x900"
+ EndSubSection
+EndSection
+Section "InputDevice"
+ Identifier "YASL Test Pen"
+ Driver "inputtest"
+ Option "DeviceType" "PointerAbsoluteProximity"
+ Option "PointerHasPressure" "true"
+ Option "SocketPath" "{pen}"
+EndSection
+Section "InputDevice"
+ Identifier "YASL Test Reverse Tool"
+ Driver "inputtest"
+ Option "DeviceType" "PointerAbsoluteProximity"
+ Option "PointerHasPressure" "true"
+ Option "SocketPath" "{eraser}"
+EndSection
+Section "InputDevice"
+ Identifier "YASL Test Keyboard"
+ Driver "inputtest"
+ Option "DeviceType" "Keyboard"
+ Option "SocketPath" "{root / 'keyboard.sock'}"
+EndSection
+Section "ServerLayout"
+ Identifier "virtual-layout"
+ Screen "virtual-screen"
+ InputDevice "YASL Test Pen" "CorePointer"
+ InputDevice "YASL Test Reverse Tool" "SendCoreEvents"
+ InputDevice "YASL Test Keyboard" "CoreKeyboard"
+EndSection
+''')
+ display_file = root / "display"
+ with display_file.open("w") as display_fd, (root / "server.log").open("w+") as log:
+ server = subprocess.Popen([
+ xorg, "-config", str(config), "-logfile", str(root / "xorg.log"),
+ "-displayfd", str(display_fd.fileno()), "-noreset", "-nolisten", "tcp",
+ "-novtswitch", "-sharevts", "-seat", f"yasl-tablet-test-{os.getpid()}",
+ ], pass_fds=(display_fd.fileno(),), stdout=log, stderr=log)
+ try:
+ deadline = time.monotonic() + 10
+ while not (display_file.read_text().strip() and pen.is_socket() and eraser.is_socket()):
+ if server.poll() is not None:
+ raise RuntimeError("The virtual X server stopped")
+ if time.monotonic() >= deadline:
+ raise TimeoutError("The virtual X server did not start")
+ time.sleep(0.02)
+ env = os.environ.copy()
+ env["DISPLAY"] = ":" + display_file.read_text().strip()
+ env.pop("XAUTHORITY", None)
+ subprocess.run([test, str(pen), str(eraser)], env=env, check=True, timeout=10)
+ except BaseException:
+ log.flush()
+ log.seek(0, 2)
+ log.seek(max(0, log.tell() - 8000))
+ print(log.read(), file=sys.stderr)
+ raise
+ finally:
+ server.terminate()
+ try:
+ server.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ server.kill()
+ server.wait()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/meson.build b/tests/meson.build
index 177e400..75ff00d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -45,11 +45,21 @@ if system == 'linux'
export_args += ['--allow-export=__start_asan_globals',
'--allow-export=__stop_asan_globals']
endif
+ exported_headers = files(
+ '../include/yasl/yasl.h',
+ '../include/yasl/timing.h',
+ '../include/yasl/loop.h',
+ '../include/yasl/native.h',
+ )
+ if has_wayland
+ exported_headers += files('../include/yasl/native/wayland.h')
+ endif
+ if has_x11
+ exported_headers += files('../include/yasl/native/x11.h')
+ endif
test('public-exports', python,
args: [files('helpers/check_exports.py')] + export_args +
- [nm, yasl_lib.get_shared_lib(),
- files('../include/yasl/yasl.h', '../include/yasl/timing.h',
- '../include/yasl/loop.h', '../include/yasl/native.h')],
+ [nm, yasl_lib.get_shared_lib(), exported_headers],
)
endif
diff --git a/tests/test_api.cpp b/tests/test_api.cpp
index 70c4ff7..9fb8525 100644
--- a/tests/test_api.cpp
+++ b/tests/test_api.cpp
@@ -1,4 +1,10 @@
#include "yasl/native.h"
+#if YASL_HAS_WAYLAND
+#include "yasl/native/wayland.h"
+#endif
+#if YASL_HAS_X11
+#include "yasl/native/x11.h"
+#endif
#include "yasl/loop.h"
#include <yasl/timing.h>
#include <yasl/yasl.h>
@@ -23,5 +29,18 @@ int main()
yasl_input_event event{};
assert(!yasl_surface_input_poll(nullptr, &event));
+ assert(!yasl_display_drag_start(nullptr, nullptr, nullptr, nullptr, nullptr));
+ assert(!yasl_surface_drag_carry(nullptr, {0, 0}));
+
+#if YASL_HAS_WAYLAND
+ assert(!yasl_wayland_display_get_wl_display(nullptr));
+ assert(!yasl_wayland_surface_get_wl_surface(nullptr));
+#endif
+
+#if YASL_HAS_X11
+ assert(!yasl_x11_display_get_display(nullptr));
+ assert(!yasl_x11_surface_get_window(nullptr));
+#endif
+
return 0;
}
diff --git a/tests/wayland/test_cursor.c b/tests/wayland/test_cursor.c
new file mode 100644
index 0000000..e43bff4
--- /dev/null
+++ b/tests/wayland/test_cursor.c
@@ -0,0 +1,118 @@
+#include "wayland/internal.h"
+#include <assert.h>
+
+/* Record cursor requests without a compositor or tablet hardware. */
+static int pointer_hides, tool_hides, pointer_shapes, tool_shapes;
+static uint32_t pointer_serial, tool_serial, pointer_shape, tool_shape;
+static struct wp_cursor_shape_device_v1 *pointer_device;
+
+static void pointer_cursor(struct wl_pointer *pointer, uint32_t serial,
+ struct wl_surface *surface, int32_t x, int32_t y)
+{
+ assert(pointer && !surface && !x && !y);
+ pointer_hides++;
+ pointer_serial = serial;
+}
+
+static void tool_cursor(struct zwp_tablet_tool_v2 *tool, uint32_t serial,
+ struct wl_surface *surface, int32_t x, int32_t y)
+{
+ assert(tool && !surface && !x && !y);
+ tool_hides++;
+ tool_serial = serial;
+}
+
+static void cursor_shape(struct wp_cursor_shape_device_v1 *device,
+ uint32_t serial, uint32_t shape)
+{
+ if (device == pointer_device) {
+ pointer_shapes++;
+ pointer_serial = serial;
+ pointer_shape = shape;
+ } else {
+ assert(device);
+ tool_shapes++;
+ tool_serial = serial;
+ tool_shape = shape;
+ }
+}
+
+static uint32_t pointer_version(struct wl_pointer *pointer)
+{
+ assert(pointer);
+ return WL_POINTER_FRAME_SINCE_VERSION;
+}
+
+#define wl_pointer_set_cursor pointer_cursor
+#define zwp_tablet_tool_v2_set_cursor tool_cursor
+#define wp_cursor_shape_device_v1_set_shape cursor_shape
+#define wl_pointer_get_version pointer_version
+#include "../../src/wayland/pointer.c"
+
+int main(void)
+{
+ struct wayland_display display = {.base.ops = &yasl_wayland_backend};
+ struct wayland_surface surface = {.base.display = &display.base, .conn = &display,
+ .scale_factor = 1, .cursor = YASL_CURSOR_TEXT};
+ surface.surface = (struct wl_surface *)&surface;
+ display.surfaces = &surface;
+ struct seat seat = {.conn = &display};
+ seat.pointer = (struct wl_pointer *)&seat;
+ display.seats = &seat;
+ struct tablet_tool tool = {.seat = &seat};
+ tool.proxy = (struct zwp_tablet_tool_v2 *)&tool;
+
+ /* The example selects hidden before either input device has entered. */
+ yasl_surface_set_cursor(&surface.base, YASL_CURSOR_HIDDEN);
+ assert(!pointer_hides && !tool_hides);
+ pointer_enter(&seat, seat.pointer, 10, surface.surface, 0, 0);
+ assert(pointer_hides == 1 && pointer_serial == 10);
+ tablet_tool_proximity_in(&tool, tool.proxy, 11, NULL, surface.surface);
+ assert(tool_hides == 1 && tool_serial == 11);
+
+ /* Hiding also works with no cursor-shape protocol, and preserves input. */
+ pointer_motion(&seat, seat.pointer, 0, wl_fixed_from_int(12), wl_fixed_from_int(34));
+ pointer_frame(&seat, seat.pointer);
+ tablet_tool_motion(&tool, tool.proxy, wl_fixed_from_int(56), wl_fixed_from_int(78));
+ tool_frame(&tool, tool.proxy, 0);
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(&surface.base, &event));
+ assert(event.kind == YASL_INPUT_MOUSE && event.mouse.device == YASL_PTR_DEV_MOUSE);
+ assert(event.mouse.pos.x == 12 && event.mouse.pos.y == 34);
+ assert(yasl_surface_input_poll(&surface.base, &event));
+ assert(event.kind == YASL_INPUT_MOUSE && event.mouse.device == YASL_PTR_DEV_PEN);
+ assert(event.mouse.pos.x == 56 && event.mouse.pos.y == 78);
+ assert(!yasl_surface_input_poll(&surface.base, &event));
+
+ /* Each new entry needs a request with its own serial. */
+ pointer_leave(&seat, seat.pointer, 12, surface.surface);
+ pointer_frame(&seat, seat.pointer);
+ tablet_tool_proximity_out(&tool, tool.proxy);
+ pointer_enter(&seat, seat.pointer, 13, surface.surface, 0, 0);
+ tool.eraser = true;
+ tablet_tool_proximity_in(&tool, tool.proxy, 14, NULL, surface.surface);
+ assert(pointer_hides == 2 && pointer_serial == 13);
+ assert(tool_hides == 2 && tool_serial == 14);
+
+ /* Restore a visible shape on both currently focused devices. */
+ pointer_device = seat.cursor_shape_dev = (struct wp_cursor_shape_device_v1 *)&seat;
+ tool.shape_dev = (struct wp_cursor_shape_device_v1 *)&tool;
+ yasl_surface_set_cursor(&surface.base, YASL_CURSOR_ARROW);
+ assert(pointer_shapes == 1 && tool_shapes == 1);
+ assert(pointer_shape == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
+ assert(tool_shape == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
+ yasl_surface_set_cursor(&surface.base, YASL_CURSOR_HIDDEN);
+ assert(pointer_hides == 3 && tool_hides == 3);
+ assert(pointer_shapes == 1 && tool_shapes == 1);
+
+ /* Updating an unfocused surface must not alter the active cursor. */
+ struct wayland_surface other = {.base.display = &display.base, .conn = &display};
+ yasl_surface_set_cursor(&other.base, YASL_CURSOR_CROSSHAIR);
+ assert(pointer_shapes == 1 && tool_shapes == 1);
+ yasl_surface_set_cursor(&surface.base, (enum yasl_cursor)-1);
+ yasl_surface_set_cursor(&surface.base, (enum yasl_cursor)(YASL_CURSOR_HIDDEN + 1));
+ assert(pointer_hides == 3 && tool_hides == 3);
+ assert(pointer_shapes == 1 && tool_shapes == 1);
+ input_queue_clear(&surface.input);
+ return 0;
+}
diff --git a/tests/wayland/test_tablet.c b/tests/wayland/test_tablet.c
new file mode 100644
index 0000000..449c446
--- /dev/null
+++ b/tests/wayland/test_tablet.c
@@ -0,0 +1,62 @@
+#include "../../src/wayland/pointer.c"
+#include <assert.h>
+
+static struct yasl_mouse_event next(struct wayland_surface *surface)
+{
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(&surface->base, &event));
+ assert(event.surface == &surface->base && event.kind == YASL_INPUT_MOUSE);
+ return event.mouse;
+}
+
+int main(void)
+{
+ struct wayland_display display = {.base.ops = &yasl_wayland_backend};
+ struct wayland_surface surface = {.base.display = &display.base, .conn = &display,
+ .scale_factor = 1.5};
+ struct seat seat = {.conn = &display, .tablet_focus = &surface};
+ struct tablet_tool tool = {.seat = &seat};
+ seat.tool = &tool;
+ tablet_tool_motion(&tool, NULL, wl_fixed_from_int(100), wl_fixed_from_int(120));
+ tool_pressure(&tool, NULL, 32768);
+ tool_tilt(&tool, NULL, wl_fixed_from_int(-35), wl_fixed_from_int(42));
+ tablet_tool_down(&tool, NULL, 1);
+ tool_frame(&tool, NULL, 0);
+ struct yasl_mouse_event ev = next(&surface);
+ assert(ev.kind == YASL_PTR_MOTION && ev.device == YASL_PTR_DEV_PEN);
+ assert(ev.pos.x == 150 && ev.pos.y == 180 && ev.pen.pressure == 32768);
+ assert(ev.pen.tilt_x == -35 && ev.pen.tilt_y == 42);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_PRESS && ev.button == 0 && ev.pen.pressure == 32768);
+
+ /* Stationary pressure and tilt changes are still motion samples. */
+ tool_pressure(&tool, NULL, 100000);
+ tool_tilt(&tool, NULL, wl_fixed_from_int(-120), wl_fixed_from_int(120));
+ tool_frame(&tool, NULL, 0);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_MOTION && ev.pen.pressure == 65535);
+ assert(ev.pos.x == 150 && ev.pos.y == 180);
+ assert(ev.pen.tilt_x == -90 && ev.pen.tilt_y == 90);
+ tablet_tool_up(&tool, NULL);
+ tool_frame(&tool, NULL, 0);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_RELEASE && ev.button == 0 && !ev.pen.pressure);
+
+ tool_type(&tool, NULL, ZWP_TABLET_TOOL_V2_TYPE_ERASER);
+ tool_pressure(&tool, NULL, 22000);
+ tool_frame(&tool, NULL, 0);
+ assert(next(&surface).device == YASL_PTR_DEV_ERASER);
+ tool_wheel(&tool, NULL, 0, 1);
+ tool_frame(&tool, NULL, 0);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_ERASER);
+ tablet_tool_proximity_out(&tool, NULL);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_LEAVE && ev.device == YASL_PTR_DEV_ERASER && !ev.pen.pressure);
+ assert(!seat.tool && !seat.tablet_focus && !seat.tool_buttons);
+ assert(!(yasl_display_caps(&display.base) & YASL_CAP_TABLET));
+ display.tablet_mgr = (void *)&display; /* Capability depends on protocol availability. */
+ assert(yasl_display_caps(&display.base) & YASL_CAP_TABLET);
+ input_queue_clear(&surface.input);
+ return 0;
+}
diff --git a/tests/x11/inputtest-protocol.h b/tests/x11/inputtest-protocol.h
new file mode 100644
index 0000000..18f62ba
--- /dev/null
+++ b/tests/x11/inputtest-protocol.h
@@ -0,0 +1,92 @@
+/* Subset of Xorg's xf86-input-inputtest protocol 1.1 used by this test.
+ * https://www.x.org/releases/individual/xserver/xorg-server-21.1.25.tar.xz
+ *
+ * Copyright © 2020 Povilas Kanapickas <povilas@radix.lt>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software
+ * and its documentation for any purpose is hereby granted without
+ * fee, provided that the above copyright notice appear in all copies
+ * and that both that copyright notice and this permission notice
+ * appear in supporting documentation, and that the name of Red Hat
+ * not be used in advertising or publicity pertaining to distribution
+ * of the software without specific, written prior permission. Red
+ * Hat makes no representations about the suitability of this software
+ * for any purpose. It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
+ * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+ * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+ * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#ifndef YASL_TEST_INPUTTEST_PROTOCOL_H
+#define YASL_TEST_INPUTTEST_PROTOCOL_H
+
+#include <stdint.h>
+
+enum xf86ITResponseType {
+ XF86IT_RESPONSE_SERVER_VERSION,
+ XF86IT_RESPONSE_SYNC_FINISHED,
+};
+
+typedef struct {
+ uint32_t length;
+ enum xf86ITResponseType type;
+} xf86ITResponseHeader;
+
+typedef struct {
+ xf86ITResponseHeader header;
+ uint16_t major, minor;
+} xf86ITResponseServerVersion;
+
+enum xf86ITEventType {
+ XF86IT_EVENT_CLIENT_VERSION,
+ XF86IT_EVENT_WAIT_FOR_SYNC,
+ XF86IT_EVENT_MOTION,
+ XF86IT_EVENT_PROXIMITY,
+ XF86IT_EVENT_BUTTON,
+};
+
+typedef struct {
+ uint32_t length;
+ enum xf86ITEventType type;
+} xf86ITEventHeader;
+
+typedef struct {
+ uint32_t has_unaccelerated;
+ uint8_t mask[8];
+ double valuators[64];
+ double unaccelerated[64];
+} xf86ITValuatorData;
+
+typedef struct {
+ xf86ITEventHeader header;
+ uint16_t major, minor;
+} xf86ITEventClientVersion;
+
+typedef struct { xf86ITEventHeader header; } xf86ITEventWaitForSync;
+
+typedef struct {
+ xf86ITEventHeader header;
+ uint32_t is_absolute;
+ xf86ITValuatorData valuators;
+} xf86ITEventMotion;
+
+typedef struct {
+ xf86ITEventHeader header;
+ uint32_t is_prox_in;
+ xf86ITValuatorData valuators;
+} xf86ITEventProximity;
+
+typedef struct {
+ xf86ITEventHeader header;
+ int32_t is_absolute;
+ int32_t button;
+ uint32_t is_press;
+ xf86ITValuatorData valuators;
+} xf86ITEventButton;
+
+#endif
diff --git a/tests/x11/test_tablet.c b/tests/x11/test_tablet.c
new file mode 100644
index 0000000..39717df
--- /dev/null
+++ b/tests/x11/test_tablet.c
@@ -0,0 +1,103 @@
+/* Exercise native XI2 samples through the backend and public input queue. */
+#include "../../src/x11/window.c"
+
+static struct yasl_mouse_event next(struct x11_surface *surface)
+{
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(&surface->base, &event));
+ assert(event.surface == &surface->base && event.kind == YASL_INPUT_MOUSE);
+ return event.mouse;
+}
+
+int main(void)
+{
+ struct x11_display display = {
+ .base.ops = &yasl_x11_backend, .abs_pressure = 10, .abs_tilt_x = 11, .abs_tilt_y = 12,
+ };
+ struct x11_surface surface = {.base.display = &display.base, .window = 42};
+ display.surfaces = &surface;
+ XIValuatorClassInfo pressure = {.type = XIValuatorClass, .number = 2,
+ .label = 10, .min = 100, .max = 1100, .mode = XIModeAbsolute};
+ XIValuatorClassInfo tilt_x = {.type = XIValuatorClass, .number = 4,
+ .label = 11, .min = -64, .max = 63, .mode = XIModeAbsolute};
+ XIValuatorClassInfo tilt_y = {.type = XIValuatorClass, .number = 7,
+ .label = 12, .min = -64, .max = 63, .mode = XIModeAbsolute};
+ XIAnyClassInfo *classes[] = {(void *)&pressure, (void *)&tilt_x, (void *)&tilt_y};
+ XIDeviceInfo info = {.deviceid = 8, .name = "Tablet pen", .num_classes = 3, .classes = classes};
+ struct x11_pointer pointer = {0};
+ pointer_info(&display, &pointer, &info);
+ assert(pointer.device == YASL_PTR_DEV_PEN);
+ display.pointers = &pointer; /* Cache avoids requiring a real X server. */
+
+ unsigned char mask[1] = {0};
+ XISetMask(mask, 2);
+ XISetMask(mask, 4);
+ XISetMask(mask, 7);
+ double values[] = {600, -35, 42};
+ XIDeviceEvent sample = {.evtype = XI_ButtonPress, .sourceid = 8, .event = 42,
+ .detail = 1, .event_x = 200.5, .event_y = 175.25,
+ .mods.effective = ShiftMask, .valuators = {sizeof(mask), mask, values}};
+ XGenericEventCookie cookie = {.evtype = XI_ButtonPress, .data = &sample};
+ xi_event(&display, &cookie);
+ struct yasl_mouse_event ev = next(&surface);
+ assert(ev.kind == YASL_PTR_PRESS && ev.button == 0 && ev.device == YASL_PTR_DEV_PEN);
+ assert(ev.pos.x == 201 && ev.pos.y == 175 && ev.mods == YASL_MOD_SHIFT);
+ assert(ev.pen.pressure == 32768 && ev.pen.tilt_x == -35 && ev.pen.tilt_y == 42);
+
+ /* XI2 sends only changed axes, with values packed in mask order. */
+ mask[0] = 0;
+ XISetMask(mask, 2);
+ values[0] = 1100;
+ sample.evtype = cookie.evtype = XI_Motion;
+ xi_event(&display, &cookie);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_MOTION && ev.pen.pressure == 65535);
+ assert(ev.pen.tilt_x == -35 && ev.pen.tilt_y == 42);
+ values[0] = 5000;
+ xi_event(&display, &cookie);
+ assert(next(&surface).pen.pressure == 65535);
+ values[0] = -50;
+ xi_event(&display, &cookie);
+ assert(next(&surface).pen.pressure == 0);
+
+ info.name = "Tablet ERASER";
+ pointer_info(&display, &pointer, &info);
+ assert(pointer.device == YASL_PTR_DEV_ERASER);
+ values[0] = 900;
+ sample.evtype = cookie.evtype = XI_ButtonPress;
+ xi_event(&display, &cookie);
+ ev = next(&surface);
+ assert(ev.device == YASL_PTR_DEV_ERASER && ev.pen.pressure > 0);
+ /* Release must clear pressure even when the driver omits that valuator. */
+ sample.valuators.mask_len = 0;
+ sample.evtype = cookie.evtype = XI_ButtonRelease;
+ xi_event(&display, &cookie);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_RELEASE && ev.button == 0 && ev.pen.pressure == 0);
+ assert(ev.device == YASL_PTR_DEV_ERASER);
+
+ XIEnterEvent crossing = {.sourceid = 8, .event = 42, .event_x = 200, .event_y = 175};
+ cookie = (XGenericEventCookie){.evtype = XI_Leave, .data = &crossing};
+ xi_event(&display, &cookie);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_LEAVE && ev.device == YASL_PTR_DEV_ERASER && !ev.pen.pressure);
+
+ /* No tablet axes must produce an ordinary, pressure-free mouse. */
+ info.num_classes = 0;
+ pointer_info(&display, &pointer, &info);
+ sample.evtype = XI_ButtonPress;
+ sample.detail = 3;
+ xi_pointer_event(&surface, &sample);
+ ev = next(&surface);
+ assert(ev.device == YASL_PTR_DEV_MOUSE && ev.button == 2 && ev.pen.pressure == 0);
+ assert(pointer_button(2) == 1 && pointer_button(8) == 3);
+ pointer.pressure = (struct tablet_axis){2, 1, 1, 50};
+ assert(pressure_value(&pointer.pressure) == 0);
+ pointer.tilt_x = (struct tablet_axis){4, -90, 90, -200};
+ assert(tilt_value(&pointer.tilt_x) == -90);
+ pointer.tilt_x.value = 200;
+ assert(tilt_value(&pointer.tilt_x) == 90);
+ display.pointers = NULL;
+ input_queue_clear(&surface.input);
+ return 0;
+}
diff --git a/tests/x11/test_tablet_native.c b/tests/x11/test_tablet_native.c
new file mode 100644
index 0000000..0ddb6d8
--- /dev/null
+++ b/tests/x11/test_tablet_native.c
@@ -0,0 +1,180 @@
+/* Real Xorg driver -> XI2 -> YASL queue -> the portable example -> CPU pixels. */
+#define main tablet_example_main
+#include "../../examples/tablet.c"
+#undef main
+#include <yasl/native.h>
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XInput2.h>
+#include <assert.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include "inputtest-protocol.h"
+
+static void send_all(int fd, const void *message, size_t n)
+{
+ const char *p = message;
+ while (n) {
+ ssize_t sent = write(fd, p, n);
+ if (sent < 0 && errno == EINTR) continue;
+ assert(sent > 0);
+ p += sent;
+ n -= (size_t)sent;
+ }
+}
+
+static void receive_all(int fd, void *message, size_t n)
+{
+ char *p = message;
+ while (n) {
+ ssize_t got = read(fd, p, n);
+ if (got < 0 && errno == EINTR) continue;
+ assert(got > 0);
+ p += got;
+ n -= (size_t)got;
+ }
+}
+
+static int connect_tool(const char *path)
+{
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ assert(fd >= 0);
+ struct sockaddr_un address = {.sun_family = AF_UNIX};
+ assert(strlen(path) < sizeof(address.sun_path));
+ strcpy(address.sun_path, path);
+ assert(connect(fd, (struct sockaddr *)&address, sizeof(address)) == 0);
+ xf86ITEventClientVersion version = {{sizeof(version), XF86IT_EVENT_CLIENT_VERSION}, 1, 1};
+ send_all(fd, &version, sizeof(version));
+ xf86ITResponseServerVersion response;
+ receive_all(fd, &response, sizeof(response));
+ assert(response.header.type == XF86IT_RESPONSE_SERVER_VERSION);
+ assert(response.header.length == sizeof(response) && response.major == 1 && response.minor >= 1);
+ return fd;
+}
+
+static void pump(int fd, struct yasl_display *display, struct yasl_surface *surface, struct app *app)
+{
+ xf86ITEventWaitForSync sync = {{sizeof(sync), XF86IT_EVENT_WAIT_FOR_SYNC}};
+ send_all(fd, &sync, sizeof(sync));
+ xf86ITResponseHeader response;
+ receive_all(fd, &response, sizeof(response));
+ assert(response.type == XF86IT_RESPONSE_SYNC_FINISHED && response.length == sizeof(response));
+ /* The driver acknowledgment guarantees input processing has finished.
+ * Round-trip on YASL's X connection before draining its event queue. */
+ XSync(yasl_display_get_native_handle(display), False);
+ assert(yasl_display_dispatch(display, 0) == 0);
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {
+ input(app, &event);
+ yasl_surface_schedule_redraw(surface);
+ }
+ yasl_display_flush(display);
+ assert(!app->failed);
+}
+
+static uint32_t canvas_pixel(struct app *app, int x, int y)
+{
+ return app->canvas[(size_t)y * app->geometry.size.x + x];
+}
+
+static void set_eraser_property(Display *display)
+{
+ int count = 0;
+ XIDeviceInfo *devices = XIQueryDevice(display, XIAllDevices, &count);
+ assert(devices);
+ bool found = false;
+ for (int i = 0; i < count; i++) {
+ /* The name deliberately doesn't contain "eraser": exercise the
+ * driver's tool property, independently of the name fallback. */
+ if (strcmp(devices[i].name, "YASL Test Reverse Tool")) continue;
+ Atom property = XInternAtom(display, "Wacom Tool Type", False);
+ uint32_t eraser = (uint32_t)XInternAtom(display, "ERASER", False);
+ XIChangeProperty(display, devices[i].deviceid, property, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&eraser, 1);
+ found = true;
+ }
+ XIFreeDeviceInfo(devices);
+ assert(found);
+ XSync(display, False);
+}
+
+int main(int argc, char **argv)
+{
+ assert(argc == 3); /* pen socket, eraser socket */
+ struct app app = {.running = true, .tablet_supported = true, .backend = YASL_BACKEND_X11};
+ struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11};
+ struct yasl_display *display = yasl_display_open(NULL, &ds);
+ assert(display && (yasl_display_caps(display) & YASL_CAP_TABLET));
+ struct yasl_surface_spec ss = {.title = "YASL native pressure test", .size = {500, 400}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &ss);
+ assert(surface);
+ struct yasl_surface_cbs cbs = {.draw = draw, .configure = configure};
+ yasl_surface_set_callbacks(surface, &cbs, &app);
+ yasl_surface_schedule_redraw(surface);
+ Display *x = yasl_display_get_native_handle(display);
+ Window window = (Window)(uintptr_t)yasl_surface_get_native_handle(surface);
+ XMoveWindow(x, window, 0, 0);
+ XSetInputFocus(x, window, RevertToParent, CurrentTime);
+ XSync(x, False);
+ assert(yasl_display_dispatch(display, 0) == 0);
+ yasl_display_flush(display);
+ set_eraser_property(x);
+ int pen_fd = connect_tool(argv[1]), eraser_fd = connect_tool(argv[2]);
+ xf86ITEventMotion motion = {.header = {sizeof(motion), XF86IT_EVENT_MOTION}, .is_absolute = 1};
+ motion.valuators.mask[0] = (1 << 0) | (1 << 1) | (1 << 4);
+ motion.valuators.valuators[0] = 150.0 * 65535 / (DisplayWidth(x, DefaultScreen(x)) - 1);
+ motion.valuators.valuators[1] = 200.0 * 65535 / (DisplayHeight(x, DefaultScreen(x)) - 1);
+ xf86ITEventProximity proximity = {.header = {sizeof(proximity), XF86IT_EVENT_PROXIMITY},
+ .is_prox_in = 1, .valuators = motion.valuators};
+ send_all(pen_fd, &proximity, sizeof(proximity));
+ send_all(pen_fd, &motion, sizeof(motion));
+ pump(pen_fd, display, surface, &app);
+ assert(app.visible && !app.down && app.pointer.device == YASL_PTR_DEV_PEN);
+ assert(app.pointer.pos.x == 150 && app.pointer.pos.y == 200);
+ assert(canvas_pixel(&app, 150, 200) == PAPER);
+ xf86ITEventButton button = {.header = {sizeof(button), XF86IT_EVENT_BUTTON},
+ .is_absolute = 1, .button = 1, .is_press = 1, .valuators = motion.valuators};
+ button.valuators.valuators[4] = 250; /* Driver pressure range is 0..1000. */
+ send_all(pen_fd, &button, sizeof(button));
+ pump(pen_fd, display, surface, &app);
+ assert(app.down && app.pointer.pen.pressure == 16384);
+ assert(canvas_pixel(&app, 150, 200) == INK && canvas_pixel(&app, 170, 200) == PAPER);
+ motion.valuators.mask[0] = 1 << 4; /* No position change. */
+ motion.valuators.valuators[4] = 1000;
+ send_all(pen_fd, &motion, sizeof(motion));
+ pump(pen_fd, display, surface, &app);
+ assert(app.down && app.pointer.pen.pressure == 65535);
+ assert(canvas_pixel(&app, 170, 200) == INK);
+ /* Check the actual window's pixels too, after the example presents. */
+ XImage *image = XGetImage(x, window, 170, 200, 1, 1, AllPlanes, ZPixmap);
+ assert(image && (XGetPixel(image, 0, 0) & 0xffffff) == (INK & 0xffffff));
+ XDestroyImage(image);
+ button.is_press = 0;
+ button.valuators.mask[0] = 0; /* Release omits all valuators. */
+ send_all(pen_fd, &button, sizeof(button));
+ pump(pen_fd, display, surface, &app);
+ assert(!app.down && !app.pointer.pen.pressure);
+
+ send_all(eraser_fd, &proximity, sizeof(proximity));
+ button.is_press = 1;
+ button.valuators = proximity.valuators;
+ button.valuators.valuators[4] = 1000;
+ send_all(eraser_fd, &button, sizeof(button));
+ pump(eraser_fd, display, surface, &app);
+ assert(app.down && app.pointer.device == YASL_PTR_DEV_ERASER && app.pointer.pen.pressure == 65535);
+ assert(canvas_pixel(&app, 150, 200) == PAPER && canvas_pixel(&app, 170, 200) == PAPER);
+ button.is_press = 0;
+ send_all(eraser_fd, &button, sizeof(button));
+ pump(eraser_fd, display, surface, &app);
+ assert(!app.down && !app.pointer.pen.pressure);
+ close(pen_fd);
+ close(eraser_fd);
+ free(app.canvas);
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ puts("Native Xorg pen pressure, stationary pressure, release, and eraser drawing passed.");
+ return 0;
+}