summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 22:12:02 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 22:12:02 +0300
commit94c5e361fbb229b0cd6853e2102a7d95f1f358b3 (patch)
tree3c0f11af9dcb84dac8067a31a6979acd6dd56fbf /docs
parent08dc9047098d6d96791024ac7d8f5837633673ef (diff)
better public interface and doxygen
Diffstat (limited to 'docs')
-rw-r--r--docs/api-policy.md37
-rw-r--r--docs/doxygen_filter.py24
-rw-r--r--docs/examples.dox12
-rw-r--r--docs/getting-started.md118
-rw-r--r--docs/guide.md298
-rw-r--r--docs/migrating.md40
-rw-r--r--docs/testing.md156
7 files changed, 685 insertions, 0 deletions
diff --git a/docs/api-policy.md b/docs/api-policy.md
new file mode 100644
index 0000000..d5c7159
--- /dev/null
+++ b/docs/api-policy.md
@@ -0,0 +1,37 @@
+# Public API policy
+
+YASL 0.1 is pre-release. The installed headers are the public API; implementation
+headers under `src/` are private even when their symbols appear in a static
+archive. The shared library exports only declarations from installed headers.
+Native backend headers and accessors are available only for built backends.
+
+There is currently no stable ABI promise and no versioned shared-library SONAME.
+Rebuild consumers against the exact headers/library pair they ship. Source and
+binary compatibility can change before 1.0. Do not assume struct layout is stable
+across releases, mix headers and binaries from different revisions, or persist
+public structs as a file/network format. The package version is available through
+`pkg-config --modversion yasl`.
+
+See [migration notes](migrating.md) for the portable keyboard and fractional
+scroll API changes.
+
+API changes should keep ordinary CPU drawing usable with only `<yasl/yasl.h>`
+and the C standard library. Prefer an explicit capability and an ordinary
+unsupported result over backend names in portable application logic. Native
+rendering and handles belong in the extension headers. Avoid adding engine,
+widget, renderer, or application policy to the surface layer.
+
+Every public addition or behavior change needs:
+
+- A header contract describing arguments, result, units, lifetime, threading,
+ callback timing, and unsupported behavior where applicable.
+- An update to the guide and its backend limits when observable behavior changes.
+- A runnable example when the new workflow cannot be understood from an isolated
+ call, and meaningful regression coverage for ownership or behavior changes.
+- A check that shared exports and installed-package consumers still work.
+
+Functions documenting errno set it on failure. Other booleans report acceptance
+or availability without a guaranteed errno; void setters are best effort.
+Callbacks and successful returns do not imply confirmed screen presentation.
+See `yasl_timing.source` for provenance. Future changes that strengthen error
+reporting must preserve these distinctions in documentation.
diff --git a/docs/doxygen_filter.py b/docs/doxygen_filter.py
new file mode 100644
index 0000000..ed6e6e8
--- /dev/null
+++ b/docs/doxygen_filter.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+"""Resolve repository source links to Doxygen reference/example pages.
+
+Markdown keeps ordinary relative links for repository and installed-document
+readers. Doxygen gets @ref links so its standalone HTML has no source-tree URLs.
+"""
+
+from pathlib import Path
+import re
+import sys
+
+text = Path(sys.argv[1]).read_text(encoding="utf-8")
+
+
+def reference(match):
+ label, target = match.groups()
+ if target.startswith("../include/yasl/") and target.endswith(".h"):
+ return f"[{label}](@ref {Path(target).name})"
+ if target.startswith("../examples/") and target.endswith(".c"):
+ return f"[{label}](@ref {Path(target).name})"
+ return match.group(0)
+
+
+sys.stdout.write(re.sub(r"\[([^\]]+)\]\(([^)]+)\)", reference, text))
diff --git a/docs/examples.dox b/docs/examples.dox
new file mode 100644
index 0000000..8e9d06a
--- /dev/null
+++ b/docs/examples.dox
@@ -0,0 +1,12 @@
+/** @example hello.c
+ * A complete beginner window using CPU pixels, checked creation/presentation,
+ * and ordered shutdown. Run with --smoke-test to present once and exit.
+ */
+
+/** @example simple_window.c
+ * Interactive pointer and tablet samples over a CPU-rendered gradient.
+ */
+
+/** @example tablet.c
+ * Retained drawing content with pressure, tilt, eraser input, and scale changes.
+ */
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..ff40a9b
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,118 @@
+# Getting started
+
+YASL gives a C application desktop windows with CPU pixel targets, scale-aware
+geometry, queued input, clipboard text, and optional frame pacing. It fits
+software-rendered tools such as editors, terminals, drawing applications, and
+custom interfaces that already have their own renderer. A window needs neither
+an OpenGL context nor a graphics device supplied by the application.
+
+The core headers expose no window system types. Native handles and Metal drawing
+are separate opt-in extensions. YASL currently supports Linux (Wayland and X11)
+and macOS (Cocoa); it does not provide widgets, font shaping, audio, an OpenGL
+context API, or a Vulkan surface API. It is pre-release software; see the
+[API policy](api-policy.md) before distributing a library that depends on its ABI.
+
+## Build and run
+
+Use a C23 compiler, Meson 1.6 or newer, and Ninja. GCC and Clang can build the library. On Linux install either backend's development dependencies:
+
+| Backend | pkg-config dependencies |
+| --- | --- |
+| Wayland | `wayland-client`, `wayland-cursor`, `xkbcommon` |
+| X11 | `x11`, `xi`, `xkbcommon`, `xext`; optional `xrandr` |
+| Cocoa | macOS 14+ SDK with AppKit, Metal, and QuartzCore; Metal-capable device |
+
+```sh
+meson setup build
+meson compile -C build
+./build/yasl-hello
+meson test -C build --print-errorlogs
+```
+
+Each Linux backend defaults to `auto`, meaning build it when its dependencies
+are available. Require one with `-Dwayland=enabled` or `-Dx11=enabled`; omit one
+with `-Dwayland=disabled` or `-Dx11=disabled`. At least one backend is required.
+Cocoa is selected on macOS and requires no extra third-party dependencies.
+Its minimum deployment target is macOS 14. Set `MACOSX_DEPLOYMENT_TARGET=14.0`
+(or newer) before configuring if your toolchain defaults to an older target.
+CPU pixel targets are presented through the system's Metal device; applications
+do not need to create or manage that device themselves.
+
+The complete beginner example is [hello.c](../examples/hello.c). It checks
+construction failures, fills every pixel, presents inside `draw`, drains input,
+responds to quit/close/lost, and destroys resources in order. The
+[interactive window](../examples/simple_window.c) adds pointer input; the
+[tablet example](../examples/tablet.c) demonstrates retained application content,
+pressure, erasers, resizing, and scale changes.
+
+## Link an application
+
+Install the library and public headers into your chosen prefix:
+
+```sh
+meson setup build --prefix=/usr/local
+meson compile -C build
+meson install -C build
+cc -std=c23 examples/hello.c -o hello $(pkg-config --cflags --libs yasl)
+./hello
+```
+
+The prefix must be writable, and your compiler, pkg-config, and runtime loader
+must search it. For a custom prefix, set `PKG_CONFIG_PATH` to its pkg-config
+directory and configure the loader for its library directory. Static linking
+uses `pkg-config --static --libs yasl` to include backend dependencies.
+
+A Meson consumer uses the installed dependency:
+
+```meson
+project('my-app', 'c', default_options: ['c_std=c23'])
+yasl = dependency('yasl', version: '>=0.1.0')
+executable('my-app', 'main.c', dependencies: yasl)
+```
+
+YASL also declares `meson.override_dependency('yasl', ...)`, so the same consumer
+can use a vendored Meson subproject through a fallback dependency. Public headers
+can be included from C++11 or newer; the library itself builds as C23.
+
+## The application cycle
+
+1. Open a display with `yasl_display_open(NULL, &spec)`. NULL creates a loop owned
+ by the display. Supply your own loop only when you need shared loop ownership.
+2. Create a toplevel with a positive **logical-pixel** size and install callbacks.
+ Zero-initialize every option and callback struct. Callback tables are copied;
+ userdata must remain alive. Registration can call back synchronously.
+3. Schedule the initial redraw. `draw` receives a borrowed target. Fill every
+ pixel with premultiplied `0xAARRGGBB`, then present that exact target, once.
+4. Dispatch, drain input for every live surface queue, update application state,
+ and flush. Flush runs scheduled drawing and sends native requests. Dispatch
+ may also invoke callbacks, including drawing during native live resize.
+5. Destroy popups before parents, surfaces before displays, and displays before
+ any supplied loop. NULL destroy/close calls do nothing; freed pointers are
+ invalid for every operation.
+
+```c
+while (running) {
+ if (yasl_display_dispatch(display, -1) < 0) {
+ perror("yasl_display_dispatch");
+ break;
+ }
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {
+ /* Read the union member selected by event.kind. */
+ }
+ yasl_display_flush(display);
+}
+```
+
+Zero timeout polls, positive timeout bounds the wait in milliseconds, and a
+negative timeout waits for work. Undrained input makes dispatch return without
+waiting. Use the return value only to detect failure, not to count input events.
+If callbacks destroy surfaces, update your application registry immediately and
+only poll surfaces that are still alive. Arrange display/loop cleanup after the
+cycle ends; destroying them inside a callback is unsupported.
+
+See the [application guide](guide.md) for drawing, input, popups, timers, and
+platform limits, or the comments in the [public header](../include/yasl/yasl.h)
+for each function's contract. To build the searchable HTML reference, install
+Doxygen and run `doxygen Doxyfile` from the repository root. Open
+`build-docs/html/index.html`; Doxygen is not needed to build the library.
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 0000000..43a7a1e
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,298 @@
+# Application guide
+
+The contracts in [yasl.h](../include/yasl/yasl.h), [loop.h](../include/yasl/loop.h),
+[timing.h](../include/yasl/timing.h), and [native.h](../include/yasl/native.h) are
+the reference for individual calls. All examples here use the display's opening
+thread, which must be the main thread on macOS. Userdata stays owned by the
+application. Copy borrowed data before retaining it beyond its documented life.
+
+## Backend selection and capabilities
+
+Zero-initialize `yasl_display_spec` or pass NULL to select automatically. On
+Linux, `WAYLAND_DISPLAY`, `WAYLAND_SOCKET`, or `XDG_SESSION_TYPE=wayland` makes
+YASL try Wayland first; otherwise it tries X11 first. It falls back to the other
+built backend if opening fails. On macOS it uses Cocoa. Explicit
+`YASL_BACKEND_WAYLAND`, `YASL_BACKEND_X11`, or `YASL_BACKEND_COCOA` requests never
+fall back. Query the result with `yasl_display_backend()`.
+
+Use `yasl_display_caps()` for optional behavior. A bit means backend/server
+support, not that a device is attached or that the current input state allows
+an operation. Always check the operation's return value too.
+
+| Capability | Meaning | Wayland | X11 | Cocoa |
+| --- | --- | --- | --- | --- |
+| `YASL_CAP_POPUPS` | Child popup surfaces | Yes, input grab required | Yes | Yes |
+| `YASL_CAP_DRAG` | Surface-drag source and target events | Data device v3 | Yes | Yes |
+| `YASL_CAP_DRAG_CARRY` | Carry a toplevel with a surface drag | Optional toplevel-drag protocol | Yes | Yes |
+| `YASL_CAP_PLACE_WINDOW` | Place a window under the pointer | No | Yes | Yes |
+| `YASL_CAP_PRIMARY_SELECTION` | Separate primary text selection | Optional primary-selection protocol | Yes | Named pasteboard emulation |
+| `YASL_CAP_CSD` | Backend advertises client decoration support | Yes | No | Yes |
+| `YASL_CAP_NATIVE_GRAPHICS` | Application-managed native drawables | No | No | Metal |
+| `YASL_CAP_FRAME_PACING` | Display/compositor-driven drawing | Yes | No | Yes |
+| `YASL_CAP_TABLET` | Tablet event translation | Optional tablet-v2 protocol | XInput 2 | Yes |
+
+This table describes the implementation, not universal operating-system
+promises. Wayland protocols and XInput availability depend on the running
+server. A capability does not guarantee every related action: Cocoa currently
+returns false for interactive resize and window-menu requests even though
+it advertises CSD. Check each result and provide an application fallback.
+
+## Geometry and scaling
+
+| Value | Units |
+| --- | --- |
+| Toplevel `yasl_surface_spec.size` | Logical pixels |
+| `yasl_geometry.size`, framebuffer size | Device pixels |
+| Pointer position, caret, window-menu position | Device pixels, relative to the surface |
+| Damage rectangles | Device pixels, relative to the draw target |
+| All popup fields, including `size` | Device pixels; anchor is relative to the parent |
+| Drag image size, hotspot, carry/place offsets | Device pixels |
+| `scale_120` | 120 = 1x, 180 = 1.5x, 240 = 2x |
+
+The origin is the top left, x increases right, and y increases down. Use
+`yasl_surface_geometry()` for the current size and scale, and update cached
+layout on `configure`. The native system may choose a different size from the
+request. X11 currently reports unit scale; automatic output DPI scaling is not
+implemented there.
+
+```c
+struct yasl_geometry geometry = yasl_surface_geometry(surface);
+int padding_px = yasl_scale_to_device(8.0, geometry.scale_120);
+int logical_width = yasl_scale_to_logical(geometry.size.x, geometry.scale_120);
+```
+
+Scale conversion rounds to the nearest integer, with ties away from zero,
+saturates out-of-range finite results, and returns zero for a zero scale.
+`yasl_scale_to_device()` also returns zero for non-finite logical coordinates.
+
+## Drawing and pacing
+
+`yasl_surface_schedule_redraw()` coalesces requests. `yasl_display_flush()`
+services them when the backend can draw. During a native resize, dispatch can
+also draw synchronously. Avoid assuming callbacks occur only in flush.
+
+The CPU target is tightly packed premultiplied `0xAARRGGBB`. Row stride is
+`target.size.x * sizeof(uint32_t)`. For example, opaque red is `0xffff0000`,
+50% transparent red is approximately `0x80800000`, and transparent black is
+`0x00000000`. The numeric format describes a 32-bit value, not a portable byte
+sequence. Keep your application's durable content separately: a new target may
+contain no previous frame contents.
+
+```c
+static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud)
+{
+ (void)ud;
+ size_t count = (size_t)frame->target.size.x * frame->target.size.y;
+ for (size_t i = 0; i < count; i++)
+ frame->target.pixels[i] = 0xff306090;
+ struct yasl_damage_list damage = {.n = -1};
+ if (!yasl_surface_present(surface, &frame->target, &damage)) {
+ /* Arrange application error handling after returning. */
+ }
+}
+```
+
+Present that exact target at most once during draw. Do not change its pointer,
+size, or generation, free it, or use it after callback return or surface
+destruction. A successful present consumes it. Invalid input returns false
+without consuming it. `damage.n = -1` means full target; `0` means no damaged
+rectangles; positive `n` uses the first `n` entries in `rects`. Extents must be
+nonnegative, and the core clips them to the target. `bounds` is a reserved hint
+and can be zero. Damage may reduce uploads, but every target must still contain
+complete valid content; backends may upload all pixels.
+
+To animate, schedule again from draw. With `YASL_CAP_FRAME_PACING`, the backend
+uses display/compositor signals to limit draws; tearing can bypass this. X11
+is unpaced: scheduling continuously will use CPU continuously. Use an application
+timer to limit redraws there. Suspended/hidden windows need not draw at each
+requested interval.
+
+`yasl_surface_timing()` reports estimates and their provenance. All timestamps
+are monotonic nanoseconds. `YASL_TIMING_SUBMISSION` is CPU submission time,
+`YASL_TIMING_FRAME_CALLBACK` is a compositor pacing signal, and only
+`YASL_TIMING_PRESENTATION` confirms compositor presentation. `UNKNOWN` means no
+sample. `refresh_ns` and `next_present_ns` remain estimates. Use
+`yasl_surface_frame_time_ns(surface, yasl_loop_now_ns())` as a stable animation
+clock; it does not establish actual scanout time.
+
+## Input, text, and payload lifetime
+
+Dispatch receives native input. `yasl_surface_input_poll()` only drains it and
+never waits. Poll every live surface queue until empty. Wayland routes popup
+pointer and keyboard events through the root parent's queue; inspect
+`event.surface` to identify the recipient. Do not assume it equals the object
+whose queue you polled.
+
+Read the union member selected by `event.kind`. The `reserved` field is internal
+and must not be interpreted. Text, paste, and file-drop payloads are UTF-8 byte
+strings, NUL-terminated; `len` excludes the NUL. They remain valid until the
+next **successful** poll on the queue or queue destruction. Dispatch, flush,
+and unsuccessful polls do not invalidate them. Copy data to keep it longer.
+
+Use `YASL_INPUT_TEXT` for document text insertion, including committed IME text.
+It contains UTF-8 and can contain several characters. Key actions are for
+commands and held-key state; releases and repeats have separate tags. A text
+commit need not correspond to one key event, and a key need not produce text.
+
+`key.key` is a portable physical identity from
+[keycodes.h](../include/yasl/keycodes.h), included by `<yasl/yasl.h>`. Names follow
+US keyboard positions: `YASL_KEY_A` identifies the same position on a French
+layout even though it types Q. Modifiers do not change this identity. Keypad
+keys and left/right modifiers have distinct names. Unknown native keys report
+`YASL_KEY_UNKNOWN`; `native_code` preserves their backend-specific identity
+(Wayland evdev code, X11 server keycode, or Cocoa virtual keycode).
+
+`codepoint` and `shifted` are optional Unicode scalar labels for the active
+layout without modifiers and with Shift. Zero means no single printable label,
+including function keys, unavailable translations, and mappings that produce
+multiple characters. Dead keys may have a spacing label or zero. These fields
+are useful for displaying shortcuts or
+choosing layout-sensitive commands; they are never document text. For example:
+
+```c
+if (event.kind == YASL_INPUT_KEY && event.key.action == YASL_KEY_PRESS &&
+ event.key.key == YASL_KEY_ESCAPE) {
+ running = false;
+}
+/* Insert event.text.text[0..event.text.len) only for YASL_INPUT_TEXT. */
+```
+
+Wayland uses the supplied XKB layout and locale compose rules for ordinary text
+and dead keys, with text-input-v3 for IME commits. Compose rules use `LC_ALL`,
+`LC_CTYPE`, then `LANG`, falling back to `C.UTF-8`. X11 uses the X input method
+when available; Cocoa uses AppKit text services. X11 input methods use the
+application's `LC_CTYPE` locale. Initialize it before opening a display when
+using locale-specific XIM, for example with `setlocale(LC_CTYPE, "")`; YASL does
+not change the process locale. IME availability and system configuration still
+determine which compositions work. Physical key events
+remain available for commands; input methods may consume keys while composing.
+
+Pointer events use `YASL_INPUT_MOUSE`, with `device` distinguishing mouse, touch,
+pen, and eraser. Press/release button 0 is primary/tip, 1 middle, and 2 secondary.
+Track those transitions; motion/leave button values are informational. Pen
+pressure is normalized to 0..65535, signed tilt is in degrees, and missing axes
+are zero. Pressure-only changes can produce motion. Zero pressure alone does
+not mean release. This interface does not expose simultaneous touch contacts.
+
+For `YASL_PTR_SCROLL`, read `mouse.scroll.x` and `.y`: fractional line offsets,
+positive right and down, after system scroll preferences. Both axes may be
+nonzero in one event. Preserve fractions and choose your application's own
+pixels per line; one wheel detent normally gives one line. `button` is unused
+for scroll. Wayland high-resolution wheel values take precedence over duplicate
+discrete/raw descriptions. Smooth Wayland offsets and precise Cocoa offsets
+use ten native units per line. X11 currently reports whole wheel steps, so it
+does not provide the same smooth-scroll precision as the other backends.
+
+Cancel held keys, buttons, active strokes, and composition on `YASL_INPUT_RESET`,
+focus loss, or pointer leave as appropriate to your application. RESET has no
+union payload. It is emitted after input-queue allocation failure and certain
+device interruptions; ignoring it can leave application input stuck.
+
+## Clipboard, file drops, and IME
+
+`yasl_display_copy()` copies a NUL-terminated UTF-8 string during the call.
+`yasl_display_paste()` requests delivery to the original target as
+`YASL_INPUT_PASTE`; destroying the target cancels delivery. There is no transfer
+ID or error callback, so unavailable or failed transfers may produce no event.
+Wayland selection offers can require a recent input serial. X11 currently does
+not implement incremental (`INCR`) selection transfers and may reject large
+transfers. X11 converts legacy `STRING` data between Latin-1 and UTF-8; it only
+offers that legacy format when the copied text fits Latin-1. Malformed UTF-8
+clipboard transfers are discarded. Unsupported primary selection does nothing
+and never aliases the clipboard. Cocoa uses a YASL named pasteboard to emulate primary selection;
+this is not a desktop-wide macOS select-to-copy convention.
+
+External file drag hover uses the surface `drop` callback; actual file URLs
+arrive as `YASL_INPUT_DROP_FILES`. `files` is a `text/uri-list` string separated by
+CRLF, not a list of native paths. Parse URL encoding, skip blank/comment lines,
+and decide which schemes/hosts your application accepts before opening files.
+
+Use `yasl_surface_ime_preedit()` to draw the uncommitted composition. The borrowed
+pointer expires on the next dispatch or surface destruction; copy to keep it.
+Set the candidate-window caret with `yasl_surface_set_ime_caret()` in device
+pixels. X11 has no inline preedit. Cocoa supplies preedit text but currently no
+cursor range. On Wayland, `yasl_surface_ime_preedit_cursor()` returns UTF-8 byte
+offsets when a range is available. A false result leaves outputs unchanged.
+
+## Popups and surface drags
+
+Create popups from a live parent, normally in response to input. All fields in
+`yasl_popup_spec` use device pixels. `size` includes transparent margins;
+`content_offset` and `content_size` describe the actual content rectangle used
+for positioning. A zero content-size axis extends to the corresponding size
+edge. `beside = true` prefers the anchor's right edge; false prefers below.
+Placement may flip or slide to stay on screen. Wayland requires a recent input
+serial to grab a popup, and older protocols may reject repositioning. Destroy
+children before parents. The close callback requests application destruction;
+it does not automatically free the popup.
+
+The surface `drag` callback handles a YASL surface drag; `drop` handles external
+data hover. `yasl_display_drag_start()` is intended for window/tab tear-off
+workflows, not arbitrary MIME data sources. Keep transferred application state
+in your own registry. Start from a recent pointer press, and check the boolean
+result even when `YASL_CAP_DRAG` is present. Supply an optional copied image and
+completion listener. One active drag is supported per display. A successful
+start calls a non-NULL `end` exactly once, including explicit cancel or display
+close; a failed start never does. `DROPPED` does not prove that another
+application accepted your application object.
+
+Where `YASL_CAP_DRAG_CARRY` is present, `yasl_surface_drag_carry()` can attach a
+toplevel to the active drag. Otherwise an image can represent it. With
+`YASL_CAP_PLACE_WINDOW`, `yasl_surface_place_under_pointer()` requests placement
+relative to the pointer. These calls may fail due to current input/drag state.
+Completion can run synchronously during cancel/close; keep listener userdata
+alive until then and do not destroy the display recursively.
+
+## Timers and file descriptors
+
+Include `<yasl/loop.h>`. Use `yasl_display_loop(display)` for application timers
+so they participate in display dispatch. Intervals are nanoseconds; the
+`YASL_MS_TO_NS(ms)` macro converts a nonnegative integer millisecond value.
+
+```c
+static uint64_t tick(void *ud, yasl_timer_id id, uint64_t interval_ns)
+{
+ (void)id;
+ yasl_surface_schedule_redraw(ud);
+ return interval_ns; /* Return zero for a one-shot timer. */
+}
+/* Check zero for failure. This timer is canceled with the surface. */
+yasl_timer_id timer = yasl_surface_add_timer(surface, YASL_MS_TO_NS(16), tick, surface);
+```
+
+A timer callback receives the scheduled interval, not actual elapsed time. A
+nonzero return repeats/reschedules; zero retires it. Missed periods coalesce.
+Timers can add/delete timers, including themselves. Loop timers end when retired,
+deleted, or the loop is destroyed; surface timers also end on surface destruction.
+Userdata remains application-owned. Delete surface timers with
+`yasl_surface_del_timer()` and ordinary timers with `yasl_loop_del_timer()`.
+
+File watches use `<poll.h>` masks. `yasl_loop_add_fd()` returns a positive watch
+ID or -1. Its callback receives `revents`, including error/hangup flags; its
+integer return is ignored. Consume readiness or disable/delete the watch.
+The loop never closes application file descriptors: delete the watch before
+closing its descriptor. Ready callbacks may add or remove watches. Do not call
+`yasl_loop_step()` or `yasl_loop_run()` recursively.
+
+Cocoa dispatch waits for native events and checks the loop nonblockingly
+afterward. An application FD alone does not currently wake an indefinite Cocoa
+dispatch; use a bounded timeout when integrating such sources. `loop_kick()`
+signals the loop pipe but is not a portable cross-thread event-posting API.
+
+## Native interoperability
+
+Portable CPU drawing needs no native headers. Opt into `<yasl/native.h>` and
+`<yasl/native/wayland.h>`, `<yasl/native/x11.h>`, or `<yasl/native/cocoa.h>` when
+integrating a platform API. Typed headers and functions are installed only for
+built backends. Accessors return borrowed handles and zero/NULL for the wrong
+backend. Do not pass freed objects, destroy borrowed handles, independently
+dispatch native connections, or change surface roles and buffer ownership.
+
+Application-managed native drawing currently supports Cocoa/Metal. Use
+`yasl_surface_create_native()` with a non-NULL Metal device, which YASL retains.
+During draw, query `yasl_surface_get_native_frame()` for a borrowed drawable,
+encode and present with your renderer, then call `yasl_surface_present()` to
+record submission. The drawable expires on successful present, callback return,
+or surface destruction. Do not infer GPU completion from the boolean result.
+Other backends return NULL with `errno = ENOTSUP` for native creation; having a
+native window handle does not imply this rendering path is supported.
diff --git a/docs/migrating.md b/docs/migrating.md
new file mode 100644
index 0000000..d224cd4
--- /dev/null
+++ b/docs/migrating.md
@@ -0,0 +1,40 @@
+# Migrating pre-release applications
+
+YASL 0.1 has no stable source or binary compatibility promise. Rebuild all
+consumers against the matching installed headers and library after updating.
+The following changes replace backend-specific input fields with a common
+contract. See the [application guide](guide.md) for the full input workflow.
+
+## Keyboard events
+
+| Previous use | Replacement |
+| --- | --- |
+| Linux keysym or Cocoa virtual keycode in `key.key` | Portable physical `YASL_KEY_*` identity |
+| `key.base` | `key.native_code`, with explicit backend-specific encoding |
+| `key.keypad` | Distinct `YASL_KEY_KP_*` identities |
+| Advisory `key.text` character | `YASL_INPUT_TEXT` UTF-8 payload for insertion |
+| Layout label from a keysym | Optional `key.codepoint` and `key.shifted` Unicode scalars |
+
+Replace magic numbers and X11 symbols with named keys. For example, a quit
+command checks `key.key == YASL_KEY_ESCAPE` on a press. `YASL_KEY_A` means the
+US A position on every layout; use layout labels when a command should follow
+the printed letter instead. Do not cast key identities to characters. Keypad
+Enter and ordinary Enter now have distinct identities without a separate flag.
+
+Text comes only from `YASL_INPUT_TEXT`. Handle its byte length, copy it when
+retaining it after the next successful poll, and allow multiple characters per
+commit. Cocoa sends key events through its text input manager; Wayland emits
+ordinary XKB text and locale compose results in addition to IME commits.
+
+## Scroll events
+
+Replace `scroll_lines`, `scroll_delta`, and scroll button numbers with
+`mouse.scroll.x` and `.y` when `mouse.kind == YASL_PTR_SCROLL`. These double
+values are fractional line offsets, positive right/down on every backend.
+Do not infer scroll direction from `button`. One event may include both axes.
+Remove backend-specific sign inversions and whole-line casts; multiply the
+offset by your application's preferred pixels per line instead.
+
+X11 still supplies whole wheel steps. Wayland high-resolution wheels and smooth
+offsets, and Cocoa precise scrolling, preserve fractions. Device acceleration
+and system preferences still affect the native offsets.
diff --git a/docs/testing.md b/docs/testing.md
new file mode 100644
index 0000000..d01b82c
--- /dev/null
+++ b/docs/testing.md
@@ -0,0 +1,156 @@
+# Testing and examples
+
+Configure isolated Linux display-server integration tests with Weston and
+Xwayland installed:
+
+```sh
+meson setup build-test -Dintegration_tests=true
+meson test -C build-test --print-errorlogs
+```
+
+The harness starts and cleans up its own servers. It checks explicit requests,
+automatic selection, fallback, both backends in one process, frame lifetimes,
+callback copying, clipboard ownership, and timers. It also runs the beginner
+and tablet examples on both backends. On macOS the option enables native window,
+input ownership, keyboard/text/scroll, cursor, tablet, and example tests; these
+require a desktop session. Linux results do not verify Cocoa behavior.
+
+For memory/undefined-behavior checks use a separate build:
+
+```sh
+meson setup build-sanitize -Db_sanitize=address,undefined -Dintegration_tests=true
+meson test -C build-sanitize --print-errorlogs
+```
+
+Default tests need no desktop connection. C++ header coverage is built when a
+C++ compiler is available. On Linux and macOS, the shared-export test ensures
+only installed API declarations are exported. Test disabled-backend configurations separately with
+`-Dwayland=disabled` or `-Dx11=disabled`; the combined integration harness needs
+both backends.
+
+Keyboard tests check physical identities, distinct keypad/modifier keys, layout
+labels, Shift/Caps Lock, shortcut text suppression, AltGr, dead-key composition,
+repeat, and composition reset. The X11 integration test changes and restores
+the isolated server's keymap to verify that labels change while physical keys
+retain their identity. The X11 text fallback is also checked without an input
+method, including Latin-1, Cyrillic, and non-BMP characters encoded as UTF-8.
+Scroll tests cover both axes, fractional Wayland offsets,
+duplicate protocol representations, and X11 core/XInput wheel directions.
+Cocoa integration tests cover UTF-16 labels, modifier-only events, native text
+commit routing, and diagonal precise scrolling. Native IME interaction and
+hardware gestures still need testing on the target desktop.
+Clipboard tests cover Latin-1 interoperability, empty transfers, and rejection
+of malformed UTF-8 on Linux.
+
+## Cocoa validation
+
+On macOS 14 or newer, use a logged-in desktop session with a Metal-capable
+device. Configure `-Dintegration_tests=true`, compile, and run the full suite.
+Cross-compiling with an Apple SDK checks compilation and linking; run
+`meson test -C build-macos public-exports` to check the dylib's exports on the
+build machine. A cross file should provide an `nm` that can read Mach-O, such
+as `llvm-nm`. These checks do not execute the native window and input tests.
+
+To move an already compiled, unsanitized Cocoa suite to another Mac without
+rebuilding it:
+
+```sh
+python3 tests/helpers/package_cocoa_tests.py build-macos cocoa-tests.tar.gz
+```
+
+On the matching Mac, extract the archive and run:
+
+```sh
+tar -xzf cocoa-tests.tar.gz
+python3 yasl-cocoa-tests/run.py
+```
+
+The bundle includes the dylib, every executable test registered by Meson, and
+the license. The build-machine export check is separate. The runner checks file
+hashes, enforces each test's timeout, saves output under `logs/`, and writes
+`results.json`. A nonzero exit means validation failed. `--list` lists the
+bundled tests without running them. Use Python 3.8 or newer and a bundle matching
+its architecture; Apple Silicon uses `aarch64`, Intel uses `x86_64`.
+
+## 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 `<yasl/yasl.h>` 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.
+
+The example uses portable physical keys: C and Space clear, Escape exits.
+Letter names describe US positions, so the clear key follows the C position
+when the user selects another layout.
+
+`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.
+
+## Benchmarks
+
+`yasl-benchmark` measures queue, timer, and surface-operation costs. Results
+depend on the build, backend, and display server; they do not measure confirmed
+screen presentation latency.
+
+```sh
+# Run via meson test runner
+meson test -C build --benchmark --verbose
+
+# Or invoke the executable directly
+ninja -C build tests/yasl-benchmark
+./build/tests/yasl-benchmark
+```
+