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/cocoa/test_cursor.m | 53 +++++++++++ tests/cocoa/test_ownership.m | 4 + tests/cocoa/test_tablet.m | 123 +++++++++++++++++++++++++ tests/common/test_backends.c | 18 ++++ tests/common/test_tablet_example.c | 53 +++++++++++ tests/helpers/run_x11_tablet.py | 124 +++++++++++++++++++++++++ tests/meson.build | 16 +++- tests/test_api.cpp | 19 ++++ tests/wayland/test_cursor.c | 118 ++++++++++++++++++++++++ tests/wayland/test_tablet.c | 62 +++++++++++++ tests/x11/inputtest-protocol.h | 92 +++++++++++++++++++ tests/x11/test_tablet.c | 103 +++++++++++++++++++++ tests/x11/test_tablet_native.c | 180 +++++++++++++++++++++++++++++++++++++ 13 files changed, 962 insertions(+), 3 deletions(-) create mode 100644 tests/cocoa/test_cursor.m create mode 100644 tests/cocoa/test_tablet.m create mode 100644 tests/common/test_tablet_example.c create mode 100644 tests/helpers/run_x11_tablet.py create mode 100644 tests/wayland/test_cursor.c create mode 100644 tests/wayland/test_tablet.c 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') 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 +#include +#include + +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 #include "yasl/yasl.h" #include @@ -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 +#include +#include + +/* 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 + +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 #include @@ -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 + +/* 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 + +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 + * + * 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