diff options
53 files changed, 3579 insertions, 631 deletions
@@ -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 @@ -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); |
