# YASL (Yet Another Surface Library) YASL is a minimal, modern, and high-performance desktop window surface library written in C. It provides window creation, frame pacing, high-DPI scaling, and input routing without bundling audio, physics, or game engine components. ## Features - **Lean Core**: No audio, no font rendering, and no widget toolkit overhead. - **Wayland Protocol Coverage**: - `xdg-shell` toplevels and hierarchical popups (`xdg_popup` + `xdg_positioner`). - Fractional scaling via `wp_fractional_scale_v1` and `wp_viewporter`. - Presentation feedback and refresh rate timing via `wp_presentation` and `wp_fifo_v1`. - Cursor shapes via `wp_cursor_shape_device_v1`. - Graphics tablets and stylus input via `zwp_tablet_v2` (pressure, tilt, eraser). - Text Input Method (IME) composition via `zwp_text_input_v3`. - Primary selection and clipboard transfers via `wl_data_device` and `zwp_primary_selection_v1`. - Interactive client-side window move and resize triggers (`xdg_toplevel_move`, `xdg_toplevel_resize`). - **macOS Cocoa & Metal**: - Native `NSWindow` and `CAMetalLayer` presentation. - Vsync pacing via `CADisplayLink`. - **Zero Framework Leaks**: - Self-contained UTF-8 validation. - Self-contained event queue. - No terminal-specific telemetry or hardcoded application names. ## Public API Include `` for surface creation, drawing, input, and display dispatch. `yasl_surface_geometry()` returns device-pixel size and scale together. `yasl_display_dispatch(display, timeout_ms)` polls with zero, waits indefinitely with a negative timeout, or waits up to a positive number of milliseconds. Pending input or redraw work returns immediately; call `yasl_display_flush()` after processing input and scheduling redraws. Call `yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN)` to hide the system mouse and tablet cursor over a surface while continuing to receive pointer input. The choice persists across motion and re-entry; select another cursor, such as `YASL_CURSOR_ARROW`, to show it again. `YASL_CURSOR_NONE` is a compatible alias for `YASL_CURSOR_HIDDEN`. Include `` to use the optional event loop, file watches, or timers. Timers use one callback model and nanoseconds throughout; use `YASL_MS_TO_NS(5)` for a five-millisecond interval. Add application timers to `yasl_display_loop(display)`. Add surface timers with `yasl_surface_add_timer()` when they should be canceled automatically on surface destruction. Include `` for platform handles and application-managed native rendering. These extensions are separate from the portable CPU drawing API. `` contains the public timing result types. ## Backend selection YASL selects the native window backend at runtime. Leave `yasl_display_spec.backend` at zero to use this mode. `YASL_BACKEND_AUTO` has the same effect. You can also pass NULL as the display spec. On Linux, YASL tries Wayland first in a Wayland session. It uses `WAYLAND_DISPLAY`, `WAYLAND_SOCKET`, and `XDG_SESSION_TYPE` to detect that session. In other sessions, it tries X11 first. If the first backend cannot open, it tries the other built backend. On macOS, it uses Cocoa. Set `backend` to request one backend: ```c struct yasl_display_spec spec = { .app_id = "org.example.app", .backend = YASL_BACKEND_X11, }; struct yasl_display *display = yasl_display_open(loop, &spec); ``` Use `YASL_BACKEND_WAYLAND` to request Wayland. Use `YASL_BACKEND_COCOA` to request Cocoa. An explicit request never falls back to another backend. Open returns NULL if the requested backend cannot open. It sets `errno` to `ENOSYS` for a backend that was not built. It sets `errno` to `EINVAL` for an invalid backend value. Call `yasl_display_backend(display)` to read the selected backend. All surfaces on a display use that backend. Open separate displays to use Wayland and X11 in one process. The public API needs no window system headers. Shared libraries export only the functions declared in the installed YASL headers. Input queues, frame clocks, backend entry points, and protocol helpers are private implementation details. Use `yasl_display_caps()` to check optional features. X11 supports CPU pixel buffers, keyboard and pointer input, and clipboard transfer. Tablet pressure, tilt, and erasers use XInput 2 when the server exposes it. It supports the primary selection and window placement. It does not yet support YASL popups, drag operations, or IME preedit text. Those features return their normal unsupported result. The surface spec needs no native graphics device for CPU drawing. On Cocoa, YASL creates its own Metal device for this mode. An app that renders with Metal includes `` and calls `yasl_surface_create_native(display, &spec, device)`. During its draw callback, `yasl_surface_get_native_frame()` returns the current Metal drawable. The app submits and presents it, then calls `yasl_surface_present()` to record submission. The native display handle is a `wl_display *` on Wayland. It is a `Display *` on X11. The native surface handle is a `wl_surface *` on Wayland. On X11, convert it to `Window` through `uintptr_t`. On Cocoa, the native display handle is `NSApplication *` and the surface handle is `NSWindow *`. Access its view through `window.contentView`. Native getters are declared only in ``; handles are borrowed until destruction. ## Ownership and backend contracts - Calls and callbacks use the thread that opened the display (the main thread on Cocoa). Supplied event loops are borrowed. Destroy popups before parents, surfaces before displays, and displays before supplied loops. Surface destruction inside surface callbacks is supported; display or loop destruction during callbacks/dispatch is not. - Callback tables, drag listeners, input strings, titles, clipboard text, and drag pixels are copied. Userdata is borrowed. Cocoa retains a supplied Metal device. Native handles are borrowed, not transferred to the caller. - Text, paste, and file-drop event payloads remain valid until the next **successful** input poll on that surface or surface destruction. Dispatch, flush, selection replacement, and failed polls do not invalidate them. Copy a payload if it must live longer. - Input queues grow rather than silently dropping events at a fixed limit. If allocation fails, pending input is discarded and `YASL_INPUT_RESET` is delivered before later events. X11 also sends a reset when input devices are removed or disabled. Apps must clear held keys/buttons and input composition state on this event. - Clipboard requests retain their original target. Destroying that target cancels delivery, rather than redirecting the paste to a focused window. Unsupported primary selections are no-ops, never aliases for the clipboard. - CPU pixels are tightly packed premultiplied `0xAARRGGBB`. Creation sizes are logical pixels; framebuffer sizes, pointer coordinates, caret and damage are device pixels. Every draw target needs complete valid pixel contents; do not assume a recycled framebuffer contains the previous frame. - Present only the current draw callback's target, at most once. Targets expire when the callback returns; preserve the opaque `generation` when copying a target. Damage is required, clipped to the target, and validated uniformly. Backends may upload the whole frame despite damage hints. - `yasl_surface_create_native()` requires `YASL_CAP_NATIVE_GRAPHICS` (currently Cocoa) and a non-NULL graphics device; other backends return `ENOTSUP`. Native drawable queries are valid only for the current draw target and expire on successful present, callback return, or surface destruction. - `YASL_CAP_FRAME_PACING` distinguishes compositor/display-driven draws from X11's unpaced flush-driven draws. `yasl_timing.source` distinguishes submission estimates, frame callbacks, and confirmed presentation feedback. Refresh and next-presentation times are estimates, not universal vsync guarantees. These are pre-release API changes. Applications must handle the new reset input kind and should inspect timing provenance instead of assuming all timestamps confirm scanout. See `include/yasl/yasl.h` for the complete contracts. ## Building Use a C23 compiler and Meson 1.6 or later. Build with Meson and Ninja: ```sh meson setup build ninja -C build ninja -C build test ``` Linux builds include each backend whose dependencies are installed. Wayland needs `wayland-client`, `wayland-cursor`, and `xkbcommon`. X11 needs `x11`, `xi` (libXi), and `xkbcommon`. Use `-Dwayland=enabled` or `-Dx11=enabled` to require a backend. Use `-Dwayland=disabled` or `-Dx11=disabled` to omit a backend. At least one backend must be built. To run the Linux window tests, install Weston and Xwayland. Then run: ```sh meson setup build-test -Dintegration_tests=true meson test -C build-test --print-errorlogs ``` The tests start isolated display servers. They test explicit requests, automatic selection, and fallback. They also test both backends in one process, callback-table copying, input payload ownership, queue growth, concurrent clipboard targets, and consistent frame/damage validation. Wayland IME ownership tests need no display server. For memory and undefined-behavior checks, configure a separate build with `-Db_sanitize=address,undefined`. On macOS, the same option runs native Cocoa window tests. These tests need a desktop session. ## Tablet drawing example Build and run the same C application on Wayland, X11, or Cocoa: ```sh meson setup build meson compile -C build ./build/yasl-tablet ./build/yasl-tablet --backend wayland ./build/yasl-tablet --backend x11 # On macOS: ./build/yasl-tablet --backend cocoa ``` Draw with the pen tip: pressure controls brush width. Flip to the eraser to remove ink. The window shows the selected backend, tool, contact state, raw pressure (0..65535), percentage, and tilt in degrees, with a live pressure bar, brush outline, and tilt vector. The system cursor is hidden so the brush overlay serves as the drawing cursor. Mouse drawing uses a fixed width. Press C or Space to clear and Escape to quit. Resizing and display scale changes preserve the drawing. `--smoke-test` presents one frame and exits for automated checks. [`examples/tablet.c`](examples/tablet.c) includes only `` and the C standard library. It uses CPU framebuffers and `yasl_surface_input_poll()` on every backend. Tablet events arrive as `YASL_INPUT_MOUSE`, distinguished by `mouse.device == YASL_PTR_DEV_PEN` or `YASL_PTR_DEV_ERASER`; `mouse.pen` contains pressure and signed tilt. Button 0 is tip contact on every backend. Track tip press/release events: zero pressure alone does not mean release. Hover and side buttons do not paint, and focus loss, leave, or `YASL_INPUT_RESET` cancels a stroke. Stationary pressure changes arrive as motion events and update the brush too. `YASL_CAP_TABLET` indicates backend/protocol support, not whether a tablet is plugged in or which axes it has. Missing axes are zero. Wayland requires the compositor's `zwp_tablet_manager_v2`; X11 requires XInput 2 and driver-provided `Abs Pressure`/`Abs Tilt X`/`Abs Tilt Y` valuators. Cocoa handles both tablet mouse subtypes and native tablet point/proximity events. X11 identifies erasers by the Wacom tool property or the driver's device name. Hardware and driver support determine which readings are available. The example displays a message when the server has no tablet protocol and still allows mouse drawing. The test suite checks synthetic Wayland/XInput samples and the drawing behavior without hardware, including pressure-only changes, sparse axes, eraser input, hover, release, reset, and scale changes. Linux integration tests launch the example on isolated Wayland and X11 servers; macOS integration tests also check Cocoa tablet event translation. Physical tablet testing is still needed to validate a particular device/driver combination. For an end-to-end X11 pressure test without hardware, install Xorg's `dummy` video driver and `inputtest` input driver, then run: ```sh meson configure build -Dx11_tablet_tests=true meson test -C build tablet-x11-native --print-errorlogs ``` This starts an isolated Xorg server with virtual pen and eraser devices, sends real driver samples, checks YASL's shared events through the example, and verifies its presented window pixels. It checks stationary pressure changes, release without valuators, and eraser identification through the driver's tool property. The test disables automatic physical input/GPU attachment and cleans up its server. Set `YASL_XORG_MODULE_PATH` to a comma-separated module search path if the test drivers are outside Xorg's default directories. ## Quick Example ```c #include static bool running = true; static void on_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) { /* Present frame target */ struct yasl_damage_list dmg = { .n = -1 }; yasl_surface_present(s, &frame->target, &dmg); } static void on_close(struct yasl_surface *s, void *ud) { (void)s; (void)ud; running = false; } int main(void) { struct yasl_display_spec dspec = { .app_id = "org.example.app" }; struct yasl_display *d = yasl_display_open(NULL, &dspec); struct yasl_surface_spec sspec = { .title = "YASL Application", .size = { .x = 800, .y = 600 }, }; struct yasl_surface *s = yasl_surface_create_toplevel(d, &sspec); struct yasl_surface_cbs cbs = { .draw = on_draw, .close = on_close, }; yasl_surface_set_callbacks(s, &cbs, NULL); yasl_surface_schedule_redraw(s); while (running) { yasl_display_dispatch(d, -1); struct yasl_input_event ev; while (yasl_surface_input_poll(s, &ev)) { if (ev.kind == YASL_INPUT_KEY && ev.key.action == YASL_KEY_PRESS) { if (ev.key.key == 0xff1b) /* Escape */ running = false; } } yasl_display_flush(d); } yasl_surface_destroy(s); yasl_display_close(d); return 0; } ``` ## License MIT License.