summaryrefslogtreecommitdiff
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
parent08dc9047098d6d96791024ac7d8f5837633673ef (diff)
better public interface and doxygen
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTING.md39
-rw-r--r--Doxyfile25
-rw-r--r--README.md363
-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
-rw-r--r--examples/hello.c102
-rw-r--r--examples/simple_window.c4
-rw-r--r--examples/tablet.c4
-rw-r--r--include/yasl/keycodes.h149
-rw-r--r--include/yasl/loop.h117
-rw-r--r--include/yasl/native.h49
-rw-r--r--include/yasl/native/cocoa.h33
-rw-r--r--include/yasl/native/wayland.h20
-rw-r--r--include/yasl/native/x11.h22
-rw-r--r--include/yasl/timing.h26
-rw-r--r--include/yasl/yasl.h614
-rw-r--r--meson.build26
-rw-r--r--src/cocoa/window.m73
-rw-r--r--src/common/backend.c72
-rw-r--r--src/common/keyboard.c399
-rw-r--r--src/common/keyboard.h18
-rw-r--r--src/common/meson.build2
-rw-r--r--src/common/utf8.c50
-rw-r--r--src/common/utf8.h14
-rw-r--r--src/wayland/clipboard.c8
-rw-r--r--src/wayland/ime.c2
-rw-r--r--src/wayland/internal.h19
-rw-r--r--src/wayland/keyboard.c94
-rw-r--r--src/wayland/pointer.c126
-rw-r--r--src/x11/window.c102
-rw-r--r--tests/cocoa/test_cursor.m12
-rw-r--r--tests/cocoa/test_keyboard.m134
-rw-r--r--tests/common/test_backends.c90
-rw-r--r--tests/common/test_contracts.c92
-rw-r--r--tests/common/test_keyboard.c32
-rw-r--r--tests/common/test_utf8.c27
-rw-r--r--tests/helpers/check_exports.py11
-rw-r--r--tests/helpers/package_cocoa_tests.py70
-rw-r--r--tests/helpers/run_backends.py8
-rw-r--r--tests/helpers/run_cocoa_bundle.py68
-rw-r--r--tests/meson.build47
-rw-r--r--tests/test_api.cpp13
-rw-r--r--tests/wayland/test_clipboard.c47
-rw-r--r--tests/wayland/test_keyboard.c128
-rw-r--r--tests/wayland/test_tablet.c37
-rw-r--r--tests/x11/test_input.c123
-rw-r--r--tests/x11/test_tablet.c13
53 files changed, 3579 insertions, 631 deletions
diff --git a/.gitignore b/.gitignore
index f0a7946..1f2f904 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
build/
+build-docs/
.cache/
*.o
*.a
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..88674ca
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,39 @@
+# Contributing
+
+YASL focuses on desktop surfaces for applications that own their renderer.
+Read the [API policy](docs/api-policy.md) and [application guide](docs/guide.md)
+before changing public behavior. Keep backend details in native extensions and
+make common operations usable through the portable headers.
+
+For a bug report, include the revision, operating system, selected backend from
+`yasl_display_backend()`, capability mask, compiler, build options, reproduction
+steps, and a small example if possible. For input issues include the relevant
+event fields, layout/device/driver, and whether the issue occurs on another
+backend. Report observations separately from expected behavior.
+
+Build and test with:
+
+```sh
+meson setup build
+meson compile -C build
+meson test -C build --print-errorlogs
+```
+
+See [testing](docs/testing.md) for isolated Linux integration tests, sanitizer
+builds, and native tablet tests. Changes to shared behavior need both backend
+configurations checked. Cocoa changes need a macOS desktop build/test; Linux
+success alone does not validate them. Keep changes focused and describe any
+platforms that were not exercised.
+
+Document public behavior in the installed header first: argument validity,
+units, ownership/lifetime, return/error behavior, thread requirements, callback
+rules, and unsupported results. Update guides/examples when the workflow
+changes. Use zero-initialized specs and check fallible operations in examples.
+Build the reference with `doxygen Doxyfile`; warnings are errors. Do not edit
+vendored generated protocol files for unrelated changes.
+
+Use the existing C style and `yasl_`/`YASL_` public names. Run the tests relevant
+to the changed behavior, including shared exports and public C/C++ compilation.
+For a pull request, explain the observable problem, resulting behavior, and
+validation, including any native tests still needed. Contributions use the
+project's MIT license.
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..406abac
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,25 @@
+PROJECT_NAME = YASL
+PROJECT_BRIEF = "Desktop surfaces for application-owned rendering"
+OUTPUT_DIRECTORY = build-docs
+INPUT = include/yasl docs
+RECURSIVE = YES
+FILE_PATTERNS = *.h *.md *.dox
+EXAMPLE_PATH = examples
+FILTER_PATTERNS = *.md="python3 docs/doxygen_filter.py"
+USE_MDFILE_AS_MAINPAGE = docs/getting-started.md
+EXTRACT_ALL = NO
+EXTRACT_STATIC = YES
+JAVADOC_AUTOBRIEF = YES
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = YES
+PREDEFINED = YASL_API= __cplusplus
+GENERATE_HTML = YES
+GENERATE_LATEX = NO
+SOURCE_BROWSER = YES
+FULL_PATH_NAMES = NO
+QUIET = YES
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = YES
+WARN_IF_DOC_ERROR = YES
+WARN_AS_ERROR = FAIL_ON_WARNINGS
diff --git a/README.md b/README.md
index 0cb2eea..8300b74 100644
--- a/README.md
+++ b/README.md
@@ -1,321 +1,74 @@
-# YASL (Yet Another Surface Library)
-
-YASL is a minimal, modern, and high-performance desktop window surface library written in C.
-
-It provides window creation, frame pacing, high-DPI scaling, and input routing without bundling audio, physics, or game engine components.
-
-## Features
-
-- **Lean Core**: No audio, no font rendering, and no widget toolkit overhead.
-- **Wayland Protocol Coverage**:
- - `xdg-shell` toplevels and hierarchical popups (`xdg_popup` + `xdg_positioner`).
- - Fractional scaling via `wp_fractional_scale_v1` and `wp_viewporter`.
- - Presentation feedback and refresh rate timing via `wp_presentation` and `wp_fifo_v1`.
- - Cursor shapes via `wp_cursor_shape_device_v1`.
- - Graphics tablets and stylus input via `zwp_tablet_v2` (pressure, tilt, eraser).
- - Text Input Method (IME) composition via `zwp_text_input_v3`.
- - Primary selection and clipboard transfers via `wl_data_device` and `zwp_primary_selection_v1`.
- - Interactive client-side window move and resize triggers (`xdg_toplevel_move`, `xdg_toplevel_resize`).
-- **macOS Cocoa & Metal**:
- - Native `NSWindow` and `CAMetalLayer` presentation.
- - Vsync pacing via `CADisplayLink`.
-- **Zero Framework Leaks**:
- - Self-contained UTF-8 validation.
- - Self-contained event queue.
- - No terminal-specific telemetry or hardcoded application names.
-
-## Public API
-
-Include `<yasl/yasl.h>` for surface creation, drawing, input, and display dispatch.
-`yasl_surface_geometry()` returns device-pixel size and scale together.
-`yasl_display_dispatch(display, timeout_ms)` polls with zero, waits indefinitely
-with a negative timeout, or waits up to a positive number of milliseconds.
-Pending input or redraw work returns immediately; call `yasl_display_flush()`
-after processing input and scheduling redraws.
-
-Call `yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN)` to hide the system
-mouse and tablet cursor over a surface while continuing to receive pointer
-input. The choice persists across motion and re-entry; select another cursor,
-such as `YASL_CURSOR_ARROW`, to show it again. `YASL_CURSOR_NONE` is a compatible
-alias for `YASL_CURSOR_HIDDEN`.
-
-Include `<yasl/loop.h>` to use the optional event loop, file watches, or timers.
-Timers use one callback model and nanoseconds throughout; use `YASL_MS_TO_NS(5)`
-for a five-millisecond interval. Add application timers to
-`yasl_display_loop(display)`. Add surface timers with `yasl_surface_add_timer()`
-when they should be canceled automatically on surface destruction.
-
-Include `<yasl/native.h>` for platform handles and application-managed native
-rendering. These extensions are separate from the portable CPU drawing API.
-`<yasl/timing.h>` contains the public timing result types.
-
-## Backend selection
-
-YASL selects the native window backend at runtime.
-Leave `yasl_display_spec.backend` at zero to use this mode.
-`YASL_BACKEND_AUTO` has the same effect.
-You can also pass NULL as the display spec.
-
-On Linux, YASL tries Wayland first in a Wayland session.
-It uses `WAYLAND_DISPLAY`, `WAYLAND_SOCKET`, and `XDG_SESSION_TYPE` to detect that session.
-In other sessions, it tries X11 first.
-If the first backend cannot open, it tries the other built backend.
-On macOS, it uses Cocoa.
-
-Set `backend` to request one backend:
-
-```c
-struct yasl_display_spec spec = {
- .app_id = "org.example.app",
- .backend = YASL_BACKEND_X11,
-};
-struct yasl_display *display = yasl_display_open(loop, &spec);
-```
-
-Use `YASL_BACKEND_WAYLAND` to request Wayland.
-Use `YASL_BACKEND_COCOA` to request Cocoa.
-An explicit request never falls back to another backend.
-Open returns NULL if the requested backend cannot open.
-It sets `errno` to `ENOSYS` for a backend that was not built.
-It sets `errno` to `EINVAL` for an invalid backend value.
-
-Call `yasl_display_backend(display)` to read the selected backend.
-All surfaces on a display use that backend.
-Open separate displays to use Wayland and X11 in one process.
-The public API needs no window system headers.
-Shared libraries export only the functions declared in the installed YASL headers.
-Input queues, frame clocks, backend entry points, and protocol helpers are private implementation details.
-Use `yasl_display_caps()` to check optional features.
-
-X11 supports CPU pixel buffers, keyboard and pointer input, and clipboard transfer.
-Tablet pressure, tilt, and erasers use XInput 2 when the server exposes it.
-It supports the primary selection, window placement, and drag operations.
-It does not yet support YASL popups or IME preedit text.
-Those features return their normal unsupported result.
-
-The surface spec needs no native graphics device for CPU drawing.
-On Cocoa, YASL creates its own Metal device for this mode.
-An app that renders with Metal includes `<yasl/native.h>` and calls
-`yasl_surface_create_native(display, &spec, device)`. During its draw callback,
-`yasl_surface_get_native_frame()` returns the current Metal drawable. The app
-submits and presents it, then calls `yasl_surface_present()` to record submission.
-
-Type-safe native handles are declared in `<yasl/native/wayland.h>`,
-`<yasl/native/x11.h>`, and `<yasl/native/cocoa.h>`:
-- Wayland: `wl_display *` and `wl_surface *` via `yasl_wayland_display_get_wl_display()` and `yasl_wayland_surface_get_wl_surface()`.
-- X11: `Display *` and `uintptr_t` window ID via `yasl_x11_display_get_display()` and `yasl_x11_surface_get_window()`.
-- Cocoa: `NSApplication *`, `NSWindow *`, and `NSView *` via `yasl_cocoa_display_get_application()`, `yasl_cocoa_surface_get_window()`, and `yasl_cocoa_surface_get_view()`.
-Generic untyped borrowed handles (`void *`) remain accessible via `<yasl/native.h>`. All handles expire on object destruction.
-
-## Ownership and backend contracts
-
-- Calls and callbacks use the thread that opened the display (the main thread
- on Cocoa). Supplied event loops are borrowed. Destroy popups before parents,
- surfaces before displays, and displays before supplied loops. Surface
- destruction inside surface callbacks is supported; display or loop destruction
- during callbacks/dispatch is not.
-- Callback tables, drag listeners, input strings, titles, clipboard text, and
- drag pixels are copied. Userdata is borrowed. Cocoa retains a supplied Metal
- device. Native handles are borrowed, not transferred to the caller.
-- Text, paste, and file-drop event payloads remain valid until the next
- **successful** input poll on that surface or surface destruction. Dispatch,
- flush, selection replacement, and failed polls do not invalidate them.
- Copy a payload if it must live longer.
-- Input queues grow rather than silently dropping events at a fixed limit.
- If allocation fails, pending input is discarded and `YASL_INPUT_RESET` is
- delivered before later events. X11 also sends a reset when input devices are
- removed or disabled. Apps must clear held keys/buttons and input composition
- state on this event.
-- Clipboard requests retain their original target. Destroying that target
- cancels delivery, rather than redirecting the paste to a focused window.
- Unsupported primary selections are no-ops, never aliases for the clipboard.
-- CPU pixels are tightly packed premultiplied `0xAARRGGBB`. Creation sizes are
- logical pixels; framebuffer sizes, pointer coordinates, caret and damage are
- device pixels. Every draw target needs complete valid pixel contents; do not
- assume a recycled framebuffer contains the previous frame.
-- Present only the current draw callback's target, at most once. Targets expire
- when the callback returns; preserve the opaque `generation` when copying a
- target. Damage is required, clipped to the target, and validated uniformly. Backends may upload the whole frame despite damage hints.
-- `yasl_surface_create_native()` requires `YASL_CAP_NATIVE_GRAPHICS` (currently
- Cocoa) and a non-NULL graphics device; other backends return `ENOTSUP`. Native
- drawable queries are valid only for the current draw target and expire on
- successful present, callback return, or surface destruction.
-- `YASL_CAP_FRAME_PACING` distinguishes compositor/display-driven draws from
- X11's unpaced flush-driven draws. `yasl_timing.source` distinguishes submission
- estimates, frame callbacks, and confirmed presentation feedback. Refresh and
- next-presentation times are estimates, not universal vsync guarantees.
-
-These are pre-release API changes. Applications must handle the new reset input
-kind and should inspect timing provenance instead of assuming all timestamps
-confirm scanout. See `include/yasl/yasl.h` for the complete contracts.
-
-## Building
-
-Use a C23 compiler and Meson 1.6 or later.
-Build with Meson and Ninja:
-
-```sh
-meson setup build
-ninja -C build
-ninja -C build test
-```
-
-Linux builds include each backend whose dependencies are installed.
-Wayland needs `wayland-client`, `wayland-cursor`, and `xkbcommon`.
-X11 needs `x11`, `xi` (libXi), and `xkbcommon`.
-Use `-Dwayland=enabled` or `-Dx11=enabled` to require a backend.
-Use `-Dwayland=disabled` or `-Dx11=disabled` to omit a backend.
-At least one backend must be built.
-
-To run the Linux window tests, install Weston and Xwayland.
-Then run:
-
-```sh
-meson setup build-test -Dintegration_tests=true
-meson test -C build-test --print-errorlogs
-```
-
-The tests start isolated display servers.
-They test explicit requests, automatic selection, and fallback.
-They also test both backends in one process, callback-table copying, input
-payload ownership, queue growth, concurrent clipboard targets, and consistent
-frame/damage validation. Wayland IME ownership tests need no display server.
-For memory and undefined-behavior checks, configure a separate build with
-`-Db_sanitize=address,undefined`.
-On macOS, the same option runs native Cocoa window tests.
-These tests need a desktop session.
-
-## Tablet drawing example
-
-Build and run the same C application on Wayland, X11, or Cocoa:
+# YASL — Yet Another Surface Library
+
+YASL is a C desktop surface library for applications that own their renderer.
+It creates windows and supplies CPU pixel targets, scale-aware geometry, queued
+input, clipboard text, popups, surface drags, and optional frame pacing.
+It runs on Wayland, X11, and macOS Cocoa.
+
+A software-rendered editor, terminal, drawing tool, or custom interface can open
+a window and paint pixels without creating an OpenGL context or adopting a
+widget toolkit. YASL also exposes tablet pressure/tilt and IME composition.
+It does not provide fonts, widgets, audio, or a graphics engine. Native handles
+and Cocoa/Metal rendering are separate extensions.
+
+**Status:** pre-release 0.1; source and ABI compatibility are not yet guaranteed.
+Physical keys have portable names, committed text arrives as UTF-8, and scroll
+offsets use fractional X/Y line units. Read the [application guide](docs/guide.md)
+for platform capabilities and limits.
+
+## Start here
+
+- [Getting started](docs/getting-started.md): build, install, link, and run a window.
+- [Application guide](docs/guide.md): geometry, drawing, input, ownership, and backend limits.
+- [Public reference](include/yasl/yasl.h), [loop](include/yasl/loop.h),
+ [timing](include/yasl/timing.h), and [native extensions](include/yasl/native.h).
+ Run `doxygen Doxyfile` for a searchable HTML reference in `build-docs/html`.
+- [API policy](docs/api-policy.md), [migration notes](docs/migrating.md), and
+ [contributing](CONTRIBUTING.md).
+- [Tests, tablet example, and benchmarks](docs/testing.md).
+
+## Build a window
+
+Use a C23 compiler, Meson 1.6 or newer, and Ninja. Linux needs the development
+packages for Wayland (`wayland-client`, `wayland-cursor`, `xkbcommon`) or X11
+(`x11`, `xi`, `xkbcommon`, `xext`; optional `xrandr`). Cocoa requires macOS 14 or
+newer and the system SDK, with a Metal-capable device for window drawing.
```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.
-
-`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
+./build/yasl-hello
+meson test -C build --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 includes a dedicated performance microbenchmark target (`yasl-benchmark`) evaluating CPU efficiency, memory footprint, cache line pressure, data structure operations, and window lifecycle operations across core hot paths (surface lookups, multi-output refresh rate scans, input queue expansions, ring buffer throughput, timer arm/disarm cycles, cold/warm window creation, single and bulk window teardowns, titlebar property updates, and multi-resolution dynamic resizing):
+Linux builds automatically include backends with available dependencies. Require
+one with `-Dwayland=enabled` or `-Dx11=enabled`; omit one with `disabled`. Runtime
+selection chooses the native session and allows fallback, or you can request an
+explicit backend with `yasl_display_spec.backend`.
-```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
-```
-
-## Quick Example
+Include `<yasl/yasl.h>` for the core. The complete [hello.c](examples/hello.c)
+handles creation failures, drawing, dispatch, input draining, and cleanup.
+Its drawing callback fills every pixel before presenting:
```c
-#include <yasl/yasl.h>
-
-static bool running = true;
-
-static void on_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud)
-{
- /* Present frame target */
- struct yasl_damage_list dmg = { .n = -1 };
- yasl_surface_present(s, &frame->target, &dmg);
-}
-
-static void on_close(struct yasl_surface *s, void *ud)
-{
- (void)s; (void)ud;
- running = false;
-}
-
-int main(void)
+static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud)
{
- struct yasl_display_spec dspec = { .app_id = "org.example.app" };
- struct yasl_display *d = yasl_display_open(NULL, &dspec);
-
- struct yasl_surface_spec sspec = {
- .title = "YASL Application",
- .size = { .x = 800, .y = 600 },
- };
- struct yasl_surface *s = yasl_surface_create_toplevel(d, &sspec);
- struct yasl_surface_cbs cbs = {
- .draw = on_draw,
- .close = on_close,
- };
- yasl_surface_set_callbacks(s, &cbs, NULL);
- yasl_surface_schedule_redraw(s);
-
- while (running) {
- yasl_display_dispatch(d, -1);
- struct yasl_input_event ev;
- while (yasl_surface_input_poll(s, &ev)) {
- if (ev.kind == YASL_INPUT_KEY && ev.key.action == YASL_KEY_PRESS) {
- if (ev.key.key == 0xff1b) /* Escape */
- running = false;
- }
- }
- yasl_display_flush(d);
+ (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. */
}
-
- yasl_surface_destroy(s);
- yasl_display_close(d);
- return 0;
}
```
+Toplevel creation sizes are logical pixels; drawing and input use device pixels.
+Targets are borrowed for one draw callback. Keep durable content in application
+memory, dispatch events, drain input queues, then flush scheduled drawing. These
+rules are explained with examples in the application guide.
+
## License
-MIT License.
+[MIT](LICENSE).
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
+```
+
diff --git a/examples/hello.c b/examples/hello.c
new file mode 100644
index 0000000..67ac88b
--- /dev/null
+++ b/examples/hello.c
@@ -0,0 +1,102 @@
+/* A complete CPU-rendered window. Build instructions: docs/getting-started.md. */
+#include <yasl/yasl.h>
+
+#include <stdio.h>
+#include <string.h>
+
+struct app {
+ bool running;
+ bool failed;
+ bool smoke_test;
+};
+
+static void quit(void *ud)
+{
+ ((struct app *)ud)->running = false;
+}
+
+static void lost(void *ud)
+{
+ struct app *app = ud;
+ app->failed = true;
+ app->running = false;
+}
+
+static void close_window(struct yasl_surface *surface, void *ud)
+{
+ (void)surface;
+ quit(ud);
+}
+
+static void configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud)
+{
+ (void)geometry;
+ (void)ud;
+ yasl_surface_schedule_redraw(surface);
+}
+
+static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud)
+{
+ struct app *app = 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; /* Opaque premultiplied ARGB. */
+
+ struct yasl_damage_list damage = {.n = -1};
+ if (!yasl_surface_present(surface, &frame->target, &damage)) {
+ fprintf(stderr, "Could not present the draw target\n");
+ app->failed = true;
+ app->running = false;
+ }
+ if (app->smoke_test) app->running = false;
+}
+
+int main(int argc, char **argv)
+{
+ struct app app = {.running = true};
+ if (argc == 2 && strcmp(argv[1], "--smoke-test") == 0)
+ app.smoke_test = true;
+ else if (argc != 1) {
+ fprintf(stderr, "Usage: %s [--smoke-test]\n", argv[0]);
+ return 1;
+ }
+
+ struct yasl_display_cbs display_callbacks = {.quit = quit, .lost = lost};
+ struct yasl_display_spec display_spec = {
+ .app_id = "org.example.hello",
+ .cbs = &display_callbacks,
+ .ud = &app,
+ };
+ struct yasl_display *display = yasl_display_open(NULL, &display_spec);
+ if (!display) {
+ perror("yasl_display_open");
+ return 1;
+ }
+ struct yasl_surface_spec spec = {.title = "Hello YASL", .size = {640, 480}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ if (!surface) {
+ perror("yasl_surface_create_toplevel");
+ yasl_display_close(display);
+ return 1;
+ }
+ struct yasl_surface_cbs callbacks = {
+ .draw = draw, .configure = configure, .close = close_window,
+ };
+ yasl_surface_set_callbacks(surface, &callbacks, &app);
+ yasl_surface_schedule_redraw(surface);
+
+ while (app.running) {
+ if (yasl_display_dispatch(display, -1) < 0) {
+ perror("yasl_display_dispatch");
+ app.failed = true;
+ break;
+ }
+ /* Drain even unused input so queued events do not cause a busy loop. */
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {}
+ if (app.running) yasl_display_flush(display);
+ }
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ return app.failed ? 1 : 0;
+}
diff --git a/examples/simple_window.c b/examples/simple_window.c
index 40a7c25..85db04a 100644
--- a/examples/simple_window.c
+++ b/examples/simple_window.c
@@ -152,9 +152,9 @@ int main(void)
while (yasl_surface_input_poll(s, &ev)) {
if (ev.kind == YASL_INPUT_KEY) {
if (ev.key.action == YASL_KEY_PRESS) {
- if (ev.key.key == 0xff1b) { /* Escape */
+ if (ev.key.key == YASL_KEY_ESCAPE) { /* Escape */
running = false;
- } else if (ev.key.key == ' ' || ev.key.key == 0xff0d) { /* Space or Return */
+ } else if (ev.key.key == YASL_KEY_SPACE || ev.key.key == YASL_KEY_ENTER) { /* Space or Return */
color_theme++;
yasl_surface_schedule_redraw(s);
}
diff --git a/examples/tablet.c b/examples/tablet.c
index 4d7a8bd..0aae370 100644
--- a/examples/tablet.c
+++ b/examples/tablet.c
@@ -148,8 +148,8 @@ static void input(struct app *app, const struct yasl_input_event *event)
(event->kind == YASL_INPUT_FOCUS && !event->focus.focused)) {
cancel_stroke(app);
} else if (event->kind == YASL_INPUT_KEY && event->key.action == YASL_KEY_PRESS) {
- if (event->key.key == 0xff1b) app->running = false;
- if (event->key.key == 'c' || event->key.key == 'C' || event->key.key == ' ')
+ if (event->key.key == YASL_KEY_ESCAPE) app->running = false;
+ if (event->key.key == YASL_KEY_C || event->key.key == YASL_KEY_SPACE)
clear_canvas(app);
} else if (event->kind == YASL_INPUT_MOUSE) {
const struct yasl_mouse_event *pointer = &event->mouse;
diff --git a/include/yasl/keycodes.h b/include/yasl/keycodes.h
new file mode 100644
index 0000000..7586945
--- /dev/null
+++ b/include/yasl/keycodes.h
@@ -0,0 +1,149 @@
+/**
+ * @file
+ * Portable physical keyboard identities, included by yasl/yasl.h.
+ * Letter/punctuation names describe positions on a US keyboard, regardless of
+ * active layout or modifiers. Use YASL_INPUT_TEXT for text and codepoint/shifted
+ * in yasl_key_event for layout labels. These values are neither Unicode nor
+ * native scancodes; do not derive a character from a key enumerator.
+ */
+#ifndef YASL_KEYCODES_H
+#define YASL_KEYCODES_H
+
+#include <stdint.h>
+
+/** Physical keys. Unrecognized native keys report YASL_KEY_UNKNOWN. */
+enum yasl_key : uint32_t {
+ YASL_KEY_UNKNOWN = 0x000, /**< No portable identity; native_code may still identify the key. */
+ YASL_KEY_SPACE = 0x020, /**< Space. */
+ YASL_KEY_APOSTROPHE = 0x027, /**< Apostrophe. */
+ YASL_KEY_COMMA = 0x02c, /**< Comma. */
+ YASL_KEY_MINUS = 0x02d, /**< Minus. */
+ YASL_KEY_PERIOD = 0x02e, /**< Period. */
+ YASL_KEY_SLASH = 0x02f, /**< Slash. */
+ YASL_KEY_0 = 0x030, /**< Physical 0 key. */
+ YASL_KEY_1 = 0x031, /**< Physical 1 key. */
+ YASL_KEY_2 = 0x032, /**< Physical 2 key. */
+ YASL_KEY_3 = 0x033, /**< Physical 3 key. */
+ YASL_KEY_4 = 0x034, /**< Physical 4 key. */
+ YASL_KEY_5 = 0x035, /**< Physical 5 key. */
+ YASL_KEY_6 = 0x036, /**< Physical 6 key. */
+ YASL_KEY_7 = 0x037, /**< Physical 7 key. */
+ YASL_KEY_8 = 0x038, /**< Physical 8 key. */
+ YASL_KEY_9 = 0x039, /**< Physical 9 key. */
+ YASL_KEY_SEMICOLON = 0x03b, /**< Semicolon. */
+ YASL_KEY_EQUAL = 0x03d, /**< Equal. */
+ YASL_KEY_A = 0x041, /**< Physical US A position; independent of layout and modifiers. */
+ YASL_KEY_B = 0x042, /**< Physical US B position; independent of layout and modifiers. */
+ YASL_KEY_C = 0x043, /**< Physical US C position; independent of layout and modifiers. */
+ YASL_KEY_D = 0x044, /**< Physical US D position; independent of layout and modifiers. */
+ YASL_KEY_E = 0x045, /**< Physical US E position; independent of layout and modifiers. */
+ YASL_KEY_F = 0x046, /**< Physical US F position; independent of layout and modifiers. */
+ YASL_KEY_G = 0x047, /**< Physical US G position; independent of layout and modifiers. */
+ YASL_KEY_H = 0x048, /**< Physical US H position; independent of layout and modifiers. */
+ YASL_KEY_I = 0x049, /**< Physical US I position; independent of layout and modifiers. */
+ YASL_KEY_J = 0x04a, /**< Physical US J position; independent of layout and modifiers. */
+ YASL_KEY_K = 0x04b, /**< Physical US K position; independent of layout and modifiers. */
+ YASL_KEY_L = 0x04c, /**< Physical US L position; independent of layout and modifiers. */
+ YASL_KEY_M = 0x04d, /**< Physical US M position; independent of layout and modifiers. */
+ YASL_KEY_N = 0x04e, /**< Physical US N position; independent of layout and modifiers. */
+ YASL_KEY_O = 0x04f, /**< Physical US O position; independent of layout and modifiers. */
+ YASL_KEY_P = 0x050, /**< Physical US P position; independent of layout and modifiers. */
+ YASL_KEY_Q = 0x051, /**< Physical US Q position; independent of layout and modifiers. */
+ YASL_KEY_R = 0x052, /**< Physical US R position; independent of layout and modifiers. */
+ YASL_KEY_S = 0x053, /**< Physical US S position; independent of layout and modifiers. */
+ YASL_KEY_T = 0x054, /**< Physical US T position; independent of layout and modifiers. */
+ YASL_KEY_U = 0x055, /**< Physical US U position; independent of layout and modifiers. */
+ YASL_KEY_V = 0x056, /**< Physical US V position; independent of layout and modifiers. */
+ YASL_KEY_W = 0x057, /**< Physical US W position; independent of layout and modifiers. */
+ YASL_KEY_X = 0x058, /**< Physical US X position; independent of layout and modifiers. */
+ YASL_KEY_Y = 0x059, /**< Physical US Y position; independent of layout and modifiers. */
+ YASL_KEY_Z = 0x05a, /**< Physical US Z position; independent of layout and modifiers. */
+ YASL_KEY_LEFT_BRACKET = 0x05b, /**< Left Bracket. */
+ YASL_KEY_BACKSLASH = 0x05c, /**< Backslash. */
+ YASL_KEY_RIGHT_BRACKET = 0x05d, /**< Right Bracket. */
+ YASL_KEY_GRAVE = 0x060, /**< Grave. */
+ YASL_KEY_ESCAPE = 0x100, /**< Escape. */
+ YASL_KEY_ENTER = 0x101, /**< Enter. */
+ YASL_KEY_TAB = 0x102, /**< Tab. */
+ YASL_KEY_BACKSPACE = 0x103, /**< Backspace. */
+ YASL_KEY_INSERT = 0x104, /**< Insert. */
+ YASL_KEY_DELETE = 0x105, /**< Delete. */
+ YASL_KEY_RIGHT = 0x106, /**< Right. */
+ YASL_KEY_LEFT = 0x107, /**< Left. */
+ YASL_KEY_DOWN = 0x108, /**< Down. */
+ YASL_KEY_UP = 0x109, /**< Up. */
+ YASL_KEY_PAGE_UP = 0x10a, /**< Page Up. */
+ YASL_KEY_PAGE_DOWN = 0x10b, /**< Page Down. */
+ YASL_KEY_HOME = 0x10c, /**< Home. */
+ YASL_KEY_END = 0x10d, /**< End. */
+ YASL_KEY_CAPS_LOCK = 0x10e, /**< Caps Lock. */
+ YASL_KEY_SCROLL_LOCK = 0x10f, /**< Scroll Lock. */
+ YASL_KEY_NUM_LOCK = 0x110, /**< Num Lock. */
+ YASL_KEY_PRINT_SCREEN = 0x111, /**< Print Screen. */
+ YASL_KEY_PAUSE = 0x112, /**< Pause. */
+ YASL_KEY_ISO_BACKSLASH = 0x113, /**< Iso Backslash. */
+ YASL_KEY_YEN = 0x114, /**< Yen. */
+ YASL_KEY_RO = 0x115, /**< Ro. */
+ YASL_KEY_KANA = 0x116, /**< Kana. */
+ YASL_KEY_EISU = 0x117, /**< Eisu. */
+ YASL_KEY_CONVERT = 0x118, /**< Convert. */
+ YASL_KEY_NONCONVERT = 0x119, /**< Nonconvert. */
+ YASL_KEY_F1 = 0x120, /**< Function key F1. */
+ YASL_KEY_F2 = 0x121, /**< Function key F2. */
+ YASL_KEY_F3 = 0x122, /**< Function key F3. */
+ YASL_KEY_F4 = 0x123, /**< Function key F4. */
+ YASL_KEY_F5 = 0x124, /**< Function key F5. */
+ YASL_KEY_F6 = 0x125, /**< Function key F6. */
+ YASL_KEY_F7 = 0x126, /**< Function key F7. */
+ YASL_KEY_F8 = 0x127, /**< Function key F8. */
+ YASL_KEY_F9 = 0x128, /**< Function key F9. */
+ YASL_KEY_F10 = 0x129, /**< Function key F10. */
+ YASL_KEY_F11 = 0x12a, /**< Function key F11. */
+ YASL_KEY_F12 = 0x12b, /**< Function key F12. */
+ YASL_KEY_F13 = 0x12c, /**< Function key F13. */
+ YASL_KEY_F14 = 0x12d, /**< Function key F14. */
+ YASL_KEY_F15 = 0x12e, /**< Function key F15. */
+ YASL_KEY_F16 = 0x12f, /**< Function key F16. */
+ YASL_KEY_F17 = 0x130, /**< Function key F17. */
+ YASL_KEY_F18 = 0x131, /**< Function key F18. */
+ YASL_KEY_F19 = 0x132, /**< Function key F19. */
+ YASL_KEY_F20 = 0x133, /**< Function key F20. */
+ YASL_KEY_F21 = 0x134, /**< Function key F21. */
+ YASL_KEY_F22 = 0x135, /**< Function key F22. */
+ YASL_KEY_F23 = 0x136, /**< Function key F23. */
+ YASL_KEY_F24 = 0x137, /**< Function key F24. */
+ YASL_KEY_F25 = 0x138, /**< Function key F25. */
+ YASL_KEY_KP_0 = 0x140, /**< Numeric keypad 0. */
+ YASL_KEY_KP_1 = 0x141, /**< Numeric keypad 1. */
+ YASL_KEY_KP_2 = 0x142, /**< Numeric keypad 2. */
+ YASL_KEY_KP_3 = 0x143, /**< Numeric keypad 3. */
+ YASL_KEY_KP_4 = 0x144, /**< Numeric keypad 4. */
+ YASL_KEY_KP_5 = 0x145, /**< Numeric keypad 5. */
+ YASL_KEY_KP_6 = 0x146, /**< Numeric keypad 6. */
+ YASL_KEY_KP_7 = 0x147, /**< Numeric keypad 7. */
+ YASL_KEY_KP_8 = 0x148, /**< Numeric keypad 8. */
+ YASL_KEY_KP_9 = 0x149, /**< Numeric keypad 9. */
+ YASL_KEY_KP_DECIMAL = 0x14a, /**< Numeric keypad decimal. */
+ YASL_KEY_KP_DIVIDE = 0x14b, /**< Numeric keypad divide. */
+ YASL_KEY_KP_MULTIPLY = 0x14c, /**< Numeric keypad multiply. */
+ YASL_KEY_KP_SUBTRACT = 0x14d, /**< Numeric keypad subtract. */
+ YASL_KEY_KP_ADD = 0x14e, /**< Numeric keypad add. */
+ YASL_KEY_KP_ENTER = 0x14f, /**< Numeric keypad enter. */
+ YASL_KEY_KP_EQUAL = 0x150, /**< Numeric keypad equal. */
+ YASL_KEY_KP_COMMA = 0x151, /**< Numeric keypad comma. */
+ YASL_KEY_KP_CLEAR = 0x152, /**< Numeric keypad clear. */
+ YASL_KEY_LEFT_SHIFT = 0x160, /**< Left Shift. */
+ YASL_KEY_LEFT_CONTROL = 0x161, /**< Left Control. */
+ YASL_KEY_LEFT_ALT = 0x162, /**< Left Alt. */
+ YASL_KEY_LEFT_SUPER = 0x163, /**< Left Super. */
+ YASL_KEY_RIGHT_SHIFT = 0x164, /**< Right Shift. */
+ YASL_KEY_RIGHT_CONTROL = 0x165, /**< Right Control. */
+ YASL_KEY_RIGHT_ALT = 0x166, /**< Right Alt. */
+ YASL_KEY_RIGHT_SUPER = 0x167, /**< Right Super. */
+ YASL_KEY_MENU = 0x168, /**< Menu. */
+ YASL_KEY_VOLUME_UP = 0x169, /**< Volume Up. */
+ YASL_KEY_VOLUME_DOWN = 0x16a, /**< Volume Down. */
+ YASL_KEY_MUTE = 0x16b, /**< Mute. */
+};
+
+#endif /* YASL_KEYCODES_H */
diff --git a/include/yasl/loop.h b/include/yasl/loop.h
index a65fa12..9faa8f7 100644
--- a/include/yasl/loop.h
+++ b/include/yasl/loop.h
@@ -1,3 +1,14 @@
+/**
+ * @file
+ * Optional event loop, file watches, and timers.
+ *
+ * Use the loop's owning thread for all calls and callbacks, except
+ * yasl_loop_now_ns(). A display borrows a supplied loop. For window applications,
+ * drive yasl_display_dispatch(), drain input, then yasl_display_flush(); the
+ * standalone loop_run() function does not replace this cycle. Timer intervals are
+ * nanoseconds; timeout_ms arguments are milliseconds. Do not recursively step/run
+ * or destroy a loop from a callback.
+ */
#ifndef YASL_LOOP_H
#define YASL_LOOP_H
@@ -10,33 +21,139 @@ extern "C" {
/* Optional event loop. Use the display's loop when adding application timers.
* A step waits at most timeout_ms: zero polls, a negative value waits indefinitely.
* Calls and callbacks use the loop's owning thread. */
+/**
+ * Create an owned standalone file-descriptor and timer loop.
+ * @return A loop to destroy with yasl_loop_destroy(), or NULL on allocation failure.
+ * Use on one owning thread. Passing it to yasl_display_open() only lends it to
+ * the display; the caller retains ownership.
+ */
YASL_API struct yasl_loop *yasl_loop_create(void);
+/**
+ * Release a loop, its watches, and its timers.
+ * @param l Loop, or NULL for no action.
+ * Close displays borrowing this loop first. Must not run during a loop callback
+ * or an active step/run. Watched file descriptors are not closed.
+ */
YASL_API void yasl_loop_destroy(struct yasl_loop *l);
+/**
+ * Step a standalone loop until yasl_loop_quit() is called.
+ * @param l Loop.
+ * @return Zero after quit, or -1 on failure.
+ * For a window application, use a dispatch, input-poll, flush cycle instead;
+ * loop_run alone does not perform that full cycle on every backend.
+ */
YASL_API int yasl_loop_run(struct yasl_loop *l);
+/**
+ * Wait for file-descriptor readiness or a timer deadline and run callbacks.
+ * @param l Loop.
+ * @param timeout_ms Maximum wait: zero polls, negative waits indefinitely.
+ * @return Number of callbacks handled, or -1 on failure.
+ * Timer deadlines shorten the wait. Callbacks can add/delete sources, including
+ * themselves. Do not recursively step/run this loop.
+ */
YASL_API int yasl_loop_step(struct yasl_loop *l, int timeout_ms);
+/**
+ * Ask the currently running yasl_loop_run() to return after this step.
+ * @param l Loop, or NULL for no action.
+ * Call on the owning thread, usually from a callback.
+ */
YASL_API void yasl_loop_quit(struct yasl_loop *l);
+/**
+ * Signal the loop's wake pipe without dispatching callbacks.
+ * @param l Loop, or NULL for no action.
+ * Multiple signals may coalesce. This does not schedule a redraw or enqueue an
+ * event, and it does not provide a cross-thread application messaging contract.
+ */
YASL_API void yasl_loop_kick(struct yasl_loop *l);
+/**
+ * Read the monotonic clock used by YASL timers and timing results.
+ * @return Nanoseconds from CLOCK_MONOTONIC, unrelated to wall-clock time.
+ * May be called from any thread.
+ */
YASL_API uint64_t yasl_loop_now_ns(void);
+/**
+ * Get the loop used for a display's file watches and application timers.
+ * @param d Display, or NULL.
+ * @return Borrowed loop, or NULL. Do not destroy a display-owned loop yourself.
+ */
YASL_API struct yasl_loop *yasl_display_loop(struct yasl_display *d);
+/**
+ * Register a borrowed file descriptor for poll-style readiness.
+ * @param l Loop.
+ * @param fd Open file descriptor; it remains owned by the caller.
+ * @param events Bitmask from <poll.h>, for example POLLIN or POLLOUT.
+ * @param fn Callback receiving poll revents (including errors/hangup); return value is ignored.
+ * @param ud Borrowed userdata until watch removal or loop destruction.
+ * @return Positive watch ID, or -1 on failure.
+ * Delete the watch before closing fd. The callback must consume readiness or
+ * change/remove its watch to avoid repeated immediate wakeups.
+ */
YASL_API int yasl_loop_add_fd(struct yasl_loop *l, int fd, short events,
int (*fn)(int fd, short revents, void *ud), void *ud);
+/**
+ * Remove a file watch without closing its descriptor.
+ * @param l Loop.
+ * @param id Watch ID returned by yasl_loop_add_fd().
+ * @return Zero if removed, or -1 for an invalid/missing watch.
+ */
YASL_API int yasl_loop_del_fd(struct yasl_loop *l, int id);
+/**
+ * Replace a watch's poll event mask.
+ * @param l Loop.
+ * @param id Watch ID.
+ * @param events New <poll.h> mask; zero disables requested readiness (errors may still arrive).
+ * @return Zero if updated, or -1 for an invalid/missing watch.
+ */
YASL_API int yasl_loop_set_fd_events(struct yasl_loop *l, int id, short events);
/* Timers use nanoseconds. Return the next interval to repeat/reschedule, or zero
* to remove the timer. A zero ID indicates failure. Loop timers live until they
* stop, are deleted, or the loop is destroyed. Surface timers are also canceled
* when their surface is destroyed. */
+/** Timer identity scoped to its loop. Zero is invalid; do not reuse a retired ID. */
typedef uint32_t yasl_timer_id;
+/** Timer callback. interval_ns is the last scheduled delay; return the next delay or zero to stop. */
typedef uint64_t (*yasl_timer_cb)(void *ud, yasl_timer_id id, uint64_t interval_ns);
+/** Convert a nonnegative integer millisecond value to nanoseconds; avoid overflow. */
#define YASL_MS_TO_NS(ms) ((uint64_t)(ms) * 1000000ull)
+/**
+ * Schedule a timer on a loop.
+ * @param l Loop.
+ * @param interval_ns Nonzero delay in monotonic nanoseconds before first firing.
+ * @param cb Required callback; returns a new nonzero interval to repeat, or zero to stop.
+ * @param ud Borrowed userdata until the timer stops, is deleted, or the loop is destroyed.
+ * @return Nonzero timer ID, or zero for invalid arguments or allocation failure.
+ * Repeating timers coalesce missed deadlines; do not use them to count elapsed
+ * wall time. The callback receives the last scheduled interval, not elapsed time.
+ */
YASL_API yasl_timer_id yasl_loop_add_timer(struct yasl_loop *l, uint64_t interval_ns,
yasl_timer_cb cb, void *ud);
+/**
+ * Cancel a loop timer, including from its own callback.
+ * @param l Loop.
+ * @param id Timer ID.
+ * @return true if an armed timer was removed, false if invalid or already retired.
+ */
YASL_API bool yasl_loop_del_timer(struct yasl_loop *l, yasl_timer_id id);
+/**
+ * Schedule a timer canceled automatically on surface destruction.
+ * @param s Surface whose display loop will run the timer.
+ * @param interval_ns Nonzero initial delay in nanoseconds.
+ * @param cb Required timer callback, with the same rules as yasl_loop_add_timer().
+ * @param ud Borrowed userdata until the timer ends or the surface is destroyed.
+ * @return Nonzero timer ID, or zero on failure.
+ * The surface does not own userdata. Cancel with yasl_surface_del_timer().
+ */
YASL_API yasl_timer_id yasl_surface_add_timer(struct yasl_surface *s, uint64_t interval_ns,
yasl_timer_cb cb, void *ud);
+/**
+ * Cancel a timer registered through yasl_surface_add_timer().
+ * @param s Owning surface.
+ * @param id Timer ID.
+ * @return true if still armed and removed; false if unknown or already retired.
+ */
YASL_API bool yasl_surface_del_timer(struct yasl_surface *s, yasl_timer_id id);
#ifdef __cplusplus
diff --git a/include/yasl/native.h b/include/yasl/native.h
index f7e3020..d65f8d3 100644
--- a/include/yasl/native.h
+++ b/include/yasl/native.h
@@ -1,3 +1,12 @@
+/**
+ * @file
+ * Optional native handles and application-managed rendering.
+ *
+ * These extensions follow the core thread and lifetime rules. Handles are borrowed;
+ * never destroy them or dispatch their native connection independently. Typed
+ * backend accessors live in the yasl/native headers and are installed only for built
+ * backends. Application-managed drawing currently supports Cocoa/Metal only.
+ */
#ifndef YASL_NATIVE_H
#define YASL_NATIVE_H
@@ -15,25 +24,61 @@ extern "C" {
* X11 Display * Window (cast through uintptr_t)
* Cocoa NSApplication * NSWindow * (view: window.contentView)
*/
+/**
+ * Get a borrowed, backend-specific display handle.
+ * @param d Display, or NULL.
+ * @return Native handle, or NULL. Inspect yasl_display_backend() before casting.
+ * Prefer a typed accessor from yasl/native/wayland.h, x11.h, or cocoa.h.
+ * The handle expires on display close; do not close or dispatch it independently.
+ */
YASL_API void *yasl_display_get_native_handle(struct yasl_display *d);
+/**
+ * Get a borrowed, backend-specific surface handle.
+ * @param s Surface, or NULL.
+ * @return Native handle, or NULL. X11 encodes a Window through uintptr_t.
+ * Prefer typed accessors. The handle expires on surface destruction. Do not
+ * change native ownership, event masks, buffer attachment, or surface roles.
+ */
YASL_API void *yasl_surface_get_native_handle(struct yasl_surface *s);
/* Create a surface for application-managed native rendering. graphics_device
* must be non-NULL; currently an id<MTLDevice> on Cocoa, retained by the surface.
* Other backends return NULL with errno=ENOTSUP. CPU rendering uses the ordinary
* yasl_surface_create_toplevel() and requires no native device. */
+/**
+ * Create a toplevel for application-managed native rendering.
+ * @param d Display with YASL_CAP_NATIVE_GRAPHICS.
+ * @param spec Copied toplevel options, with logical-pixel size.
+ * @param graphics_device Required native graphics device; currently id<MTLDevice> on Cocoa.
+ * @return Owned surface, or NULL with errno set: EINVAL for invalid arguments,
+ * ENOTSUP for an unsupported backend, or a backend error.
+ * Cocoa retains the device. Query the drawable during draw, submit/present it,
+ * then call yasl_surface_present() to record submission.
+ */
YASL_API struct yasl_surface *yasl_surface_create_native(struct yasl_display *d,
const struct yasl_surface_spec *spec, void *graphics_device);
+/** Borrowed native drawable for the current draw target, currently Cocoa/Metal. */
struct yasl_native_frame {
- void *drawable; /* id<CAMetalDrawable> on Cocoa */
- bool transaction; /* CAMetalLayer.presentsWithTransaction */
+ /** Borrowed id<CAMetalDrawable> on Cocoa. */
+ void *drawable;
+ /** Cocoa CAMetalLayer presentsWithTransaction setting for this frame. */
+ bool transaction;
};
/* Query the current draw target's native drawable. Returns false for unsupported
* backends, stale targets, or outside the draw callback. The result expires on
* successful present, callback return, or surface destruction. Native renderers
* submit/present the drawable, then call yasl_surface_present() to record it. */
+/**
+ * Get the current draw target's native drawable.
+ * @param s Surface currently being drawn.
+ * @param target Exact framebuffer identity supplied to this draw callback.
+ * @param out Required output native frame.
+ * @return true if available, false for NULL arguments, unsupported backends,
+ * stale targets, or calls outside draw. The drawable is borrowed and expires on
+ * successful present, callback return, or surface destruction. Currently Cocoa only.
+ */
YASL_API bool yasl_surface_get_native_frame(struct yasl_surface *s,
const struct yasl_framebuffer *target, struct yasl_native_frame *out);
diff --git a/include/yasl/native/cocoa.h b/include/yasl/native/cocoa.h
index 9066a19..9b221cf 100644
--- a/include/yasl/native/cocoa.h
+++ b/include/yasl/native/cocoa.h
@@ -1,3 +1,7 @@
+/**
+ * @file
+ * Typed borrowed Cocoa handles; available on macOS. Use on the main thread.
+ */
#ifndef YASL_NATIVE_COCOA_H
#define YASL_NATIVE_COCOA_H
@@ -8,9 +12,9 @@
@class NSWindow;
@class NSView;
#else
-typedef struct objc_object NSApplication;
-typedef struct objc_object NSWindow;
-typedef struct objc_object NSView;
+typedef struct objc_object NSApplication; /**< Opaque Cocoa application in plain C/C++. */
+typedef struct objc_object NSWindow; /**< Opaque Cocoa window in plain C/C++. */
+typedef struct objc_object NSView; /**< Opaque Cocoa view in plain C/C++. */
#endif
#ifdef __cplusplus
@@ -18,9 +22,30 @@ extern "C" {
#endif
/* Return borrowed Cocoa handles (expire on display/surface destruction).
- * Return NULL / nil if display/surface is not a Cocoa object or destroyed. */
+ * Return NULL / nil if display/surface is not a Cocoa object or NULL. */
+/**
+ * Get the borrowed NSApplication for an object of the matching backend.
+ * @param d Live display, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on display destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API NSApplication *yasl_cocoa_display_get_application(struct yasl_display *d);
+/**
+ * Get the borrowed NSWindow for an object of the matching backend.
+ * @param s Live surface, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on surface destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API NSWindow *yasl_cocoa_surface_get_window(struct yasl_surface *s);
+/**
+ * Get the borrowed NSView for an object of the matching backend.
+ * @param s Live surface, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on surface destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API NSView *yasl_cocoa_surface_get_view(struct yasl_surface *s);
#ifdef __cplusplus
diff --git a/include/yasl/native/wayland.h b/include/yasl/native/wayland.h
index 4a759d4..b11e3b8 100644
--- a/include/yasl/native/wayland.h
+++ b/include/yasl/native/wayland.h
@@ -1,3 +1,7 @@
+/**
+ * @file
+ * Typed borrowed Wayland handles; available when built with Wayland support.
+ */
#ifndef YASL_NATIVE_WAYLAND_H
#define YASL_NATIVE_WAYLAND_H
@@ -11,8 +15,22 @@ struct wl_display;
struct wl_surface;
/* Return borrowed Wayland handles (expire on display/surface destruction).
- * Return NULL if display/surface is not a Wayland object or destroyed. */
+ * Return NULL if display/surface is not a Wayland object or NULL. */
+/**
+ * Get the borrowed Wayland connection for an object of the matching backend.
+ * @param d Live display, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on display destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API struct wl_display *yasl_wayland_display_get_wl_display(struct yasl_display *d);
+/**
+ * Get the borrowed Wayland surface for an object of the matching backend.
+ * @param s Live surface, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on surface destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API struct wl_surface *yasl_wayland_surface_get_wl_surface(struct yasl_surface *s);
#ifdef __cplusplus
diff --git a/include/yasl/native/x11.h b/include/yasl/native/x11.h
index c0105cb..b719b4b 100644
--- a/include/yasl/native/x11.h
+++ b/include/yasl/native/x11.h
@@ -1,3 +1,7 @@
+/**
+ * @file
+ * Typed borrowed X11 handles; available when built with X11 support.
+ */
#ifndef YASL_NATIVE_X11_H
#define YASL_NATIVE_X11_H
@@ -8,11 +12,25 @@
extern "C" {
#endif
-typedef struct _XDisplay Display;
+typedef struct _XDisplay Display; /**< Opaque Xlib connection, compatible with Xlib.h. */
/* Return borrowed X11 handles (expire on display/surface destruction).
- * Return NULL / 0 if display/surface is not an X11 object or destroyed. */
+ * Return NULL / 0 if display/surface is not an X11 object or NULL. */
+/**
+ * Get the borrowed X11 Display for an object of the matching backend.
+ * @param d Live display, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on display destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API Display *yasl_x11_display_get_display(struct yasl_display *d);
+/**
+ * Get the borrowed X11 Window ID for an object of the matching backend.
+ * @param s Live surface, or NULL.
+ * @return The handle, or zero/NULL for NULL or a different backend.
+ * Expires on surface destruction. Available only when this backend is built.
+ * Never pass a pointer to a destroyed object.
+ */
YASL_API uintptr_t yasl_x11_surface_get_window(struct yasl_surface *s);
#ifdef __cplusplus
diff --git a/include/yasl/timing.h b/include/yasl/timing.h
index d443bf6..7e4af9e 100644
--- a/include/yasl/timing.h
+++ b/include/yasl/timing.h
@@ -1,3 +1,8 @@
+/**
+ * @file
+ * Timing provenance, refresh estimates, and buffer-resize statistics.
+ * All timestamps are monotonic nanoseconds, not wall-clock dates.
+ */
#ifndef YASL_TIMING_H
#define YASL_TIMING_H
@@ -7,27 +12,40 @@
extern "C" {
#endif
+/** Cumulative backend buffer-resize measurements; unavailable counters are zero. */
struct yasl_resize_stats {
+ /** Number of measured buffer resizes. */
uint64_t count;
+ /** Mean measured resize duration in microseconds. */
double avg_us;
+ /** Maximum measured resize duration in microseconds. */
double max_us;
+ /** Backend minor page-fault counter, if collected. */
long minflt;
};
/* Describes last_present_ns, not merely availability of an extension. */
+/** Provenance of last_present_ns; only PRESENTATION confirms compositor presentation. */
enum yasl_timing_source {
- YASL_TIMING_UNKNOWN = 0,
- YASL_TIMING_SUBMISSION, /* CPU submission time; not scanout confirmation */
- YASL_TIMING_FRAME_CALLBACK, /* compositor pacing callback, not scanout */
- YASL_TIMING_PRESENTATION, /* compositor-confirmed presentation */
+ YASL_TIMING_UNKNOWN = 0, /**< No timing sample yet. */
+ YASL_TIMING_SUBMISSION, /**< CPU submission time, not scanout confirmation. */
+ YASL_TIMING_FRAME_CALLBACK, /**< Compositor pacing callback, not scanout confirmation. */
+ YASL_TIMING_PRESENTATION, /**< Compositor-confirmed presentation. */
};
+/** Snapshot of estimates and timing provenance. Zero timestamps mean no sample yet. */
struct yasl_timing {
+ /** Estimated refresh interval in nanoseconds; zero if unavailable. */
double refresh_ns;
+ /** Last timestamp, whose meaning is specified by source. */
uint64_t last_present_ns;
+ /** Estimated next presentation time, or zero without a prior sample. */
uint64_t next_present_ns;
+ /** Backend skipped-frame counter; not comparable across backends. */
uint64_t frames_skipped;
+ /** Provenance of last_present_ns. */
enum yasl_timing_source source;
+ /** Cumulative backend resize statistics. */
struct yasl_resize_stats resize;
};
diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h
index df94800..f40264e 100644
--- a/include/yasl/yasl.h
+++ b/include/yasl/yasl.h
@@ -1,3 +1,26 @@
+/**
+ * @file
+ * Portable desktop surfaces, CPU drawing, input, and display dispatch.
+ *
+ * All calls and callbacks use the display's opening thread (the main thread on
+ * Cocoa), unless explicitly documented otherwise. Userdata is borrowed; callback
+ * tables and creation specs are copied. Do not recursively dispatch/flush or
+ * destroy a display/loop while dispatch or a callback is active. Destroy child
+ * popups before parents, surfaces before displays, displays before borrowed loops.
+ * Surface destruction from its own callback is supported; stop using it immediately.
+ *
+ * Zero-initialize option and callback structs before setting fields. Pointers to
+ * destroyed objects are invalid, even for getters and destroy functions. NULL
+ * handling is documented per function; it does not make freed pointers safe.
+ * Only functions explicitly documenting errno guarantee a diagnostic there.
+ * Other boolean results report success/availability; void setters are best effort.
+ *
+ * Toplevel creation sizes use logical pixels. Geometry, framebuffers, pointer
+ * positions, damage, IME carets, popup specs, and drag offsets use device pixels.
+ * Coordinates have their origin at the upper left; x grows right and y grows down.
+ * Scale is an integer ratio with YASL_SCALE_UNIT (120) representing 1x.
+ * See the application guide for the dispatch/poll/flush cycle and backend limits.
+ */
#ifndef YASL_H
#define YASL_H
@@ -9,11 +32,13 @@
#include <math.h>
#include "yasl/timing.h"
+#include "yasl/keycodes.h"
#ifdef __cplusplus
extern "C" {
#endif
+/** Public shared-library symbol visibility annotation. */
#if defined(_WIN32) || defined(__CYGWIN__)
#if defined(YASL_BUILD_SHARED)
#define YASL_API __declspec(dllexport)
@@ -33,23 +58,46 @@ struct yasl_surface;
struct yasl_loop;
/* Geometry */
+/** A two-dimensional integer size, position, or offset; units depend on the caller. */
struct yasl_vector2i {
+ /** Horizontal coordinate or width. */
int x;
+ /** Vertical coordinate or height. */
int y;
};
+/** Two-dimensional fractional offset; units depend on the containing structure. */
+struct yasl_vector2d {
+ /** Horizontal component. */
+ double x;
+ /** Vertical component. */
+ double y;
+};
+
+/** Integer rectangle; origin is upper left and extents must be nonnegative. */
struct yasl_rect2i {
+ /** Left coordinate. */
int x;
+ /** Top coordinate. */
int y;
+ /** Nonnegative horizontal extent. */
int width;
+ /** Nonnegative vertical extent. */
int height;
};
+/** Scale numerator representing 1x. */
#define YASL_SCALE_UNIT 120
+/** Backend minimum logical dimension used when constraining toplevel sizes. */
#define YASL_MIN_LOGICAL_PX 64
/* Nearest integer, ties away from zero. Zero scale returns zero; finite
* out-of-range results saturate. Non-finite logical coordinates return zero. */
+/** Convert a logical coordinate to device pixels, rounding ties away from zero.
+ * @param logical Logical coordinate; non-finite values return zero.
+ * @param scale_120 Scale numerator; zero returns zero.
+ * @return Rounded device coordinate, saturated to the range of int.
+ */
static inline int yasl_scale_to_device(double logical, uint32_t scale_120)
{
if (!scale_120 || !(logical >= -DBL_MAX && logical <= DBL_MAX)) return 0;
@@ -59,6 +107,11 @@ static inline int yasl_scale_to_device(double logical, uint32_t scale_120)
return (int)(px + copysign(0.5, px));
}
+/** Convert a device coordinate to logical pixels, rounding ties away from zero.
+ * @param device Device coordinate.
+ * @param scale_120 Scale numerator; zero returns zero.
+ * @return Rounded logical coordinate, saturated to the range of int.
+ */
static inline int yasl_scale_to_logical(int device, uint32_t scale_120)
{
if (!scale_120) return 0;
@@ -73,219 +126,293 @@ static inline int yasl_scale_to_logical(int device, uint32_t scale_120)
}
/* Capabilities */
+/** Optional backend/server features, returned as a bitmask by yasl_display_caps(). */
enum yasl_display_cap : uint32_t {
- YASL_CAP_POPUPS = 1 << 0,
- YASL_CAP_DRAG = 1 << 1,
- YASL_CAP_DRAG_CARRY = 1 << 2,
- YASL_CAP_PLACE_WINDOW = 1 << 3,
- YASL_CAP_PRIMARY_SELECTION = 1 << 4,
- YASL_CAP_CSD = 1 << 5,
- YASL_CAP_NATIVE_GRAPHICS = 1 << 6,
- YASL_CAP_FRAME_PACING = 1 << 7, /* compositor/display-driven draws */
+ YASL_CAP_POPUPS = 1 << 0, /**< Popup creation and placement. */
+ YASL_CAP_DRAG = 1 << 1, /**< Surface-drag source and target support. */
+ YASL_CAP_DRAG_CARRY = 1 << 2, /**< Carry a toplevel during a drag. */
+ YASL_CAP_PLACE_WINDOW = 1 << 3, /**< Place a window relative to the pointer. */
+ YASL_CAP_PRIMARY_SELECTION = 1 << 4, /**< Separate primary selection. */
+ YASL_CAP_CSD = 1 << 5, /**< Backend advertises client decoration support; check each action result. */
+ YASL_CAP_NATIVE_GRAPHICS = 1 << 6, /**< Application-managed native rendering. */
+ YASL_CAP_FRAME_PACING = 1 << 7, /**< Compositor/display-driven draws. */
/* Backend/protocol supports tablet axes, independent of attached hardware. */
- YASL_CAP_TABLET = 1 << 8,
+ YASL_CAP_TABLET = 1 << 8, /**< Tablet protocol support, independent of attached hardware. */
};
+/** Independent selections. An unsupported selection never aliases the clipboard. */
enum yasl_selection : uint8_t {
- YASL_CLIPBOARD,
- YASL_PRIMARY,
+ YASL_CLIPBOARD, /**< Explicit copy/paste selection. */
+ YASL_PRIMARY, /**< Independent primary selection, if supported. */
};
+/** Reported surface state flags; absent flags may also mean unsupported state. */
enum yasl_surface_state : uint32_t {
- YASL_STATE_FULLSCREEN = 1 << 0,
- YASL_STATE_MAXIMIZED = 1 << 1,
- YASL_STATE_ACTIVATED = 1 << 2,
- YASL_STATE_SUSPENDED = 1 << 3,
- YASL_STATE_CSD = 1 << 4,
+ YASL_STATE_FULLSCREEN = 1 << 0, /**< Fullscreen. */
+ YASL_STATE_MAXIMIZED = 1 << 1, /**< Maximized. */
+ YASL_STATE_ACTIVATED = 1 << 2, /**< Active window. */
+ YASL_STATE_SUSPENDED = 1 << 3, /**< Compositor reports suspension. */
+ YASL_STATE_CSD = 1 << 4, /**< Backend reports client-side decorations. */
};
+/** System cursor shapes. HIDDEN preserves pointer input; NONE is its compatibility alias. */
enum yasl_cursor {
- YASL_CURSOR_ARROW,
- YASL_CURSOR_TEXT,
- YASL_CURSOR_LINK,
- YASL_CURSOR_CROSSHAIR,
- YASL_CURSOR_MOVE,
- YASL_CURSOR_RESIZE_NWSE,
- YASL_CURSOR_RESIZE_NESW,
- YASL_CURSOR_RESIZE_EW,
- YASL_CURSOR_RESIZE_NS,
- YASL_CURSOR_HIDDEN, /* Hide the system cursor; pointer input is unchanged. */
- YASL_CURSOR_NONE = YASL_CURSOR_HIDDEN, /* Compatibility alias. */
-};
-
+ YASL_CURSOR_ARROW, /**< Default arrow. */
+ YASL_CURSOR_TEXT, /**< Text insertion. */
+ YASL_CURSOR_LINK, /**< Link/pointer hand. */
+ YASL_CURSOR_CROSSHAIR, /**< Crosshair. */
+ YASL_CURSOR_MOVE, /**< Move in any direction. */
+ YASL_CURSOR_RESIZE_NWSE, /**< Northwest/southeast resize. */
+ YASL_CURSOR_RESIZE_NESW, /**< Northeast/southwest resize. */
+ YASL_CURSOR_RESIZE_EW, /**< Horizontal resize. */
+ YASL_CURSOR_RESIZE_NS, /**< Vertical resize. */
+ YASL_CURSOR_HIDDEN, /**< Hidden system cursor; pointer input continues. */
+ YASL_CURSOR_NONE = YASL_CURSOR_HIDDEN, /**< Compatibility alias for HIDDEN. */
+};
+
+/** Valid interactive resize edges; combinations are restricted to the named values. */
enum yasl_resize_edge : uint32_t {
- YASL_EDGE_NONE = 0,
- YASL_EDGE_TOP = 1,
- YASL_EDGE_BOTTOM = 2,
- YASL_EDGE_LEFT = 4,
- YASL_EDGE_TOP_LEFT = 5,
- YASL_EDGE_BOTTOM_LEFT = 6,
- YASL_EDGE_RIGHT = 8,
- YASL_EDGE_TOP_RIGHT = 9,
- YASL_EDGE_BOTTOM_RIGHT = 10,
+ YASL_EDGE_NONE = 0, /**< No resize edge; rejected by interactive resize. */
+ YASL_EDGE_TOP = 1, /**< Top edge. */
+ YASL_EDGE_BOTTOM = 2, /**< Bottom edge. */
+ YASL_EDGE_LEFT = 4, /**< Left edge. */
+ YASL_EDGE_TOP_LEFT = 5, /**< Top-left corner. */
+ YASL_EDGE_BOTTOM_LEFT = 6, /**< Bottom-left corner. */
+ YASL_EDGE_RIGHT = 8, /**< Right edge. */
+ YASL_EDGE_TOP_RIGHT = 9, /**< Top-right corner. */
+ YASL_EDGE_BOTTOM_RIGHT = 10, /**< Bottom-right corner. */
};
/* Damage & Framebuffer
* CPU pixels are tightly packed premultiplied ARGB8888 (0xAARRGGBB).
* Size, pointer positions, IME caret and damage use device pixels; surface
- * creation sizes use logical pixels. Damage is a hint: backends may upload
+ * toplevel creation sizes use logical pixels. Damage is a hint: backends may upload
* the whole frame. Every pixel must contain valid content on each draw.
*/
+/** Maximum number of damage rectangles in one presentation. */
#define YASL_DAMAGE_MAX 64
+/** Damage hints for one presentation; every target still needs complete valid pixels. */
struct yasl_damage_list {
- int n; /* -1 means full repaint */
+ /** -1: full target; 0: no damaged rectangles; otherwise number of rects. */
+ int n;
+ /** Reserved hint; callers may leave zero. Core presentation uses rects. */
struct yasl_rect2i bounds;
+ /** First n rectangles, in device pixels; ignored for n == -1. */
struct yasl_rect2i rects[YASL_DAMAGE_MAX];
};
+/** Borrowed draw target, valid only for the current draw callback. Never free pixels. */
struct yasl_framebuffer {
- uint64_t generation; /* Opaque draw identity; preserve when copying target. */
+ /** Opaque draw identity. Preserve it and all target fields when copying. */
+ uint64_t generation;
+ /** Writable premultiplied ARGB8888, row stride size.x * sizeof(uint32_t); may be NULL for native rendering. */
uint32_t *pixels;
+ /** Target extent in device pixels. */
struct yasl_vector2i size;
};
+/** One draw invocation. Do not retain its pointer after the callback returns. */
struct yasl_frame {
+ /** Surface being drawn. */
struct yasl_surface *surface;
+ /** Target to initialize and present before returning. */
struct yasl_framebuffer target;
};
/* Drag and Drop */
+/** Target notifications for YASL surface drags, separate from external file drops. */
enum yasl_drag_event {
- YASL_DRAG_OVER,
- YASL_DRAG_LEAVE,
- YASL_DRAG_DROP,
+ YASL_DRAG_OVER, /**< Surface drag entered or moved over the target. */
+ YASL_DRAG_LEAVE, /**< Surface drag left the target. */
+ YASL_DRAG_DROP, /**< Surface drag dropped on the target. */
};
+/** External text/file drag hover notifications; completed data arrives through input. */
enum yasl_drop_event {
- YASL_DROP_OVER,
- YASL_DROP_LEAVE,
+ YASL_DROP_OVER, /**< External data drag entered or moved over the target. */
+ YASL_DROP_LEAVE, /**< External data drag left the target. */
};
+/** Source-side drag completion; DROPPED does not prove application-level acceptance. */
enum yasl_drag_end {
- YASL_DRAG_DROPPED,
- YASL_DRAG_CANCELLED,
+ YASL_DRAG_DROPPED, /**< Drag ended in a drop; not an application acceptance receipt. */
+ YASL_DRAG_CANCELLED, /**< Drag canceled, including display close. */
};
+/** Copied drag icon in tightly packed premultiplied 0xAARRGGBB device pixels. */
struct yasl_drag_image {
+ /** Copied pixel array; required when an image is supplied. */
const uint32_t *pixels;
+ /** Positive width and height in device pixels. */
struct yasl_vector2i size;
+ /** Pointer position within the image, in device pixels. */
struct yasl_vector2i hotspot;
};
+/** Copied drag completion listener; userdata remains owned by the application. */
struct yasl_drag_listener {
+ /** Called exactly once after a successful start, possibly synchronously on cancel/close. */
void (*end)(void *ud, enum yasl_drag_end how);
};
+/** Configured device-pixel content size and the corresponding display scale. */
struct yasl_geometry {
+ /** Current content extent in device pixels. */
struct yasl_vector2i size;
+ /** Positive scale numerator; 120 means 1x and 180 means 1.5x. */
uint32_t scale_120;
};
/* Input Model */
+/** Keyboard press, release, or autorepeat; releases must not insert text. */
enum yasl_key_action {
- YASL_KEY_PRESS,
- YASL_KEY_RELEASE,
- YASL_KEY_REPEAT,
+ YASL_KEY_PRESS, /**< Key pressed. */
+ YASL_KEY_RELEASE, /**< Key released. */
+ YASL_KEY_REPEAT, /**< Key autorepeat. */
};
+/** Modifier bitmask. SUPER is Command on macOS. */
enum yasl_mod : uint32_t {
- YASL_MOD_SHIFT = 1 << 0,
- YASL_MOD_ALT = 1 << 1,
- YASL_MOD_CTRL = 1 << 2,
- YASL_MOD_SUPER = 1 << 3,
- YASL_MOD_CAPS = 1 << 4,
+ YASL_MOD_SHIFT = 1 << 0, /**< Shift modifier. */
+ YASL_MOD_ALT = 1 << 1, /**< Alt/Option modifier. */
+ YASL_MOD_CTRL = 1 << 2, /**< Control modifier. */
+ YASL_MOD_SUPER = 1 << 3, /**< Super/Command modifier. */
+ YASL_MOD_CAPS = 1 << 4, /**< Caps Lock modifier. */
};
+/** Pointer transition or motion type; fields are meaningful according to this tag. */
enum yasl_pointer_kind {
- YASL_PTR_PRESS,
- YASL_PTR_RELEASE,
- YASL_PTR_MOTION,
- YASL_PTR_LEAVE,
- YASL_PTR_SCROLL,
+ YASL_PTR_PRESS, /**< Button/tip pressed. */
+ YASL_PTR_RELEASE, /**< Button/tip released. */
+ YASL_PTR_MOTION, /**< Position or tablet axes changed. */
+ YASL_PTR_LEAVE, /**< Pointer/tool left the surface. */
+ YASL_PTR_SCROLL, /**< Scroll sample. */
};
+/** Pointer source class; capability support does not imply an attached device. */
enum yasl_pointer_device {
- YASL_PTR_DEV_MOUSE,
- YASL_PTR_DEV_TOUCH,
- YASL_PTR_DEV_PEN,
- YASL_PTR_DEV_ERASER,
+ YASL_PTR_DEV_MOUSE, /**< Mouse or trackpad pointer. */
+ YASL_PTR_DEV_TOUCH, /**< Touch translated into pointer input. */
+ YASL_PTR_DEV_PEN, /**< Tablet stylus. */
+ YASL_PTR_DEV_ERASER, /**< Tablet eraser tool. */
};
+/** Normalized tablet axes; zero for unavailable axes and non-tablet devices. */
struct yasl_pointer_pen {
- uint16_t pressure; /* 0..65535, normalized to the device's pressure range. */
- int8_t tilt_x; /* Degrees, -90..90; positive toward the right. */
- int8_t tilt_y; /* Degrees, -90..90; positive toward the bottom. */
-};
-
+ /** 0..65535 normalized to device range. Zero alone does not end contact. */
+ uint16_t pressure;
+ /** Degrees in -90..90; positive right. */
+ int8_t tilt_x;
+ /** Degrees in -90..90; positive down. */
+ int8_t tilt_y;
+};
+
+/** Physical keyboard transition with optional layout labels.
+ * key is independent of modifiers/layout and stable between press and release.
+ * codepoint and shifted are optional single Unicode scalars for the current
+ * layout at levels zero/one, not text input. Zero means unavailable, a control
+ * key, or a layout mapping that produces multiple characters. Native scancodes
+ * are backend-specific. Insert committed YASL_INPUT_TEXT into documents.
+ */
struct yasl_key_event {
- uint32_t key;
- uint32_t keypad;
+ /** Portable physical key; YASL_KEY_UNKNOWN when unmapped. */
+ enum yasl_key key;
+ /** Press, release, or autorepeat. */
enum yasl_key_action action;
+ /** yasl_mod bitmask at this event. */
uint32_t mods;
+ /** Optional unshifted layout Unicode scalar; zero if unavailable. */
uint32_t codepoint;
+ /** Optional shifted layout Unicode scalar; zero if unavailable. */
uint32_t shifted;
- uint32_t base;
- uint32_t text;
+ /** Native code: Wayland evdev, X11 server keycode, Cocoa virtual keycode. */
+ uint32_t native_code;
};
+/** Pointer sample. Pen and eraser events share this structure with mouse input.
+Track contact with primary press/release, not pressure alone. Leave, focus loss,
+and YASL_INPUT_RESET must cancel held application input state. This is not a
+multi-contact touch API. Scroll offsets use fractional line units: positive x
+means right and positive y means down, after system scroll preferences. */
struct yasl_mouse_event {
+ /** Surface-relative device-pixel position. */
struct yasl_vector2i pos;
- int16_t button; /* Press/release: 0 = primary/tip, 1 = middle, 2 = secondary.
- * Motion/leave: informational; track presses and releases. */
- int16_t scroll_lines;
+ /** Press/release: 0 primary/tip, 1 middle, 2 secondary. Unused for scroll. */
+ int16_t button;
+ /** Sample type. */
enum yasl_pointer_kind kind;
+ /** Mouse, touch, pen, or eraser. */
enum yasl_pointer_device device;
+ /** yasl_mod bitmask at this event. */
uint32_t mods;
- float scroll_delta;
- /* Pen/eraser samples include the current axes, also on pressure-only
- * motion. Missing axes and non-tablet devices report zero. A primary
- * release or leave ends contact; zero pressure alone is not a release. */
+ /** Fractional line offsets, valid for SCROLL only; positive right/down. */
+ struct yasl_vector2d scroll;
+
+ /** Current tablet axes, including on pressure-only motion; missing axes are zero. */
struct yasl_pointer_pen pen;
};
+/** Input union tag. RESET has no payload and requires clearing held state. */
enum yasl_input_kind {
- YASL_INPUT_KEY,
- YASL_INPUT_MOUSE,
- YASL_INPUT_FOCUS,
- YASL_INPUT_PASTE,
- YASL_INPUT_DROP_FILES,
- YASL_INPUT_TEXT,
+ YASL_INPUT_KEY, /**< key member. */
+ YASL_INPUT_MOUSE, /**< mouse member. */
+ YASL_INPUT_FOCUS, /**< focus member. */
+ YASL_INPUT_PASTE, /**< paste member. */
+ YASL_INPUT_DROP_FILES, /**< drop_files member. */
+ YASL_INPUT_TEXT, /**< text member, containing committed UTF-8. */
/* The input stream was interrupted (e.g. allocation failure or device
* removal). Clear held keys/buttons and composition state. */
- YASL_INPUT_RESET,
+ YASL_INPUT_RESET, /**< No payload. Clear held keys/buttons and composition after an interruption. */
};
+/** Keyboard focus transition; cancel held state when focus is lost. */
struct yasl_focus_event {
+ /** true on focus gain, false on loss. */
bool focused;
};
+/** Committed UTF-8 text; insert this instead of deriving characters from key events. */
struct yasl_text_event {
+ /** Borrowed NUL-terminated UTF-8. */
const char *text;
+ /** Length in bytes excluding the terminating NUL. */
size_t len;
};
+/** UTF-8 text received from an explicit selection request. */
struct yasl_paste_event {
+ /** Borrowed NUL-terminated UTF-8. */
const char *text;
+ /** Length in bytes excluding the terminating NUL. */
size_t len;
};
+/** File URLs in text/uri-list form, separated by CRLF; not filesystem paths. */
struct yasl_drop_files_event {
+ /** Borrowed NUL-terminated text/uri-list; parse URLs and percent escapes. */
const char *files;
+ /** Length in bytes excluding the terminating NUL. */
size_t len;
};
+/** One tagged input event; read only the union member selected by kind.
+Payload strings follow yasl_surface_input_poll() lifetime rules. RESET carries
+no union payload; clear keys, buttons, strokes, and composition state. */
struct yasl_input_event {
+ /** Recipient; can differ from the polled surface for routed popup input. */
struct yasl_surface *surface;
+ /** Selects the valid union member. */
enum yasl_input_kind kind;
+ /** Reserved for YASL; applications must not interpret it. */
uint32_t reserved;
union {
- struct yasl_key_event key;
- struct yasl_mouse_event mouse;
- struct yasl_focus_event focus;
- struct yasl_text_event text;
- struct yasl_paste_event paste;
- struct yasl_drop_files_event drop_files;
+ struct yasl_key_event key; /**< Keyboard payload. */
+ struct yasl_mouse_event mouse; /**< Pointer payload. */
+ struct yasl_focus_event focus; /**< Focus payload. */
+ struct yasl_text_event text; /**< Committed text payload. */
+ struct yasl_paste_event paste; /**< Selection text payload. */
+ struct yasl_drop_files_event drop_files; /**< File URL list payload. */
};
};
@@ -300,128 +427,417 @@ struct yasl_input_event {
*/
/* Callbacks */
+/** Optional display notifications. NULL callback members are ignored. */
struct yasl_display_cbs {
+ /** Application quit request; arrange orderly shutdown after dispatch returns. */
void (*quit)(void *ud);
+ /** Application reopen/activation request, currently Cocoa. */
void (*reopen)(void *ud);
+ /** Display connection lost; arrange cleanup after dispatch returns. */
void (*lost)(void *ud);
};
+/** Optional surface notifications, copied by yasl_surface_set_callbacks().
+Callbacks may destroy their surface. They must then return without further use
+of the surface or draw target. NULL members are ignored. */
struct yasl_surface_cbs {
+ /** Initialize the entire target and optionally present. Requested through schedule_redraw/flush. */
void (*draw)(struct yasl_surface *s, struct yasl_frame *frame, void *ud);
+ /** Configured size/scale changed; reallocate application content and request a redraw. */
void (*configure)(struct yasl_surface *s, struct yasl_geometry geometry, void *ud);
+ /** Window/popup close request; application is responsible for destruction. */
void (*close)(struct yasl_surface *s, void *ud);
+ /** Reported yasl_surface_state bitmask changed. */
void (*state)(struct yasl_surface *s, uint32_t state, void *ud);
+ /** YASL surface-drag target hover, leave, or drop; pos is in device pixels. */
void (*drag)(struct yasl_surface *s, enum yasl_drag_event ev,
struct yasl_vector2i pos, void *ud);
+ /** External data-drag hover or leave; pos is in device pixels. */
void (*drop)(struct yasl_surface *s, enum yasl_drop_event ev,
struct yasl_vector2i pos, void *ud);
};
/* Display Lifecycle */
+/** Runtime backend choice. AUTO selects a native backend and allows fallback. */
enum yasl_backend {
- YASL_BACKEND_AUTO = 0,
- YASL_BACKEND_WAYLAND,
- YASL_BACKEND_X11,
- YASL_BACKEND_COCOA,
+ YASL_BACKEND_AUTO = 0, /**< Choose native backend with fallback. */
+ YASL_BACKEND_WAYLAND, /**< Require Wayland. */
+ YASL_BACKEND_X11, /**< Require X11. */
+ YASL_BACKEND_COCOA, /**< Require Cocoa. */
};
+/** Display options. Zero-initialize for automatic backend selection. */
struct yasl_display_spec {
+ /** Copied application identifier, conventionally a reverse-domain UTF-8 string; NULL uses backend defaults. */
const char *app_id;
+ /** Copied optional display callbacks; NULL disables them. */
const struct yasl_display_cbs *cbs;
+ /** Borrowed display callback userdata. */
void *ud;
- /* Zero selects the native backend. An explicit choice never falls back. */
+
+ /** Zero/AUTO chooses native backend; explicit choices never fall back. */
enum yasl_backend backend;
};
/* Return NULL and set errno if the requested backend cannot open. */
+/**
+ * Open a desktop connection and its event machinery.
+ * @param loop Borrowed loop, or NULL to create a display-owned loop.
+ * @param spec Copied options, or NULL for automatic backend selection and defaults.
+ * @return An owned display, or NULL with errno set. EINVAL means an invalid backend;
+ * ENOSYS means the requested backend was not built; ENODEV is the fallback when
+ * opening fails without a more specific platform error. Explicit choices never
+ * fall back. Close with yasl_display_close() after destroying its surfaces.
+ */
YASL_API struct yasl_display *yasl_display_open(struct yasl_loop *loop,
const struct yasl_display_spec *spec);
+/**
+ * Read the selected backend.
+ * @param d Display, or NULL.
+ * @return The actual backend, or YASL_BACKEND_AUTO for NULL.
+ */
YASL_API enum yasl_backend yasl_display_backend(const struct yasl_display *d);
+/**
+ * Close a display and release its owned loop, if any.
+ * @param d Display to close, or NULL for no action.
+ * Destroy all surfaces first. Cancels an active drag and may invoke its end
+ * listener. Must not be called during dispatch or from a callback.
+ */
YASL_API void yasl_display_close(struct yasl_display *d);
+/**
+ * Query currently available optional features.
+ * @param d Display, or NULL.
+ * @return A bitmask of yasl_display_cap values, or zero for NULL.
+ * Capabilities describe backend/server support, not attached input hardware or
+ * permission to perform an operation at any arbitrary moment.
+ */
YASL_API uint32_t yasl_display_caps(const struct yasl_display *d);
/* Process events, waiting at most timeout_ms: zero polls, negative waits.
* Pending input or redraw work returns immediately. Returns -1 on failure. */
+/**
+ * Receive events and run ready loop sources.
+ * @param d Display to dispatch.
+ * @param timeout_ms Maximum wait in milliseconds: zero polls, negative waits indefinitely.
+ * @return A nonnegative backend-specific work count on success, or -1 with errno
+ * set on failure (EINVAL for NULL, EIO if no specific backend error is available).
+ * Do not interpret the count as the number of queued input events. Pending input
+ * or runnable redraw work prevents waiting. Drain input and call
+ * yasl_display_flush() after updating application state. Do not dispatch recursively.
+ */
YASL_API int yasl_display_dispatch(struct yasl_display *d, int timeout_ms);
+/**
+ * Draw scheduled surfaces and send pending display requests.
+ * @param d Display, or NULL for no action.
+ * May synchronously invoke draw and other callbacks. A paced surface may wait
+ * for its next display/compositor signal. This is not a GPU completion wait.
+ */
YASL_API void yasl_display_flush(struct yasl_display *d);
+/**
+ * Read the latest modifier state known to the backend.
+ * @param d Display, or NULL.
+ * @return A bitmask of yasl_mod values, or zero for NULL.
+ * For an individual event use its own mods field instead.
+ */
YASL_API int yasl_display_modifiers(const struct yasl_display *d);
/* Unsupported/invalid selections are no-ops, never clipboard aliases. */
+/**
+ * Offer a copy of a NUL-terminated UTF-8 string as a selection.
+ * @param d Display, or NULL for no action.
+ * @param sel YASL_CLIPBOARD or YASL_PRIMARY.
+ * @param text Text copied during this call; NULL does nothing.
+ * Unsupported selections do nothing. The void return does not confirm that the
+ * system accepted the offer; Wayland may require a recent input serial.
+ */
YASL_API void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text);
+/**
+ * Request text delivery to a particular surface.
+ * @param d Display that owns target.
+ * @param sel YASL_CLIPBOARD or YASL_PRIMARY.
+ * @param target Destination surface; NULL or a surface on another display does nothing.
+ * Results arrive as YASL_INPUT_PASTE, possibly during a later dispatch. No result
+ * is promised for unavailable data or a failed transfer. Destroying target cancels
+ * delivery. Unsupported selections do nothing.
+ */
YASL_API void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target);
/* A successful drag invokes the copied listener's non-NULL end once, including
* cancel and display close. Failed starts do not invoke it. */
+/**
+ * Start a surface drag from a recent pointer press.
+ * @param d Display that owns origin.
+ * @param origin Origin surface.
+ * @param image Optional copied premultiplied ARGB8888 drag image, or NULL.
+ * @param l Optional copied completion listener, or NULL.
+ * @param ud Borrowed listener userdata until completion.
+ * @return true if started; false if unsupported, invalid, or not currently permitted.
+ * A successful start invokes a non-NULL end callback exactly once, including
+ * cancellation or display close. Failed starts do not invoke it. This API carries
+ * surfaces, not arbitrary MIME payloads; use application state to identify the item.
+ */
YASL_API bool yasl_display_drag_start(struct yasl_display *d, struct yasl_surface *origin,
const struct yasl_drag_image *image,
const struct yasl_drag_listener *l, void *ud);
+/**
+ * Cancel the active surface drag, if any.
+ * @param d Display, or NULL for no action.
+ * May invoke the end listener synchronously.
+ */
YASL_API void yasl_display_drag_cancel(struct yasl_display *d);
/* Surface Lifecycle */
+/** Toplevel options. Zero-initialize, then supply a positive logical-pixel size. */
struct yasl_surface_spec {
+ /** Copied UTF-8 title; NULL uses backend defaults. */
const char *title;
+ /** Requested logical-pixel width and height; both must be positive. */
struct yasl_vector2i size;
+ /** Request initial fullscreen; actual state is reported asynchronously. */
bool fullscreen;
+ /** Permit unpaced presentation where supported; a hint, not a guarantee. */
bool tearing;
};
+/** Popup options, entirely in device pixels, including size.
+The content rectangle excludes transparent margins/shadows used for placement.
+An axis of content_size equal to zero extends to that axis of size. The offset
+and resolved content size must fit in size. Anchor extents must be nonnegative.
+Dimensions must be positive, at most 32767, and fit an INT32_MAX-byte buffer;
+backends may impose further limits after scaling or resolving placement. */
struct yasl_popup_spec {
+ /** Full popup framebuffer extent in device pixels, including transparent margins. */
struct yasl_vector2i size;
+ /** Rectangle in parent framebuffer coordinates, in device pixels. */
struct yasl_rect2i anchor;
+ /** true prefers right of anchor; false prefers below. Placement may flip/slide. */
bool beside;
+ /** Top-left content offset within the popup, in device pixels. */
struct yasl_vector2i content_offset;
+ /** Content extent; zero per axis extends to the corresponding size edge. */
struct yasl_vector2i content_size;
};
+/**
+ * Create a resizable desktop window with CPU drawing targets.
+ * @param d Owning display.
+ * @param spec Required copied options; size is in logical pixels.
+ * @return An owned surface, or NULL with errno set. EINVAL means invalid options,
+ * including a nonpositive dimension, a dimension above 32767, or a buffer above
+ * INT32_MAX bytes. ENODEV is the fallback backend failure. Actual configured
+ * size may differ from the request. Install callbacks and schedule a redraw.
+ */
YASL_API struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d,
const struct yasl_surface_spec *spec);
+/**
+ * Create an owned popup attached to a parent surface.
+ * @param parent Borrowed parent; destroy the popup before the parent.
+ * @param spec Required copied options, entirely in device pixels.
+ * @return A popup, or NULL with errno set: EINVAL for invalid options, ENOTSUP
+ * for missing popup support, or a backend error (ENODEV if none was supplied).
+ * Wayland requires a recent input serial for its popup grab. The backend may
+ * flip or slide placement to fit the screen. See yasl_popup_spec.
+ */
YASL_API struct yasl_surface *yasl_surface_create_popup(struct yasl_surface *parent,
const struct yasl_popup_spec *spec);
+/**
+ * Update a popup's geometry and placement.
+ * @param popup An existing popup.
+ * @param spec Required copied replacement options, in device pixels.
+ * @return true on success; false with errno set: EINVAL for invalid options,
+ * ENOTSUP for missing popup support, or a backend error (ENODEV if unspecified).
+ * Older Wayland popup protocol versions can reject repositioning.
+ */
YASL_API bool yasl_surface_popup_reposition(struct yasl_surface *popup,
const struct yasl_popup_spec *spec);
+/**
+ * Destroy a surface and cancel its surface timers and pending input.
+ * @param s Surface, or NULL for no action.
+ * Destroy child popups first. May be called from a surface callback; return from
+ * that callback without further use of the surface or its draw target. Every
+ * pointer or native handle obtained from this surface becomes invalid.
+ */
YASL_API void yasl_surface_destroy(struct yasl_surface *s);
+/**
+ * Get the display that owns a surface.
+ * @param s Surface, or NULL.
+ * @return A borrowed display, or NULL.
+ */
YASL_API struct yasl_display *yasl_surface_display(const struct yasl_surface *s);
+/**
+ * Replace the surface's callback table and userdata.
+ * @param s Surface, or NULL for no action.
+ * @param cbs Table copied by value; NULL removes all callbacks. NULL members are ignored.
+ * @param ud Borrowed userdata passed to the installed callbacks.
+ * Callbacks may run synchronously during this call; initialize userdata first.
+ * Use yasl_surface_geometry() to read current geometry; do not rely on receiving
+ * an initial configure callback when registering.
+ */
YASL_API void yasl_surface_set_callbacks(struct yasl_surface *s,
const struct yasl_surface_cbs *cbs, void *ud);
+/**
+ * Replace the window title with copied UTF-8 text.
+ * @param s Surface.
+ * @param title NUL-terminated UTF-8 title. Pass an empty string to clear it.
+ * Backends may truncate the title.
+ */
YASL_API void yasl_surface_set_title(struct yasl_surface *s, const char *title);
+/**
+ * Request a future draw callback, coalescing repeated requests.
+ * @param s Surface, or NULL for no action.
+ * Call yasl_display_flush() to service the request. Schedule again from draw to
+ * animate. Without YASL_CAP_FRAME_PACING, throttle animation with a timer.
+ */
YASL_API void yasl_surface_schedule_redraw(struct yasl_surface *s);
/* Present exactly the target supplied to the current draw callback, at most
* once. Targets expire when that callback returns or the surface is destroyed.
* damage is required: n must be -1 or 0..YASL_DAMAGE_MAX. Rectangles must have
* nonnegative extents; they are clipped to the target. Invalid input returns
* false without consuming the frame. See yasl/native.h for native rendering. */
+/**
+ * Submit the current draw target at most once.
+ * @param s Surface currently being drawn.
+ * @param fb Exact target from the current draw callback, including its generation.
+ * @param damage Required damage hint: n is -1 for all pixels or 0..YASL_DAMAGE_MAX.
+ * @return true if accepted; false for invalid, stale, already presented, or failed targets.
+ * Rectangles must have nonnegative extents and are clipped to the target. Invalid
+ * input does not consume the frame. All pixels must be initialized even with
+ * partial damage. The backend may upload all pixels. Success records submission,
+ * not confirmed scanout. The target expires when draw returns or s is destroyed.
+ */
YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_framebuffer *fb,
const struct yasl_damage_list *damage);
/* Event payload strings are library-owned, NUL-terminated, and valid until
* the next SUCCESSFUL poll on this surface or surface destruction. Dispatch,
* flush and unsuccessful polls do not invalidate them. Copy to keep longer. */
+/**
+ * Remove one event from a surface's input queue without waiting.
+ * @param s Surface whose queue to drain.
+ * @param ev Required output event; unchanged when no event is available.
+ * @return true for an event, false for an empty queue or NULL arguments.
+ * Dispatch receives native input; polling only drains the queue. Read the union
+ * member selected by kind. Payload strings are borrowed, NUL-terminated, and
+ * valid until the next successful poll on this queue or its destruction; dispatch,
+ * flush, and unsuccessful polls do not invalidate them. On Wayland, popup pointer
+ * and keyboard input can be routed through the root parent's queue; inspect
+ * ev->surface to identify the receiving surface.
+ */
YASL_API bool yasl_surface_input_poll(struct yasl_surface *s, struct yasl_input_event *ev);
/* Current device-pixel size and scale, matching the configure callback. */
+/**
+ * Read current device-pixel size and scale together.
+ * @param s Surface, or NULL.
+ * @return Geometry matching configure callbacks; NULL gives zero size and unit scale.
+ */
YASL_API struct yasl_geometry yasl_surface_geometry(const struct yasl_surface *s);
+/**
+ * Read current window state.
+ * @param s Surface, or NULL.
+ * @return A bitmask of yasl_surface_state values, or zero for NULL.
+ * Flags are reported state, not requests; unsupported states are absent.
+ */
YASL_API uint32_t yasl_surface_state(const struct yasl_surface *s);
/* Timing uses monotonic nanoseconds. Check source before treating timestamps
* as presentation confirmation. refresh/next_present are estimates. Without
* YASL_CAP_FRAME_PACING, redraws run on flush without display synchronization.
* Tearing may bypass pacing even when the capability is present. */
+/**
+ * Read frame timing estimates and their provenance.
+ * @param s Surface, or NULL.
+ * @return Current timing, or a zero-initialized result for NULL.
+ * Timestamps use monotonic nanoseconds. Inspect source before treating a timestamp
+ * as confirmation of presentation. Refresh and next-present values are estimates.
+ */
YASL_API struct yasl_timing yasl_surface_timing(const struct yasl_surface *s);
+/**
+ * Get a nondecreasing animation timestamp aligned to estimated refresh.
+ * @param s Surface, or NULL.
+ * @param now Current monotonic nanoseconds, usually from yasl_loop_now_ns().
+ * @return Animation timestamp, or zero for NULL. Before timing is known, returns now.
+ * Use this to advance animations; it is not presentation feedback.
+ */
YASL_API uint64_t yasl_surface_frame_time_ns(struct yasl_surface *s, uint64_t now);
/* Set the mouse and tablet cursor for this surface, including future entries.
* YASL_CURSOR_HIDDEN stays hidden until another cursor is selected. */
+/**
+ * Select the surface's system mouse and tablet cursor.
+ * @param s Surface.
+ * @param cursor Cursor shape; YASL_CURSOR_HIDDEN hides the cursor without disabling input.
+ * The choice persists across motion and re-entry until another shape is selected.
+ */
YASL_API void yasl_surface_set_cursor(struct yasl_surface *s, enum yasl_cursor cursor);
+/**
+ * Set a backend window background hint.
+ * @param s Surface.
+ * @param bg Packed 0xAARRGGBB color; backends may ignore alpha or the entire hint.
+ * Does not initialize framebuffer pixels or replace drawing.
+ */
YASL_API void yasl_surface_set_background_color(struct yasl_surface *s, uint32_t bg);
/* IME Support: preedit is borrowed until the next dispatch or destruction. */
+/**
+ * Read the current uncommitted UTF-8 composition text.
+ * @param s Surface, or NULL.
+ * @return Borrowed NUL-terminated text, or NULL if absent/unsupported.
+ * Copy to retain across the next dispatch or surface destruction. Composition
+ * changes can schedule a redraw. Insert committed YASL_INPUT_TEXT into documents,
+ * not preedit text.
+ */
YASL_API const char *yasl_surface_ime_preedit(const struct yasl_surface *s);
+/**
+ * Read the cursor/selection range in the preedit string.
+ * @param s Surface.
+ * @param begin Required output UTF-8 byte offset for the range start.
+ * @param end Required output UTF-8 byte offset for the range end.
+ * @return true if available; false without changing outputs otherwise.
+ * Currently available on Wayland; Cocoa preedit text has no range result.
+ */
YASL_API bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *s, int *begin, int *end);
+/**
+ * Place the composition candidate window near the application caret.
+ * @param s Surface.
+ * @param caret Caret rectangle relative to the surface, in device pixels.
+ * Update after caret motion or scale changes. Unsupported backends ignore it.
+ */
YASL_API void yasl_surface_set_ime_caret(struct yasl_surface *s, struct yasl_rect2i caret);
/* Client-Side Decorations (CSD) Actions */
+/**
+ * Ask the window system to begin an interactive move.
+ * @param s Toplevel surface, normally during handling of a pointer press.
+ * @return true if the request was issued; false if unsupported or not permitted.
+ * A recent input serial may be required. Success does not confirm a final position.
+ */
YASL_API bool yasl_surface_start_interactive_move(struct yasl_surface *s);
+/**
+ * Ask the window system to begin an interactive resize.
+ * @param s Toplevel surface, normally during handling of a pointer press.
+ * @param edge One nonzero yasl_resize_edge enumerator.
+ * @return true if issued; false for an invalid edge, missing support, or rejected request.
+ */
YASL_API bool yasl_surface_start_interactive_resize(struct yasl_surface *s, enum yasl_resize_edge edge);
+/**
+ * Ask the window system to display its window management menu.
+ * @param s Toplevel surface.
+ * @param pos Surface-relative position in device pixels.
+ * @return true if issued; false if unsupported or not currently permitted.
+ */
YASL_API bool yasl_surface_show_window_menu(struct yasl_surface *s, struct yasl_vector2i pos);
/* Drag Support */
+/**
+ * Attach a toplevel surface to the current surface drag.
+ * @param s Surface to carry; check YASL_CAP_DRAG_CARRY first.
+ * @param offset Device-pixel offset of the pointer within the carried surface.
+ * @return true if accepted; false if unsupported or no compatible drag is active.
+ */
YASL_API bool yasl_surface_drag_carry(struct yasl_surface *s, struct yasl_vector2i offset);
+/**
+ * Place a toplevel surface relative to the current pointer.
+ * @param s Surface; check YASL_CAP_PLACE_WINDOW first.
+ * @param at Device-pixel position within the surface to place under the pointer.
+ * @return true if issued; false if unsupported or unavailable.
+ */
YASL_API bool yasl_surface_place_under_pointer(struct yasl_surface *s, struct yasl_vector2i at);
#ifdef __cplusplus
diff --git a/meson.build b/meson.build
index 1c4ea10..07f4390 100644
--- a/meson.build
+++ b/meson.build
@@ -48,7 +48,15 @@ if system == 'linux'
deps += [wl_client, wl_cursor, xkbcommon]
endif
elif system == 'darwin'
- add_languages('objc')
+ add_languages('objc', native: false)
+ if not cc.compiles('''
+ #if !defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) || __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 140000
+ #error YASL requires macOS 14 or newer
+ #endif
+ int main(void) { return 0; }
+ ''', name: 'macOS deployment target is at least 14')
+ error('Cocoa requires macOS 14 or newer. Set MACOSX_DEPLOYMENT_TARGET=14.0 or a newer target before configuring.')
+ endif
deps += [
dependency('appleframeworks', modules: ['AppKit', 'Metal', 'QuartzCore']),
]
@@ -84,6 +92,7 @@ yasl_lib = both_libraries(
install_headers(
'include/yasl/yasl.h',
'include/yasl/timing.h',
+ 'include/yasl/keycodes.h',
'include/yasl/loop.h',
'include/yasl/native.h',
subdir: 'yasl',
@@ -115,6 +124,13 @@ yasl_dep = declare_dependency(
)
meson.override_dependency('yasl', yasl_dep)
+hello_example = executable(
+ 'yasl-hello',
+ 'examples/hello.c',
+ include_directories: inc,
+ link_with: yasl_lib.get_shared_lib(),
+)
+
executable(
'yasl-example',
'examples/simple_window.c',
@@ -130,3 +146,11 @@ tablet_example = executable(
)
subdir('tests')
+
+doc_dir = get_option('datadir') / 'doc' / 'yasl'
+install_data('README.md', 'CONTRIBUTING.md', 'Doxyfile', 'LICENSE', install_dir: doc_dir)
+# Preserve source-relative links and keep the reference buildable offline.
+install_subdir('docs', install_dir: doc_dir)
+install_subdir('examples', install_dir: doc_dir)
+install_subdir('include', install_dir: doc_dir)
+install_data('LICENSE', install_dir: get_option('datadir') / 'licenses' / 'yasl')
diff --git a/src/cocoa/window.m b/src/cocoa/window.m
index 645ae07..028ba64 100644
--- a/src/cocoa/window.m
+++ b/src/cocoa/window.m
@@ -7,6 +7,7 @@
#include "common/timing.h"
#include "common/loop.h"
#include "cocoa/keycodes.h"
+#include "common/keyboard.h"
#include <assert.h>
#include <math.h>
@@ -67,10 +68,6 @@ struct cocoa_surface {
char *preedit;
struct yasl_rect2i ime_caret;
- char *key_text;
- size_t key_text_capacity;
- size_t key_text_len;
- bool in_key_down;
struct yasl_vector2i size;
uint32_t scale_120;
@@ -269,23 +266,35 @@ static unsigned cocoa_mods(NSEventModifierFlags m)
return mods;
}
+/* Decode a single Unicode scalar, never a lone UTF-16 surrogate. Layout labels
+ * are optional: multi-scalar mappings and native function-key characters are 0. */
+static uint32_t cocoa_key_label(NSString *characters)
+{
+ if (!characters.length) return 0;
+ unichar first = [characters characterAtIndex:0];
+ if (characters.length == 1)
+ return yasl_key_label(first);
+ if (characters.length == 2 && first >= 0xd800 && first <= 0xdbff) {
+ unichar second = [characters characterAtIndex:1];
+ if (second >= 0xdc00 && second <= 0xdfff)
+ return yasl_key_label(0x10000 + ((first - 0xd800) << 10) + second - 0xdc00);
+ }
+ return 0;
+}
+
static struct yasl_key_event cocoa_key_event(NSEvent *ev, enum yasl_key_action action)
{
- unsigned short vk = ev.keyCode;
struct yasl_key_event k = {
- .key = vk,
- .keypad = (vk >= kVK_ANSI_Keypad0 && vk <= kVK_ANSI_Keypad9) ||
- vk == kVK_ANSI_KeypadDecimal || vk == kVK_ANSI_KeypadMultiply ||
- vk == kVK_ANSI_KeypadPlus || vk == kVK_ANSI_KeypadClear ||
- vk == kVK_ANSI_KeypadDivide || vk == kVK_ANSI_KeypadEnter ||
- vk == kVK_ANSI_KeypadMinus || vk == kVK_ANSI_KeypadEquals,
+ .key = yasl_key_from_cocoa(ev.keyCode),
.action = action,
.mods = cocoa_mods(ev.modifierFlags),
- .base = vk,
+ .native_code = ev.keyCode,
};
- NSString *chars = ev.characters;
- if (chars && chars.length > 0)
- k.text = [chars characterAtIndex:0];
+ /* characters properties are invalid on flagsChanged events. */
+ if (ev.type == NSEventTypeKeyDown || ev.type == NSEventTypeKeyUp) {
+ k.codepoint = cocoa_key_label([ev charactersByApplyingModifiers:0]);
+ k.shifted = cocoa_key_label([ev charactersByApplyingModifiers:NSEventModifierFlagShift]);
+ }
return k;
}
@@ -741,9 +750,7 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id<NSDraggingInfo> send
struct cocoa_surface *w = self.surface;
if (!w) return;
on_key_event(w, event, event.isARepeat ? YASL_KEY_REPEAT : YASL_KEY_PRESS);
- NSString *chars = event.characters;
- if (chars && chars.length > 0)
- on_text_input(w, chars.UTF8String, strlen(chars.UTF8String));
+ [self interpretKeyEvents:@[event]];
}
- (void)keyUp:(NSEvent *)event
@@ -818,28 +825,25 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id<NSDraggingInfo> send
{
struct cocoa_surface *w = self.surface;
if (!w) return;
- double dx = event.scrollingDeltaX, dy = event.scrollingDeltaY;
+ double dx = -event.scrollingDeltaX, dy = -event.scrollingDeltaY;
if (event.hasPreciseScrollingDeltas) {
dx /= SCROLL_POINTS_PER_LINE;
dy /= SCROLL_POINTS_PER_LINE;
}
+ if (!isfinite(dx) || !isfinite(dy) || (dx == 0 && dy == 0)) return;
w->mods = cocoa_mods(event.modifierFlags);
w->pointer = event_pos(w, event);
- if (dy != 0.0) {
- struct yasl_input_event input_ev = {
- .kind = YASL_INPUT_MOUSE,
- .surface = &w->base,
- .mouse = {
- .pos = w->pointer,
- .button = dy > 0.0 ? 4 : 5,
- .kind = YASL_PTR_SCROLL,
- .mods = w->mods,
- .scroll_delta = (float)dy,
- .scroll_lines = (int)dy,
- },
- };
- input_queue_push(&w->input, &input_ev);
- }
+ struct yasl_input_event input_ev = {
+ .kind = YASL_INPUT_MOUSE,
+ .surface = &w->base,
+ .mouse = {
+ .pos = w->pointer,
+ .kind = YASL_PTR_SCROLL,
+ .mods = w->mods,
+ .scroll = {dx, dy},
+ },
+ };
+ input_queue_push(&w->input, &input_ev);
}
static NSString *plain_string(id string)
@@ -1409,7 +1413,6 @@ static void surface_free(struct cocoa_surface *w)
[w->view release];
[w->controller release];
[w->window release];
- free(w->key_text);
free(w->preedit);
free(w->pixels);
free(w);
diff --git a/src/common/backend.c b/src/common/backend.c
index ccb9c27..23275d6 100644
--- a/src/common/backend.c
+++ b/src/common/backend.c
@@ -81,10 +81,18 @@ uint32_t yasl_display_caps(const struct yasl_display *d)
int yasl_display_dispatch(struct yasl_display *d, int timeout_ms)
{
- if (!d) return -1;
- if (d->ops->display_dispatch_timeout)
- return d->ops->display_dispatch_timeout(d, timeout_ms);
- return -1;
+ if (!d) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (!d->ops->display_dispatch_timeout) {
+ errno = ENOTSUP;
+ return -1;
+ }
+ errno = 0;
+ int result = d->ops->display_dispatch_timeout(d, timeout_ms);
+ if (result < 0 && !errno) errno = EIO;
+ return result;
}
void yasl_display_flush(struct yasl_display *d)
@@ -127,7 +135,12 @@ bool yasl_display_drag_start(struct yasl_display *d, struct yasl_surface *origin
return 0;
if (!origin || origin->display != d)
return false;
- return d->ops->display_drag_start(d, origin, image, l, ud);
+ if (image && (!image->pixels || image->size.x <= 0 || image->size.y <= 0 ||
+ image->size.x > 32767 || image->size.y > 32767 ||
+ (int64_t)image->size.x * image->size.y * 4 > INT32_MAX))
+ return false;
+ const struct yasl_drag_listener empty = {0};
+ return d->ops->display_drag_start(d, origin, image, l ? l : &empty, ud);
}
void yasl_display_drag_cancel(struct yasl_display *d)
@@ -151,7 +164,10 @@ struct yasl_surface *yasl_create_toplevel(struct yasl_display *d,
errno = ENOTSUP;
return NULL;
}
- return d->ops->surface_create_toplevel(d, spec, graphics_device);
+ errno = 0;
+ struct yasl_surface *s = d->ops->surface_create_toplevel(d, spec, graphics_device);
+ if (!s && !errno) errno = ENODEV;
+ return s;
}
struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d,
@@ -160,18 +176,50 @@ struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d,
return yasl_create_toplevel(d, spec, NULL);
}
+static bool popup_spec_valid(const struct yasl_popup_spec *spec)
+{
+ return spec && spec->size.x > 0 && spec->size.y > 0 &&
+ spec->size.x <= 32767 && spec->size.y <= 32767 &&
+ (int64_t)spec->size.x * spec->size.y * 4 <= INT32_MAX &&
+ spec->anchor.width >= 0 && spec->anchor.height >= 0 &&
+ spec->content_offset.x >= 0 && spec->content_offset.y >= 0 &&
+ spec->content_size.x >= 0 && spec->content_size.y >= 0 &&
+ (int64_t)spec->content_offset.x + spec->content_size.x <= spec->size.x &&
+ (int64_t)spec->content_offset.y + spec->content_size.y <= spec->size.y;
+}
+
struct yasl_surface *yasl_surface_create_popup(struct yasl_surface *parent, const struct yasl_popup_spec *spec)
{
- if (!parent || !parent->display->ops->surface_create_popup)
- return 0;
- return parent->display->ops->surface_create_popup(parent, spec);
+ if (!parent || !popup_spec_valid(spec)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ if (!(yasl_display_caps(parent->display) & YASL_CAP_POPUPS) ||
+ !parent->display->ops->surface_create_popup) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+ errno = 0;
+ struct yasl_surface *s = parent->display->ops->surface_create_popup(parent, spec);
+ if (!s && !errno) errno = ENODEV;
+ return s;
}
bool yasl_surface_popup_reposition(struct yasl_surface *popup, const struct yasl_popup_spec *spec)
{
- if (!popup || !popup->display->ops->surface_popup_reposition)
- return 0;
- return popup->display->ops->surface_popup_reposition(popup, spec);
+ if (!popup || !popup_spec_valid(spec)) {
+ errno = EINVAL;
+ return false;
+ }
+ if (!(yasl_display_caps(popup->display) & YASL_CAP_POPUPS) ||
+ !popup->display->ops->surface_popup_reposition) {
+ errno = ENOTSUP;
+ return false;
+ }
+ errno = 0;
+ bool result = popup->display->ops->surface_popup_reposition(popup, spec);
+ if (!result && !errno) errno = ENODEV;
+ return result;
}
void yasl_surface_destroy(struct yasl_surface *s)
diff --git a/src/common/keyboard.c b/src/common/keyboard.c
new file mode 100644
index 0000000..80c6702
--- /dev/null
+++ b/src/common/keyboard.c
@@ -0,0 +1,399 @@
+#include "keyboard.h"
+#include "cocoa/keycodes.h"
+#include <string.h>
+
+/* Numeric Linux evdev codes; no platform headers are needed by this table. */
+enum yasl_key yasl_key_from_evdev(uint32_t code)
+{
+ switch (code) {
+ case 1: return YASL_KEY_ESCAPE; /* KEY_ESC */
+ case 2: return YASL_KEY_1; /* KEY_1 */
+ case 3: return YASL_KEY_2; /* KEY_2 */
+ case 4: return YASL_KEY_3; /* KEY_3 */
+ case 5: return YASL_KEY_4; /* KEY_4 */
+ case 6: return YASL_KEY_5; /* KEY_5 */
+ case 7: return YASL_KEY_6; /* KEY_6 */
+ case 8: return YASL_KEY_7; /* KEY_7 */
+ case 9: return YASL_KEY_8; /* KEY_8 */
+ case 10: return YASL_KEY_9; /* KEY_9 */
+ case 11: return YASL_KEY_0; /* KEY_0 */
+ case 12: return YASL_KEY_MINUS; /* KEY_MINUS */
+ case 13: return YASL_KEY_EQUAL; /* KEY_EQUAL */
+ case 14: return YASL_KEY_BACKSPACE; /* KEY_BACKSPACE */
+ case 15: return YASL_KEY_TAB; /* KEY_TAB */
+ case 16: return YASL_KEY_Q; /* KEY_Q */
+ case 17: return YASL_KEY_W; /* KEY_W */
+ case 18: return YASL_KEY_E; /* KEY_E */
+ case 19: return YASL_KEY_R; /* KEY_R */
+ case 20: return YASL_KEY_T; /* KEY_T */
+ case 21: return YASL_KEY_Y; /* KEY_Y */
+ case 22: return YASL_KEY_U; /* KEY_U */
+ case 23: return YASL_KEY_I; /* KEY_I */
+ case 24: return YASL_KEY_O; /* KEY_O */
+ case 25: return YASL_KEY_P; /* KEY_P */
+ case 26: return YASL_KEY_LEFT_BRACKET; /* KEY_LEFTBRACE */
+ case 27: return YASL_KEY_RIGHT_BRACKET; /* KEY_RIGHTBRACE */
+ case 28: return YASL_KEY_ENTER; /* KEY_ENTER */
+ case 29: return YASL_KEY_LEFT_CONTROL; /* KEY_LEFTCTRL */
+ case 30: return YASL_KEY_A; /* KEY_A */
+ case 31: return YASL_KEY_S; /* KEY_S */
+ case 32: return YASL_KEY_D; /* KEY_D */
+ case 33: return YASL_KEY_F; /* KEY_F */
+ case 34: return YASL_KEY_G; /* KEY_G */
+ case 35: return YASL_KEY_H; /* KEY_H */
+ case 36: return YASL_KEY_J; /* KEY_J */
+ case 37: return YASL_KEY_K; /* KEY_K */
+ case 38: return YASL_KEY_L; /* KEY_L */
+ case 39: return YASL_KEY_SEMICOLON; /* KEY_SEMICOLON */
+ case 40: return YASL_KEY_APOSTROPHE; /* KEY_APOSTROPHE */
+ case 41: return YASL_KEY_GRAVE; /* KEY_GRAVE */
+ case 42: return YASL_KEY_LEFT_SHIFT; /* KEY_LEFTSHIFT */
+ case 43: return YASL_KEY_BACKSLASH; /* KEY_BACKSLASH */
+ case 44: return YASL_KEY_Z; /* KEY_Z */
+ case 45: return YASL_KEY_X; /* KEY_X */
+ case 46: return YASL_KEY_C; /* KEY_C */
+ case 47: return YASL_KEY_V; /* KEY_V */
+ case 48: return YASL_KEY_B; /* KEY_B */
+ case 49: return YASL_KEY_N; /* KEY_N */
+ case 50: return YASL_KEY_M; /* KEY_M */
+ case 51: return YASL_KEY_COMMA; /* KEY_COMMA */
+ case 52: return YASL_KEY_PERIOD; /* KEY_DOT */
+ case 53: return YASL_KEY_SLASH; /* KEY_SLASH */
+ case 54: return YASL_KEY_RIGHT_SHIFT; /* KEY_RIGHTSHIFT */
+ case 55: return YASL_KEY_KP_MULTIPLY; /* KEY_KPASTERISK */
+ case 56: return YASL_KEY_LEFT_ALT; /* KEY_LEFTALT */
+ case 57: return YASL_KEY_SPACE; /* KEY_SPACE */
+ case 58: return YASL_KEY_CAPS_LOCK; /* KEY_CAPSLOCK */
+ case 59: return YASL_KEY_F1; /* KEY_F1 */
+ case 60: return YASL_KEY_F2; /* KEY_F2 */
+ case 61: return YASL_KEY_F3; /* KEY_F3 */
+ case 62: return YASL_KEY_F4; /* KEY_F4 */
+ case 63: return YASL_KEY_F5; /* KEY_F5 */
+ case 64: return YASL_KEY_F6; /* KEY_F6 */
+ case 65: return YASL_KEY_F7; /* KEY_F7 */
+ case 66: return YASL_KEY_F8; /* KEY_F8 */
+ case 67: return YASL_KEY_F9; /* KEY_F9 */
+ case 68: return YASL_KEY_F10; /* KEY_F10 */
+ case 69: return YASL_KEY_NUM_LOCK; /* KEY_NUMLOCK */
+ case 70: return YASL_KEY_SCROLL_LOCK; /* KEY_SCROLLLOCK */
+ case 71: return YASL_KEY_KP_7; /* KEY_KP7 */
+ case 72: return YASL_KEY_KP_8; /* KEY_KP8 */
+ case 73: return YASL_KEY_KP_9; /* KEY_KP9 */
+ case 74: return YASL_KEY_KP_SUBTRACT; /* KEY_KPMINUS */
+ case 75: return YASL_KEY_KP_4; /* KEY_KP4 */
+ case 76: return YASL_KEY_KP_5; /* KEY_KP5 */
+ case 77: return YASL_KEY_KP_6; /* KEY_KP6 */
+ case 78: return YASL_KEY_KP_ADD; /* KEY_KPPLUS */
+ case 79: return YASL_KEY_KP_1; /* KEY_KP1 */
+ case 80: return YASL_KEY_KP_2; /* KEY_KP2 */
+ case 81: return YASL_KEY_KP_3; /* KEY_KP3 */
+ case 82: return YASL_KEY_KP_0; /* KEY_KP0 */
+ case 83: return YASL_KEY_KP_DECIMAL; /* KEY_KPDOT */
+ case 86: return YASL_KEY_ISO_BACKSLASH; /* KEY_102ND */
+ case 87: return YASL_KEY_F11; /* KEY_F11 */
+ case 88: return YASL_KEY_F12; /* KEY_F12 */
+ case 89: return YASL_KEY_RO; /* KEY_RO */
+ case 92: return YASL_KEY_CONVERT; /* KEY_HENKAN */
+ case 93: return YASL_KEY_KANA; /* KEY_KATAKANAHIRAGANA */
+ case 94: return YASL_KEY_NONCONVERT; /* KEY_MUHENKAN */
+ case 96: return YASL_KEY_KP_ENTER; /* KEY_KPENTER */
+ case 97: return YASL_KEY_RIGHT_CONTROL; /* KEY_RIGHTCTRL */
+ case 98: return YASL_KEY_KP_DIVIDE; /* KEY_KPSLASH */
+ case 99: return YASL_KEY_PRINT_SCREEN; /* KEY_SYSRQ */
+ case 100: return YASL_KEY_RIGHT_ALT; /* KEY_RIGHTALT */
+ case 102: return YASL_KEY_HOME; /* KEY_HOME */
+ case 103: return YASL_KEY_UP; /* KEY_UP */
+ case 104: return YASL_KEY_PAGE_UP; /* KEY_PAGEUP */
+ case 105: return YASL_KEY_LEFT; /* KEY_LEFT */
+ case 106: return YASL_KEY_RIGHT; /* KEY_RIGHT */
+ case 107: return YASL_KEY_END; /* KEY_END */
+ case 108: return YASL_KEY_DOWN; /* KEY_DOWN */
+ case 109: return YASL_KEY_PAGE_DOWN; /* KEY_PAGEDOWN */
+ case 110: return YASL_KEY_INSERT; /* KEY_INSERT */
+ case 111: return YASL_KEY_DELETE; /* KEY_DELETE */
+ case 113: return YASL_KEY_MUTE; /* KEY_MUTE */
+ case 114: return YASL_KEY_VOLUME_DOWN; /* KEY_VOLUMEDOWN */
+ case 115: return YASL_KEY_VOLUME_UP; /* KEY_VOLUMEUP */
+ case 117: return YASL_KEY_KP_EQUAL; /* KEY_KPEQUAL */
+ case 119: return YASL_KEY_PAUSE; /* KEY_PAUSE */
+ case 121: return YASL_KEY_KP_COMMA; /* KEY_KPCOMMA */
+ case 124: return YASL_KEY_YEN; /* KEY_YEN */
+ case 125: return YASL_KEY_LEFT_SUPER; /* KEY_LEFTMETA */
+ case 126: return YASL_KEY_RIGHT_SUPER; /* KEY_RIGHTMETA */
+ case 127: return YASL_KEY_MENU; /* KEY_COMPOSE */
+ case 183: return YASL_KEY_F13; /* KEY_F13 */
+ case 184: return YASL_KEY_F14; /* KEY_F14 */
+ case 185: return YASL_KEY_F15; /* KEY_F15 */
+ case 186: return YASL_KEY_F16; /* KEY_F16 */
+ case 187: return YASL_KEY_F17; /* KEY_F17 */
+ case 188: return YASL_KEY_F18; /* KEY_F18 */
+ case 189: return YASL_KEY_F19; /* KEY_F19 */
+ case 190: return YASL_KEY_F20; /* KEY_F20 */
+ case 191: return YASL_KEY_F21; /* KEY_F21 */
+ case 192: return YASL_KEY_F22; /* KEY_F22 */
+ case 193: return YASL_KEY_F23; /* KEY_F23 */
+ case 194: return YASL_KEY_F24; /* KEY_F24 */
+ default: return YASL_KEY_UNKNOWN;
+ }
+}
+
+enum yasl_key yasl_key_from_cocoa(uint32_t code)
+{
+ switch (code) {
+ case kVK_ANSI_A: return YASL_KEY_A;
+ case kVK_ANSI_B: return YASL_KEY_B;
+ case kVK_ANSI_C: return YASL_KEY_C;
+ case kVK_ANSI_D: return YASL_KEY_D;
+ case kVK_ANSI_E: return YASL_KEY_E;
+ case kVK_ANSI_F: return YASL_KEY_F;
+ case kVK_ANSI_G: return YASL_KEY_G;
+ case kVK_ANSI_H: return YASL_KEY_H;
+ case kVK_ANSI_I: return YASL_KEY_I;
+ case kVK_ANSI_J: return YASL_KEY_J;
+ case kVK_ANSI_K: return YASL_KEY_K;
+ case kVK_ANSI_L: return YASL_KEY_L;
+ case kVK_ANSI_M: return YASL_KEY_M;
+ case kVK_ANSI_N: return YASL_KEY_N;
+ case kVK_ANSI_O: return YASL_KEY_O;
+ case kVK_ANSI_P: return YASL_KEY_P;
+ case kVK_ANSI_Q: return YASL_KEY_Q;
+ case kVK_ANSI_R: return YASL_KEY_R;
+ case kVK_ANSI_S: return YASL_KEY_S;
+ case kVK_ANSI_T: return YASL_KEY_T;
+ case kVK_ANSI_U: return YASL_KEY_U;
+ case kVK_ANSI_V: return YASL_KEY_V;
+ case kVK_ANSI_W: return YASL_KEY_W;
+ case kVK_ANSI_X: return YASL_KEY_X;
+ case kVK_ANSI_Y: return YASL_KEY_Y;
+ case kVK_ANSI_Z: return YASL_KEY_Z;
+ case kVK_ANSI_0: return YASL_KEY_0;
+ case kVK_ANSI_1: return YASL_KEY_1;
+ case kVK_ANSI_2: return YASL_KEY_2;
+ case kVK_ANSI_3: return YASL_KEY_3;
+ case kVK_ANSI_4: return YASL_KEY_4;
+ case kVK_ANSI_5: return YASL_KEY_5;
+ case kVK_ANSI_6: return YASL_KEY_6;
+ case kVK_ANSI_7: return YASL_KEY_7;
+ case kVK_ANSI_8: return YASL_KEY_8;
+ case kVK_ANSI_9: return YASL_KEY_9;
+ case kVK_Return: return YASL_KEY_ENTER;
+ case kVK_Tab: return YASL_KEY_TAB;
+ case kVK_Space: return YASL_KEY_SPACE;
+ case kVK_Escape: return YASL_KEY_ESCAPE;
+ case kVK_Home: return YASL_KEY_HOME;
+ case kVK_End: return YASL_KEY_END;
+ case kVK_Delete: return YASL_KEY_BACKSPACE;
+ case kVK_ForwardDelete: return YASL_KEY_DELETE;
+ case kVK_PageUp: return YASL_KEY_PAGE_UP;
+ case kVK_PageDown: return YASL_KEY_PAGE_DOWN;
+ case kVK_Help: return YASL_KEY_INSERT;
+ case kVK_LeftArrow: return YASL_KEY_LEFT;
+ case kVK_RightArrow: return YASL_KEY_RIGHT;
+ case kVK_DownArrow: return YASL_KEY_DOWN;
+ case kVK_UpArrow: return YASL_KEY_UP;
+ case kVK_Command: return YASL_KEY_LEFT_SUPER;
+ case kVK_Shift: return YASL_KEY_LEFT_SHIFT;
+ case kVK_CapsLock: return YASL_KEY_CAPS_LOCK;
+ case kVK_Option: return YASL_KEY_LEFT_ALT;
+ case kVK_Control: return YASL_KEY_LEFT_CONTROL;
+ case kVK_RightCommand: return YASL_KEY_RIGHT_SUPER;
+ case kVK_RightShift: return YASL_KEY_RIGHT_SHIFT;
+ case kVK_RightOption: return YASL_KEY_RIGHT_ALT;
+ case kVK_RightControl: return YASL_KEY_RIGHT_CONTROL;
+ case kVK_ContextualMenu: return YASL_KEY_MENU;
+ case kVK_VolumeUp: return YASL_KEY_VOLUME_UP;
+ case kVK_VolumeDown: return YASL_KEY_VOLUME_DOWN;
+ case kVK_Mute: return YASL_KEY_MUTE;
+ case kVK_ISO_Section: return YASL_KEY_ISO_BACKSLASH;
+ case kVK_JIS_Yen: return YASL_KEY_YEN;
+ case kVK_JIS_Underscore: return YASL_KEY_RO;
+ case kVK_JIS_Eisu: return YASL_KEY_EISU;
+ case kVK_JIS_Kana: return YASL_KEY_KANA;
+ case kVK_JIS_KeypadComma: return YASL_KEY_KP_COMMA;
+ case kVK_ANSI_Equal: return YASL_KEY_EQUAL;
+ case kVK_ANSI_Minus: return YASL_KEY_MINUS;
+ case kVK_ANSI_LeftBracket: return YASL_KEY_LEFT_BRACKET;
+ case kVK_ANSI_RightBracket: return YASL_KEY_RIGHT_BRACKET;
+ case kVK_ANSI_Quote: return YASL_KEY_APOSTROPHE;
+ case kVK_ANSI_Semicolon: return YASL_KEY_SEMICOLON;
+ case kVK_ANSI_Backslash: return YASL_KEY_BACKSLASH;
+ case kVK_ANSI_Comma: return YASL_KEY_COMMA;
+ case kVK_ANSI_Slash: return YASL_KEY_SLASH;
+ case kVK_ANSI_Period: return YASL_KEY_PERIOD;
+ case kVK_ANSI_Grave: return YASL_KEY_GRAVE;
+ case kVK_F1: return YASL_KEY_F1;
+ case kVK_F2: return YASL_KEY_F2;
+ case kVK_F3: return YASL_KEY_F3;
+ case kVK_F4: return YASL_KEY_F4;
+ case kVK_F5: return YASL_KEY_F5;
+ case kVK_F6: return YASL_KEY_F6;
+ case kVK_F7: return YASL_KEY_F7;
+ case kVK_F8: return YASL_KEY_F8;
+ case kVK_F9: return YASL_KEY_F9;
+ case kVK_F10: return YASL_KEY_F10;
+ case kVK_F11: return YASL_KEY_F11;
+ case kVK_F12: return YASL_KEY_F12;
+ case kVK_F13: return YASL_KEY_F13;
+ case kVK_F14: return YASL_KEY_F14;
+ case kVK_F15: return YASL_KEY_F15;
+ case kVK_F16: return YASL_KEY_F16;
+ case kVK_F17: return YASL_KEY_F17;
+ case kVK_F18: return YASL_KEY_F18;
+ case kVK_F19: return YASL_KEY_F19;
+ case kVK_F20: return YASL_KEY_F20;
+ case kVK_ANSI_Keypad0: return YASL_KEY_KP_0;
+ case kVK_ANSI_Keypad1: return YASL_KEY_KP_1;
+ case kVK_ANSI_Keypad2: return YASL_KEY_KP_2;
+ case kVK_ANSI_Keypad3: return YASL_KEY_KP_3;
+ case kVK_ANSI_Keypad4: return YASL_KEY_KP_4;
+ case kVK_ANSI_Keypad5: return YASL_KEY_KP_5;
+ case kVK_ANSI_Keypad6: return YASL_KEY_KP_6;
+ case kVK_ANSI_Keypad7: return YASL_KEY_KP_7;
+ case kVK_ANSI_Keypad8: return YASL_KEY_KP_8;
+ case kVK_ANSI_Keypad9: return YASL_KEY_KP_9;
+ case kVK_ANSI_KeypadDecimal: return YASL_KEY_KP_DECIMAL;
+ case kVK_ANSI_KeypadMultiply: return YASL_KEY_KP_MULTIPLY;
+ case kVK_ANSI_KeypadPlus: return YASL_KEY_KP_ADD;
+ case kVK_ANSI_KeypadClear: return YASL_KEY_KP_CLEAR;
+ case kVK_ANSI_KeypadDivide: return YASL_KEY_KP_DIVIDE;
+ case kVK_ANSI_KeypadEnter: return YASL_KEY_KP_ENTER;
+ case kVK_ANSI_KeypadMinus: return YASL_KEY_KP_SUBTRACT;
+ case kVK_ANSI_KeypadEquals: return YASL_KEY_KP_EQUAL;
+ default: return YASL_KEY_UNKNOWN;
+ }
+}
+
+/* XKB standard physical key names, independent of server numeric keycodes. */
+enum yasl_key yasl_key_from_xkb_name(const char name[4])
+{
+ static const struct { char name[4]; enum yasl_key key; } keys[] = {
+ {{'T', 'L', 'D', 'E'}, YASL_KEY_GRAVE},
+ {{'A', 'E', '0', '1'}, YASL_KEY_1},
+ {{'A', 'E', '0', '2'}, YASL_KEY_2},
+ {{'A', 'E', '0', '3'}, YASL_KEY_3},
+ {{'A', 'E', '0', '4'}, YASL_KEY_4},
+ {{'A', 'E', '0', '5'}, YASL_KEY_5},
+ {{'A', 'E', '0', '6'}, YASL_KEY_6},
+ {{'A', 'E', '0', '7'}, YASL_KEY_7},
+ {{'A', 'E', '0', '8'}, YASL_KEY_8},
+ {{'A', 'E', '0', '9'}, YASL_KEY_9},
+ {{'A', 'E', '1', '0'}, YASL_KEY_0},
+ {{'A', 'E', '1', '1'}, YASL_KEY_MINUS},
+ {{'A', 'E', '1', '2'}, YASL_KEY_EQUAL},
+ {{'B', 'K', 'S', 'P'}, YASL_KEY_BACKSPACE},
+ {{'T', 'A', 'B', 0}, YASL_KEY_TAB},
+ {{'A', 'D', '0', '1'}, YASL_KEY_Q},
+ {{'A', 'D', '0', '2'}, YASL_KEY_W},
+ {{'A', 'D', '0', '3'}, YASL_KEY_E},
+ {{'A', 'D', '0', '4'}, YASL_KEY_R},
+ {{'A', 'D', '0', '5'}, YASL_KEY_T},
+ {{'A', 'D', '0', '6'}, YASL_KEY_Y},
+ {{'A', 'D', '0', '7'}, YASL_KEY_U},
+ {{'A', 'D', '0', '8'}, YASL_KEY_I},
+ {{'A', 'D', '0', '9'}, YASL_KEY_O},
+ {{'A', 'D', '1', '0'}, YASL_KEY_P},
+ {{'A', 'D', '1', '1'}, YASL_KEY_LEFT_BRACKET},
+ {{'A', 'D', '1', '2'}, YASL_KEY_RIGHT_BRACKET},
+ {{'B', 'K', 'S', 'L'}, YASL_KEY_BACKSLASH},
+ {{'R', 'T', 'R', 'N'}, YASL_KEY_ENTER},
+ {{'C', 'A', 'P', 'S'}, YASL_KEY_CAPS_LOCK},
+ {{'A', 'C', '0', '1'}, YASL_KEY_A},
+ {{'A', 'C', '0', '2'}, YASL_KEY_S},
+ {{'A', 'C', '0', '3'}, YASL_KEY_D},
+ {{'A', 'C', '0', '4'}, YASL_KEY_F},
+ {{'A', 'C', '0', '5'}, YASL_KEY_G},
+ {{'A', 'C', '0', '6'}, YASL_KEY_H},
+ {{'A', 'C', '0', '7'}, YASL_KEY_J},
+ {{'A', 'C', '0', '8'}, YASL_KEY_K},
+ {{'A', 'C', '0', '9'}, YASL_KEY_L},
+ {{'A', 'C', '1', '0'}, YASL_KEY_SEMICOLON},
+ {{'A', 'C', '1', '1'}, YASL_KEY_APOSTROPHE},
+ {{'L', 'F', 'S', 'H'}, YASL_KEY_LEFT_SHIFT},
+ {{'L', 'S', 'G', 'T'}, YASL_KEY_ISO_BACKSLASH},
+ {{'A', 'B', '0', '1'}, YASL_KEY_Z},
+ {{'A', 'B', '0', '2'}, YASL_KEY_X},
+ {{'A', 'B', '0', '3'}, YASL_KEY_C},
+ {{'A', 'B', '0', '4'}, YASL_KEY_V},
+ {{'A', 'B', '0', '5'}, YASL_KEY_B},
+ {{'A', 'B', '0', '6'}, YASL_KEY_N},
+ {{'A', 'B', '0', '7'}, YASL_KEY_M},
+ {{'A', 'B', '0', '8'}, YASL_KEY_COMMA},
+ {{'A', 'B', '0', '9'}, YASL_KEY_PERIOD},
+ {{'A', 'B', '1', '0'}, YASL_KEY_SLASH},
+ {{'R', 'T', 'S', 'H'}, YASL_KEY_RIGHT_SHIFT},
+ {{'L', 'C', 'T', 'L'}, YASL_KEY_LEFT_CONTROL},
+ {{'L', 'W', 'I', 'N'}, YASL_KEY_LEFT_SUPER},
+ {{'L', 'A', 'L', 'T'}, YASL_KEY_LEFT_ALT},
+ {{'S', 'P', 'C', 'E'}, YASL_KEY_SPACE},
+ {{'R', 'A', 'L', 'T'}, YASL_KEY_RIGHT_ALT},
+ {{'R', 'W', 'I', 'N'}, YASL_KEY_RIGHT_SUPER},
+ {{'C', 'O', 'M', 'P'}, YASL_KEY_MENU},
+ {{'R', 'C', 'T', 'L'}, YASL_KEY_RIGHT_CONTROL},
+ {{'E', 'S', 'C', 0}, YASL_KEY_ESCAPE},
+ {{'F', 'K', '0', '1'}, YASL_KEY_F1},
+ {{'F', 'K', '0', '2'}, YASL_KEY_F2},
+ {{'F', 'K', '0', '3'}, YASL_KEY_F3},
+ {{'F', 'K', '0', '4'}, YASL_KEY_F4},
+ {{'F', 'K', '0', '5'}, YASL_KEY_F5},
+ {{'F', 'K', '0', '6'}, YASL_KEY_F6},
+ {{'F', 'K', '0', '7'}, YASL_KEY_F7},
+ {{'F', 'K', '0', '8'}, YASL_KEY_F8},
+ {{'F', 'K', '0', '9'}, YASL_KEY_F9},
+ {{'F', 'K', '1', '0'}, YASL_KEY_F10},
+ {{'F', 'K', '1', '1'}, YASL_KEY_F11},
+ {{'F', 'K', '1', '2'}, YASL_KEY_F12},
+ {{'P', 'R', 'S', 'C'}, YASL_KEY_PRINT_SCREEN},
+ {{'S', 'C', 'L', 'K'}, YASL_KEY_SCROLL_LOCK},
+ {{'P', 'A', 'U', 'S'}, YASL_KEY_PAUSE},
+ {{'I', 'N', 'S', 0}, YASL_KEY_INSERT},
+ {{'H', 'O', 'M', 'E'}, YASL_KEY_HOME},
+ {{'P', 'G', 'U', 'P'}, YASL_KEY_PAGE_UP},
+ {{'D', 'E', 'L', 'E'}, YASL_KEY_DELETE},
+ {{'E', 'N', 'D', 0}, YASL_KEY_END},
+ {{'P', 'G', 'D', 'N'}, YASL_KEY_PAGE_DOWN},
+ {{'U', 'P', 0, 0}, YASL_KEY_UP},
+ {{'L', 'E', 'F', 'T'}, YASL_KEY_LEFT},
+ {{'D', 'O', 'W', 'N'}, YASL_KEY_DOWN},
+ {{'R', 'G', 'H', 'T'}, YASL_KEY_RIGHT},
+ {{'N', 'M', 'L', 'K'}, YASL_KEY_NUM_LOCK},
+ {{'K', 'P', 'D', 'V'}, YASL_KEY_KP_DIVIDE},
+ {{'K', 'P', 'M', 'U'}, YASL_KEY_KP_MULTIPLY},
+ {{'K', 'P', 'S', 'U'}, YASL_KEY_KP_SUBTRACT},
+ {{'K', 'P', '7', 0}, YASL_KEY_KP_7},
+ {{'K', 'P', '8', 0}, YASL_KEY_KP_8},
+ {{'K', 'P', '9', 0}, YASL_KEY_KP_9},
+ {{'K', 'P', 'A', 'D'}, YASL_KEY_KP_ADD},
+ {{'K', 'P', '4', 0}, YASL_KEY_KP_4},
+ {{'K', 'P', '5', 0}, YASL_KEY_KP_5},
+ {{'K', 'P', '6', 0}, YASL_KEY_KP_6},
+ {{'K', 'P', '1', 0}, YASL_KEY_KP_1},
+ {{'K', 'P', '2', 0}, YASL_KEY_KP_2},
+ {{'K', 'P', '3', 0}, YASL_KEY_KP_3},
+ {{'K', 'P', 'E', 'N'}, YASL_KEY_KP_ENTER},
+ {{'K', 'P', '0', 0}, YASL_KEY_KP_0},
+ {{'K', 'P', 'D', 'L'}, YASL_KEY_KP_DECIMAL},
+ {{'K', 'P', 'E', 'Q'}, YASL_KEY_KP_EQUAL},
+ {{'F', 'K', '1', '3'}, YASL_KEY_F13},
+ {{'F', 'K', '1', '4'}, YASL_KEY_F14},
+ {{'F', 'K', '1', '5'}, YASL_KEY_F15},
+ {{'F', 'K', '1', '6'}, YASL_KEY_F16},
+ {{'F', 'K', '1', '7'}, YASL_KEY_F17},
+ {{'F', 'K', '1', '8'}, YASL_KEY_F18},
+ {{'F', 'K', '1', '9'}, YASL_KEY_F19},
+ {{'F', 'K', '2', '0'}, YASL_KEY_F20},
+ {{'F', 'K', '2', '1'}, YASL_KEY_F21},
+ {{'F', 'K', '2', '2'}, YASL_KEY_F22},
+ {{'F', 'K', '2', '3'}, YASL_KEY_F23},
+ {{'F', 'K', '2', '4'}, YASL_KEY_F24},
+ {{'H', 'K', 'T', 'G'}, YASL_KEY_KANA},
+ {{'A', 'B', '1', '1'}, YASL_KEY_RO},
+ {{'H', 'E', 'N', 'K'}, YASL_KEY_CONVERT},
+ {{'M', 'U', 'H', 'E'}, YASL_KEY_NONCONVERT},
+ {{'A', 'E', '1', '3'}, YASL_KEY_YEN},
+ {{'M', 'U', 'T', 'E'}, YASL_KEY_MUTE},
+ {{'I', '1', '2', '9'}, YASL_KEY_KP_COMMA},
+ };
+ for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++)
+ if (memcmp(keys[i].name, name, 4) == 0) return keys[i].key;
+ return YASL_KEY_UNKNOWN;
+}
diff --git a/src/common/keyboard.h b/src/common/keyboard.h
new file mode 100644
index 0000000..50ddbb3
--- /dev/null
+++ b/src/common/keyboard.h
@@ -0,0 +1,18 @@
+#ifndef YASL_COMMON_KEYBOARD_H
+#define YASL_COMMON_KEYBOARD_H
+
+#include "yasl/yasl.h"
+
+enum yasl_key yasl_key_from_evdev(uint32_t code);
+enum yasl_key yasl_key_from_xkb_name(const char name[4]);
+enum yasl_key yasl_key_from_cocoa(uint32_t code);
+
+/* Layout labels omit controls, invalid Unicode, and Cocoa function-key PUA. */
+static inline uint32_t yasl_key_label(uint32_t cp)
+{
+ return cp >= 0x20 && cp != 0x7f && cp <= 0x10ffff &&
+ !(cp >= 0x80 && cp <= 0x9f) && !(cp >= 0xd800 && cp <= 0xdfff) &&
+ !(cp >= 0xf700 && cp <= 0xf7ff) ? cp : 0;
+}
+
+#endif
diff --git a/src/common/meson.build b/src/common/meson.build
index ddd0194..1247ff5 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -1 +1 @@
-sources += files('backend.c', 'input.c', 'loop.c', 'native.c', 'timing.c', 'utf8.c')
+sources += files('backend.c', 'input.c', 'keyboard.c', 'loop.c', 'native.c', 'timing.c', 'utf8.c')
diff --git a/src/common/utf8.c b/src/common/utf8.c
index 1d4cf13..78a5d8e 100644
--- a/src/common/utf8.c
+++ b/src/common/utf8.c
@@ -1,3 +1,4 @@
+#include "utf8.h"
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
@@ -12,11 +13,12 @@
bool yasl_utf8_validate(const char *str, size_t len)
{
+ if (!str) return len == 0;
const unsigned char *p = (const unsigned char *)str;
const unsigned char *end = p + len;
while (p < end) {
- while (p + 8 <= end) {
+ while ((size_t)(end - p) >= 8) {
uint64_t v;
memcpy(&v, p, 8);
if (v & 0x8080808080808080ull)
@@ -28,18 +30,18 @@ bool yasl_utf8_validate(const char *str, size_t len)
if (likely(*p < 0x80)) {
p++;
} else if ((*p & 0xe0) == 0xc0) {
- if (unlikely(p + 1 >= end || (p[1] & 0xc0) != 0x80 || (*p & 0x1e) == 0))
+ if (unlikely(end - p < 2 || (p[1] & 0xc0) != 0x80 || (*p & 0x1e) == 0))
return false;
p += 2;
} else if ((*p & 0xf0) == 0xe0) {
- if (unlikely(p + 2 >= end || (p[1] & 0xc0) != 0x80 || (p[2] & 0xc0) != 0x80))
+ if (unlikely(end - p < 3 || (p[1] & 0xc0) != 0x80 || (p[2] & 0xc0) != 0x80))
return false;
uint32_t cp = ((p[0] & 0x0f) << 12) | ((p[1] & 0x3f) << 6) | (p[2] & 0x3f);
if (unlikely(cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff)))
return false;
p += 3;
} else if ((*p & 0xf8) == 0xf0) {
- if (unlikely(p + 3 >= end || (p[1] & 0xc0) != 0x80 || (p[2] & 0xc0) != 0x80 || (p[3] & 0xc0) != 0x80))
+ if (unlikely(end - p < 4 || (p[1] & 0xc0) != 0x80 || (p[2] & 0xc0) != 0x80 || (p[3] & 0xc0) != 0x80))
return false;
uint32_t cp = ((p[0] & 0x07) << 18) | ((p[1] & 0x3f) << 12) | ((p[2] & 0x3f) << 6) | (p[3] & 0x3f);
if (unlikely(cp < 0x10000 || cp > 0x10ffff))
@@ -52,6 +54,46 @@ bool yasl_utf8_validate(const char *str, size_t len)
return true;
}
+char *yasl_utf8_from_latin1(const char *str, size_t len, size_t *out_len)
+{
+ if (len > (SIZE_MAX - 1) / 2) return NULL;
+ char *out = malloc(len * 2 + 1);
+ if (!out) return NULL;
+ size_t n = 0;
+ for (size_t i = 0; i < len; i++) {
+ unsigned char ch = (unsigned char)str[i];
+ if (ch < 0x80) out[n++] = (char)ch;
+ else {
+ out[n++] = (char)(0xc0 | (ch >> 6));
+ out[n++] = (char)(0x80 | (ch & 0x3f));
+ }
+ }
+ out[n] = '\0';
+ *out_len = n;
+ return out;
+}
+
+char *yasl_utf8_to_latin1(const char *str, size_t *out_len)
+{
+ char *out = malloc(strlen(str) + 1);
+ if (!out) return NULL;
+ const unsigned char *p = (const unsigned char *)str;
+ size_t n = 0;
+ while (*p) {
+ if (*p < 0x80) out[n++] = (char)*p++;
+ else if ((*p == 0xc2 || *p == 0xc3) && (p[1] & 0xc0) == 0x80) {
+ out[n++] = (char)(((*p & 3) << 6) | (p[1] & 0x3f));
+ p += 2;
+ } else {
+ free(out);
+ return NULL;
+ }
+ }
+ out[n] = '\0';
+ *out_len = n;
+ return out;
+}
+
char *yasl_utf8_sanitize_dup(const char *str)
{
if (unlikely(!str)) return NULL;
diff --git a/src/common/utf8.h b/src/common/utf8.h
new file mode 100644
index 0000000..f31c99d
--- /dev/null
+++ b/src/common/utf8.h
@@ -0,0 +1,14 @@
+#ifndef YASL_COMMON_UTF8_H
+#define YASL_COMMON_UTF8_H
+
+#include <stdbool.h>
+#include <stddef.h>
+
+bool yasl_utf8_validate(const char *str, size_t len);
+char *yasl_utf8_sanitize_dup(const char *str);
+/* Owned conversions for X11 STRING interoperability. to_latin1 rejects text
+ * outside Latin-1; byte lengths exclude the allocated NUL terminator. */
+char *yasl_utf8_from_latin1(const char *str, size_t len, size_t *out_len);
+char *yasl_utf8_to_latin1(const char *str, size_t *out_len);
+
+#endif
diff --git a/src/wayland/clipboard.c b/src/wayland/clipboard.c
index e3eec0f..a6922c4 100644
--- a/src/wayland/clipboard.c
+++ b/src/wayland/clipboard.c
@@ -131,9 +131,11 @@ static int transfer_io(int fd, short events, void *ud)
if (n > 0) {
t->len += (size_t)n;
} else if (n == 0) {
- t->owner->receive(t->text ? t->text : "", t->len, t->uri_list,
- t->drop_surface, t->owner->ud);
- transfer_end(t, true);
+ bool valid = yasl_utf8_validate(t->text, t->len);
+ if (valid)
+ t->owner->receive(t->text ? t->text : "", t->len, t->uri_list,
+ t->drop_surface, t->owner->ud);
+ transfer_end(t, valid);
} else if (errno != EAGAIN && errno != EINTR) transfer_destroy(t);
}
return 0;
diff --git a/src/wayland/ime.c b/src/wayland/ime.c
index 74db53f..bd9375b 100644
--- a/src/wayland/ime.c
+++ b/src/wayland/ime.c
@@ -98,6 +98,7 @@ static void text_input_preedit_string(void *data,
(void)text_input;
struct wayland_surface *w = ((struct seat *)data)->ime_focus;
if (!w) return;
+ keyboard_text_reset((struct seat *)data);
free(w->ime_preedit);
w->ime_preedit = text ? strndup(text, IME_TEXT_MAX) : NULL;
int len = w->ime_preedit ? (int)strlen(w->ime_preedit) : 0;
@@ -113,6 +114,7 @@ static void text_input_commit_string(void *data,
(void)text_input;
struct wayland_surface *w = ((struct seat *)data)->ime_focus;
if (!w) return;
+ keyboard_text_reset((struct seat *)data);
free(w->ime_commit);
w->ime_commit = (text && text[0]) ? strndup(text, IME_TEXT_MAX) : NULL;
}
diff --git a/src/wayland/internal.h b/src/wayland/internal.h
index ad6531b..b687a61 100644
--- a/src/wayland/internal.h
+++ b/src/wayland/internal.h
@@ -5,6 +5,7 @@
#include "api.h"
#include "common/timing.h"
+#include "common/utf8.h"
#include "clipboard.h"
#include <wayland-client.h>
@@ -38,6 +39,7 @@
#include <unistd.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h>
+#include <xkbcommon/xkbcommon-compose.h>
#define NBUF 3
@@ -140,6 +142,8 @@ struct seat {
struct clipboard *clipboard;
struct xkb_keymap *xkb_keymap;
struct xkb_state *xkb_state;
+ struct xkb_compose_state *compose;
+ bool compose_attempted;
uint64_t repeat_started_ns;
struct wayland_surface *pointer_focus;
struct wayland_surface *keyboard_focus;
@@ -152,8 +156,10 @@ struct seat {
struct touch_point *touch_primary;
uint64_t pointer_buttons;
uint64_t tool_buttons;
- double axis_v, axis_h, frame_v;
- double touch_scroll_y, touch_scroll_rest;
+ double axis[2], axis_value120[2];
+ int64_t axis_discrete[2];
+ bool axis_has_value120[2], axis_has_discrete[2];
+ double touch_scroll_y;
/* Embedded structs */
xkb_mod_mask_t input_mod_masks[8];
@@ -174,18 +180,12 @@ struct seat {
uint32_t press_serial;
uint32_t pointer_serial;
int cursor_shape;
- int frame_v120;
- int axis_discrete_v, axis_discrete_h;
- int axis_value120_v, axis_value120_h;
struct yasl_pointer_pen pen;
int tool_wheel;
/* 1-byte booleans grouped tightly */
bool input_mods_ready;
bool repeat_held;
- bool axis_has_v120_v;
- bool axis_has_v120_h;
- bool axis_smooth;
bool motion_pending;
bool ptr_left_motion;
bool tool_motion;
@@ -325,8 +325,6 @@ struct wayland_surface {
bool want_draw;
};
-char *yasl_utf8_sanitize_dup(const char *str);
-bool yasl_utf8_validate(const char *str, size_t len);
int device_px(int logical, uint32_t scale_120);
bool buffer_size(int width, int height, size_t *bytes);
@@ -415,6 +413,7 @@ void seat_pointer_reset(struct seat *s);
extern const struct zwp_text_input_v3_listener text_input_listener;
void ime_reset(struct wayland_surface *w);
+void keyboard_text_reset(struct seat *s);
void ime_send_caret(struct wayland_surface *w);
int surface_init_shell(struct wayland_surface *w, const char *title);
diff --git a/src/wayland/keyboard.c b/src/wayland/keyboard.c
index 11c7f3f..f1170c6 100644
--- a/src/wayland/keyboard.c
+++ b/src/wayland/keyboard.c
@@ -1,8 +1,14 @@
#include "internal.h"
+#include "common/keyboard.h"
#define XKB_EVDEV_OFFSET 8
#define KEYMAP_MAX_BYTES (8u * 1024 * 1024)
+void keyboard_text_reset(struct seat *s)
+{
+ if (s->compose) xkb_compose_state_reset(s->compose);
+}
+
void keyboard_repeat_stop(struct seat *s)
{
if (!s->repeat_held) return;
@@ -28,11 +34,10 @@ void keyboard_repeat_sync(struct seat *s)
timerfd_settime(s->repeat_fd, 0, &timer, NULL);
}
-static const char *const input_mod_names[8] = {
+static const char *const input_mod_names[5] = {
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_ALT,
XKB_MOD_NAME_CTRL, XKB_MOD_NAME_LOGO,
- "Hyper", "Meta",
- XKB_MOD_NAME_CAPS, XKB_MOD_NAME_NUM
+ XKB_MOD_NAME_CAPS
};
static bool mod_active(const struct seat *s, const char *name)
@@ -48,7 +53,7 @@ static void resolve_input_mod_masks(struct seat *s)
if (!probe) return;
for (unsigned real = 0; real < 8; real++) {
xkb_state_update_mask(probe, 1u << real, 0, 0, 0, 0, 0);
- for (unsigned i = 0; i < 8; i++) {
+ for (unsigned i = 0; i < 5; i++) {
if (xkb_state_mod_name_is_active(probe, input_mod_names[i],
XKB_STATE_MODS_EFFECTIVE) > 0)
s->input_mod_masks[i] |= 1u << real;
@@ -64,6 +69,7 @@ static void keyboard_keymap(void *data, struct wl_keyboard *keyboard,
(void)keyboard;
struct seat *s = data;
keyboard_repeat_stop(s);
+ keyboard_text_reset(s);
s->input_mods_ready = false;
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
close(fd);
@@ -116,6 +122,7 @@ static void keyboard_reset_keys(struct seat *s, struct wayland_surface *w)
{
(void)w;
keyboard_repeat_stop(s);
+ keyboard_text_reset(s);
}
bool conn_keyboard_on(const struct wayland_display *c, const struct wayland_surface *w,
@@ -194,7 +201,7 @@ static unsigned input_modifiers(struct seat *s)
unsigned mods = 0;
xkb_mod_mask_t reported = 0;
- for (unsigned i = 0; i < 8; i++) {
+ for (unsigned i = 0; i < 5; i++) {
xkb_mod_mask_t mask = s->input_mod_masks[i];
if (mask && (mask & ~reported) && mod_active(s, input_mod_names[i])) {
mods |= 1u << i;
@@ -215,23 +222,75 @@ static struct yasl_key_event xkb_key_event(struct seat *s, uint32_t key,
enum yasl_key_action action)
{
uint32_t code = key + XKB_EVDEV_OFFSET;
- xkb_keysym_t sym = xkb_state_key_get_one_sym(s->xkb_state, code);
xkb_layout_index_t layout = xkb_state_key_get_layout(s->xkb_state, code);
struct yasl_key_event k = {
- .key = sym,
- .keypad = key >= KEY_KP0 && key <= KEY_KPCOMMA,
+ .key = yasl_key_from_evdev(key),
.action = action,
.mods = input_modifiers(s),
- .codepoint = key_codepoint_at_level(s, key, layout, 0),
- .shifted = key_codepoint_at_level(s, key, layout, 1),
- .base = key,
- .text = xkb_state_key_get_utf32(s->xkb_state, code),
+ .codepoint = yasl_key_label(key_codepoint_at_level(s, key, layout, 0)),
+ .shifted = yasl_key_label(key_codepoint_at_level(s, key, layout, 1)),
+ .native_code = key,
};
- if (!k.codepoint) k.codepoint = xkb_keysym_to_utf32(sym);
return k;
}
+static void keyboard_text(struct seat *s, struct wayland_surface *w, uint32_t key)
+{
+ uint32_t code = key + XKB_EVDEV_OFFSET;
+ unsigned mods = input_modifiers(s);
+ if ((mods & (YASL_MOD_CTRL | YASL_MOD_SUPER)) ||
+ ((mods & YASL_MOD_ALT) &&
+ !xkb_state_mod_index_is_consumed(s->xkb_state, code, s->mod_alt))) {
+ keyboard_text_reset(s);
+ return;
+ }
+ if (!s->compose_attempted) {
+ s->compose_attempted = true;
+ const char *locale = getenv("LC_ALL");
+ if (!locale || !*locale) locale = getenv("LC_CTYPE");
+ if (!locale || !*locale) locale = getenv("LANG");
+ if (!locale || !*locale) locale = "C.UTF-8";
+ struct xkb_compose_table *table = xkb_compose_table_new_from_locale(
+ s->conn->xkb_ctx, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
+ if (table) {
+ s->compose = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
+ xkb_compose_table_unref(table);
+ }
+ }
+ bool composed = false;
+ if (s->compose) {
+ xkb_compose_state_feed(s->compose, xkb_state_key_get_one_sym(s->xkb_state, code));
+ switch (xkb_compose_state_get_status(s->compose)) {
+ case XKB_COMPOSE_COMPOSING: return;
+ case XKB_COMPOSE_CANCELLED: keyboard_text_reset(s); return;
+ case XKB_COMPOSE_COMPOSED: composed = true; break;
+ default: break;
+ }
+ }
+ int len = composed ? xkb_compose_state_get_utf8(s->compose, NULL, 0) :
+ xkb_state_key_get_utf8(s->xkb_state, code, NULL, 0);
+ if (len > 0) {
+ char local[128];
+ char *text = (size_t)len < sizeof(local) ? local : malloc((size_t)len + 1);
+ if (text) {
+ if (composed) xkb_compose_state_get_utf8(s->compose, text, (size_t)len + 1);
+ else xkb_state_key_get_utf8(s->xkb_state, code, text, (size_t)len + 1);
+ /* Editing commands belong to key events, not text insertion. */
+ if ((unsigned char)text[0] >= 0x20 && text[0] != 0x7f) {
+ struct yasl_input_event ev = {.kind = YASL_INPUT_TEXT,
+ .surface = &w->base, .text = {text, (size_t)len}};
+ input_queue_push(&w->input, &ev);
+ }
+ if (text != local) free(text);
+ } else {
+ struct yasl_input_event ev = {.kind = YASL_INPUT_RESET, .surface = &w->base};
+ input_queue_push(&w->input, &ev);
+ }
+ }
+ if (composed) keyboard_text_reset(s);
+}
+
static bool keyboard_dispatch_key(struct seat *s, struct wayland_surface *w, uint32_t key,
enum yasl_key_action action)
{
@@ -243,7 +302,11 @@ static bool keyboard_dispatch_key(struct seat *s, struct wayland_surface *w, uin
.surface = &w->base,
.key = k,
};
- input_queue_push(&w->input, &ev);
+ if (!input_queue_push(&w->input, &ev)) {
+ keyboard_text_reset(s);
+ return false;
+ }
+ if (action != YASL_KEY_RELEASE) keyboard_text(s, w, key);
return action != YASL_KEY_RELEASE;
}
@@ -351,7 +414,8 @@ int seat_modifiers(const struct seat *s)
return (mod_index_active(s, s->mod_shift) * YASL_MOD_SHIFT) |
(mod_index_active(s, s->mod_alt) * YASL_MOD_ALT) |
(mod_index_active(s, s->mod_ctrl) * YASL_MOD_CTRL) |
- (mod_index_active(s, s->mod_super) * YASL_MOD_SUPER);
+ (mod_index_active(s, s->mod_super) * YASL_MOD_SUPER) |
+ (mod_active(s, XKB_MOD_NAME_CAPS) * YASL_MOD_CAPS);
}
int wayland_display_modifiers(const struct wayland_display *c)
diff --git a/src/wayland/pointer.c b/src/wayland/pointer.c
index 065669f..90b7ecc 100644
--- a/src/wayland/pointer.c
+++ b/src/wayland/pointer.c
@@ -109,12 +109,12 @@ static void report_mouse(struct seat *s, struct wayland_surface *w, struct yasl_
});
}
-static void report_wheel(struct seat *s, struct wayland_surface *w, struct yasl_vector2i pos,
- int button, int lines, float delta)
+static void report_wheel(struct seat *s, struct wayland_surface *w,
+ struct yasl_vector2i pos, double x, double y)
{
+ if ((x == 0 && y == 0) || !isfinite(x) || !isfinite(y)) return;
push_mouse(s, w, (struct yasl_mouse_event){
- .pos = pos, .button = button, .kind = YASL_PTR_SCROLL,
- .scroll_lines = lines, .scroll_delta = delta,
+ .pos = pos, .kind = YASL_PTR_SCROLL, .scroll = {x, y},
});
}
@@ -295,12 +295,11 @@ void seat_pointer_reset(struct seat *s)
s->pointer_buttons = 0;
s->pointer_serial = 0;
s->motion_pending = false;
- s->axis_v = s->axis_h = s->frame_v = 0;
- s->frame_v120 = 0;
- s->axis_discrete_v = s->axis_discrete_h = 0;
- s->axis_value120_v = s->axis_value120_h = 0;
- s->axis_has_v120_v = s->axis_has_v120_h = false;
- s->axis_smooth = false;
+ memset(s->axis, 0, sizeof(s->axis));
+ memset(s->axis_value120, 0, sizeof(s->axis_value120));
+ memset(s->axis_discrete, 0, sizeof(s->axis_discrete));
+ memset(s->axis_has_value120, 0, sizeof(s->axis_has_value120));
+ memset(s->axis_has_discrete, 0, sizeof(s->axis_has_discrete));
s->cursor_shape = -1;
}
@@ -405,8 +404,7 @@ static void pointer_axis(void *data, struct wl_pointer *pointer,
if (unlikely(!s->pointer_focus)) return;
double v = wl_fixed_to_double(value);
if (likely(axis < 2)) {
- (&s->axis_v)[axis] += v;
- s->frame_v += v * (!axis);
+ s->axis[axis] += v;
}
pointer_event_done(s);
}
@@ -414,81 +412,49 @@ static void pointer_axis(void *data, struct wl_pointer *pointer,
static void pointer_axis_source(void *data, struct wl_pointer *pointer, uint32_t axis_source)
{
(void)pointer;
- struct seat *s = data;
- s->axis_smooth = (axis_source == WL_POINTER_AXIS_SOURCE_FINGER ||
- axis_source == WL_POINTER_AXIS_SOURCE_CONTINUOUS);
+ (void)data; (void)axis_source;
}
static void pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis)
{
- (void)pointer; (void)time;
- struct seat *s = data;
- if (likely(axis < 2)) {
- (&s->axis_value120_v)[axis] = 0;
- (&s->axis_has_v120_v)[axis] = false;
- }
+ /* A stop ends a gesture, not the offsets already received in this frame. */
+ (void)data; (void)pointer; (void)time; (void)axis;
}
static void pointer_axis_discrete(void *data, struct wl_pointer *pointer, uint32_t axis, int32_t discrete)
{
(void)pointer;
struct seat *s = data;
- if (likely(axis < 2))
- (&s->axis_discrete_v)[axis] += discrete;
+ if (axis < 2) {
+ s->axis_discrete[axis] += discrete;
+ s->axis_has_discrete[axis] = true;
+ }
}
static void pointer_axis_value120(void *data, struct wl_pointer *pointer, uint32_t axis, int32_t value120)
{
(void)pointer;
struct seat *s = data;
- if (likely(axis < 2)) {
- (&s->axis_value120_v)[axis] += value120;
- (&s->axis_has_v120_v)[axis] = true;
- s->frame_v120 += value120 * (!axis);
+ if (axis < 2) {
+ s->axis_value120[axis] += value120;
+ s->axis_has_value120[axis] = true;
}
}
-static void pointer_frame_vertical(struct seat *s, struct wayland_surface *w)
-{
- if (s->axis_has_v120_v) {
- int discrete = s->axis_value120_v / 120;
- s->axis_value120_v -= discrete * 120;
- float delta = -(float)s->frame_v120 / 120.0f;
- int btn = discrete ? (discrete < 0 ? 4 : 5) : (delta > 0 ? 4 : 5);
- if (discrete || delta != 0.0f)
- report_wheel(s, w, s->pointer_pos, btn, -discrete, delta);
- s->axis_v = 0;
- } else if (s->axis_discrete_v) {
- report_wheel(s, w, s->pointer_pos, s->axis_discrete_v > 0 ? 5 : 4, -s->axis_discrete_v, 0.0f);
- s->axis_v = 0;
- } else if (s->frame_v != 0.0) {
- int lines = -(int)(s->axis_v / SCROLL_PX_PER_LINE);
- s->axis_v += (double)lines * SCROLL_PX_PER_LINE;
- float delta = (float)(-s->frame_v / SCROLL_PX_PER_LINE);
- report_wheel(s, w, s->pointer_pos, delta > 0 ? 4 : 5, lines, delta);
- }
- s->frame_v = 0;
- s->frame_v120 = 0;
-}
-
-static void pointer_frame_horizontal(struct seat *s, struct wayland_surface *w)
-{
- if (s->axis_has_v120_h) {
- int discrete = s->axis_value120_h / 120;
- s->axis_value120_h -= discrete * 120;
- if (discrete)
- report_wheel(s, w, s->pointer_pos, discrete > 0 ? 7 : 6, discrete, 0.0f);
- s->axis_h = 0;
- } else if (s->axis_discrete_h) {
- report_wheel(s, w, s->pointer_pos, s->axis_discrete_h > 0 ? 7 : 6, s->axis_discrete_h, 0.0f);
- s->axis_h = 0;
- } else if (s->axis_h) {
- int steps = (int)(s->axis_h / SCROLL_PX_PER_LINE);
- if (steps != 0) {
- s->axis_h -= (double)steps * SCROLL_PX_PER_LINE;
- report_wheel(s, w, s->pointer_pos, steps > 0 ? 7 : 6, steps, 0.0f);
- }
+static void pointer_frame_scroll(struct seat *s, struct wayland_surface *w)
+{
+ double lines[2];
+ for (int axis = 0; axis < 2; axis++) {
+ /* A wheel may provide all three representations. Pick one, never sum
+ * them. value120 preserves high-resolution detents on both axes. */
+ lines[axis] = s->axis_has_value120[axis] ? s->axis_value120[axis] / 120.0 :
+ s->axis_has_discrete[axis] ? (double)s->axis_discrete[axis] :
+ s->axis[axis] / SCROLL_PX_PER_LINE;
+ s->axis[axis] = s->axis_value120[axis] = 0;
+ s->axis_discrete[axis] = 0;
+ s->axis_has_value120[axis] = s->axis_has_discrete[axis] = false;
}
+ report_wheel(s, w, s->pointer_pos, lines[1], lines[0]);
}
static void pointer_frame(void *data, struct wl_pointer *pointer)
@@ -521,18 +487,7 @@ static void pointer_frame(void *data, struct wl_pointer *pointer)
flush_pending_motion(s, w);
press_queue_flush(s, &s->ptr_presses, NULL, &s->pointer_buttons, (struct yasl_mouse_event){0});
- if (s->axis_smooth) {
- if (!s->axis_has_v120_v) s->axis_value120_v = 0;
- if (!s->axis_has_v120_h) s->axis_value120_h = 0;
- s->axis_smooth = false;
- }
- pointer_frame_vertical(s, w);
- pointer_frame_horizontal(s, w);
-
- s->axis_discrete_v = 0;
- s->axis_discrete_h = 0;
- s->axis_has_v120_v = false;
- s->axis_has_v120_h = false;
+ pointer_frame_scroll(s, w);
}
static void pointer_axis_relative_direction(void *data, struct wl_pointer *pointer, uint32_t axis, uint32_t direction)
@@ -651,20 +606,15 @@ static void touch_frame(void *data, struct wl_touch *touch)
s->touch_pressed = false;
s->touch_scrolling = true;
s->touch_scroll_y = centroid_n ? centroid_sum / centroid_n : 0;
- s->touch_scroll_rest = 0;
} else if (s->touch_scrolling && primary && active_n >= 2) {
double y = centroid_n ? centroid_sum / centroid_n : 0;
double dy = y - s->touch_scroll_y;
s->touch_scroll_y = y;
if (dy != 0.0) {
double lines = dy / SCROLL_PX_PER_LINE;
- s->touch_scroll_rest += lines;
- int whole = (int)s->touch_scroll_rest;
- s->touch_scroll_rest -= whole;
push_mouse(s, primary->surface, (struct yasl_mouse_event){
- .pos = touch_pos(primary), .button = lines > 0 ? 4 : 5,
- .kind = YASL_PTR_SCROLL, .scroll_lines = whole,
- .scroll_delta = (float)lines, .device = YASL_PTR_DEV_TOUCH,
+ .pos = touch_pos(primary), .kind = YASL_PTR_SCROLL,
+ .scroll = {0, -lines}, .device = YASL_PTR_DEV_TOUCH,
});
}
} else if (primary && primary->moved && s->touch_pressed) {
@@ -870,7 +820,7 @@ static void tool_frame(void *d, struct zwp_tablet_tool_v2 *t, uint32_t time)
struct yasl_mouse_event ev = like;
ev.pos = s->tool_pos;
ev.kind = YASL_PTR_SCROLL;
- ev.scroll_lines = -s->tool_wheel;
+ ev.scroll.y = s->tool_wheel;
push_mouse(s, w, ev);
s->tool_wheel = 0;
}
@@ -1105,6 +1055,7 @@ static void seat_drop_pointer(struct seat *s)
static void seat_drop_keyboard(struct seat *s)
{
keyboard_repeat_stop(s);
+ keyboard_text_reset(s);
if (s->keyboard) {
if (wl_keyboard_get_version(s->keyboard) >= WL_KEYBOARD_RELEASE_SINCE_VERSION)
wl_keyboard_release(s->keyboard);
@@ -1281,6 +1232,7 @@ void seat_destroy(struct seat *s)
if (s->cursor_viewport) wp_viewport_destroy(s->cursor_viewport);
if (s->cursor_surface) wl_surface_destroy(s->cursor_surface);
if (s->xkb_state) xkb_state_unref(s->xkb_state);
+ if (s->compose) xkb_compose_state_unref(s->compose);
if (s->xkb_keymap) xkb_keymap_unref(s->xkb_keymap);
if (s->proxy) {
if (wl_seat_get_version(s->proxy) >= WL_SEAT_RELEASE_SINCE_VERSION)
diff --git a/src/x11/window.c b/src/x11/window.c
index 1a0db7d..f7d036f 100644
--- a/src/x11/window.c
+++ b/src/x11/window.c
@@ -2,6 +2,8 @@
#define _DEFAULT_SOURCE
#include "common/backend.h"
#include "common/input.h"
+#include "common/keyboard.h"
+#include "common/utf8.h"
#include "common/timing.h"
#include <X11/Xatom.h>
@@ -103,6 +105,8 @@ struct x11_display {
bool xi2;
bool has_shm;
bool has_xkb;
+ int xkb_event;
+ enum yasl_key keys[256];
Cursor cursors[16];
Atom abs_pressure, abs_tilt_x, abs_tilt_y, tool_type, eraser;
struct x11_pointer *pointers;
@@ -379,7 +383,8 @@ static void xi_pointer_event(struct x11_surface *s, const XIDeviceEvent *ev)
else if (ev->detail >= 4 && ev->detail <= 7) {
if (ev->evtype == XI_ButtonRelease) return;
mouse.kind = YASL_PTR_SCROLL;
- mouse.scroll_lines = (ev->detail == 4 || ev->detail == 6) ? -1 : 1;
+ if (ev->detail <= 5) mouse.scroll.y = ev->detail == 4 ? -1 : 1;
+ else mouse.scroll.x = ev->detail == 6 ? -1 : 1;
} else {
mouse.kind = ev->evtype == XI_ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
mouse.button = pointer_button(ev->detail);
@@ -915,20 +920,24 @@ static void selection_request(struct x11_display *d, XSelectionRequestEvent *req
Atom property = request->property ? request->property : request->target;
int slot = request->selection == XA_PRIMARY ? YASL_PRIMARY : YASL_CLIPBOARD;
const char *text = d->selection[slot];
+ size_t latin1_len = 0;
+ char *latin1 = text ? yasl_utf8_to_latin1(text, &latin1_len) : NULL;
if (text && request->target == d->targets) {
Atom targets[] = {d->targets, d->utf8, XA_STRING};
XChangeProperty(d->native, request->requestor, property, XA_ATOM, 32,
- PropModeReplace, (unsigned char *)targets, 3);
+ PropModeReplace, (unsigned char *)targets, latin1 ? 3 : 2);
reply.xselection.property = property;
} else if (text && (request->target == d->utf8 || request->target == XA_STRING)) {
- size_t len = strlen(text);
+ const char *payload = request->target == XA_STRING ? latin1 : text;
+ size_t len = request->target == XA_STRING ? latin1_len : strlen(text);
long max = XMaxRequestSize(d->native) * 4 - 128;
- if (len <= (size_t)max) {
+ if (payload && len <= (size_t)max) {
XChangeProperty(d->native, request->requestor, property, request->target,
- 8, PropModeReplace, (const unsigned char *)text, (int)len);
+ 8, PropModeReplace, (const unsigned char *)payload, (int)len);
reply.xselection.property = property;
}
}
+ free(latin1);
XSendEvent(d->native, request->requestor, False, 0, &reply);
}
@@ -1038,6 +1047,7 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
char *buf = NULL;
size_t total = 0;
unsigned long offset = 0;
+ bool complete = false;
/* Read up to TEXT_LIMIT bytes in 64KB chunks to avoid huge single-request limits */
while (total < TEXT_LIMIT) {
@@ -1053,7 +1063,7 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
if (data) { XFree(data); data = NULL; }
break;
}
- if ((actual != d->utf8 && actual != XA_STRING) || format != 8 || count == 0) {
+ if ((actual != d->utf8 && actual != XA_STRING) || format != 8) {
if (data) { XFree(data); data = NULL; }
break;
}
@@ -1063,22 +1073,41 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event)
break;
}
buf = nb;
- memcpy(buf + total, data, count);
+ if (count) memcpy(buf + total, data, count);
total += count;
buf[total] = '\0';
offset += count / 4;
XFree(data);
data = NULL;
- if (left == 0) break;
+ if (left == 0) { complete = true; break; }
}
- if (buf && total > 0) {
- push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, buf, total);
+ if (complete && actual == XA_STRING) {
+ size_t utf8_len;
+ char *utf8 = yasl_utf8_from_latin1(buf, total, &utf8_len);
free(buf);
- } else free(buf);
+ buf = utf8;
+ if (buf) total = utf8_len;
+ }
+ if (complete && buf && yasl_utf8_validate(buf, total))
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, buf, total);
+ free(buf);
XDestroyWindow(d->native, r->window);
free(r);
}
+static void refresh_keyboard(struct x11_display *d)
+{
+ memset(d->keys, 0, sizeof(d->keys));
+ if (!d->has_xkb) return;
+ XkbDescPtr keyboard = XkbGetKeyboard(d->native, XkbAllComponentsMask, XkbUseCoreKbd);
+ if (!keyboard) return;
+ if (keyboard->names && keyboard->names->keys) {
+ for (unsigned code = keyboard->min_key_code; code <= keyboard->max_key_code; code++)
+ d->keys[code] = yasl_key_from_xkb_name(keyboard->names->keys[code].name);
+ }
+ XkbFreeKeyboard(keyboard, XkbAllComponentsMask, True);
+}
+
static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
{
struct x11_display *d = display_of(&s->base);
@@ -1086,10 +1115,11 @@ static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
unsigned clean_state = key->state & ~(Mod2Mask | LockMask);
KeySym sym = NoSymbol, base = NoSymbol, shifted = NoSymbol;
+ int group = XkbGroupForCoreState(key->state);
if (d->has_xkb) {
- sym = XkbKeycodeToKeysym(d->native, key->keycode, 0, (clean_state & ShiftMask) ? 1 : 0);
- base = XkbKeycodeToKeysym(d->native, key->keycode, 0, 0);
- shifted = XkbKeycodeToKeysym(d->native, key->keycode, 0, 1);
+ sym = XkbKeycodeToKeysym(d->native, key->keycode, group, (clean_state & ShiftMask) ? 1 : 0);
+ base = XkbKeycodeToKeysym(d->native, key->keycode, group, 0);
+ shifted = XkbKeycodeToKeysym(d->native, key->keycode, group, 1);
}
if (sym == NoSymbol) {
XKeyEvent clean_key = *key;
@@ -1111,21 +1141,26 @@ static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
else len = 0;
}
if (status != XLookupChars && status != XLookupBoth) len = 0;
- } else len = XLookupString(key, local, sizeof(local), &sym, NULL);
+ } else {
+ /* XLookupString's bytes are Latin-1. Use its modifier-aware keysym
+ * and encode UTF-8 even when no input method is available. */
+ XLookupString(key, local, sizeof(local), &sym, NULL);
+ len = xkb_keysym_to_utf8((xkb_keysym_t)sym, local, sizeof(local));
+ len = len > 0 ? len - 1 : 0; /* xkb includes the terminating NUL. */
+ }
}
uint32_t cp = xkb_keysym_to_utf32((xkb_keysym_t)base);
- if (!cp) cp = xkb_keysym_to_utf32((xkb_keysym_t)sym);
push(s, (struct yasl_input_event){
.kind = YASL_INPUT_KEY,
- .key = {.key = (uint32_t)sym, .codepoint = cp,
- .base = key->keycode >= 8 ? key->keycode - 8 : 0,
- .keypad = sym >= XK_KP_Space && sym <= XK_KP_Equal,
- .shifted = xkb_keysym_to_utf32((xkb_keysym_t)shifted),
- .text = xkb_keysym_to_utf32((xkb_keysym_t)sym), .mods = d->mods,
+ .key = {.key = key->keycode < 256 ? d->keys[key->keycode] : YASL_KEY_UNKNOWN,
+ .codepoint = yasl_key_label(cp), .native_code = key->keycode,
+ .shifted = yasl_key_label(xkb_keysym_to_utf32((xkb_keysym_t)shifted)),
+ .mods = d->mods,
.action = key->type == KeyRelease ? YASL_KEY_RELEASE :
repeat ? YASL_KEY_REPEAT : YASL_KEY_PRESS},
}, NULL, 0);
- if (len > 0 && text)
+ if (len > 0 && text && (unsigned char)text[0] >= 0x20 && text[0] != 0x7f &&
+ !(d->mods & (YASL_MOD_CTRL | YASL_MOD_SUPER | YASL_MOD_ALT)))
push(s, (struct yasl_input_event){.kind = YASL_INPUT_TEXT}, text, (size_t)len);
if (text != local) free(text);
}
@@ -1256,6 +1291,17 @@ static void dispatch(struct yasl_display *base)
XFreeEventData(d->native, &event.xcookie);
continue;
}
+ if (event.type == MappingNotify) {
+ XRefreshKeyboardMapping(&event.xmapping);
+ refresh_keyboard(d);
+ continue;
+ }
+ if (d->has_xkb && event.type == d->xkb_event) {
+ XkbEvent *xkb = (XkbEvent *)&event;
+ if (xkb->any.xkb_type == XkbMapNotify) XkbRefreshKeyboardMapping(&xkb->map);
+ refresh_keyboard(d);
+ continue;
+ }
if (XFilterEvent(&event, None)) continue;
if (event.type == SelectionRequest) {
selection_request(d, &event.xselectionrequest);
@@ -1353,7 +1399,8 @@ static void dispatch(struct yasl_display *base)
if (button >= 4 && button <= 7) {
if (event.type == ButtonRelease) break;
mouse.kind = YASL_PTR_SCROLL;
- mouse.scroll_lines = (button == 4 || button == 6) ? -1 : 1;
+ if (button <= 5) mouse.scroll.y = button == 4 ? -1 : 1;
+ else mouse.scroll.x = button == 6 ? -1 : 1;
} else {
mouse.kind = event.type == ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
mouse.button = pointer_button(button);
@@ -1519,8 +1566,15 @@ static struct yasl_display *display_open(struct yasl_loop *loop,
d->base.native_handle = d->native;
d->screen = DefaultScreen(d->native);
d->has_shm = XShmQueryExtension(d->native);
- int xkb_opcode, xkb_event, xkb_error, xkb_major = XkbMajorVersion, xkb_minor = XkbMinorVersion;
+ int xkb_opcode = 0, xkb_event = 0, xkb_error = 0;
+ int xkb_major = XkbMajorVersion, xkb_minor = XkbMinorVersion;
d->has_xkb = XkbQueryExtension(d->native, &xkb_opcode, &xkb_event, &xkb_error, &xkb_major, &xkb_minor);
+ d->xkb_event = xkb_event;
+ if (d->has_xkb) {
+ unsigned events = XkbNewKeyboardNotifyMask | XkbMapNotifyMask | XkbNamesNotifyMask;
+ XkbSelectEvents(d->native, XkbUseCoreKbd, events, events);
+ refresh_keyboard(d);
+ }
int xi_event_base, xi_error_base;
int major = 2, minor = 0;
d->xi2 = XQueryExtension(d->native, "XInputExtension", &d->xi_opcode,
diff --git a/tests/cocoa/test_cursor.m b/tests/cocoa/test_cursor.m
index 0f3ba06..7a328f2 100644
--- a/tests/cocoa/test_cursor.m
+++ b/tests/cocoa/test_cursor.m
@@ -22,8 +22,12 @@ int main(void)
assert(surface);
NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
NSView *view = window.contentView;
+ NSEvent *cursor = [NSEvent enterExitEventWithType:NSEventTypeCursorUpdate
+ location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0
+ windowNumber:window.windowNumber context:nil eventNumber:0 trackingNumber:0 userData:NULL];
+ assert(cursor);
yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN);
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
NSCursor *hidden = [NSCursor currentCursor];
assert_transparent(hidden);
@@ -31,7 +35,7 @@ int main(void)
location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0
windowNumber:window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0];
[view mouseMoved:motion];
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == hidden);
struct yasl_input_event event;
bool moved = false;
@@ -41,10 +45,10 @@ int main(void)
assert(moved);
[[NSCursor arrowCursor] set]; /* Simulate leaving and re-entering. */
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == hidden);
yasl_surface_set_cursor(surface, YASL_CURSOR_ARROW);
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == [NSCursor arrowCursor]);
yasl_surface_destroy(surface);
yasl_display_close(display);
diff --git a/tests/cocoa/test_keyboard.m b/tests/cocoa/test_keyboard.m
new file mode 100644
index 0000000..4854065
--- /dev/null
+++ b/tests/cocoa/test_keyboard.m
@@ -0,0 +1,134 @@
+#import <AppKit/AppKit.h>
+#include <yasl/native.h>
+#include <assert.h>
+#include <math.h>
+#include <string.h>
+
+/* Reject character queries on modifier-only events, as AppKit does. */
+@interface InputSample : NSObject
+@property NSEventType type;
+@property unsigned short keyCode;
+@property NSEventModifierFlags modifierFlags;
+@property(copy) NSString *label;
+@property(copy) NSString *shiftedLabel;
+@property NSPoint locationInWindow;
+@property CGFloat scrollingDeltaX;
+@property CGFloat scrollingDeltaY;
+@property BOOL hasPreciseScrollingDeltas;
+@end
+
+@implementation InputSample
+- (NSString *)charactersByApplyingModifiers:(NSEventModifierFlags)modifiers
+{
+ assert(self.type == NSEventTypeKeyDown || self.type == NSEventTypeKeyUp);
+ return modifiers & NSEventModifierFlagShift ? self.shiftedLabel : self.label;
+}
+- (void)dealloc
+{
+ [_label release];
+ [_shiftedLabel release];
+ [super dealloc];
+}
+@end
+
+static struct yasl_input_event next(struct yasl_surface *surface, enum yasl_input_kind kind)
+{
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == kind && event.surface == surface);
+ return event;
+}
+
+static void empty(struct yasl_surface *surface)
+{
+ struct yasl_input_event event;
+ assert(!yasl_surface_input_poll(surface, &event));
+}
+
+int main(void)
+{
+ @autoreleasepool {
+ struct yasl_display *display = yasl_display_open(NULL, NULL);
+ assert(display);
+ struct yasl_surface_spec spec = {.title = "Keyboard test", .size = {320, 240}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ assert(surface);
+ NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
+ NSView<NSTextInputClient> *view = (id)window.contentView;
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {}
+
+ InputSample *sample = [InputSample new];
+ NSEvent *native = (NSEvent *)sample;
+ sample.type = NSEventTypeKeyUp;
+ sample.keyCode = 0; /* US A, even with a different active layout. */
+ sample.label = @"q";
+ sample.shiftedLabel = @"Q";
+ [view keyUp:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.key == YASL_KEY_A && event.key.native_code == 0);
+ assert(event.key.action == YASL_KEY_RELEASE);
+ assert(event.key.codepoint == 'q' && event.key.shifted == 'Q');
+ empty(surface);
+ sample.label = @"😀"; /* One scalar, two UTF-16 units. */
+ sample.shiftedLabel = @"ab";
+ [view keyUp:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.codepoint == 0x1f600 && event.key.shifted == 0);
+ sample.type = NSEventTypeFlagsChanged;
+ sample.keyCode = 56; /* Left Shift. */
+ sample.modifierFlags = NSEventModifierFlagShift | 0x0002;
+ [view flagsChanged:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.key == YASL_KEY_LEFT_SHIFT && event.key.action == YASL_KEY_PRESS);
+ assert(event.key.mods == YASL_MOD_SHIFT && !event.key.codepoint && !event.key.shifted);
+ sample.modifierFlags = 0;
+ [view flagsChanged:native];
+ assert(next(surface, YASL_INPUT_KEY).key.action == YASL_KEY_RELEASE);
+
+ /* Native text services own preedit and commits. Marked text is not an
+ * insertion, and commits retain all UTF-8 bytes, including non-BMP. */
+ [view setMarkedText:@"é" selectedRange:NSMakeRange(1, 0)
+ replacementRange:NSMakeRange(NSNotFound, 0)];
+ assert(strcmp(yasl_surface_ime_preedit(surface), "é") == 0);
+ empty(surface);
+ [view insertText:@"😀é" replacementRange:NSMakeRange(NSNotFound, 0)];
+ event = next(surface, YASL_INPUT_TEXT);
+ assert(event.text.len == strlen("😀é") && strcmp(event.text.text, "😀é") == 0);
+ assert(!yasl_surface_ime_preedit(surface));
+ empty(surface);
+
+ /* Exercise the keyDown -> interpretKeyEvents -> insertText route with
+ * an actual AppKit key event rather than the strict event double. */
+ NSEvent *press = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint
+ modifierFlags:0 timestamp:0 windowNumber:window.windowNumber context:nil
+ characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:0];
+ [view keyDown:press];
+ assert(next(surface, YASL_INPUT_KEY).key.key == YASL_KEY_A);
+ event = next(surface, YASL_INPUT_TEXT);
+ assert(event.text.len == 1 && event.text.text[0] == 'a');
+ empty(surface);
+
+ sample.type = NSEventTypeScrollWheel;
+ sample.hasPreciseScrollingDeltas = YES;
+ sample.scrollingDeltaX = -2.5;
+ sample.scrollingDeltaY = 1.25;
+ [view scrollWheel:native];
+ event = next(surface, YASL_INPUT_MOUSE);
+ assert(event.mouse.kind == YASL_PTR_SCROLL);
+ assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.125);
+ sample.hasPreciseScrollingDeltas = NO;
+ sample.scrollingDeltaX = 1;
+ sample.scrollingDeltaY = -1;
+ [view scrollWheel:native];
+ event = next(surface, YASL_INPUT_MOUSE);
+ assert(event.mouse.scroll.x == -1 && event.mouse.scroll.y == 1);
+ sample.scrollingDeltaX = NAN;
+ [view scrollWheel:native];
+ empty(surface);
+ [sample release];
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ }
+ return 0;
+}
diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c
index 4c99646..cf5b1f9 100644
--- a/tests/common/test_backends.c
+++ b/tests/common/test_backends.c
@@ -194,23 +194,105 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct
yasl_display_copy(d, YASL_CLIPBOARD, "clipboard text");
yasl_display_paste(d, YASL_CLIPBOARD, w->surface);
pump(loop, d, NULL);
- bool saw_key = false, saw_paste = false;
+ bool saw_key = false, saw_paste = false, saw_text = false;
struct yasl_input_event event;
while (yasl_surface_input_poll(w->surface, &event)) {
assert(event.surface == w->surface);
if (event.kind == YASL_INPUT_KEY) {
assert(event.key.codepoint == 'a');
assert(event.key.shifted == 'A');
- assert(event.key.base == 30);
- saw_key = event.key.key == XK_a;
+ assert(event.key.native_code == key.xkey.keycode);
+ saw_key = event.key.key == YASL_KEY_A;
}
if (event.kind == YASL_INPUT_PASTE) {
assert(event.paste.len == strlen("clipboard text"));
assert(memcmp(event.paste.text, "clipboard text", event.paste.len) == 0);
saw_paste = true;
}
+ if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'a');
+ saw_text = true;
+ }
+ }
+ assert(saw_key && saw_paste && saw_text);
+ /* Physical identity survives modifier and layout changes, including a
+ * release whose modifiers differ from its press. */
+ key.xkey.state = ShiftMask;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ key.xkey.type = KeyRelease;
+ key.xkey.state = 0;
+ XSendEvent(native, window, False, KeyReleaseMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ int key_count = 0, text_count = 0;
+ while (yasl_surface_input_poll(w->surface, &event)) {
+ if (event.kind == YASL_INPUT_KEY) {
+ assert(event.key.key == YASL_KEY_A);
+ assert(event.key.action == (key_count ? YASL_KEY_RELEASE : YASL_KEY_PRESS));
+ key_count++;
+ } else if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'A');
+ text_count++;
+ }
+ }
+ assert(key_count == 2 && text_count == 1);
+ key.xkey.type = KeyPress;
+ key.xkey.state = ControlMask;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A);
+ assert(event.key.mods & YASL_MOD_CTRL);
+ assert(!yasl_surface_input_poll(w->surface, &event));
+
+ int symbols_per_key = 0;
+ KeySym *saved_symbols = XGetKeyboardMapping(native, key.xkey.keycode, 1, &symbols_per_key);
+ assert(saved_symbols && symbols_per_key >= 2);
+ KeySym *changed_symbols = calloc((size_t)symbols_per_key, sizeof(KeySym));
+ assert(changed_symbols);
+ changed_symbols[0] = XK_q;
+ changed_symbols[1] = XK_Q;
+ XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, changed_symbols, 1);
+ XSync(native, False);
+ pump(loop, d, NULL); /* Consume MappingNotify before translating the key. */
+ key.xkey.state = 0;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ saw_key = saw_text = false;
+ while (yasl_surface_input_poll(w->surface, &event)) {
+ if (event.kind == YASL_INPUT_KEY) {
+ assert(event.key.key == YASL_KEY_A && event.key.codepoint == 'q');
+ saw_key = true;
+ } else if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'q');
+ saw_text = true;
+ }
+ }
+ assert(saw_key && saw_text);
+ XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, saved_symbols, 1);
+ free(changed_symbols);
+ XFree(saved_symbols);
+ XSync(native, False);
+ pump(loop, d, NULL);
+
+ for (unsigned button = 4; button <= 7; button++) {
+ XEvent wheel = {.xbutton = {.type = ButtonPress, .display = native,
+ .window = window, .root = DefaultRootWindow(native),
+ .same_screen = True, .button = button}};
+ /* Direct delivery exercises the core fallback even with XI2 enabled. */
+ XSendEvent(native, window, False, NoEventMask, &wheel);
+ wheel.xbutton.type = ButtonRelease;
+ XSendEvent(native, window, False, NoEventMask, &wheel);
+ XFlush(native);
+ pump(loop, d, NULL);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_MOUSE && event.mouse.kind == YASL_PTR_SCROLL);
+ assert(event.mouse.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0));
+ assert(event.mouse.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0));
+ assert(!yasl_surface_input_poll(w->surface, &event));
}
- assert(saw_key && saw_paste);
assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION);
assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING));
diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c
index 4e22de1..8580dd6 100644
--- a/tests/common/test_contracts.c
+++ b/tests/common/test_contracts.c
@@ -232,6 +232,97 @@ static void test_scale(void)
assert(yasl_scale_to_logical(1, 0) == 0);
}
+static int popup_calls;
+static struct yasl_surface *popup_create(struct yasl_surface *parent,
+ const struct yasl_popup_spec *spec)
+{
+ (void)parent; (void)spec;
+ popup_calls++;
+ return NULL; /* Backend fails without setting errno. */
+}
+
+static bool popup_reposition(struct yasl_surface *popup,
+ const struct yasl_popup_spec *spec)
+{
+ (void)popup; (void)spec;
+ popup_calls++;
+ errno = EBUSY;
+ return false;
+}
+
+static uint32_t popup_caps(const struct yasl_display *d)
+{ (void)d; return YASL_CAP_POPUPS; }
+
+static int dispatch_failure(struct yasl_display *d, int timeout)
+{ (void)d; (void)timeout; return -1; }
+
+static struct yasl_surface *create_failure(struct yasl_display *d,
+ const struct yasl_surface_spec *spec, void *device)
+{ (void)d; (void)spec; (void)device; return NULL; }
+
+static int drag_starts;
+static bool drag_without_listener(struct yasl_display *d, struct yasl_surface *s,
+ const struct yasl_drag_image *image, const struct yasl_drag_listener *listener,
+ void *ud)
+{
+ (void)d; (void)s; (void)image; (void)ud;
+ /* Backends may require a table even when no completion callback is needed. */
+ assert(listener && !listener->end);
+ drag_starts++;
+ return true;
+}
+
+static void test_errors(void)
+{
+ const struct yasl_backend_ops ops = {.display_caps = popup_caps,
+ .surface_create_toplevel = create_failure,
+ .surface_create_popup = popup_create,
+ .surface_popup_reposition = popup_reposition,
+ .display_drag_start = drag_without_listener,
+ .display_dispatch_timeout = dispatch_failure};
+ struct yasl_display d = {.ops = &ops};
+ struct yasl_surface s = {.display = &d};
+ struct yasl_popup_spec spec = {.size = {120, 80}, .anchor = {0, 0, 1, 1}};
+ assert(!yasl_surface_create_popup(NULL, &spec) && errno == EINVAL);
+ assert(!yasl_surface_create_popup(&s, NULL) && errno == EINVAL);
+ spec.anchor.width = -1; /* Must not reach Wayland's protocol marshaler. */
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EINVAL);
+ spec.anchor.width = 1;
+ spec.content_offset.x = INT_MAX;
+ spec.content_size.x = INT_MAX;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ spec.content_offset.x = spec.content_size.x = 0;
+ spec.size.x = 32768;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ assert(!popup_calls);
+ spec.size.x = 120;
+ errno = ERANGE;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == ENODEV);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EBUSY);
+ assert(popup_calls == 2);
+ const struct yasl_backend_ops unsupported = {0};
+ d.ops = &unsupported;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == ENOTSUP);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == ENOTSUP);
+ assert(yasl_display_dispatch(&d, 0) == -1 && errno == ENOTSUP);
+ assert(yasl_display_dispatch(NULL, 0) == -1 && errno == EINVAL);
+ d.ops = &ops;
+ errno = ERANGE;
+ assert(yasl_display_dispatch(&d, 0) == -1 && errno == EIO);
+ struct yasl_surface_spec toplevel = {.size = {120, 80}};
+ errno = ERANGE;
+ assert(!yasl_surface_create_toplevel(&d, &toplevel) && errno == ENODEV);
+ assert(yasl_display_drag_start(&d, &s, NULL, NULL, NULL));
+ struct yasl_drag_image image = {.size = {1, 1}};
+ assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL));
+ uint32_t pixel = 0;
+ image.pixels = &pixel;
+ image.size.x = INT_MAX;
+ assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL));
+ assert(drag_starts == 1);
+}
+
int main(void)
{
test_timing_clock();
@@ -239,5 +330,6 @@ int main(void)
test_validation();
test_native();
test_scale();
+ test_errors();
return 0;
}
diff --git a/tests/common/test_keyboard.c b/tests/common/test_keyboard.c
new file mode 100644
index 0000000..5a7f18e
--- /dev/null
+++ b/tests/common/test_keyboard.c
@@ -0,0 +1,32 @@
+#include "common/keyboard.h"
+#include "cocoa/keycodes.h"
+#include <assert.h>
+
+int main(void)
+{
+ /* Same physical positions across native encodings, including keypad and
+ * left/right modifiers. macOS A is code zero, which is not UNKNOWN. */
+ assert(yasl_key_from_evdev(30) == YASL_KEY_A);
+ assert(yasl_key_from_cocoa(kVK_ANSI_A) == YASL_KEY_A);
+ assert(yasl_key_from_xkb_name("AC01") == YASL_KEY_A);
+ assert(yasl_key_from_evdev(1) == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_cocoa(kVK_Escape) == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_xkb_name("ESC\0") == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_evdev(96) == YASL_KEY_KP_ENTER);
+ assert(yasl_key_from_cocoa(kVK_ANSI_KeypadEnter) == YASL_KEY_KP_ENTER);
+ assert(yasl_key_from_cocoa(kVK_Return) == YASL_KEY_ENTER);
+ assert(yasl_key_from_evdev(100) == YASL_KEY_RIGHT_ALT);
+ assert(yasl_key_from_cocoa(kVK_RightOption) == YASL_KEY_RIGHT_ALT);
+ assert(yasl_key_from_evdev(42) == YASL_KEY_LEFT_SHIFT);
+ assert(yasl_key_from_cocoa(kVK_Shift) == YASL_KEY_LEFT_SHIFT);
+ assert(yasl_key_from_evdev(86) == YASL_KEY_ISO_BACKSLASH);
+ assert(yasl_key_from_cocoa(kVK_ISO_Section) == YASL_KEY_ISO_BACKSLASH);
+ assert(yasl_key_from_evdev(0) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_evdev(UINT32_MAX) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_cocoa(UINT32_MAX) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_xkb_name("ZZZZ") == YASL_KEY_UNKNOWN);
+ assert(yasl_key_label(0x1f600) == 0x1f600);
+ assert(yasl_key_label(0xd800) == 0 && yasl_key_label(0x110000) == 0);
+ assert(yasl_key_label(0xf700) == 0 && yasl_key_label('\t') == 0);
+ return 0;
+}
diff --git a/tests/common/test_utf8.c b/tests/common/test_utf8.c
new file mode 100644
index 0000000..72f8904
--- /dev/null
+++ b/tests/common/test_utf8.c
@@ -0,0 +1,27 @@
+#include "common/utf8.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+ char latin1[256];
+ for (int i = 1; i <= 255; i++) latin1[i - 1] = (char)i;
+ latin1[255] = '\0';
+ size_t len = 0, back_len = 0;
+ char *utf8 = yasl_utf8_from_latin1(latin1, 255, &len);
+ assert(utf8 && yasl_utf8_validate(utf8, len));
+ char *back = yasl_utf8_to_latin1(utf8, &back_len);
+ assert(back && back_len == 255 && memcmp(back, latin1, 256) == 0);
+ free(back);
+ free(utf8);
+ assert(!yasl_utf8_to_latin1("😀", &len));
+ assert(!yasl_utf8_to_latin1("\xc3", &len));
+ assert(!yasl_utf8_validate("\xc3", 1));
+ assert(!yasl_utf8_validate("\xe2\x82", 2));
+ assert(!yasl_utf8_validate("\xf0\x9f\x98", 3));
+ assert(!yasl_utf8_validate("\xed\xa0\x80", 3)); /* Surrogate. */
+ assert(yasl_utf8_validate("😀", sizeof("😀") - 1));
+ assert(yasl_utf8_validate(NULL, 0));
+ return 0;
+}
diff --git a/tests/helpers/check_exports.py b/tests/helpers/check_exports.py
index 0d10014..acb727d 100644
--- a/tests/helpers/check_exports.py
+++ b/tests/helpers/check_exports.py
@@ -9,6 +9,7 @@ import subprocess
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--allow-export', action='append', default=[])
+ parser.add_argument('--platform', choices=['linux', 'darwin'], default='linux')
parser.add_argument('nm')
parser.add_argument('library')
parser.add_argument('headers', nargs='+')
@@ -23,10 +24,14 @@ def main():
if not expected:
raise SystemExit("No public function declarations found")
- output = subprocess.check_output(
- [args.nm, "-D", "--defined-only", "--format=posix", args.library], text=True
- )
+ nm_options = ["-g", "-U", "-P"] if args.platform == 'darwin' else [
+ "-D", "--defined-only", "--format=posix"
+ ]
+ output = subprocess.check_output([args.nm, *nm_options, args.library], text=True)
exported = {line.split()[0] for line in output.splitlines() if line.strip()}
+ if args.platform == 'darwin':
+ # Mach-O decorates C symbols with one leading underscore.
+ exported = {name[1:] if name.startswith('_') else name for name in exported}
missing = expected - exported
leaked = exported - expected - set(args.allow_export)
if missing or leaked:
diff --git a/tests/helpers/package_cocoa_tests.py b/tests/helpers/package_cocoa_tests.py
new file mode 100644
index 0000000..8f801f8
--- /dev/null
+++ b/tests/helpers/package_cocoa_tests.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+"""Package compiled Cocoa tests for execution on a matching macOS desktop."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import shutil
+import tarfile
+import tempfile
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('build', type=Path)
+ parser.add_argument('output', type=Path)
+ args = parser.parse_args()
+ build = args.build.resolve()
+ info = build / 'meson-info'
+ host = json.loads((info / 'intro-machines.json').read_text())['host']
+ if host['system'] != 'darwin':
+ parser.error('Use a configured and compiled Cocoa build.')
+ tests = json.loads((info / 'intro-tests.json').read_text())
+ required = {'native', 'native-ownership', 'keyboard-cocoa', 'tablet-cocoa',
+ 'cursor-cocoa', 'hello-cocoa-window', 'tablet-cocoa-window'}
+ missing = required - {test['name'] for test in tests}
+ if missing:
+ parser.error('Enable integration_tests; missing: ' + ', '.join(sorted(missing)))
+ options = json.loads((info / 'intro-buildoptions.json').read_text())
+ sanitizer = next(option['value'] for option in options if option['name'] == 'b_sanitize')
+ if sanitizer not in ('none', [], ['none']):
+ parser.error('Use an unsanitized build; sanitizer runtimes are not bundled.')
+
+ with tempfile.TemporaryDirectory(prefix='yasl-cocoa-bundle-') as directory:
+ root = Path(directory) / 'yasl-cocoa-tests'
+ root.mkdir()
+ files = {}
+
+ def copy(source, relative):
+ destination = root / relative
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(source, destination)
+ files[str(relative)] = hashlib.sha256(destination.read_bytes()).hexdigest()
+
+ copy(build / 'libyasl.dylib', Path('libyasl.dylib'))
+ source_root = Path(__file__).resolve().parents[2]
+ copy(source_root / 'LICENSE', Path('LICENSE'))
+ copy(Path(__file__).with_name('run_cocoa_bundle.py'), Path('run.py'))
+ commands = []
+ for test in tests:
+ if test['name'] == 'public-exports':
+ continue # Check the dylib with nm on the build machine first.
+ executable = Path(test['cmd'][0]).resolve()
+ try:
+ relative = executable.relative_to(build)
+ except ValueError:
+ parser.error('Unexpected external test command: ' + str(executable))
+ copy(executable, relative)
+ commands.append({'name': test['name'], 'cmd': [str(relative), *test['cmd'][1:]],
+ 'timeout': test['timeout']})
+ manifest = {'architecture': host['cpu_family'], 'minimum_macos': 14,
+ 'tests': commands, 'sha256': files}
+ (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
+ with tarfile.open(args.output, 'w:gz') as archive:
+ archive.add(root, arcname=root.name)
+ print(f'Packaged {len(commands)} runtime tests for {host["cpu_family"]}: {args.output}')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py
index 57cc9ab..70c82fb 100644
--- a/tests/helpers/run_backends.py
+++ b/tests/helpers/run_backends.py
@@ -21,7 +21,7 @@ def wait_for(process, ready):
def main():
- weston, xwayland, test, timers, tablet = sys.argv[1:]
+ weston, xwayland, test, timers, tablet, hello, input_x11 = sys.argv[1:]
servers = []
with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory:
root = Path(directory)
@@ -80,10 +80,16 @@ def main():
x_env["XDG_SESSION_TYPE"] = "x11"
print("Test: X11 surface timers", flush=True)
subprocess.run([timers], env=x_env, check=True, timeout=8)
+ print("Test: X11 UTF-8 keyboard fallback and clipboard", flush=True)
+ subprocess.run([input_x11], env=x_env, check=True, timeout=8)
for backend in ("wayland", "x11"):
print("Test: tablet example on", backend, flush=True)
subprocess.run([tablet, "--backend", backend, "--smoke-test"],
env=env, check=True, timeout=8)
+ print("Test: hello example on", backend, flush=True)
+ subprocess.run([hello, "--smoke-test"],
+ env=env if backend == "wayland" else x_env,
+ check=True, timeout=8)
except BaseException:
for log in (wl_log, x_log):
log.flush()
diff --git a/tests/helpers/run_cocoa_bundle.py b/tests/helpers/run_cocoa_bundle.py
new file mode 100644
index 0000000..338f38e
--- /dev/null
+++ b/tests/helpers/run_cocoa_bundle.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""Run a packaged Cocoa suite, preserving output and a machine-readable report."""
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import platform
+import re
+import subprocess
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--list', action='store_true', help='List tests without executing them.')
+ args = parser.parse_args()
+ root = Path(__file__).resolve().parent
+ manifest = json.loads((root / 'manifest.json').read_text())
+ if args.list:
+ print('Architecture:', manifest['architecture'])
+ for test in manifest['tests']:
+ print(test['name'])
+ return
+ if sys.platform != 'darwin':
+ parser.error('Run this suite in a macOS desktop session.')
+ if int(platform.mac_ver()[0].split('.')[0]) < manifest['minimum_macos']:
+ parser.error('This suite requires macOS 14 or newer.')
+ architecture = {'arm64': 'aarch64'}.get(platform.machine(), platform.machine())
+ if architecture != manifest['architecture']:
+ parser.error('Use the bundle matching this Python process architecture.')
+ for relative, expected in manifest['sha256'].items():
+ if hashlib.sha256((root / relative).read_bytes()).hexdigest() != expected:
+ parser.error('Bundle file changed: ' + relative)
+
+ logs = root / 'logs'
+ logs.mkdir(exist_ok=True)
+ env = os.environ.copy()
+ env['DYLD_LIBRARY_PATH'] = str(root)
+ results = []
+ for test in manifest['tests']:
+ name = test['name']
+ log = logs / (re.sub(r'[^A-Za-z0-9_-]', '_', name) + '.log')
+ command = [str(root / test['cmd'][0]), *test['cmd'][1:]]
+ with log.open('w') as output:
+ try:
+ process = subprocess.run(command, cwd=root, env=env, stdout=output,
+ stderr=subprocess.STDOUT, timeout=test['timeout'] or None)
+ result = {'name': name, 'returncode': process.returncode,
+ 'status': 'pass' if process.returncode == 0 else 'fail'}
+ except subprocess.TimeoutExpired:
+ output.write('\nTest timed out.\n')
+ result = {'name': name, 'status': 'timeout'}
+ except OSError as error:
+ output.write(str(error) + '\n')
+ result = {'name': name, 'status': 'error'}
+ results.append(result)
+ print(f'{name}: {result["status"]}', flush=True)
+ report = root / 'results.json'
+ report.write_text(json.dumps(results, indent=2) + '\n')
+ failed = sum(result['status'] != 'pass' for result in results)
+ print(f'{len(results) - failed} passed, {failed} failed. Logs: {logs}')
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/meson.build b/tests/meson.build
index 910f17c..c6570d9 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -30,16 +30,20 @@ if add_languages('cpp', native: false, required: false)
'yasl-cpp-api-test',
'test_api.cpp',
dependencies: shared_dep,
- cpp_args: cpp.get_supported_arguments('-pedantic-errors'),
+ cpp_args: cpp.get_supported_arguments('-pedantic-errors') + [
+ '-DYASL_HAS_WAYLAND=' + (has_wayland ? '1' : '0'),
+ '-DYASL_HAS_X11=' + (has_x11 ? '1' : '0'),
+ '-DYASL_HAS_COCOA=' + (has_cocoa ? '1' : '0'),
+ ],
override_options: ['cpp_std=c++11'],
)
test('cpp-api', cpp_api_test)
endif
-python = find_program('python3')
-if system == 'linux'
- nm = find_program('nm')
- export_args = []
+python = find_program('python3', native: true)
+if system == 'linux' or system == 'darwin'
+ nm = find_program('nm', native: false)
+ export_args = ['--platform=' + system]
if get_option('b_sanitize').contains('address')
# Clang's ASan instrumentation exports these linker-generated markers.
export_args += ['--allow-export=__start_asan_globals',
@@ -57,6 +61,9 @@ if system == 'linux'
if has_x11
exported_headers += files('../include/yasl/native/x11.h')
endif
+ if has_cocoa
+ exported_headers += files('../include/yasl/native/cocoa.h')
+ endif
test('public-exports', python,
args: [files('helpers/check_exports.py')] + export_args +
[nm, yasl_lib.get_shared_lib(), exported_headers],
@@ -71,6 +78,16 @@ contracts_test = executable(
)
test('contracts', contracts_test)
+keyboard_test = executable('yasl-keyboard-test', 'common/test_keyboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+)
+test('keyboard', keyboard_test)
+
+utf8_test = executable('yasl-utf8-test', 'common/test_utf8.c',
+ include_directories: inc, dependencies: yasl_dep,
+)
+test('utf8', utf8_test)
+
timer_test = executable(
'yasl-timer-test',
'common/test_timers.c',
@@ -118,6 +135,14 @@ elif get_option('x11_tablet_tests')
endif
if has_wayland
+ clipboard_wayland_test = executable('yasl-clipboard-wayland-test', 'wayland/test_clipboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
+ test('clipboard-wayland', clipboard_wayland_test)
+ keyboard_wayland_test = executable('yasl-keyboard-wayland-test', 'wayland/test_keyboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
+ test('keyboard-wayland', keyboard_wayland_test)
ime_test = executable('yasl-ime-test', 'wayland/test_input.c',
include_directories: inc,
dependencies: yasl_dep,
@@ -143,6 +168,9 @@ benchmark('performance', benchmark_exe)
if get_option('integration_tests')
if has_cocoa
+ test('hello-cocoa-window', hello_example,
+ args: ['--smoke-test'], timeout: 20,
+ )
native_test = executable('yasl-native-test', 'cocoa/test_native.c',
dependencies: shared_dep,
)
@@ -151,6 +179,10 @@ if get_option('integration_tests')
dependencies: [shared_dep, deps],
)
test('native-ownership', ownership_test, timeout: 20)
+ keyboard_cocoa_test = executable('yasl-keyboard-cocoa-test', 'cocoa/test_keyboard.m',
+ dependencies: [shared_dep, deps],
+ )
+ test('keyboard-cocoa', keyboard_cocoa_test, timeout: 20)
tablet_cocoa_test = executable('yasl-tablet-cocoa-test', 'cocoa/test_tablet.m',
dependencies: [shared_dep, deps],
)
@@ -171,11 +203,14 @@ if get_option('integration_tests')
'common/test_backends.c',
dependencies: [shared_dep, deps],
)
+ input_x11_test = executable('yasl-input-x11-test', 'x11/test_input.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
weston = find_program('weston')
xwayland = find_program('Xwayland')
test('backends', python,
args: [files('helpers/run_backends.py'), weston, xwayland,
- backend_test, timer_test, tablet_example],
+ backend_test, timer_test, tablet_example, hello_example, input_x11_test],
timeout: 60,
)
endif
diff --git a/tests/test_api.cpp b/tests/test_api.cpp
index 9fb8525..129a41e 100644
--- a/tests/test_api.cpp
+++ b/tests/test_api.cpp
@@ -5,6 +5,9 @@
#if YASL_HAS_X11
#include "yasl/native/x11.h"
#endif
+#if YASL_HAS_COCOA
+#include "yasl/native/cocoa.h"
+#endif
#include "yasl/loop.h"
#include <yasl/timing.h>
#include <yasl/yasl.h>
@@ -28,6 +31,10 @@ int main()
yasl_loop_destroy(loop);
yasl_input_event event{};
+ event.key.key = YASL_KEY_A;
+ assert(event.key.key == YASL_KEY_A);
+ event.mouse.scroll = {0.25, -0.5};
+ assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.5);
assert(!yasl_surface_input_poll(nullptr, &event));
assert(!yasl_display_drag_start(nullptr, nullptr, nullptr, nullptr, nullptr));
assert(!yasl_surface_drag_carry(nullptr, {0, 0}));
@@ -42,5 +49,11 @@ int main()
assert(!yasl_x11_surface_get_window(nullptr));
#endif
+#if YASL_HAS_COCOA
+ assert(!yasl_cocoa_display_get_application(nullptr));
+ assert(!yasl_cocoa_surface_get_window(nullptr));
+ assert(!yasl_cocoa_surface_get_view(nullptr));
+#endif
+
return 0;
}
diff --git a/tests/wayland/test_clipboard.c b/tests/wayland/test_clipboard.c
new file mode 100644
index 0000000..553e84d
--- /dev/null
+++ b/tests/wayland/test_clipboard.c
@@ -0,0 +1,47 @@
+#include "../../src/wayland/clipboard.c"
+#include <assert.h>
+
+struct received { int count; size_t len; char text[32]; };
+
+static void receive(const char *text, size_t len, bool uri_list,
+ struct wl_surface *surface, void *ud)
+{
+ (void)surface;
+ struct received *r = ud;
+ assert(!uri_list && len < sizeof(r->text));
+ memcpy(r->text, text, len);
+ r->text[len] = '\0';
+ r->len = len;
+ r->count++;
+}
+
+int main(void)
+{
+ struct received received = {0};
+ struct clipboard c = {.loop = yasl_loop_create(), .receive = receive, .ud = &received};
+ assert(c.loop);
+ const struct { const char *bytes; size_t len; bool valid; } cases[] = {
+ {"é字😀", sizeof("é字😀") - 1, true},
+ {"", 0, true},
+ {"\xff", 1, false},
+ {"\xf0\x9f\x98", 3, false}, /* Incomplete non-BMP character. */
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ received = (struct received){0};
+ int fds[2];
+ assert(pipe(fds) == 0);
+ assert(start_transfer(&c, fds[0], false, false));
+ assert(write(fds[1], cases[i].bytes, cases[i].len) == (ssize_t)cases[i].len);
+ close(fds[1]);
+ for (int steps = 0; c.transfers && steps < 10; steps++)
+ assert(yasl_loop_step(c.loop, 0) >= 0);
+ assert(!c.transfers);
+ assert(received.count == (int)cases[i].valid);
+ if (cases[i].valid) {
+ assert(received.len == cases[i].len);
+ assert(memcmp(received.text, cases[i].bytes, received.len) == 0);
+ }
+ }
+ yasl_loop_destroy(c.loop);
+ return 0;
+}
diff --git a/tests/wayland/test_keyboard.c b/tests/wayland/test_keyboard.c
new file mode 100644
index 0000000..dbe0a94
--- /dev/null
+++ b/tests/wayland/test_keyboard.c
@@ -0,0 +1,128 @@
+#include "../../src/wayland/keyboard.c"
+#include <assert.h>
+
+static struct yasl_input_event next(struct wayland_surface *w, enum yasl_input_kind kind)
+{
+ struct yasl_input_event ev;
+ assert(yasl_surface_input_poll(&w->base, &ev));
+ assert(ev.kind == kind && ev.surface == &w->base);
+ return ev;
+}
+
+static void empty(struct wayland_surface *w)
+{
+ struct yasl_input_event ev;
+ assert(!yasl_surface_input_poll(&w->base, &ev));
+}
+
+static void layout(struct seat *s, const char *name, const char *variant)
+{
+ keyboard_text_reset(s);
+ if (s->xkb_state) xkb_state_unref(s->xkb_state);
+ if (s->xkb_keymap) xkb_keymap_unref(s->xkb_keymap);
+ struct xkb_rule_names names = {.layout = name, .variant = variant};
+ s->xkb_keymap = xkb_keymap_new_from_names(s->conn->xkb_ctx, &names, 0);
+ assert(s->xkb_keymap);
+ s->xkb_state = xkb_state_new(s->xkb_keymap);
+ assert(s->xkb_state);
+ s->input_mods_ready = false;
+ s->mod_alt = xkb_keymap_mod_get_index(s->xkb_keymap, XKB_MOD_NAME_ALT);
+}
+
+static void modifiers(struct seat *s, const char *name)
+{
+ xkb_mod_mask_t mask = name ? 1u << xkb_keymap_mod_get_index(s->xkb_keymap, name) : 0;
+ xkb_state_update_mask(s->xkb_state, mask, 0, 0, 0, 0, 0);
+}
+
+static void key(struct seat *s, uint32_t code, bool down)
+{
+ keyboard_listener.key(s, NULL, 1, 0, code, down ?
+ WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
+}
+
+int main(void)
+{
+ struct wayland_display d = {.base.ops = &yasl_wayland_backend};
+ d.xkb_ctx = xkb_context_new(0);
+ assert(d.xkb_ctx);
+ struct wayland_surface w = {.base.display = &d.base, .conn = &d};
+ wayland_surface_set_callbacks(&w, NULL, NULL);
+ struct seat s = {.conn = &d, .keyboard_focus = &w, .repeat_fd = -1,
+ .compose_attempted = true};
+ const char compose[] = "<dead_acute> <e> : \"é\" U00E9\n";
+ struct xkb_compose_table *table = xkb_compose_table_new_from_buffer(d.xkb_ctx,
+ compose, sizeof(compose) - 1, "C.UTF-8", XKB_COMPOSE_FORMAT_TEXT_V1, 0);
+ assert(table);
+ s.compose = xkb_compose_state_new(table, 0);
+ assert(s.compose);
+ xkb_compose_table_unref(table);
+ layout(&s, "us", NULL);
+ key(&s, KEY_A, true);
+ struct yasl_input_event ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.native_code == KEY_A);
+ assert(ev.key.codepoint == 'a' && ev.key.shifted == 'A' && !ev.key.mods);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "a") == 0);
+ modifiers(&s, XKB_MOD_NAME_SHIFT);
+ key(&s, KEY_A, false); /* Modifier changes cannot change physical identity. */
+ ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.action == YASL_KEY_RELEASE);
+ assert(ev.key.mods == YASL_MOD_SHIFT);
+ empty(&w);
+ key(&s, KEY_A, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0);
+ modifiers(&s, XKB_MOD_NAME_CAPS);
+ key(&s, KEY_A, true);
+ assert(next(&w, YASL_INPUT_KEY).key.mods == YASL_MOD_CAPS);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0);
+ const char *shortcuts[] = {XKB_MOD_NAME_CTRL, XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO};
+ for (size_t i = 0; i < sizeof(shortcuts)/sizeof(shortcuts[0]); i++) {
+ modifiers(&s, shortcuts[i]);
+ key(&s, KEY_A, true);
+ next(&w, YASL_INPUT_KEY);
+ empty(&w); /* Shortcut characters never leak into committed text. */
+ }
+ layout(&s, "fr", NULL);
+ key(&s, KEY_A, true);
+ ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.codepoint == 'q');
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "q") == 0);
+ layout(&s, "de", NULL);
+ modifiers(&s, "Mod5"); /* AltGr is text input, not an Alt shortcut. */
+ key(&s, KEY_E, true);
+ assert(next(&w, YASL_INPUT_KEY).key.key == YASL_KEY_E);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "€") == 0);
+ layout(&s, "us", "intl");
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ empty(&w);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "é") == 0);
+ assert(keyboard_dispatch_key(&s, &w, KEY_E, YASL_KEY_REPEAT));
+ assert(next(&w, YASL_INPUT_KEY).key.action == YASL_KEY_REPEAT);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ keyboard_reset_keys(&s, &w);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ s.ime_focus = &w;
+ text_input_listener.commit_string(&s, NULL, "字");
+ text_input_listener.done(&s, NULL, 0);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "字") == 0);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ empty(&w);
+ xkb_compose_state_unref(s.compose);
+ xkb_state_unref(s.xkb_state);
+ xkb_keymap_unref(s.xkb_keymap);
+ xkb_context_unref(d.xkb_ctx);
+ input_queue_clear(&w.input);
+ return 0;
+}
diff --git a/tests/wayland/test_tablet.c b/tests/wayland/test_tablet.c
index 449c446..f8982b0 100644
--- a/tests/wayland/test_tablet.c
+++ b/tests/wayland/test_tablet.c
@@ -50,6 +50,7 @@ int main(void)
tool_frame(&tool, NULL, 0);
ev = next(&surface);
assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_ERASER);
+ assert(ev.scroll.x == 0 && ev.scroll.y == 1);
tablet_tool_proximity_out(&tool, NULL);
ev = next(&surface);
assert(ev.kind == YASL_PTR_LEAVE && ev.device == YASL_PTR_DEV_ERASER && !ev.pen.pressure);
@@ -57,6 +58,42 @@ int main(void)
assert(!(yasl_display_caps(&display.base) & YASL_CAP_TABLET));
display.tablet_mgr = (void *)&display; /* Capability depends on protocol availability. */
assert(yasl_display_caps(&display.base) & YASL_CAP_TABLET);
+
+ seat.pointer_focus = &surface;
+ /* A compositor may describe the same detent in three ways. value120 wins,
+ * and both axes belong to one event, without whole-line truncation. */
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(10));
+ pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 1);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 30);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, -60);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_MOUSE);
+ assert(ev.scroll.x == -0.5 && ev.scroll.y == 0.25);
+ struct yasl_input_event unused;
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+ pointer_frame(&seat, NULL);
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+
+ /* Stops preserve offsets from the same frame. Repeated tiny movements
+ * must be visible immediately rather than held until a whole detent. */
+ for (int i = 0; i < 3; i++) {
+ pointer_axis_source(&seat, NULL, WL_POINTER_AXIS_SOURCE_FINGER);
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(1.25));
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(-2.5));
+ pointer_axis_stop(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.scroll.x == -0.25 && ev.scroll.y == 0.125);
+ }
+ pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, 2);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.scroll.x == 2 && ev.scroll.y == 0);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 60);
+ seat_pointer_reset(&seat);
+ pointer_frame(&seat, NULL);
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
input_queue_clear(&surface.input);
return 0;
}
diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c
new file mode 100644
index 0000000..11dca4f
--- /dev/null
+++ b/tests/x11/test_input.c
@@ -0,0 +1,123 @@
+/* An isolated real X server supplies the keymap; disabling this surface's XIC
+ * deliberately exercises text insertion when no input method is available. */
+#include "../../src/x11/window.c"
+
+static void check_selection_encoding(struct x11_display *d, struct x11_surface *s)
+{
+ Window requestor = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native),
+ 0, 0, 1, 1, 0, 0, 0);
+ Atom property = XInternAtom(d->native, "YASL_TEST_STRING", False);
+ XSelectionRequestEvent request = {.display = d->native, .requestor = requestor,
+ .selection = d->clipboard, .target = XA_STRING, .property = property};
+ yasl_display_copy(&d->base, YASL_CLIPBOARD, "café £");
+ selection_request(d, &request);
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *data = NULL;
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == XA_STRING && format == 8 && count == 6);
+ assert(memcmp(data, "caf\xe9 \xa3", count) == 0);
+ XFree(data);
+ yasl_display_copy(&d->base, YASL_CLIPBOARD, "字");
+ selection_request(d, &request);
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == None); /* Never mislabel UTF-8 as Latin-1. */
+ if (data) XFree(data);
+ request.target = d->targets;
+ selection_request(d, &request);
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == XA_ATOM && format == 32 && count == 2);
+ assert(((Atom *)data)[0] == d->targets && ((Atom *)data)[1] == d->utf8);
+ XFree(data);
+ XDestroyWindow(d->native, requestor);
+
+ /* Simulate a legacy owner returning STRING to the paste conversion window,
+ * then an empty UTF-8 owner and malformed UTF-8. */
+ const struct { Atom type; const char *bytes; size_t len; const char *expected; } cases[] = {
+ {XA_STRING, "caf\xe9 \xa3", 6, "café £"},
+ {d->utf8, "", 0, ""},
+ {d->utf8, "\xff", 1, NULL},
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ struct paste_request *pending = calloc(1, sizeof(*pending));
+ assert(pending);
+ pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native),
+ 0, 0, 1, 1, 0, 0, 0);
+ pending->target = s;
+ pending->next = d->pastes;
+ d->pastes = pending;
+ XChangeProperty(d->native, pending->window, property, cases[i].type, 8,
+ PropModeReplace, (const unsigned char *)cases[i].bytes, (int)cases[i].len);
+ XSelectionEvent reply = {.requestor = pending->window, .property = property};
+ selection_notify(d, &reply);
+ struct yasl_input_event event;
+ if (cases[i].expected) {
+ assert(yasl_surface_input_poll(&s->base, &event));
+ assert(event.kind == YASL_INPUT_PASTE);
+ assert(event.paste.len == strlen(cases[i].expected));
+ assert(strcmp(event.paste.text, cases[i].expected) == 0);
+ }
+ assert(!yasl_surface_input_poll(&s->base, &event));
+ }
+}
+
+int main(void)
+{
+ struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11};
+ struct yasl_display *base = yasl_display_open(NULL, &ds);
+ assert(base);
+ struct x11_display *d = (struct x11_display *)base;
+ struct yasl_surface_spec spec = {.title = "Keyboard fallback", .size = {100, 100}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(base, &spec);
+ assert(surface);
+ struct x11_surface *s = (struct x11_surface *)surface;
+ XIC saved_ic = s->ic;
+ s->ic = NULL;
+ unsigned code = XKeysymToKeycode(d->native, XK_a);
+ int width = 0;
+ KeySym *saved = XGetKeyboardMapping(d->native, code, 1, &width);
+ assert(saved && width >= 2);
+ KeySym *symbols = calloc((size_t)width, sizeof(*symbols));
+ assert(symbols);
+ const struct { KeySym symbol, shifted; const char *text; uint32_t label; } cases[] = {
+ {XK_eacute, XK_Eacute, "é", 0xe9},
+ {0x01000416, 0x01000416, "Ж", 0x416},
+ {0x0101f600, 0x0101f600, "😀", 0x1f600},
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ symbols[0] = cases[i].symbol;
+ symbols[1] = cases[i].shifted;
+ XChangeKeyboardMapping(d->native, code, width, symbols, 1);
+ XSync(d->native, False);
+ dispatch(base);
+ input_queue_clear(&s->input);
+ XKeyEvent native = {.type = KeyPress, .display = d->native,
+ .window = s->window, .keycode = code};
+ key_event(s, &native, false);
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A);
+ assert(event.key.codepoint == cases[i].label);
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == YASL_INPUT_TEXT && event.text.len == strlen(cases[i].text));
+ assert(strcmp(event.text.text, cases[i].text) == 0);
+ assert(!yasl_surface_input_poll(surface, &event));
+ native.type = KeyRelease;
+ key_event(s, &native, false);
+ assert(yasl_surface_input_poll(surface, &event) && event.kind == YASL_INPUT_KEY);
+ assert(!yasl_surface_input_poll(surface, &event));
+ }
+ XChangeKeyboardMapping(d->native, code, width, saved, 1);
+ XSync(d->native, False);
+ free(symbols);
+ XFree(saved);
+ s->ic = saved_ic;
+ check_selection_encoding(d, s);
+ yasl_surface_destroy(surface);
+ yasl_display_close(base);
+ return 0;
+}
diff --git a/tests/x11/test_tablet.c b/tests/x11/test_tablet.c
index c35a266..ba815cb 100644
--- a/tests/x11/test_tablet.c
+++ b/tests/x11/test_tablet.c
@@ -341,6 +341,19 @@ int main(void)
xi_pointer_event(&surface, &sample);
ev = next(&surface);
assert(ev.device == YASL_PTR_DEV_MOUSE && ev.button == 2 && ev.pen.pressure == 0);
+ for (int button = 4; button <= 7; button++) {
+ sample.detail = button;
+ sample.evtype = XI_ButtonPress;
+ xi_pointer_event(&surface, &sample);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_SCROLL);
+ assert(ev.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0));
+ assert(ev.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0));
+ sample.evtype = XI_ButtonRelease;
+ xi_pointer_event(&surface, &sample);
+ struct yasl_input_event unused;
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+ }
assert(pointer_button(2) == 1 && pointer_button(8) == 3);
pointer.pressure = (struct tablet_axis){2, 1, 1, 50};
assert(pressure_value(&pointer.pressure) == 0);