diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 10:53:22 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 10:53:22 +0300 |
| commit | 5206933399775e5ff46baefbbb3a776c2a9bb84c (patch) | |
| tree | e7549216ac626c1f3ac22345f79fbf80cc5a90fb | |
| parent | 881fb6cb1d64868132287bd9637f4ae1cbc3504c (diff) | |
feat: add type-safe native platform headers and portable drag support
- Add include/yasl/native/{wayland,cocoa,x11}.h with type-safe handle accessors.
- Unify drag-and-drop and window carry support across Wayland, X11, and Cocoa.
- Preserve full ABI compatibility for capability flags.
- Install native headers conditionally based on enabled backend.
- Include missing tablet example and backend test suites.
| -rw-r--r-- | examples/tablet.c | 355 | ||||
| -rw-r--r-- | include/yasl/native/cocoa.h | 30 | ||||
| -rw-r--r-- | include/yasl/native/wayland.h | 22 | ||||
| -rw-r--r-- | include/yasl/native/x11.h | 22 | ||||
| -rw-r--r-- | meson.build | 10 | ||||
| -rw-r--r-- | src/cocoa/window.m | 84 | ||||
| -rw-r--r-- | src/common/native.c | 36 | ||||
| -rw-r--r-- | src/wayland/api.h | 1 | ||||
| -rw-r--r-- | src/x11/window.c | 72 | ||||
| -rw-r--r-- | tests/cocoa/test_cursor.m | 53 | ||||
| -rw-r--r-- | tests/cocoa/test_ownership.m | 4 | ||||
| -rw-r--r-- | tests/cocoa/test_tablet.m | 123 | ||||
| -rw-r--r-- | tests/common/test_backends.c | 18 | ||||
| -rw-r--r-- | tests/common/test_tablet_example.c | 53 | ||||
| -rw-r--r-- | tests/helpers/run_x11_tablet.py | 124 | ||||
| -rw-r--r-- | tests/meson.build | 16 | ||||
| -rw-r--r-- | tests/test_api.cpp | 19 | ||||
| -rw-r--r-- | tests/wayland/test_cursor.c | 118 | ||||
| -rw-r--r-- | tests/wayland/test_tablet.c | 62 | ||||
| -rw-r--r-- | tests/x11/inputtest-protocol.h | 92 | ||||
| -rw-r--r-- | tests/x11/test_tablet.c | 103 | ||||
| -rw-r--r-- | tests/x11/test_tablet_native.c | 180 |
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; +} |
