From 5206933399775e5ff46baefbbb3a776c2a9bb84c Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 10:53:22 +0300 Subject: 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. --- tests/x11/inputtest-protocol.h | 92 +++++++++++++++++++++ tests/x11/test_tablet.c | 103 +++++++++++++++++++++++ tests/x11/test_tablet_native.c | 180 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 tests/x11/inputtest-protocol.h create mode 100644 tests/x11/test_tablet.c create mode 100644 tests/x11/test_tablet_native.c (limited to 'tests/x11') 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 + * + * 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 + +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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} -- cgit v1.2.3