summaryrefslogtreecommitdiff
path: root/docs/guide.md
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-13 23:41:31 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-13 23:41:31 +0300
commit76073fe5fad8799e5850805f2f090af2cb62c741 (patch)
tree9658019babe4760a3634bdab4da0731a10e42e68 /docs/guide.md
parentdd1775a9927922c14f8679ccbc4d3ca8d5917e20 (diff)
Simplify the public event and CPU API
Applications now need far less ceremony: hello.c is a 20-line loop that handles REFRESH, CLOSE and QUIT and draws into the surface's own pixels. - Fold GEOMETRY, STATE and FOCUS into one WINDOW event carrying size, scale and state; unchanged values send nothing. - Guarantee REFRESH once a window has a size and after every resize, never at size zero. - Merge DROP_HOVER into DROP with over/leave/data actions and give drag and drop actions enums. - Keep event strings valid until the next wait and drop yasl_event_release; empty or failed waits report NONE. - Replace copy-in presentation with a lent pixel buffer: yasl_cpu_pixels and yasl_cpu_present. - Remove yasl_poll_event, yasl_cpu_read_pixels, unused event fields and comments that restated names. - Wayland: resize floating windows in request_size instead of reporting it unsupported, and stop marking new GPU surfaces as Vulkan so yasl_vulkan_create_surface no longer fails with NATIVE_WINDOW_IN_USE. Also includes the in-progress public API refactor that was uncommitted in the tree (removal of the app/loop layer, new surface, GL and Vulkan entry points). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/guide.md')
-rw-r--r--docs/guide.md617
1 files changed, 141 insertions, 476 deletions
diff --git a/docs/guide.md b/docs/guide.md
index 1b64880..d0db071 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -1,476 +1,141 @@
-# 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 `<yasl/app.h>` 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 `<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.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 `<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.
-
-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 `<yasl/desktop.h>` 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 `<yasl/native.h>` and
-`<yasl/native/wayland.h>`, `<yasl/native/x11.h>`, or `<yasl/native/cocoa.h>` 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.
+# Surface and input guide
+
+## Lifetime, threads, and errors
+
+The first successful `yasl_surface_create` or `yasl_gl_surface_create` initializes
+shared native state. Subsequent surfaces share one backend. Optional `yasl_set_app_id`
+and `yasl_set_backend` configure the next initialization; they fail after initialization.
+Destroying the final surface preserves shared state. `yasl_shutdown` destroys remaining
+surfaces, releases the backend, and restores defaults. Release application-owned GPU
+objects before their surface or shutdown. Parent destruction destroys popup descendants.
+
+Creation, window operations, event pumping, and shutdown use the first creation's
+thread, which must be the main thread on Cocoa. GL context binding/drawing/swapping
+may use a rendering thread; detach its context before destroying the window. The
+application synchronizes renderer state/geometry and joins workers before shutdown.
+`yasl_wake` is thread-safe and does nothing before initialization or after shutdown.
+
+Constructors return NULL on failure; fallible mutations return bool. Event reads
+return 1 for an event, 0 for empty/timeout/wake, -1 for failure. `yasl_get_error`
+returns a stable per-thread error and static description; `yasl_get_native_error`
+returns supplementary native diagnostics. Success does not clear a preceding error.
+Valid-object size/scale/userdata getters return ordinary values. Destroyed pointers
+are invalid. Strings passed to creation and setters are copied; userdata is borrowed.
+
+## Events and coordinates
+
+`yasl_poll_event` and `yasl_wait_event` process native events and consume one shared
+FIFO. No manual dispatch/flush cycle. A failed/empty read leaves output unchanged.
+Wait timeout zero polls, negative waits for work, positive milliseconds bound waiting.
+A wake can return zero. Polling before initialization returns zero; waiting reports
+NOT_INITIALIZED. Close and quit are requests; the application decides how to respond.
+Display loss is a terminal native failure; destroy resources and shut down.
+
+Read the union member selected by type. Events own their text through
+`yasl_event_release`, independently of later polls and source destruction. Release
+every successful event exactly once; numeric events need no allocation to release.
+Do not duplicate event ownership by shallow copying. Event surface pointers remain
+borrowed and become invalid when their surface is destroyed. Undelivered events for
+a destroyed surface are canceled. Sequence orders capture by YASL, not OS causality.
+Timestamps use monotonic nanoseconds; native_time states whether normalized native
+sample timing was available or capture time was used.
+
+Creation dimensions, pointer/pen coordinates, popup placement, caret, and CSD actions
+use logical content units. Fractional positions use doubles. Image dimensions,
+byte stride, framebuffer size, and damage use device pixels. Query actual framebuffer
+extent for swapchain/viewport sizing; scale alone need not determine integer rounding.
+Request functions report acceptance, while geometry/state events report actual results.
+There is no portable absolute window-position promise. State's known mask distinguishes
+reported false from unavailable state fields.
+
+## Rendering
+
+GPU creation (flags zero) does not create a device, context, or software framebuffer.
+One presenter attaches to each surface. Incompatible second attachments fail.
+
+- Vulkan: inject vkGetInstanceProcAddr, query instance extensions and flags through
+ the live GPU surface, create the instance, then `yasl_vulkan_create_surface`.
+ The application owns VkInstance/VkDevice/queues/swapchains/VkSurfaceKHR, handles
+ image acquisition, synchronization and recreation, and presents with Vulkan.
+- Metal: `yasl_metal_layer` returns a borrowed CAMetalLayer on Cocoa. YASL owns
+ the native view/layer and updates its dimensions. The app sets device/pixel format,
+ obtains drawables, and submits/presents commands. No additional YASL present call.
+- GL: `yasl_gl_surface_create` accepts title, dimensions, flags, a per-call int
+ key/value attribute list terminated by YASL_GL_END, and an optional sharing surface.
+ NULL attributes default to GL3.2 core, RGBA8, depth24/stencil8. The surface owns
+ its context. Bind, load procedures, set swap interval, and swap explicitly.
+ GL API support depends on the native driver; Cocoa rejects OpenGL ES/debug contexts
+ and supports its native legacy/core profiles.
+- CPU: create with YASL_SURFACE_CPU and call `yasl_cpu_present` with premultiplied
+ numeric 0xAARRGGBB, width/height, byte stride, and optional damage rows containing
+ x/y/width/height. Extent must match current framebuffer. NULL/zero damage means full;
+ explicit lists must be nonempty. Initial submission and changed extent require full
+ images. Later patches update the retained submitted image. Source bytes are copied
+ before return, so the app may reuse/free them. Native uploads may copy the entire
+ retained image, but never unsubmitted source changes. `yasl_cpu_read_pixels` reads
+ that submitted CPU image, not the composited screen.
+
+GPU creation/presentation uses native requests directly; YASL acknowledges Wayland
+configuration before external GPU submission. Native refresh events can be handled
+in the ordinary loop. An optional refresh callback permits drawing while the native
+window system remains in a modal resize path. It runs only during pumping and never
+recursively; it may query geometry and render, but must not pump, create/destroy
+surfaces, or shut down. Geometry events may not yet have been consumed, so query
+current geometry there. Without a hook, CPU exposes reuse committed pixels and GPU
+redraw waits for the application's loop to resume.
+
+## Keyboard, pen, and scrolling
+
+Physical YASL_KEY names identify US keyboard positions independent of layout.
+Layout labels support shortcuts; committed UTF-8 TEXT is for document insertion.
+Keys and text need not correspond one-to-one. Input RESET/focus loss cancels held
+application input. Keep pointer and pen streams separate to avoid duplicate strokes.
+
+PEN events carry connection-local device/tool IDs, tool type, proximity/contact,
+button transitions, coherent position and axes. Pressure is 0..1; zero does not end
+contact. Tilt uses degrees -90..90; rotation uses clockwise degrees; distance is
+normalized, not a physical unit. Axis validity distinguishes unavailable from zero.
+Changed masks indicate native updates. Tools/axes depend on hardware and backend;
+IDs do not guarantee persistent hardware serials. Pen-generated mouse duplicates
+are suppressed where recognized; do not treat tool class as a multi-contact API.
+
+SCROLL preserves wheel-line and precise-pixel values with validity, source, phase,
+and momentum metadata where supplied. Applications choose layout pixels per wheel
+line. X11 supplies native wheel/smooth valuator data where available. Missing native
+metadata stays explicitly unavailable instead of being inferred.
+
+## Text input, clipboard, popups, and drags
+
+Activate text input for a text control with `yasl_surface_set_text_input`. Set purpose,
+caret, and copied UTF-8 surrounding text/selection/revision through direct setters.
+Snapshot offsets and TEXT_EDIT replacement ranges are UTF-8 bytes; COMPOSITION is
+uncommitted preedit. Apply a matching-revision edit once, update your snapshot, and
+ignore/rebase stale edits. Activation generation distinguishes controls. Ordinary key
+text remains TEXT. Never insert preedit as committed document text. Wayland requires
+text-input-v3 for native IME; absence is reported as unsupported. Cocoa translates
+native UTF-16 replacement/deletion to snapshot UTF-8 ranges. X11 XIM supplies ordinary
+text/preedit but cannot promise surrounding-text replacement support.
+
+Clipboard set copies UTF-8; read returns a nonzero request ID and later CLIPBOARD
+completion with OK, UNAVAILABLE, or FAILED. Empty successful text is distinct from
+failure. Destroying the target cancels transfer and delivery. Primary selection stays
+independent and reports unsupported where unavailable. Linux external transfers time
+out after 30 seconds; X11 currently lacks INCR for large transfers. File drops carry
+URI-list text; `yasl_drop_next_path` decodes supported local paths into caller buffers.
+
+Popup arguments use logical parent anchor coordinates, anchor/gravity directions,
+and flip/slide/resize constraint bits. Popup size includes its content. Destroying a
+parent retires descendants too. Native grabs, CSD moves/resizes, menus, and surface
+drags require a live triggering press event; unsupported/expired input fails explicitly.
+Surface drags support application tab/window carry; they are not an arbitrary MIME
+source API. Check operation results even when feature bits advertise support.
+
+## Backend limits
+
+Wayland support varies by compositor protocols; no portable absolute placement or
+minimized-state query is assumed. X11 currently reports unit scale and lacks an
+implemented relative-pointer lock. Cocoa requires macOS 14+; native Metal/GL and
+Objective-C paths require validation on macOS. EGL/wayland-egl and Vulkan SDK support
+are independently optional at build time. Feature bits describe implementation/
+server support, not guaranteed permission or attached hardware. The current work was
+compiled and tested on Linux; macOS validation remains a separate required check.