summaryrefslogtreecommitdiff
path: root/tests/x11
diff options
context:
space:
mode:
Diffstat (limited to 'tests/x11')
-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
3 files changed, 375 insertions, 0 deletions
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;
+}