# Application guide The contracts in [yasl.h](../include/yasl/yasl.h), [loop.h](../include/yasl/loop.h), [timing.h](../include/yasl/timing.h), [desktop.h](../include/yasl/desktop.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. ## Window creation and callbacks All examples include `` and call `yasl_run()` with a `yasl_app_desc`. Set `window` to a `yasl_window_spec` for a single toplevel, which the runner creates before `init` with its first frame requested; `yasl_run()` returns an error if creation fails. Create further surfaces in the optional `init`, handle `YASL_EVENT_*` notifications in `event`, and render into `frame->target.pixels` in `frame`. Present with `yasl_frame_present(frame, NULL, 0)` and return `YASL_APP_QUIT` from an event handler to exit. Without an `event` callback, closing any window stops the runner. The runner handles dispatch, queued event delivery, resize redraws, and remaining surface/display cleanup. Display quit stops the runner; connection loss and dispatch failures return an error after cleanup. The runner delivers the same display-wide FIFO consumed by `yasl_display_poll_event()`, including window state, drag hover, composition, clipboard completion, and display lifecycle events. `event.sequence` increases in enqueue order across all windows; it is YASL capture order, not a claim about ordering between native producers. Return `YASL_APP_ERROR` on application failure, or call `yasl_app_exit(display, YASL_APP_ERROR)` from a frame, timer, or fd callback. Cleanup still runs, and `yasl_run()` returns failure. During `init`, `yasl_app_window(display)` exposes the optional initial window. Application data can be supplied in `window.userdata` or attached with `yasl_surface_set_userdata()`; it is independent of callback userdata. Unified pointer button and motion events retain `device` and `pen` axes, including erasers and pressure-only motion. Cancel held input or active strokes on `YASL_EVENT_INPUT_RESET`, pointer leave, or focus loss. Reset also starts the next motion delta from zero. [hello.c](../examples/hello.c) shows the minimal lifecycle; [tablet.c](../examples/tablet.c) demonstrates device-aware drawing. For applications managing their own event loop, use the core API below. Open the display with application-wide options and notifications in `yasl_display_spec`. Create each toplevel with `yasl_window_spec`, including its title, `size_logical`, optional `yasl_window_cbs` table in `cbs`, and userdata in `ud`. One display can own multiple windows with separate callback tables. The window copies its callbacks during creation. Its callbacks cannot run before creation returns, so the application can store the returned surface first. Userdata stays borrowed until callbacks are replaced or the surface is destroyed. Read initial geometry with `yasl_surface_geometry()`; creation does not guarantee an initial `configure` notification. Request the first redraw once application state is ready. `yasl_surface_set_callbacks()` replaces callbacks later and also installs popup callbacks; the setter does not invoke the callbacks. When using `yasl_run()`, its runner installs callbacks for windows on its display. Use `yasl_app_desc.event` and `.frame` rather than `yasl_window_spec.cbs` and `.ud`. Replacing callbacks on a runner-owned surface fails with `EBUSY`. With a manual loop, install just the draw callback and drain the display FIFO after dispatch; lifecycle and input share the same event handler logic. Read window state with `yasl_surface_state()`. Its `YASL_STATE_*` flags can coexist, and the `state` callback receives the same current bitmask. Wayland also reports `YASL_STATE_TILED` when any window edge is tiled; X11 and Cocoa currently leave that flag unset. Unsupported flags are absent. On Wayland, an ordinary floating toplevel has fullscreen, maximized, and tiled all absent. `YASL_STATE_SUSPENDED` describes repaint suspension rather than minimization; there is no portable minimized flag. ## Geometry and scaling | Value | Units | | --- | --- | | Toplevel `yasl_window_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_request_frame()` 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; if (!yasl_frame_present(frame, NULL, 0)) { /* Arrange application error handling after returning. */ } } ``` Present the exact active frame at most once during its callback. Do not mutate the target identity or retain frame/pixel pointers after return. NULL rectangles with count zero mean full damage; a non-NULL array with zero count means no damage. All supplied extents must be nonnegative. Rectangles are clipped, and more than `YASL_DAMAGE_MAX` rectangles fall back to full damage after all are validated. Damage may reduce uploads, but every target must contain complete valid pixels. Native rendering uses the same frame, with a drawable queried in the extension. `yasl_surface_set_pacing()` works in either loop style. ON_DEMAND coalesces explicit requests and sleeps when idle. CONTINUOUS uses native display signals where available and a refresh-based timer otherwise, with about 60 Hz as a fallback estimate. It reports allocation/argument failures instead of silently ignoring surfaces outside the runner. Suspension can suppress actual drawing. Legacy `yasl_surface_present()` and `yasl_surface_schedule_redraw()` remain migration adapters; new code uses the frame presentation and request functions. `yasl_surface_read_pixels()` copies the last presented CPU frame, for example for a screenshot or a debugging tool. Call it outside draw callbacks. A frame is available from a successful present until the next draw starts, and again once that draw presents. Call it with no buffer first to learn the extent. The copy is YASL's frame, not the composited screen, and native rendering has none. `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_display_poll_event()` drains the display FIFO without waiting. Drain it once for the whole display, including multiple windows and popup input. `event.surface` identifies the live recipient and `surface_id` is its stable identity. Destruction cancels undelivered recipient events. `yasl_surface_poll_event()` remains an input-only migration adapter and discards pending non-input notifications, which legacy callbacks already delivered. Do not mix the two polling styles. Read the union member selected by `event.type`. `time_ns` is monotonic and `sequence` orders capture across the display. TEXT and DROP use `.text`, PASTE uses `.paste`, and COMPOSITION uses `.composition`. Each string is copied when queued, NUL-terminated, and borrowed until the next successful display/surface poll or display close. Dispatch, unsuccessful polls, and recipient destruction do not invalidate the last polled string. Copy it to retain it longer. Use `YASL_EVENT_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 ``. 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.type == YASL_EVENT_KEY && event.key.action == YASL_KEY_PRESS && event.key.key == YASL_KEY_ESCAPE) { running = false; } /* Insert event.text.data[0..event.text.len) only for YASL_EVENT_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 the `YASL_EVENT_POINTER_*` types and share `event.pointer`, with `device` distinguishing mouse, touch, pen, and eraser. `POINTER_DOWN` and `POINTER_UP` carry a `YASL_BUTTON_*` value: `PRIMARY` is the left button, pen tip, or touch contact. Other types leave `button` meaningless. Every pointer event also carries `buttons`, a `YASL_BUTTON_BIT()` mask of the buttons held after that event, tracked from the queue's down and up events. Use it to tell a drag from a hover. Count clicks against `yasl_display_double_click_ms()`, the system interval: XSETTINGS on X11, the KDE or GNOME setting through the Settings portal on Wayland, and the system value on Cocoa. It falls back to `YASL_DOUBLE_CLICK_MS_DEFAULT` (400 ms). The query can wait briefly on Linux, so read it when a window gains focus. `YASL_EVENT_INPUT_RESET` clears the mask. On Wayland, `POINTER_LEAVE` also clears it, because the compositor sends leave during a press only when it takes the pointer away. X11 and Cocoa keep the press across leave and still deliver the release. `POINTER_MOTION` reports `delta`, the device-pixel movement since the previous sample, which restarts from zero after enter, leave, or reset. `yasl_surface_set_pointer_mode()` with `YASL_POINTER_RELATIVE` hides and locks the cursor while the surface is focused; motion then carries unbounded `delta` and `pos` stays at the lock position. Wayland implements it with the relative-pointer and pointer-constraints protocols, reporting the compositor's accelerated motion in device pixels. The compositor decides when the lock engages; until then, and while it is released on focus loss, motion stays absolute with a computed `delta`. X11, Cocoa, and compositors without those protocols return false and keep the current mode. 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_EVENT_POINTER_SCROLL`, read `pointer.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_EVENT_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_clipboard_set(display, selection, text)` copies a NUL-terminated UTF-8 offer and reports failure through its return and errno. Wayland may require a recent input serial. `yasl_clipboard_request(surface, selection)` returns a nonzero request ID, or zero with errno when rejected. Each accepted request produces one `YASL_EVENT_PASTE` carrying `.paste.request_id`, `.status`, `.data`, and `.len`, unless its target/display is destroyed. OK includes empty text; UNAVAILABLE means no text offer, and FAILED means transfer/decoding failure. External Linux transfers have a 30-second timeout. X11 does not implement INCR and reports unsupported large transfers as FAILED. Legacy STRING uses Latin-1 conversion. Malformed UTF-8 completes as failure. Unsupported primary selection reports ENOTSUP and never aliases the clipboard. Cocoa uses a YASL named pasteboard for primary selection. Legacy copy/paste calls remain adapters; legacy paste completions carry request ID zero. External file drag hover uses `YASL_EVENT_DROP_HOVER`; actual file URLs arrive as `YASL_EVENT_DROP`. `text.data` is a `text/uri-list` string separated by CRLF, not a list of native paths. `yasl_drop_next_path()` walks it and decodes each local `file:` URL into a path. It skips comments, other schemes, remote hosts, and malformed lines: ```c const char *cursor = event.text.data; char path[4096]; size_t len; while ((len = yasl_drop_next_path(&cursor, path, sizeof(path))) != 0) { if (len >= sizeof(path)) break; /* Too long; grow the buffer instead. */ /* Open path. */ } ``` Decoded paths are raw bytes and may contain any character the file system allows, including newlines. Quote them before pasting into a shell. Handle `YASL_EVENT_COMPOSITION` for immutable UTF-8 preedit snapshots, byte selection offsets, and composition ending. Missing selection information uses -1 offsets. Insert only committed `YASL_EVENT_TEXT` into the document. Set the candidate-window caret with `yasl_surface_set_ime_caret()` in device pixels. X11 uses XIM preedit callbacks when offered; fallback input methods may provide no inline composition. The old preedit getters remain migration helpers. ## 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. `YASL_EVENT_DRAG` handles a YASL surface drag; `YASL_EVENT_DROP_HOVER` handles external data hover. Legacy callbacks remain migration adapters. `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 ``. 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 `` 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. Every backend's dispatch wakes when a loop descriptor becomes ready. Cocoa waits for native events and mirrors the loop's descriptors in a kqueue while it waits. If kqueue rejects a descriptor, Cocoa bounds the wait to 10 ms instead. Loop wake and post calls work from any thread. `yasl_loop_kick()` only wakes the loop, and it is also safe in a signal handler. `yasl_loop_post()` queues a function to run on the owning thread during a later step or dispatch. Use it to hand results from worker threads to the loop: ```c static void results_ready(void *ud) { worker_drain(ud); /* Runs on the loop thread. */ } /* On the worker thread; false means allocation failed. */ yasl_loop_post(loop, results_ready, worker); ``` `yasl_loop_watch_signals()` delivers POSIX signals to a callback on the loop thread. It installs a process-wide handler for each signal, with `SA_RESTART`, and restores the previous action on unwatch. YASL leaves thread signal masks alone: a watched signal must stay unblocked in at least one thread. Only one loop can watch a given signal at a time. ## Worker messages and window requests Capture `yasl_surface_id(surface)` on the display thread and pass that value to workers. `yasl_display_post(display, &message)` accepts an identity instead of a borrowed surface pointer. Zero targets the display. On success, optional `message.release(payload)` runs exactly once on the display thread after delivery or cancellation, including shutdown. It may post messages but must not poll, dispatch, or destroy YASL objects. Failed posting leaves ownership with the caller. Stop and join producers before closing the display. The same API works with a manual loop and with the runner; `yasl_post_event()` is a legacy adapter. Toplevel requests `yasl_surface_set_size(surface, logical_size)`, `yasl_surface_set_fullscreen`, `yasl_surface_set_maximized`, and `yasl_surface_minimize` return acceptance with errno on failure. Confirm actual geometry/state through queued events. Popup targets are invalid. Wayland cannot request an arbitrary live size and returns ENOTSUP for that operation; fullscreen, maximize, and minimize use xdg-shell requests. X11 and Cocoa implement all four. ## Desktop services Include `` for services that sit next to windowing. Display and surface calls use the display thread; path and locale calls work from any thread. | Call | Linux | Cocoa | | --- | --- | --- | | `yasl_display_open_uri()` | `xdg-open` from `PATH` | NSWorkspace | | `yasl_display_notify()` | Notifications service over D-Bus | User notifications; app bundle required | | `yasl_display_appearance()` | Settings portal over D-Bus, then `GTK_THEME` | Effective appearance and increase-contrast setting | | `yasl_surface_bell()` | Wayland xdg-system-bell; X11 XKB bell | `NSBeep` | | `yasl_executable_path()` | `/proc/self/exe` | Executable path, resolved | | `yasl_resource_dir()` | Always zero | App bundle resources | | `yasl_default_locale()` | `LC_ALL`, `LC_CTYPE`, `LANG`, then `C.UTF-8` | Current language and region | `yasl_display_open_uri()` checks only that the URI has a scheme. Decide which schemes and hosts your application trusts before calling it, for example before opening a link from untrusted text. On Linux the opener starts in its own session with default signal handling and none of the application's descriptors above stderr. D-Bus services need YASL built with `dbus-1`; without it, notifications return false and appearance falls back to `GTK_THEME`. Appearance changes send no notification, so query again when a window gains focus. Functions that fill a buffer return the full length, excluding the NUL. A result less than the buffer size means the string was written. Otherwise the buffer is unchanged, so call again with a larger one: ```c char path[4096]; size_t len = yasl_executable_path(path, sizeof(path)); if (len == 0 || len >= sizeof(path)) { /* No path, or a longer buffer is needed. */ } ``` ## Native interoperability Portable CPU drawing needs no native headers. Opt into `` and ``, ``, or `` when integrating a platform API. Typed native functions are exported 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_frame_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.