From dd1775a9927922c14f8679ccbc4d3ca8d5917e20 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 21:23:03 +0300 Subject: public api refactor --- README.md | 4 +- docs/api-policy.md | 2 +- docs/api-redesign.md | 147 +++++++++++ docs/getting-started.md | 21 +- docs/guide.md | 149 ++++++----- docs/migrating.md | 45 +++- examples/game.c | 18 +- examples/hello.c | 12 +- examples/notepad.c | 66 +++-- examples/simple_window.c | 17 +- examples/tablet.c | 16 +- include/yasl/app.h | 87 ++----- include/yasl/yasl.h | 186 ++++++++++++-- meson.build | 21 ++ src/common/app.c | 468 +++++++---------------------------- src/common/backend.c | 222 ++++++++++++++++- src/common/backend.h | 45 ++++ src/common/events.c | 372 ++++++++++++++++++++++++++++ src/common/input.c | 13 +- src/common/input.h | 4 + src/common/meson.build | 2 +- src/platform/cocoa/window.m | 275 +++++++++++++++------ src/platform/wayland/api.h | 3 + src/platform/wayland/backend.c | 78 ++++++ src/platform/wayland/buffer.c | 15 +- src/platform/wayland/clipboard.c | 152 +++++++++--- src/platform/wayland/clipboard.h | 9 +- src/platform/wayland/drag.c | 8 +- src/platform/wayland/ime.c | 5 +- src/platform/wayland/keyboard.c | 54 +++- src/platform/wayland/pointer.c | 10 +- src/platform/wayland/shell.c | 7 +- src/platform/wayland/surface.c | 7 +- src/platform/x11/window.c | 504 +++++++++++++++++++++++++++++++++----- tests/cocoa/test_ownership.m | 75 +++++- tests/common/test_app.c | 72 +++++- tests/common/test_events.c | 209 ++++++++++++++++ tests/common/test_hello_example.c | 2 +- tests/helpers/run_backends.py | 7 +- tests/meson.build | 7 +- tests/test_api.cpp | 16 ++ tests/wayland/test_app_input.c | 17 +- tests/wayland/test_clipboard.c | 75 +++++- tests/x11/test_input.c | 236 +++++++++++++++++- tests/x11/test_tablet_native.c | 10 +- 45 files changed, 2902 insertions(+), 868 deletions(-) create mode 100644 docs/api-redesign.md create mode 100644 src/common/events.c create mode 100644 tests/common/test_events.c diff --git a/README.md b/README.md index 992b331..1fea452 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ for platform capabilities and limits. - [Public reference](include/yasl/yasl.h), [app runner](include/yasl/app.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 redesign](docs/api-redesign.md): queue, frame, ownership, and backend contracts. - [API policy](docs/api-policy.md), [migration notes](docs/migrating.md), and [contributing](CONTRIBUTING.md). - [Tests, tablet example, and benchmarks](docs/testing.md). @@ -54,7 +55,8 @@ The complete [hello.c](examples/hello.c) describes its window in pixel with an RGB gradient in its frame callback. Toplevel creation sizes are logical pixels; drawing and input use device pixels. -Targets are borrowed for one frame callback. Keep durable content in application +Both loop styles consume one display event FIFO and receive `yasl_frame`; +`yasl_frame_present()` submits its target. Targets are borrowed for one frame callback. Keep durable content in application memory; the runner handles dispatch, input polling, pacing, and cleanup. Applications managing their own loop can use `` directly. Both workflows are explained in the application guide. diff --git a/docs/api-policy.md b/docs/api-policy.md index dd22ca7..3e510f3 100644 --- a/docs/api-policy.md +++ b/docs/api-policy.md @@ -4,7 +4,7 @@ YASL 0.1 is pre-release. The headers under `include/yasl/` are the public API; i headers under `src/` are private even when their symbols appear in a static archive. The shared library exports only declarations from public headers. Native backend accessors are exported only for built backends. Installation -copies only the shared and static libraries; use matching source-tree headers. +copies both libraries, public headers, enabled native headers, and pkg-config metadata. 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 diff --git a/docs/api-redesign.md b/docs/api-redesign.md new file mode 100644 index 0000000..a111a92 --- /dev/null +++ b/docs/api-redesign.md @@ -0,0 +1,147 @@ +# Public API redesign + +This is the implementation contract for the pre-release API revision. The +portable layer owns event ordering, queued payloads, surface identities, and +frame validation. Backends translate native behavior into that contract. + +## One event stream + +`yasl_display_poll_event(display, &event)` consumes one display-wide FIFO. Input, +window resize/close/state, drag hover/drop, composition, clipboard results, and +display quit/reopen/loss use the same `yasl_event`. Events have an increasing +`sequence` assigned when YASL queues them, and a monotonic `time_ns`. Sequence +orders capture by YASL, not unknowable ordering between OS threads or devices. +The runner consumes this same queue; it does not drain separate queues by window. + +Event strings are copied on enqueue and borrowed until the next successful +display poll or display close, even if the recipient is destroyed. Destroying a +surface removes its undelivered events. A callback may destroy its recipient; +it must stop using that surface. Draws remain synchronous to satisfy native +live-resize rules; the runner drains queued events before invoking a frame. + +Legacy surface polling consumes input from the same queue and discards pending +non-input notifications, which were already delivered through legacy callbacks. +Applications must choose one polling style; discarding prevents idle spin and +unbounded retention in old manual loops. Legacy window +callbacks remain for migration, but applications should choose queue delivery +and a draw callback. Backend notifications must queue before calling a legacy +callback, and must not access a surface after a callback that can destroy it. + +## One frame contract + +Both manual drawing and `yasl_app_desc.frame` receive `struct yasl_frame`. +Its `target` describes CPU pixels (NULL for native rendering), device-pixel size, +and row stride. The frame also carries frame index, scale, and an estimated +animation timestamp. Native drawables are queried through ``; +there is no untyped GPU layer or public internal pointer in the portable frame. + +`yasl_frame_present(frame, rects, count)` submits the exact active frame once. +NULL with zero count means full damage; a non-NULL array with zero count means +no damage. NULL with nonzero count and negative extents fail with EINVAL. +More than the backend rectangle limit falls back to full damage after validating +every rectangle; damage must never be silently lost. Targets expire at callback +return. Pixels must always describe a complete frame. + +`yasl_surface_request_frame` is the normal coalescing redraw operation. +`yasl_surface_set_pacing` returns success/failure and works with either loop +style. Continuous drawing uses native pacing where available and a refresh-based +timer otherwise. On-demand surfaces require explicit invalidation. + +## Identities, posting, and shutdown + +`yasl_surface_id(surface)` returns a nonzero identity unique within its display. +`yasl_display_post(display, &message)` is thread-safe; messages target an identity +(zero means display-wide), never a borrowed surface pointer. The display must +remain alive during posting, and producer threads must stop before display close. +After a successful post the optional release callback runs exactly once on the +display thread, after consumption or cancellation (including surface destruction +and shutdown). A failed post leaves payload ownership with the caller. Posting +to a retired identity fails. The display queue owns only its copy of the message; +the release callback defines payload cleanup. Release may post, but must not +poll, dispatch, or destroy YASL objects. Legacy `yasl_post_event` adapts to +this mechanism, with the caller responsible for its borrowed target lifetime. + +Application callbacks may return CONTINUE, QUIT, or ERROR. ERROR makes +`yasl_run` return failure after cleanup. `yasl_app_exit(display, result)` permits +timers, fd callbacks, and frames to request the same orderly termination. +The optional initial window is available through `yasl_app_window(display)`. +Surface userdata is independent of legacy callback userdata and accessible +through `yasl_surface_set_userdata` / `yasl_surface_userdata`. + +## Window requests and units + +New code spells toplevel creation size `size_logical`; `size` remains an alias +for migration. Framebuffer geometry, pointer coordinates, popup placement, +damage, IME carets, and drag-image coordinates are device pixels as documented +on their structures. Conversion helpers remain explicit. + +`yasl_surface_set_size(surface, logical_size)`, +`yasl_surface_set_fullscreen(surface, enabled)`, +`yasl_surface_set_maximized(surface, enabled)`, and +`yasl_surface_minimize(surface)` return whether the native request was issued. +They set errno on failure: EINVAL for invalid input/non-toplevel surfaces, +ENOTSUP for unsupported requests, or an OS error. Success is acceptance of a +request, not a promise that the window manager honored it. Resulting size/state +is reported through events. No portable absolute window-position setter exists. + +## Clipboard and composition + +`yasl_clipboard_set(display, selection, text)` returns whether a copied UTF-8 +offer was issued, with errno on failure. `yasl_clipboard_request(surface, +selection)` returns a nonzero request ID, or zero with errno on failure. +The backend operation `display_request_paste(d, selection, surface, id)` returns +true only after accepting responsibility for one completion. It completes via +`yasl_surface_complete_paste(surface, id, status, text, len)`; status is +YASL_TRANSFER_OK, UNAVAILABLE, or FAILED. Results use YASL_EVENT_PASTE and its +`.paste` payload (data/len/request_id/status); legacy `.text` aliases data/len. +Surface destruction cancels delivery and native transfers. A backend must +complete errors as well as success, including empty text. Request IDs are +monotonic per display. Legacy paste uses ID zero. External Linux transfers time out after 30 seconds. + +YASL_EVENT_COMPOSITION carries copied UTF-8 preedit text, byte selection offsets +(both -1 if unavailable), and an active flag. Each native change queues a +snapshot, including composition ending; committed text remains YASL_EVENT_TEXT. +The query functions remain migration helpers. + +## Backend boundary + +Common helpers declared in `common/backend.h`: + +* `yasl_surface_notify_configure(s, geometry)` +* `yasl_surface_notify_close(s)` +* `yasl_surface_notify_state(s, state)` +* `yasl_surface_notify_drag(s, event, position_px)` +* `yasl_surface_notify_drop(s, event, position_px)` +* `yasl_surface_notify_composition(s, text, begin, end)` (NULL/empty ends it) +* `yasl_surface_draw(s, frame)` (fills frame metadata and invokes draw) +* `yasl_display_notify_event(d, YASL_EVENT_DISPLAY_QUIT/REOPEN/LOST)` +* `yasl_surface_complete_paste(s, request_id, status, text, length)` + +Backends keep using their existing input_queue; a new `surface_input_queue` +operation exposes that queue to common code for attaching its event publisher. +Backends must replace direct lifecycle/draw/drag callbacks with these helpers, +without weakening their native destruction guards. They implement optional +surface_set_size/fullscreen/maximized/minimize and display_copy_text / +display_request_paste operations. Common wrappers validate arguments and +report unsupported operations uniformly. + +Linux backends are compiled and tested independently and together. Cocoa changes +can be inspected here but require macOS compilation/runtime verification. + +## Validation + +The combined Linux configuration passes all 26 Meson tests, including isolated +Weston/Xwayland integration, runner error/cleanup paths, event ordering, +clipboard outcomes, composition, and destruction during callbacks. Separate +Wayland-only and X11-only builds pass their unit suites and the final public +export, C++11, and event-contract checks. + +AddressSanitizer and UndefinedBehaviorSanitizer pass the common contract tests +and the isolated backend integration suite. Leak detection was disabled because +LeakSanitizer is unavailable in this execution environment. Doxygen builds with +warnings treated as errors. A staged install links both shared and static +consumers using only the installed headers and pkg-config metadata. + +Cocoa code and native regression tests were updated and reviewed, but were not +compiled or executed: macOS validation remains required. Existing X11 INCR and +Wayland arbitrary-size limitations are reported explicitly rather than hidden. diff --git a/docs/getting-started.md b/docs/getting-started.md index 5e569fe..d1f284d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -50,20 +50,20 @@ pressure, erasers, resizing, and scale changes. ## Link an application -Install the shared and static libraries into your chosen prefix. Headers, -examples, documentation, and pkg-config metadata remain in the source tree: +Install the libraries, public headers, and pkg-config metadata into your prefix: ```sh meson setup build --prefix=/usr/local --libdir=lib meson compile -C build meson install -C build -cc -std=c23 -Iinclude examples/hello.c -o hello -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lyasl +cc -std=c23 examples/hello.c -o hello $(pkg-config --cflags --libs yasl) ./hello ``` -The prefix must be writable. Use matching headers from `include/`, and adjust -the library search path and runtime path for your chosen prefix and `libdir`. -Static consumers must also link the enabled backends' dependencies. +The prefix must be writable. For a custom prefix, add its pkg-config directory +to `PKG_CONFIG_PATH` and its library directory to the runtime loader path. +Use `pkg-config --static --libs yasl` for static consumers so backend dependencies +are included. Installed native accessor headers match the built backends. A Meson application can instead vendor the source under `subprojects/yasl`: @@ -75,7 +75,7 @@ executable('my-app', 'main.c', dependencies: yasl) The subproject dependency includes headers and backend link dependencies. Public headers can be included from C++11 or newer; the library itself builds -as C23. Only the library artifacts are installed by `meson install`. +as C23. The libraries, public headers, and pkg-config metadata are installed together. ## The application cycle @@ -84,9 +84,10 @@ as C23. Only the library artifacts are installed by `meson install`. 2. In `init`, create a toplevel with a positive **logical-pixel** size and call `yasl_surface_request_frame(surface)` for the first redraw. 3. In `event`, handle the union member selected by `event->type`. Return - `YASL_APP_QUIT` for a close request; otherwise return `YASL_APP_CONTINUE`. -4. In `frame`, fill every pixel in `frame->cpu.pixels` with premultiplied - `0xAARRGGBB` and present once with `yasl_frame_present_cpu(frame, NULL, 0)`. + `YASL_APP_QUIT` for a close request, `YASL_APP_ERROR` for failure, or + `YASL_APP_CONTINUE` to keep running. +4. In `frame`, fill every pixel in `frame->target.pixels` with premultiplied + `0xAARRGGBB` and present once with `yasl_frame_present(frame, NULL, 0)`. 5. Let the runner close remaining surfaces and the display. Release your own resources in the optional `quit` callback or after `yasl_run()` returns. diff --git a/docs/guide.md b/docs/guide.md index 76279c4..1b64880 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -44,13 +44,21 @@ All examples include `` and call `yasl_run()` with a which the runner creates before `init` with its first frame requested; `yasl_run()` returns an error if creation fails. Create further surfaces in the optional `init`, handle `YASL_EVENT_*` notifications in `event`, and render into -`frame->cpu.pixels` in `frame`. Present with `yasl_frame_present_cpu(frame, NULL, +`frame->target.pixels` in `frame`. Present with `yasl_frame_present(frame, NULL, 0)` and return `YASL_APP_QUIT` from an event handler to exit. Without an `event` -callback, closing any window stops the runner. The runner handles dispatch, input polling, resize redraws, +callback, closing any window stops the runner. The runner handles dispatch, queued event delivery, resize redraws, and remaining surface/display cleanup. Display quit stops the runner; connection loss and dispatch failures return an error after cleanup. The runner delivers -input exactly as `yasl_surface_poll_event()` returns it, plus -`YASL_EVENT_WINDOW_RESIZE`, `YASL_EVENT_WINDOW_CLOSE`, and `YASL_EVENT_USER`. +the same display-wide FIFO consumed by `yasl_display_poll_event()`, including +window state, drag hover, composition, clipboard completion, and display lifecycle +events. `event.sequence` increases in enqueue order across all windows; it is +YASL capture order, not a claim about ordering between native producers. +Return `YASL_APP_ERROR` on application failure, or call +`yasl_app_exit(display, YASL_APP_ERROR)` from a frame, timer, or fd callback. +Cleanup still runs, and `yasl_run()` returns failure. During `init`, +`yasl_app_window(display)` exposes the optional initial window. Application data +can be supplied in `window.userdata` or attached with +`yasl_surface_set_userdata()`; it is independent of callback userdata. Unified pointer button and motion events retain `device` and `pen` axes, including erasers and pressure-only motion. Cancel held input or active strokes @@ -62,7 +70,7 @@ For applications managing their own event loop, use the core API below. Open the display with application-wide options and notifications in `yasl_display_spec`. Create each toplevel with `yasl_window_spec`, including its -title, logical size, optional `yasl_window_cbs` table in `cbs`, and userdata in +title, `size_logical`, optional `yasl_window_cbs` table in `cbs`, and userdata in `ud`. One display can own multiple windows with separate callback tables. The window copies its callbacks during creation. Its callbacks cannot run before @@ -75,6 +83,9 @@ installs popup callbacks; the setter does not invoke the callbacks. When using `yasl_run()`, its runner installs callbacks for windows on its display. Use `yasl_app_desc.event` and `.frame` rather than `yasl_window_spec.cbs` and `.ud`. +Replacing callbacks on a runner-owned surface fails with `EBUSY`. +With a manual loop, install just the draw callback and drain the display FIFO +after dispatch; lifecycle and input share the same event handler logic. Read window state with `yasl_surface_state()`. Its `YASL_STATE_*` flags can coexist, and the `state` callback receives the same current bitmask. Wayland @@ -114,7 +125,7 @@ saturates out-of-range finite results, and returns zero for a zero scale. ## Drawing and pacing -`yasl_surface_schedule_redraw()` coalesces requests. `yasl_display_flush()` +`yasl_surface_request_frame()` coalesces requests. `yasl_display_flush()` services them when the backend can draw. During a native resize, dispatch can also draw synchronously. Avoid assuming callbacks occur only in flush. @@ -132,27 +143,27 @@ static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *u 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)) { + if (!yasl_frame_present(frame, NULL, 0)) { /* 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. +Present the exact active frame at most once during its callback. Do not mutate +the target identity or retain frame/pixel pointers after return. NULL rectangles +with count zero mean full damage; a non-NULL array with zero count means no damage. +All supplied extents must be nonnegative. Rectangles are clipped, and more than +`YASL_DAMAGE_MAX` rectangles fall back to full damage after all are validated. +Damage may reduce uploads, but every target must contain complete valid pixels. +Native rendering uses the same frame, with a drawable queried in the extension. + +`yasl_surface_set_pacing()` works in either loop style. ON_DEMAND coalesces +explicit requests and sleeps when idle. CONTINUOUS uses native display signals +where available and a refresh-based timer otherwise, with about 60 Hz as a +fallback estimate. It reports allocation/argument failures instead of silently +ignoring surfaces outside the runner. Suspension can suppress actual drawing. +Legacy `yasl_surface_present()` and `yasl_surface_schedule_redraw()` remain +migration adapters; new code uses the frame presentation and request functions. `yasl_surface_read_pixels()` copies the last presented CPU frame, for example for a screenshot or a debugging tool. Call it outside draw callbacks. A frame @@ -170,19 +181,20 @@ clock; it does not establish actual scanout time. ## Input, text, and payload lifetime -Dispatch receives native input. `yasl_surface_poll_event()` 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.type`; `time_ns` is a monotonic -timestamp on the `yasl_loop_now_ns()` clock. Polling returns only input types, -`YASL_EVENT_KEY` through `YASL_EVENT_INPUT_RESET`. Text, paste, and file-drop -payloads share `event.text`: 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. +Dispatch receives native input; `yasl_display_poll_event()` drains the display +FIFO without waiting. Drain it once for the whole display, including multiple +windows and popup input. `event.surface` identifies the live recipient and +`surface_id` is its stable identity. Destruction cancels undelivered recipient +events. `yasl_surface_poll_event()` remains an input-only migration adapter and +discards pending non-input notifications, which legacy callbacks already +delivered. Do not mix the two polling styles. + +Read the union member selected by `event.type`. `time_ns` is monotonic and +`sequence` orders capture across the display. TEXT and DROP use `.text`, PASTE +uses `.paste`, and COMPOSITION uses `.composition`. Each string is copied when +queued, NUL-terminated, and borrowed until the next successful display/surface +poll or display close. Dispatch, unsuccessful polls, and recipient destruction +do not invalidate the last polled string. Copy it to retain it longer. Use `YASL_EVENT_TEXT` for document text insertion, including committed IME text. It contains UTF-8 and can contain several characters. Key actions are for @@ -265,19 +277,22 @@ 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_EVENT_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 +`yasl_clipboard_set(display, selection, text)` copies a NUL-terminated UTF-8 +offer and reports failure through its return and errno. Wayland may require a +recent input serial. `yasl_clipboard_request(surface, selection)` returns a +nonzero request ID, or zero with errno when rejected. Each accepted request +produces one `YASL_EVENT_PASTE` carrying `.paste.request_id`, `.status`, `.data`, +and `.len`, unless its target/display is destroyed. OK includes empty text; +UNAVAILABLE means no text offer, and FAILED means transfer/decoding failure. +External Linux transfers have a 30-second timeout. X11 does not implement INCR +and reports unsupported large transfers as FAILED. Legacy STRING uses Latin-1 +conversion. Malformed UTF-8 completes as failure. + +Unsupported primary selection reports ENOTSUP and never aliases the clipboard. +Cocoa uses a YASL named pasteboard for primary selection. Legacy copy/paste calls +remain adapters; legacy paste completions carry request ID zero. + +External file drag hover uses `YASL_EVENT_DROP_HOVER`; actual file URLs arrive as `YASL_EVENT_DROP`. `text.data` is a `text/uri-list` string separated by CRLF, not a list of native paths. `yasl_drop_next_path()` walks it and decodes each local `file:` URL into a path. It skips comments, other schemes, remote @@ -296,12 +311,12 @@ while ((len = yasl_drop_next_path(&cursor, path, sizeof(path))) != 0) { Decoded paths are raw bytes and may contain any character the file system allows, including newlines. Quote them before pasting into a shell. -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. +Handle `YASL_EVENT_COMPOSITION` for immutable UTF-8 preedit snapshots, byte +selection offsets, and composition ending. Missing selection information uses +-1 offsets. Insert only committed `YASL_EVENT_TEXT` into the document. Set the +candidate-window caret with `yasl_surface_set_ime_caret()` in device pixels. +X11 uses XIM preedit callbacks when offered; fallback input methods may provide +no inline composition. The old preedit getters remain migration helpers. ## Popups and surface drags @@ -315,8 +330,8 @@ 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 +`YASL_EVENT_DRAG` handles a YASL surface drag; `YASL_EVENT_DROP_HOVER` handles +external data hover. Legacy callbacks remain migration adapters. `yasl_display_drag_start()` is intended for window/tab tear-off workflows, not arbitrary MIME data sources. Keep transferred application state in your own registry. Start from a recent pointer press, and check the boolean result even when `YASL_CAP_DRAG` is present. Supply an optional copied image and @@ -367,7 +382,7 @@ Every backend's dispatch wakes when a loop descriptor becomes ready. Cocoa waits for native events and mirrors the loop's descriptors in a kqueue while it waits. If kqueue rejects a descriptor, Cocoa bounds the wait to 10 ms instead. -Two calls work from any thread. `yasl_loop_kick()` only wakes the loop, and it +Loop wake and post calls work from any thread. `yasl_loop_kick()` only wakes the loop, and it is also safe in a signal handler. `yasl_loop_post()` queues a function to run on the owning thread during a later step or dispatch. Use it to hand results from worker threads to the loop: @@ -387,6 +402,24 @@ and restores the previous action on unwatch. YASL leaves thread signal masks alone: a watched signal must stay unblocked in at least one thread. Only one loop can watch a given signal at a time. +## Worker messages and window requests + +Capture `yasl_surface_id(surface)` on the display thread and pass that value to +workers. `yasl_display_post(display, &message)` accepts an identity instead of a +borrowed surface pointer. Zero targets the display. On success, optional +`message.release(payload)` runs exactly once on the display thread after delivery +or cancellation, including shutdown. It may post messages but must not poll, +dispatch, or destroy YASL objects. Failed posting leaves ownership with the caller. +Stop and join producers before closing the display. The same API works with a +manual loop and with the runner; `yasl_post_event()` is a legacy adapter. + +Toplevel requests `yasl_surface_set_size(surface, logical_size)`, +`yasl_surface_set_fullscreen`, `yasl_surface_set_maximized`, and +`yasl_surface_minimize` return acceptance with errno on failure. Confirm actual +geometry/state through queued events. Popup targets are invalid. Wayland cannot +request an arbitrary live size and returns ENOTSUP for that operation; fullscreen, +maximize, and minimize use xdg-shell requests. X11 and Cocoa implement all four. + ## Desktop services Include `` for services that sit next to windowing. Display and @@ -436,7 +469,7 @@ 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 +encode and present with your renderer, then call `yasl_frame_present()` to record submission. The drawable expires on successful present, callback return, or surface destruction. Do not infer GPU completion from the boolean result. Other backends return NULL with `errno = ENOTSUP` for native creation; having a diff --git a/docs/migrating.md b/docs/migrating.md index 86804c8..912ea4d 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -4,6 +4,45 @@ YASL 0.1 has no stable source or binary compatibility promise. Rebuild all consumers against matching public headers and the library after updating. See the [application guide](guide.md) for the full window and input workflow. +## Display queue, frames, and application failures + +The runner now consumes the display-wide FIFO directly. Manual loops should +replace per-surface polling with `yasl_display_poll_event(display, &event)`. +Surface polling remains an input-only adapter that discards pending non-input +notifications delivered through legacy callbacks. Do not mix polling styles. Strings from either poll +now survive recipient destruction and expire at the next successful poll on +that display, or display close. Queue allocation failure is reported by dispatch +as an error instead of silently losing a lifecycle notification. + +Replace `struct yasl_frame_context` with `struct yasl_frame`, `frame->cpu.pixels` +with `frame->target.pixels`, and `frame->size` with `frame->target.size`. +`target.stride_bytes` replaces `cpu.stride_bytes`. Native drawing uses +`yasl_surface_get_native_frame(surface, &frame->target, &native)`. +`yasl_frame_present(frame, NULL, 0)` replaces the CPU presentation wrapper. +A non-NULL damage array with count zero now explicitly means no damaged pixels. +Lists larger than the backend limit become full damage instead of truncating. + +`YASL_APP_ERROR` and `yasl_app_exit(display, result)` report application failures +through `yasl_run`. `yasl_app_window(display)` gives init access to desc.window. +Use `yasl_surface_set_userdata` / `yasl_surface_userdata` for per-window state, +or `window.userdata` during creation. `yasl_surface_set_callbacks` now returns a +boolean and rejects runner-owned surfaces with EBUSY. + +Spell toplevel creation sizes `size_logical`; `size` remains an alias. +New window request functions report acceptance with errno. Pacing now works +outside the runner too, and returns a boolean instead of silently ignoring calls. + +Replace clipboard copy/paste with `yasl_clipboard_set` / `yasl_clipboard_request`. +Check the returned request ID and `event.paste.status` on completion. Empty text +is successful; missing data and transfer errors now have explicit outcomes. +Preedit snapshots arrive through `YASL_EVENT_COMPOSITION`; state and hover use +`YASL_EVENT_WINDOW_STATE`, `YASL_EVENT_DRAG`, and `YASL_EVENT_DROP_HOVER`. + +For worker messages, capture a surface ID on the display thread and use +`yasl_display_post` with an optional release callback. Do not also free the +payload in the event handler when release owns cleanup. Stop producer threads +before display shutdown; retired IDs reject posting with ENOENT. + ## Application callbacks All examples now use `` and `yasl_run()`. Describe a single window @@ -11,7 +50,7 @@ in `yasl_app_desc.window`, or create windows in `.init`; move input handling to `.event` and drawing to `.frame`. Replace manual dispatch/poll/flush loops with the runner and request redraws with `yasl_surface_request_frame()`. Present the frame context with -`yasl_frame_present_cpu()`; the runner destroys remaining surfaces on exit. +`yasl_frame_present()`; the runner destroys remaining surfaces on exit. Window state remains a `YASL_STATE_*` bitmask; `YASL_STATE_TILED` is reported for tiled Wayland toplevels. @@ -19,8 +58,8 @@ for tiled Wayland toplevels. ## Events The core API and the runner now share one `struct yasl_event`, declared in -``. `yasl_surface_poll_event()` returns the same events that -`yasl_app_desc.event` receives, so input handling code works under both. +``. `yasl_display_poll_event()` returns the same events that +`yasl_app_desc.event` receives. Legacy surface polling returns only input types. | Previous use | Replacement | | --- | --- | diff --git a/examples/game.c b/examples/game.c index 4517519..33c2f6e 100644 --- a/examples/game.c +++ b/examples/game.c @@ -147,13 +147,14 @@ static enum yasl_app_result on_init(void *ud, struct yasl_display *d) struct yasl_window_spec spec = { .title = "YASL 3D Game Loop (Approach A)", - .size = {800, 600}, + .size_logical = {800, 600}, }; game->surface = yasl_surface_create_toplevel(d, &spec); - if (!game->surface) return YASL_APP_QUIT; + if (!game->surface) return YASL_APP_ERROR; /* Drive continuous 60/144Hz ticks */ - yasl_surface_set_pacing(game->surface, YASL_PACING_CONTINUOUS); + if (!yasl_surface_set_pacing(game->surface, YASL_PACING_CONTINUOUS)) + return YASL_APP_ERROR; game->cam_z = 3.5f; game->relative_mouse = false; @@ -237,13 +238,13 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) return YASL_APP_CONTINUE; } -static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { (void)s; struct game_state *game = ud; - int w = ctx->size.x; - int h = ctx->size.y; - uint32_t *pix = ctx->cpu.pixels; + int w = ctx->target.size.x; + int h = ctx->target.size.y; + uint32_t *pix = ctx->target.pixels; if (!pix || w <= 0 || h <= 0) return; /* Update game simulation */ @@ -339,7 +340,8 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context draw_number(pix, w, h, 16 * scale, 16 * scale, (int)game->current_fps, 0xff00ff00, scale); /* Submit frame */ - yasl_frame_present_cpu(ctx, NULL, 0); + if (!yasl_frame_present(ctx, NULL, 0)) + yasl_app_exit(yasl_surface_display(s), YASL_APP_ERROR); } int main(void) diff --git a/examples/hello.c b/examples/hello.c index 4c171ec..31ab18b 100644 --- a/examples/hello.c +++ b/examples/hello.c @@ -3,12 +3,12 @@ #include static bool failed; -static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) +static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame *frame) { (void)ud; - int w = frame->size.x, h = frame->size.y; + int w = frame->target.size.x, h = frame->target.size.y; for (int y = 0; y < h; y++) { - uint32_t *row = frame->cpu.pixels + (size_t)y * (frame->cpu.stride_bytes / 4); + uint32_t *row = frame->target.pixels + (size_t)y * (frame->target.stride_bytes / 4); for (int x = 0; x < w; x++) { /* Red grows rightward, green downward, blue fades; opaque premultiplied ARGB. */ uint32_t r = (uint32_t)(x * 255 / (w > 1 ? w - 1 : 1)); @@ -17,16 +17,16 @@ static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_conte } } - if (!yasl_frame_present_cpu(frame, NULL, 0)) { + if (!yasl_frame_present(frame, NULL, 0)) { fprintf(stderr, "Could not present the draw target\n"); failed = true; } - if (failed) yasl_surface_destroy(surface); + if (failed) yasl_app_exit(yasl_surface_display(surface), YASL_APP_ERROR); } int main(void) { - struct yasl_window_spec window = {.title = "Hello YASL", .size = {640, 480}}; + struct yasl_window_spec window = {.title = "Hello YASL", .size_logical = {640, 480}}; struct yasl_app_desc desc = { .app_id = "org.example.hello", .window = &window, diff --git a/examples/notepad.c b/examples/notepad.c index ea74a27..0b331e8 100644 --- a/examples/notepad.c +++ b/examples/notepad.c @@ -14,7 +14,7 @@ * - Zoom In / Out / Reset (Ctrl+=, Ctrl+-, Ctrl+0) * - Mouse selection with click, drag, double-click (word), triple-click (line) * - Unsaved changes confirmation on exit - * - Real-time asynchronous background stats via yasl_post_event() + * - Real-time asynchronous background stats via yasl_display_post() * - 0% CPU consumption when idle (YASL_PACING_ON_DEMAND) */ @@ -211,7 +211,9 @@ struct editor { /* Background stats from async worker */ struct doc_stats stats; - bool worker_running; + _Atomic bool worker_running; + bool worker_started, worker_mutex_ready, worker_cond_ready; + uint64_t worker_surface_id; bool worker_dirty; pthread_t worker_tid; pthread_mutex_t worker_mutex; @@ -361,7 +363,10 @@ static void *worker_thread(void *arg) if (s) { s->words = word_count; s->chars = char_count; - yasl_post_event(ed->display, ed->surface, MSG_STATS_READY, s, sizeof(*s)); + if (!yasl_display_post(ed->display, &(struct yasl_message){ + .target_id = ed->worker_surface_id, .code = MSG_STATS_READY, + .payload = s, .size = sizeof(*s), .release = free})) + free(s); } } return NULL; @@ -580,7 +585,8 @@ static void editor_copy(struct editor *ed) snprintf(buf, (size_t)MAX_LINES * MAX_LINE_LEN, "%s\n", ed->lines[ed->cursor_row]); } - yasl_display_copy(ed->display, YASL_CLIPBOARD, buf); + if (!yasl_clipboard_set(ed->display, YASL_CLIPBOARD, buf)) + editor_set_status(ed, "Clipboard copy failed"); free(buf); editor_set_status(ed, "Copied to clipboard"); } @@ -997,18 +1003,23 @@ static enum yasl_app_result on_init(void *ud, struct yasl_display *d) struct yasl_window_spec spec = { .title = "Untitled - YASL Notepad", - .size = {960, 640}, + .size_logical = {960, 640}, }; ed->surface = yasl_surface_create_toplevel(d, &spec); - if (!ed->surface) return YASL_APP_QUIT; + if (!ed->surface) return YASL_APP_ERROR; + ed->worker_surface_id = yasl_surface_id(ed->surface); yasl_surface_set_pacing(ed->surface, YASL_PACING_ON_DEMAND); - yasl_app_add_timer(d, YASL_MS_TO_NS(500), on_blink_timer, ed); + if (!yasl_surface_add_timer(ed->surface, YASL_MS_TO_NS(500), on_blink_timer, ed)) + return YASL_APP_ERROR; ed->worker_running = true; - pthread_mutex_init(&ed->worker_mutex, NULL); - pthread_cond_init(&ed->worker_cond, NULL); - pthread_create(&ed->worker_tid, NULL, worker_thread, ed); + if (pthread_mutex_init(&ed->worker_mutex, NULL)) return YASL_APP_ERROR; + ed->worker_mutex_ready = true; + if (pthread_cond_init(&ed->worker_cond, NULL)) return YASL_APP_ERROR; + ed->worker_cond_ready = true; + if (pthread_create(&ed->worker_tid, NULL, worker_thread, ed)) return YASL_APP_ERROR; + ed->worker_started = true; if (ed->has_file) { editor_open_file(ed, ed->file_path); @@ -1041,6 +1052,12 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) break; case YASL_EVENT_PASTE: + if (ev->paste.status != YASL_TRANSFER_OK) { + editor_set_status(ed, ev->paste.status == YASL_TRANSFER_UNAVAILABLE + ? "Clipboard has no text" : "Clipboard transfer failed"); + yasl_surface_request_frame(ed->surface); + break; + } if (ev->text.data && ev->text.len > 0) { insert_text(ed, ev->text.data, ev->text.len); yasl_surface_request_frame(ed->surface); @@ -1167,7 +1184,8 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_V: - yasl_display_paste(ed->display, YASL_CLIPBOARD, ed->surface); + if (!yasl_clipboard_request(ed->surface, YASL_CLIPBOARD)) + editor_set_status(ed, "Clipboard paste failed"); return YASL_APP_CONTINUE; case YASL_KEY_Z: if (shift) editor_redo(ed); @@ -1397,7 +1415,6 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) if (ev->user.code == MSG_STATS_READY && ev->user.payload) { struct doc_stats *s = (struct doc_stats *)ev->user.payload; ed->stats = *s; - free(s); yasl_surface_request_frame(ed->surface); } break; @@ -1412,13 +1429,13 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) return YASL_APP_CONTINUE; } -static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { (void)s; struct editor *ed = ud; - int w = ctx->size.x; - int h = ctx->size.y; - uint32_t *pix = ctx->cpu.pixels; + int w = ctx->target.size.x; + int h = ctx->target.size.y; + uint32_t *pix = ctx->target.pixels; if (!pix || w <= 0 || h <= 0) return; int scale = (ctx->scale_120 >= 240 ? 2 : 1) + ed->zoom_offset; @@ -1554,7 +1571,8 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context ed->is_modified ? 0xfffab387 : status_accent, scale); } - yasl_frame_present_cpu(ctx, NULL, 0); + if (!yasl_frame_present(ctx, NULL, 0)) + yasl_app_exit(yasl_surface_display(s), YASL_APP_ERROR); } static void on_quit(void *ud, struct yasl_display *d) @@ -1562,12 +1580,14 @@ static void on_quit(void *ud, struct yasl_display *d) (void)d; struct editor *ed = ud; ed->worker_running = false; - pthread_mutex_lock(&ed->worker_mutex); - pthread_cond_broadcast(&ed->worker_cond); - pthread_mutex_unlock(&ed->worker_mutex); - pthread_join(ed->worker_tid, NULL); - pthread_mutex_destroy(&ed->worker_mutex); - pthread_cond_destroy(&ed->worker_cond); + if (ed->worker_started) { + pthread_mutex_lock(&ed->worker_mutex); + pthread_cond_broadcast(&ed->worker_cond); + pthread_mutex_unlock(&ed->worker_mutex); + pthread_join(ed->worker_tid, NULL); + } + if (ed->worker_cond_ready) pthread_cond_destroy(&ed->worker_cond); + if (ed->worker_mutex_ready) pthread_mutex_destroy(&ed->worker_mutex); } int main(int argc, char **argv) diff --git a/examples/simple_window.c b/examples/simple_window.c index fca2f75..5a8b314 100644 --- a/examples/simple_window.c +++ b/examples/simple_window.c @@ -22,12 +22,12 @@ static const char *device_name(enum yasl_pointer_device dev) } } -static void on_draw(void *ud, struct yasl_surface *s, struct yasl_frame_context *frame) +static void on_draw(void *ud, struct yasl_surface *s, struct yasl_frame *frame) { (void)ud; - int w = frame->size.x; - int h = frame->size.y; - uint32_t *pixels = frame->cpu.pixels; + int w = frame->target.size.x; + int h = frame->target.size.y; + uint32_t *pixels = frame->target.pixels; if (!pixels || w <= 0 || h <= 0) return; @@ -80,11 +80,12 @@ static void on_draw(void *ud, struct yasl_surface *s, struct yasl_frame_context } } - if (!yasl_frame_present_cpu(frame, NULL, 0)) { + if (!yasl_frame_present(frame, NULL, 0)) { fprintf(stderr, "Failed to present frame\n"); failed = true; } - if (failed || smoke_test) yasl_surface_destroy(s); + if (failed) yasl_app_exit(yasl_surface_display(s), YASL_APP_ERROR); + else if (smoke_test) yasl_surface_destroy(s); } static enum yasl_app_result on_init(void *ud, struct yasl_display *display) @@ -92,13 +93,13 @@ static enum yasl_app_result on_init(void *ud, struct yasl_display *display) (void)ud; struct yasl_window_spec window_spec = { .title = "YASL Interactive Window (Pen & Touch Enabled)", - .size = { .x = 640, .y = 480 }, + .size_logical = { .x = 640, .y = 480 }, }; struct yasl_surface *s = yasl_surface_create_toplevel(display, &window_spec); if (!s) { fprintf(stderr, "Failed to create surface\n"); failed = true; - return YASL_APP_QUIT; + return YASL_APP_ERROR; } yasl_surface_request_frame(s); diff --git a/examples/tablet.c b/examples/tablet.c index 01dcdfc..ba1a94f 100644 --- a/examples/tablet.c +++ b/examples/tablet.c @@ -148,7 +148,7 @@ static enum yasl_app_result input(void *ud, const struct yasl_event *event) if (event->type == YASL_EVENT_WINDOW_RESIZE) { if (!resize_canvas(app, event->resize.geometry)) { app->failed = true; - return YASL_APP_QUIT; + return YASL_APP_ERROR; } } else if (event->type == YASL_EVENT_INPUT_RESET || event->type == YASL_EVENT_POINTER_LEAVE || (event->type == YASL_EVENT_FOCUS && !event->focus.focused)) { @@ -233,18 +233,19 @@ static void text(struct yasl_framebuffer *fb, double dpi, int x, int y, } } -static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) +static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame *frame) { struct app *app = ud; - struct yasl_geometry geometry = {frame->size, frame->scale_120}; + struct yasl_geometry geometry = {frame->target.size, frame->scale_120}; if (!resize_canvas(app, geometry)) { fprintf(stderr, "Cannot allocate drawing canvas\n"); app->failed = true; + yasl_app_exit(yasl_surface_display(surface), YASL_APP_ERROR); yasl_surface_destroy(surface); app->surface = NULL; return; } - struct yasl_framebuffer target = {.size = frame->size, .pixels = frame->cpu.pixels}; + struct yasl_framebuffer target = {.size = frame->target.size, .pixels = frame->target.pixels}; struct yasl_framebuffer *fb = ⌖ memcpy(fb->pixels, app->canvas, (size_t)geometry.size.x * geometry.size.y * sizeof(uint32_t)); double dpi = (double)app->geometry.scale_120 / YASL_SCALE_UNIT; @@ -280,9 +281,10 @@ static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_conte for (int i = 0; i <= 64; i++) rect(fb, (int)lround(cx + tx * i / 64), (int)lround(cy + ty * i / 64), 2, 2, ACCENT); } - if (!yasl_frame_present_cpu(frame, NULL, 0)) { + if (!yasl_frame_present(frame, NULL, 0)) { app->failed = true; } + if (app->failed) yasl_app_exit(yasl_surface_display(surface), YASL_APP_ERROR); if (app->failed || app->smoke_test) { yasl_surface_destroy(surface); app->surface = NULL; @@ -295,13 +297,13 @@ static enum yasl_app_result init(void *ud, struct yasl_display *display) app->backend = yasl_display_backend(display); app->tablet_supported = (yasl_display_caps(display) & YASL_CAP_TABLET) != 0; struct yasl_window_spec spec = { - .title = "YASL Tablet - pressure and tilt", .size = {900, 640}, + .title = "YASL Tablet - pressure and tilt", .size_logical = {900, 640}, }; app->surface = yasl_surface_create_toplevel(display, &spec); if (!app->surface || !resize_canvas(app, yasl_surface_geometry(app->surface))) { fprintf(stderr, "Cannot create drawing surface\n"); app->failed = true; - return YASL_APP_QUIT; + return YASL_APP_ERROR; } yasl_surface_set_cursor(app->surface, YASL_CURSOR_HIDDEN); yasl_surface_request_frame(app->surface); diff --git a/include/yasl/app.h b/include/yasl/app.h index b7e1c13..78e62c4 100644 --- a/include/yasl/app.h +++ b/include/yasl/app.h @@ -21,74 +21,12 @@ extern "C" { typedef enum yasl_app_result { YASL_APP_CONTINUE = 0, /**< Continue running the application. */ YASL_APP_QUIT, /**< Request graceful termination of yasl_run(). */ + YASL_APP_ERROR, /**< Request orderly cleanup and a failing run result. */ } yasl_app_result; -/** Pacing mode governing when frame() callbacks are invoked. */ -typedef enum yasl_pacing_mode { - /** - * Reactive / on-demand presentation (ideal for IDEs, text editors, tools). - * The application sleeps at 0% CPU when idle. frame() is only invoked when - * explicitly requested via yasl_surface_request_frame() or upon OS window - * expose/resize events. - */ - YASL_PACING_ON_DEMAND = 0, - - /** - * Continuous vsync-aligned tick (ideal for game engines and animations). - * frame() is driven continuously at the display's native refresh rate - * (e.g. 60/120/144 Hz) via DisplayLink, Wayland frame callbacks, or loop timers. - */ - YASL_PACING_CONTINUOUS, -} yasl_pacing_mode; - -/** Frame context provided to yasl_app_desc.frame() during presentation. */ -struct yasl_frame_context { - struct yasl_surface *surface; - uint64_t frame_index; - uint64_t target_time_ns; /**< Monotonic timestamp aligned to display refresh. */ - struct yasl_vector2i size; /**< Surface content size in device pixels. */ - uint32_t scale_120; - - /** CPU software rendering target, if surface was created for CPU blitting. */ - struct { - uint32_t *pixels; /**< Writable premultiplied 0xAARRGGBB device pixels. */ - int stride_bytes; /**< Row stride in bytes. */ - } cpu; - - /** Native GPU rendering layer/surface, if surface was created for GPU rendering. */ - struct { - void *native_layer; /**< CAMetalLayer* on Cocoa, wl_surface* on Wayland, Window on X11. */ - } gpu; - - /** Internal handle used by yasl_frame_present_cpu. Applications must not touch. */ - void *internal_handle; -}; - -/** - * Present CPU software target pixels. - * @param ctx Active frame context passed to yasl_app_desc.frame(). - * @param dirty_rects Optional array of damaged rectangles in device pixels. - * Pass NULL or count <= 0 to indicate full-surface damage. - * @param count Number of rectangles in dirty_rects. - * @return true if accepted, false on presentation failure. - */ -YASL_API bool yasl_frame_present_cpu(struct yasl_frame_context *ctx, - const struct yasl_rect2i *dirty_rects, - int count); - -/** - * Configure the presentation pacing mode for a surface. - * @param s Surface. - * @param mode YASL_PACING_ON_DEMAND for reactive UI, YASL_PACING_CONTINUOUS for games. - */ -YASL_API void yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode); - -/** - * Mark a surface as needing a redraw on the next frame pass. - * Wakes the event loop if sleeping under YASL_PACING_ON_DEMAND. - * @param s Surface. - */ -YASL_API void yasl_surface_request_frame(struct yasl_surface *s); +/** Migration adapter; prefer yasl_frame_present() from yasl.h. */ +YASL_API bool yasl_frame_present_cpu(struct yasl_frame *frame, + const struct yasl_rect2i *dirty_rects, int count); /** * Safely post a custom user event to the main event loop from ANY thread. @@ -99,7 +37,9 @@ YASL_API void yasl_surface_request_frame(struct yasl_surface *s); * @param payload Arbitrary pointer passed through to the event. * @param size Optional payload size. * @return true if queued; false if memory failed or d is not run by yasl_run(). - * Applications that run their own loop use yasl_loop_post() instead. + * Migration adapter: keep s alive through the call, and stop producers before + * shutdown. Prefer yasl_display_post() with a captured ID and release callback + * in either loop style; legacy payloads have no cancellation cleanup. */ YASL_API bool yasl_post_event(struct yasl_display *d, struct yasl_surface *s, uint32_t code, void *payload, size_t size); @@ -159,15 +99,15 @@ struct yasl_app_desc { /** * Optional; called once after display and event machinery initialization. * Use this to create further windows and initialize application resources. - * Return YASL_APP_CONTINUE or YASL_APP_QUIT. + * Return YASL_APP_CONTINUE, YASL_APP_QUIT, or YASL_APP_ERROR. */ enum yasl_app_result (*init)(void *ud, struct yasl_display *d); /** - * Unified event handler for input, window and user events, in arrival order. + * Unified event handler for input, window and user events, in display-wide enqueue order. * Event payloads and borrowed strings are valid for the duration of the callback. * INPUT_RESET has no payload and requires clearing held application input. - * Return YASL_APP_CONTINUE or YASL_APP_QUIT. Optional; without it, a + * Return YASL_APP_CONTINUE, YASL_APP_QUIT, or YASL_APP_ERROR. Optional; without it, a * WINDOW_CLOSE request on any window stops yasl_run(). */ enum yasl_app_result (*event)(void *ud, const struct yasl_event *ev); @@ -176,7 +116,7 @@ struct yasl_app_desc { * Invoked when a surface needs to present a frame. * Synchronized automatically with display pacing and live window resizing. */ - void (*frame)(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx); + void (*frame)(void *ud, struct yasl_surface *s, struct yasl_frame *frame); /** * Called right before the display is closed and yasl_run() exits. @@ -185,6 +125,11 @@ struct yasl_app_desc { void (*quit)(void *ud, struct yasl_display *d); }; +/** Request orderly exit from any display-thread callback; ERROR returns failure. */ +YASL_API void yasl_app_exit(struct yasl_display *d, enum yasl_app_result result); +/** Borrow the optional desc.window surface during init or later; NULL if absent/destroyed. */ +YASL_API struct yasl_surface *yasl_app_window(struct yasl_display *d); + /** * Run the application with the inverted lifecycle runner. * Handles the display event loop, live-resizing, pacing, and cleanup. diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index f798304..889f70a 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -14,12 +14,15 @@ * 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. + * New window requests, clipboard operations, frame presentation, and posting + * document errno on failure. Event strings belong to the display queue. * * 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. + * See the application guide for the display-wide dispatch/poll/flush cycle + * and backend limits. Legacy surface polling is an input-only filtered adapter. */ #ifndef YASL_H #define YASL_H @@ -211,6 +214,8 @@ struct yasl_framebuffer { uint32_t *pixels; /** Target extent in device pixels. */ struct yasl_vector2i size; + /** Row stride in bytes, populated for the active draw callback. */ + int stride_bytes; }; /** One draw invocation. Do not retain its pointer after the callback returns. */ @@ -219,8 +224,20 @@ struct yasl_frame { struct yasl_surface *surface; /** Target to initialize and present before returning. */ struct yasl_framebuffer target; + /** Zero-based index of this surface draw. */ + uint64_t frame_index; + /** Estimated animation time; not confirmed presentation. */ + uint64_t target_time_ns; + /** Device scale numerator, with 120 representing 1x. */ + uint32_t scale_120; }; +/** Coalesced redraw policy, valid with either loop style. */ +typedef enum yasl_pacing_mode { + YASL_PACING_ON_DEMAND = 0, /**< Draw only when invalidated or exposed. */ + YASL_PACING_CONTINUOUS, /**< Draw continuously using native pacing or a timer. */ +} yasl_pacing_mode; + /* Drag and Drop */ /** Target notifications for YASL surface drags, separate from external file drops. */ enum yasl_drag_event { @@ -338,11 +355,18 @@ enum yasl_event_type { YASL_EVENT_DROP, /**< .text — dropped text/uri-list: CRLF-separated file URLs, not paths. */ YASL_EVENT_INPUT_RESET, /**< No payload. Input stream interrupted; clear held keys, buttons, strokes, IME. */ - /* Window and application: delivered only through yasl_app_desc.event(). - * Low-level users receive the same information via yasl_window_cbs. */ + /* Window and application: part of the display FIFO, as well as runner delivery. + * Legacy window callbacks also receive matching native notifications. */ YASL_EVENT_WINDOW_RESIZE, /**< .resize — size and/or scale changed; reallocate and redraw. */ YASL_EVENT_WINDOW_CLOSE, /**< No payload. User asked to close the window. */ - YASL_EVENT_USER, /**< .user — posted with yasl_post_event(). */ + YASL_EVENT_USER, /**< .user — posted application message. */ + YASL_EVENT_WINDOW_STATE, /**< .state — reported state changed. */ + YASL_EVENT_DRAG, /**< .drag — surface-drag target notification. */ + YASL_EVENT_DROP_HOVER, /**< .drop — external drag hover/leave. */ + YASL_EVENT_COMPOSITION, /**< .composition — preedit snapshot, including ending. */ + YASL_EVENT_DISPLAY_QUIT, /**< No payload; system requested application quit. */ + YASL_EVENT_DISPLAY_REOPEN, /**< No payload; system requested activation. */ + YASL_EVENT_DISPLAY_LOST, /**< No payload; display connection failed. */ }; /** Physical keyboard transition with optional layout labels. @@ -401,6 +425,43 @@ struct yasl_text_event { size_t len; }; +/** Clipboard transfer outcome. */ +enum yasl_transfer_status { + YASL_TRANSFER_OK = 0, /**< Text received successfully, including empty text. */ + YASL_TRANSFER_UNAVAILABLE, /**< No text selection is available. */ + YASL_TRANSFER_FAILED, /**< Transfer or decoding failed. */ +}; + +/** Clipboard completion; data/len alias the legacy text payload. */ +struct yasl_paste_event { + const char *data; /**< Borrowed NUL-terminated UTF-8; empty on failure. */ + size_t len; /**< Byte length excluding NUL. */ + uint64_t request_id; /**< Zero for a legacy paste request. */ + enum yasl_transfer_status status; /**< Completion outcome. */ +}; + +/** Borrowed UTF-8 preedit snapshot, valid until the next successful display poll. */ +struct yasl_composition_event { + const char *data; /**< Borrowed NUL-terminated UTF-8 snapshot. */ + size_t len; /**< Byte length excluding NUL. */ + int begin; /**< UTF-8 byte offset, or -1 if unavailable. */ + int end; /**< UTF-8 byte offset, or -1 if unavailable. */ + bool active; /**< Whether composition text is present. */ +}; + +/** Reported window-state change. */ +struct yasl_state_event { uint32_t flags; /**< Current YASL_STATE_* flags. */ }; +/** Surface-drag target notification; position is in device pixels. */ +struct yasl_drag_target_event { + enum yasl_drag_event action; /**< Hover, leave, or drop. */ + struct yasl_vector2i pos; /**< Surface-relative device pixels. */ +}; +/** External drag hover notification; position is in device pixels. */ +struct yasl_drop_hover_event { + enum yasl_drop_event action; /**< Hover or leave. */ + struct yasl_vector2i pos; /**< Surface-relative device pixels. */ +}; + /** Keyboard focus transition. */ struct yasl_focus_event { /** true on focus gain, false on loss. */ @@ -434,10 +495,19 @@ struct yasl_event { struct yasl_surface *surface; /** Monotonic timestamp in nanoseconds, same clock as yasl_loop_now_ns(). */ uint64_t time_ns; + /** Increasing display-wide enqueue order; zero for standalone private queues. */ + uint64_t sequence; + /** Stable recipient identity; zero for display-wide events. */ + uint64_t surface_id; union { struct yasl_key_event key; /**< KEY */ struct yasl_pointer_event pointer; /**< POINTER_* */ - struct yasl_text_event text; /**< TEXT, PASTE, DROP */ + struct yasl_text_event text; /**< TEXT, legacy PASTE, DROP */ + struct yasl_paste_event paste; /**< PASTE */ + struct yasl_composition_event composition; /**< COMPOSITION */ + struct yasl_state_event state; /**< WINDOW_STATE */ + struct yasl_drag_target_event drag; /**< DRAG */ + struct yasl_drop_hover_event drop; /**< DROP_HOVER */ struct yasl_focus_event focus; /**< FOCUS */ struct yasl_resize_event resize; /**< WINDOW_RESIZE */ struct yasl_user_event user; /**< USER */ @@ -633,15 +703,20 @@ struct yasl_window_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; + union { + struct yasl_vector2i size_logical; /**< Requested logical-pixel extent. */ + struct yasl_vector2i size; /**< Migration alias for size_logical. */ + }; /** Request initial fullscreen; actual state is reported asynchronously. */ bool fullscreen; /** Permit unpaced presentation where supported; a hint, not a guarantee. */ bool tearing; /** Copied optional window callbacks; NULL disables them. */ const struct yasl_window_cbs *cbs; - /** Borrowed window callback userdata. */ + /** Borrowed window callback userdata; ignored by the runner. */ void *ud; + /** Borrowed application data, independent of callbacks and available in either loop style. */ + void *userdata; }; /** Popup options, entirely in device pixels, including size. @@ -718,14 +793,15 @@ 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 s Surface. * @param cbs Table copied by value; NULL removes all callbacks. NULL members are ignored. * @param ud Borrowed userdata passed to the installed callbacks. + * @return true if replaced; false with EINVAL for NULL, EBUSY on a runner-owned surface. * This call only replaces callbacks; it does not invoke them. * 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, +YASL_API bool yasl_surface_set_callbacks(struct yasl_surface *s, const struct yasl_window_cbs *cbs, void *ud); /** * Replace the window title with copied UTF-8 text. @@ -759,21 +835,16 @@ YASL_API void yasl_surface_schedule_redraw(struct yasl_surface *s); */ 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 input event from a surface's 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. Only input - * types (KEY through INPUT_RESET) are returned. Read the union member selected - * by type. 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. +/** + * Migration adapter: consume one input event for a surface from the display FIFO. + * Popup input can belong to the root queue; inspect ev->surface. Returns false + * for empty/invalid arguments, leaving ev unchanged. Strings remain borrowed + * until the next successful display or surface poll, or display close. Pending + * recipient events are canceled on destruction. Prefer yasl_display_poll_event() + * to receive window, composition, and display events as well as input. This + * input-only adapter discards pending non-input events across the display + * (legacy callbacks already delivered lifecycle changes). Do not mix + * the two polling styles in one application. Use the display thread. */ YASL_API bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev); /** @@ -825,6 +896,73 @@ YASL_API bool yasl_surface_read_pixels(struct yasl_surface *s, uint32_t *out, si */ YASL_API bool yasl_surface_set_pointer_mode(struct yasl_surface *s, enum yasl_pointer_mode mode); +/** + * Consume one event from the display-wide FIFO without waiting. + * Returns false for an empty queue or invalid arguments, leaving ev unchanged. + * Strings remain valid until the next successful poll or display close. + * Surface destruction cancels undelivered events, not the last polled payload. + * Use the display thread. Do not mix display and surface polling in one app. + */ +YASL_API bool yasl_display_poll_event(struct yasl_display *d, struct yasl_event *ev); + +/** Stable nonzero identity within a display; zero for NULL. Use on the display thread. */ +YASL_API uint64_t yasl_surface_id(const struct yasl_surface *s); +/** Store borrowed application data independently of callback userdata. */ +YASL_API void yasl_surface_set_userdata(struct yasl_surface *s, void *userdata); +/** Read borrowed application data, or NULL for NULL; use on the display thread. */ +YASL_API void *yasl_surface_userdata(const struct yasl_surface *s); + +/** Thread-safe message descriptor. Zero target_id means display-wide. */ +struct yasl_message { + uint64_t target_id; /**< Surface identity, or zero for the display. */ + uint32_t code; /**< Application-defined message code. */ + void *payload; /**< Borrowed until release; passed through unchanged. */ + size_t size; /**< Application-defined payload length. */ + /** Optional cleanup on display thread after delivery/cancellation, exactly once. + * May post messages, but must not dispatch, poll, or destroy YASL objects. */ + void (*release)(void *payload); +}; +/** + * Copy and enqueue a message from any thread; the display must stay alive. + * Stop producer threads before closing the display. On success release runs + * once after consumption or cancellation. On failure ownership stays with the + * caller; errno is EINVAL, ENOENT (retired identity), ENOMEM, or EOVERFLOW. + */ +YASL_API bool yasl_display_post(struct yasl_display *d, const struct yasl_message *message); + +/** + * Present the active frame once. NULL/0 means full damage; non-NULL/0 means none. + * Validate all rectangles; excess rectangles fall back to full damage. + * Returns false with EINVAL for invalid/stale targets, EALREADY for a consumed + * frame, or errno from the backend (EIO if unspecified). + * Frame and pixels expire at callback return; use the display thread. + */ +YASL_API bool yasl_frame_present(struct yasl_frame *frame, + const struct yasl_rect2i *rects, size_t count); +/** Coalesce a redraw request; use on the display thread. NULL is ignored. */ +YASL_API void yasl_surface_request_frame(struct yasl_surface *s); +/** Set drawing policy for either loop style. Returns false with errno on failure. */ +YASL_API bool yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode); + +/** Request a positive logical-pixel size. Success means issued, not configured. */ +YASL_API bool yasl_surface_set_size(struct yasl_surface *s, struct yasl_vector2i size_logical); +/** Request fullscreen state; returns acceptance, not confirmation. */ +YASL_API bool yasl_surface_set_fullscreen(struct yasl_surface *s, bool enabled); +/** Request maximized state; returns acceptance, not confirmation. */ +YASL_API bool yasl_surface_set_maximized(struct yasl_surface *s, bool enabled); +/** Request minimization. Window requests set errno on failure; display thread only. */ +YASL_API bool yasl_surface_minimize(struct yasl_surface *s); + +/** Offer copied UTF-8 text. Returns false with errno if invalid/unsupported/failed. */ +YASL_API bool yasl_clipboard_set(struct yasl_display *d, enum yasl_selection selection, + const char *text); +/** + * Request text; returns a nonzero ID or zero with errno. One PASTE completion + * follows each accepted request unless its surface/display is destroyed. + * Inspect paste.status even when data is empty. Use the display thread. + */ +YASL_API uint64_t yasl_clipboard_request(struct yasl_surface *s, enum yasl_selection selection); + /* Current device-pixel size and scale, matching the configure callback. */ /** * Read current device-pixel size and scale together. diff --git a/meson.build b/meson.build index 5049872..9a59016 100644 --- a/meson.build +++ b/meson.build @@ -102,6 +102,27 @@ yasl_dep = declare_dependency( ) meson.override_dependency('yasl', yasl_dep) +install_headers( + 'include/yasl/yasl.h', 'include/yasl/app.h', 'include/yasl/loop.h', + 'include/yasl/timing.h', 'include/yasl/keycodes.h', 'include/yasl/native.h', + 'include/yasl/desktop.h', subdir: 'yasl', +) +if has_wayland + install_headers('include/yasl/native/wayland.h', subdir: 'yasl/native') +endif +if has_x11 + install_headers('include/yasl/native/x11.h', subdir: 'yasl/native') +endif +if has_cocoa + install_headers('include/yasl/native/cocoa.h', subdir: 'yasl/native') +endif +pkgconfig = import('pkgconfig') +pkgconfig.generate(yasl_lib, name: 'yasl', + description: 'Desktop surfaces for application-owned rendering', + libraries_private: deps, +) + + hello_example = executable( 'yasl-hello', 'examples/hello.c', diff --git a/src/common/app.c b/src/common/app.c index 435d020..63cf2ce 100644 --- a/src/common/app.c +++ b/src/common/app.c @@ -1,451 +1,153 @@ #include "yasl/app.h" #include "common/app_internal.h" #include "common/backend.h" -#include "platform/sync.h" #include -#include -#include - -struct user_event_node { - struct yasl_surface *surface; - uint32_t code; - void *payload; - size_t size; - struct user_event_node *next; -}; - -/* `surface` is NULL once the surface is destroyed. A callback may still - * hold the entry then, so only app_runner_free_dead() frees it. */ -struct app_surface { - struct yasl_surface *surface; - struct app_runner *runner; - yasl_pacing_mode pacing; - uint32_t last_scale_120; - uint64_t frame_index; - bool presented; - struct app_surface *next; -}; struct app_runner { struct yasl_display *display; const struct yasl_app_desc *desc; - bool running; - bool failed; - bool had_surfaces; - int surface_count; - struct app_surface *surfaces; - - yasl_mutex user_event_mutex; - struct user_event_node *user_event_head; - struct user_event_node *user_event_tail; + uint64_t initial_window; + bool running, failed, had_surfaces; + int error; }; -/* Deliver one event to the application; a QUIT result stops the runner. */ -static void emit(struct app_runner *ar, const struct yasl_event *ev) +void yasl_app_exit(struct yasl_display *d, enum yasl_app_result result) { - if (ar->desc->event && ar->desc->event(ar->desc->userdata, ev) == YASL_APP_QUIT) - ar->running = false; -} - -static void app_runner_surface_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) -{ - struct app_surface *as = ud; - struct app_runner *ar = as ? as->runner : NULL; - if (!ar || !ar->desc || !ar->desc->frame) - return; - - as->presented = false; - - struct yasl_frame_context ctx = { - .surface = s, - .frame_index = as->frame_index++, - .target_time_ns = yasl_surface_frame_time_ns(s, yasl_loop_now_ns()), - .size = frame->target.size, - .scale_120 = s->scale_120, - .cpu = { - .pixels = frame->target.pixels, - .stride_bytes = frame->target.size.x * (int)sizeof(uint32_t), - }, - .gpu = { - .native_layer = NULL, - }, - .internal_handle = frame, - }; - - struct yasl_native_frame nf; - if (yasl_surface_get_native_frame(s, &frame->target, &nf)) { - ctx.gpu.native_layer = nf.drawable; - } - - ar->desc->frame(ar->desc->userdata, s, &ctx); - - if (as->surface && as->pacing == YASL_PACING_CONTINUOUS && ar->running) - yasl_surface_schedule_redraw(s); + if (!d || !d->app_runner || result == YASL_APP_CONTINUE) return; + struct app_runner *runner = d->app_runner; + runner->running = false; + if (result == YASL_APP_ERROR) { runner->failed = true; runner->error = EIO; } } -static void app_runner_surface_configure(struct yasl_surface *s, struct yasl_geometry geom, void *ud) +struct yasl_surface *yasl_app_window(struct yasl_display *d) { - struct app_surface *as = ud; - struct app_runner *ar = as ? as->runner : NULL; - if (!ar || !ar->desc || !ar->desc->event) - return; - - struct yasl_event ev = { - .type = YASL_EVENT_WINDOW_RESIZE, - .surface = s, - .time_ns = yasl_loop_now_ns(), - .resize = { - .geometry = geom, - .scale_changed = as->last_scale_120 && as->last_scale_120 != geom.scale_120, - }, - }; - as->last_scale_120 = geom.scale_120; - emit(ar, &ev); - - if (as->surface) - yasl_surface_schedule_redraw(s); + if (!d || !d->app_runner) return NULL; + struct app_runner *runner = d->app_runner; + return runner->initial_window ? yasl_events_find(d, runner->initial_window) : NULL; } -static void app_runner_surface_close(struct yasl_surface *s, void *ud) +static void dispatch_events(struct app_runner *runner) { - struct app_surface *as = ud; - struct app_runner *ar = as ? as->runner : NULL; - if (!ar || !ar->desc) - return; - /* Without an event callback nothing could answer the request, so quit. */ - if (!ar->desc->event) { - ar->running = false; - return; + struct yasl_event event; + while (runner->running && yasl_display_poll_event(runner->display, &event)) { + if (runner->desc->event) + yasl_app_exit(runner->display, runner->desc->event(runner->desc->userdata, &event)); + else if (event.type == YASL_EVENT_WINDOW_CLOSE) + yasl_app_exit(runner->display, YASL_APP_QUIT); + if (event.type == YASL_EVENT_DISPLAY_QUIT) + yasl_app_exit(runner->display, YASL_APP_QUIT); + if (event.type == YASL_EVENT_DISPLAY_LOST) + yasl_app_exit(runner->display, YASL_APP_ERROR); } - - emit(ar, &(struct yasl_event){ - .type = YASL_EVENT_WINDOW_CLOSE, - .surface = s, - .time_ns = yasl_loop_now_ns(), - }); -} - -void app_runner_register_surface(void *runner, struct yasl_surface *s) -{ - if (!runner || !s) - return; - struct app_runner *ar = runner; - struct app_surface *as = calloc(1, sizeof(*as)); - if (!as) - return; - - as->surface = s; - as->runner = ar; - as->pacing = YASL_PACING_ON_DEMAND; - as->last_scale_120 = s->scale_120; - s->app_surface = as; - - as->next = ar->surfaces; - ar->surfaces = as; - ar->surface_count++; - ar->had_surfaces = true; - - struct yasl_window_cbs cbs = { - .draw = app_runner_surface_draw, - .configure = app_runner_surface_configure, - .close = app_runner_surface_close, - }; - yasl_surface_set_callbacks(s, &cbs, as); } -void app_runner_unregister_surface(void *runner, struct yasl_surface *s) +static void draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) { - if (!runner || !s || !s->app_surface) - return; - struct app_runner *ar = runner; - struct app_surface *as = s->app_surface; - as->surface = NULL; - s->app_surface = NULL; - ar->surface_count--; -} - -/* Call only when no callback runs. */ -static void app_runner_free_dead(struct app_runner *ar) -{ - for (struct app_surface **p = &ar->surfaces; *p;) { - struct app_surface *as = *p; - if (as->surface) { - p = &as->next; - continue; - } - *p = as->next; - free(as); - } + struct app_runner *runner = ud; + uint64_t id = s->id; + dispatch_events(runner); + /* Draining events can destroy the surface whose native draw is active. */ + struct yasl_surface *live = yasl_events_find(runner->display, id); + if (live && runner->running && runner->desc->frame) + runner->desc->frame(runner->desc->userdata, live, frame); } -bool yasl_frame_present_cpu(struct yasl_frame_context *ctx, - const struct yasl_rect2i *dirty_rects, - int count) +void app_runner_register_surface(void *data, struct yasl_surface *s) { - if (!ctx || !ctx->surface || !ctx->internal_handle) - return false; - - struct yasl_frame *frame = (struct yasl_frame *)ctx->internal_handle; - struct yasl_damage_list damage; - if (!dirty_rects || count <= 0) { - damage.n = -1; - damage.bounds = (struct yasl_rect2i){0}; - } else { - damage.n = count > YASL_DAMAGE_MAX ? YASL_DAMAGE_MAX : count; - damage.bounds = (struct yasl_rect2i){0}; - for (int i = 0; i < damage.n; i++) { - damage.rects[i] = dirty_rects[i]; - } - } - - return yasl_surface_present(ctx->surface, &frame->target, &damage); + struct app_runner *runner = data; + struct yasl_window_cbs callbacks = {.draw = draw}; + yasl_surface_set_callbacks(s, &callbacks, runner); + s->app_surface = runner; + runner->had_surfaces = true; } -void yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode) +void app_runner_unregister_surface(void *data, struct yasl_surface *s) { - if (!s || !s->app_surface) - return; - struct app_surface *as = s->app_surface; - as->pacing = mode; - if (mode == YASL_PACING_CONTINUOUS) { - yasl_surface_schedule_redraw(s); - } + (void)data; + s->app_surface = NULL; } -void yasl_surface_request_frame(struct yasl_surface *s) +bool yasl_frame_present_cpu(struct yasl_frame *frame, const struct yasl_rect2i *rects, int count) { - if (!s) - return; - yasl_surface_schedule_redraw(s); - if (s->display && s->display->loop) { - yasl_loop_kick(s->display->loop); - } + /* Preserve the migration adapter's old NULL/nonpositive full-damage rule. */ + return yasl_frame_present(frame, count <= 0 ? NULL : rects, count <= 0 ? 0 : (size_t)count); } bool yasl_post_event(struct yasl_display *d, struct yasl_surface *s, uint32_t code, void *payload, size_t size) { - if (!d || !d->app_runner) - return false; - struct app_runner *ar = d->app_runner; - - struct user_event_node *node = malloc(sizeof(*node)); - if (!node) - return false; - node->surface = s; - node->code = code; - node->payload = payload; - node->size = size; - node->next = NULL; - - yasl_mutex_lock(&ar->user_event_mutex); - if (ar->user_event_tail) { - ar->user_event_tail->next = node; - ar->user_event_tail = node; - } else { - ar->user_event_head = node; - ar->user_event_tail = node; - } - yasl_mutex_unlock(&ar->user_event_mutex); - - if (d->loop) { - yasl_loop_kick(d->loop); - } - return true; + if (!d || !d->app_runner) { errno = EINVAL; return false; } + /* Legacy callers must keep s alive through this call. New code passes an ID. */ + return yasl_display_post(d, &(struct yasl_message){ + .target_id = s ? yasl_surface_id(s) : 0, .code = code, .payload = payload, .size = size, + }); } -int yasl_app_watch_fd(struct yasl_display *d, int fd, short poll_events, - int (*cb)(int fd, short revents, void *ud), void *ud) +int yasl_app_watch_fd(struct yasl_display *d, int fd, short events, + int (*cb)(int, short, void *), void *ud) { - if (!d || !d->loop) - return -1; - return yasl_loop_add_fd(d->loop, fd, poll_events, cb, ud); + if (!d || !d->loop) { errno = EINVAL; return -1; } + return yasl_loop_add_fd(d->loop, fd, events, cb, ud); } - -void yasl_app_unwatch_fd(struct yasl_display *d, int watch_id) +void yasl_app_unwatch_fd(struct yasl_display *d, int id) { - if (!d || !d->loop) - return; - yasl_loop_del_fd(d->loop, watch_id); + if (d && d->loop) yasl_loop_del_fd(d->loop, id); } - -yasl_timer_id yasl_app_add_timer(struct yasl_display *d, uint64_t interval_ns, - yasl_timer_cb cb, void *ud) +yasl_timer_id yasl_app_add_timer(struct yasl_display *d, uint64_t interval, yasl_timer_cb cb, void *ud) { - if (!d || !d->loop) - return 0; - return yasl_loop_add_timer(d->loop, interval_ns, cb, ud); + if (!d || !d->loop) return 0; + return yasl_loop_add_timer(d->loop, interval, cb, ud); } - void yasl_app_del_timer(struct yasl_display *d, yasl_timer_id id) { - if (!d || !d->loop) - return; - yasl_loop_del_timer(d->loop, id); -} - -static void app_runner_dispatch_user_events(struct app_runner *ar) -{ - yasl_mutex_lock(&ar->user_event_mutex); - struct user_event_node *node = ar->user_event_head; - ar->user_event_head = NULL; - ar->user_event_tail = NULL; - yasl_mutex_unlock(&ar->user_event_mutex); - - while (node && ar->running) { - struct user_event_node *next = node->next; - emit(ar, &(struct yasl_event){ - .type = YASL_EVENT_USER, - .surface = node->surface, - .time_ns = yasl_loop_now_ns(), - .user = {.code = node->code, .payload = node->payload, .size = node->size}, - }); - free(node); - node = next; - } - while (node) { - struct user_event_node *next = node->next; - free(node); - node = next; - } -} - -static void app_runner_dispatch_input_events(struct app_runner *ar) -{ - for (struct app_surface *as = ar->surfaces; as && ar->running; as = as->next) { - struct yasl_event ev; - while (ar->running && as->surface && yasl_surface_poll_event(as->surface, &ev)) { - if (!ev.surface) - ev.surface = as->surface; - emit(ar, &ev); - } - } -} - -static void app_runner_quit(void *ud) -{ - ((struct app_runner *)ud)->running = false; -} - -static void app_runner_lost(void *ud) -{ - struct app_runner *runner = ud; - runner->failed = true; - runner->running = false; + if (d && d->loop) yasl_loop_del_timer(d->loop, id); } int yasl_run(const struct yasl_app_desc *desc) { - if (!desc) - return -1; - - struct app_runner runner = {.desc = desc, .running = true}; - const struct yasl_display_cbs callbacks = { - .quit = app_runner_quit, .lost = app_runner_lost, - }; - struct yasl_display_spec dspec = { - .app_id = desc->app_id, - .backend = desc->backend, - .cbs = &callbacks, - .ud = &runner, - }; - struct yasl_display *d = yasl_display_open(NULL, &dspec); - if (!d) - return -1; - - runner.display = d; - int sync_error = yasl_mutex_init(&runner.user_event_mutex); - if (sync_error) { - yasl_display_close(d); - errno = sync_error; - return -1; - } + if (!desc) { errno = EINVAL; return -1; } + struct yasl_display *d = yasl_display_open(NULL, &(struct yasl_display_spec){ + .app_id = desc->app_id, .backend = desc->backend, + }); + if (!d) return -1; + struct app_runner runner = {.display = d, .desc = desc, .running = true}; d->app_runner = &runner; - if (desc->window) { struct yasl_surface *s = yasl_surface_create_toplevel(d, desc->window); if (!s) { - int create_error = errno; + int error = errno; d->app_runner = NULL; - yasl_mutex_destroy(&runner.user_event_mutex); yasl_display_close(d); - errno = create_error; + errno = error; return -1; } + runner.initial_window = s->id; yasl_surface_request_frame(s); } - - if (desc->init && desc->init(desc->userdata, d) == YASL_APP_QUIT) { - runner.running = false; - } - - uint32_t caps = yasl_display_caps(d); - bool has_frame_pacing = (caps & YASL_CAP_FRAME_PACING) != 0; - + if (desc->init) yasl_app_exit(d, desc->init(desc->userdata, d)); while (runner.running) { - app_runner_free_dead(&runner); - if (runner.had_surfaces && runner.surface_count == 0) { - runner.running = false; - break; - } - - bool any_continuous = false; - for (struct app_surface *as = runner.surfaces; as; as = as->next) { - if (as->pacing == YASL_PACING_CONTINUOUS) - any_continuous = true; - } - - int timeout_ms = -1; - if (any_continuous) { - timeout_ms = has_frame_pacing ? -1 : 16; - } - - int res = yasl_display_dispatch(d, timeout_ms); - if (res < 0) { + dispatch_events(&runner); + if (!runner.running || (runner.had_surfaces && !d->surfaces)) break; + if (yasl_display_dispatch(d, -1) < 0) { + int dispatch_error = errno; + dispatch_events(&runner); runner.failed = true; + runner.error = dispatch_error; break; } - - app_runner_dispatch_user_events(&runner); - app_runner_dispatch_input_events(&runner); - - if (runner.running) { - yasl_display_flush(d); - } - - if (!has_frame_pacing && any_continuous && runner.running) { - for (struct app_surface *as = runner.surfaces; as; as = as->next) { - if (as->surface && as->pacing == YASL_PACING_CONTINUOUS) { - yasl_surface_schedule_redraw(as->surface); - } - } - } - } - - if (desc->quit) { - desc->quit(desc->userdata, d); - } - - /* Destroy any surfaces that were not explicitly destroyed by the application */ - for (struct app_surface *as = runner.surfaces; as; as = as->next) { - if (as->surface) - yasl_surface_destroy(as->surface); - } - app_runner_free_dead(&runner); - + dispatch_events(&runner); + if (runner.running) yasl_display_flush(d); + } + int queue_error = yasl_events_error(d); + if (queue_error) { runner.failed = true; runner.error = queue_error; } + int error = runner.error; + if (desc->quit) desc->quit(desc->userdata, d); + /* Backend registries create children after parents: reverse creation order + * ensures child popups are destroyed before their parent. */ + while (d->surfaces) yasl_surface_destroy(d->surfaces); d->app_runner = NULL; - - yasl_mutex_lock(&runner.user_event_mutex); - struct user_event_node *node = runner.user_event_head; - while (node) { - struct user_event_node *next = node->next; - free(node); - node = next; - } - yasl_mutex_unlock(&runner.user_event_mutex); - yasl_mutex_destroy(&runner.user_event_mutex); - yasl_display_close(d); + if (runner.failed) errno = error; return runner.failed ? -1 : 0; } diff --git a/src/common/backend.c b/src/common/backend.c index bb5ae33..a81e1e7 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -1,6 +1,7 @@ #include "backend.h" #include "loop.h" #include "common/app_internal.h" +#include "common/utf8.h" #include #include @@ -33,6 +34,15 @@ static struct yasl_display *open_backend(enum yasl_backend backend, errno = 0; struct yasl_display *d = ops->display_open(loop, spec); if (!d && !errno) errno = ENODEV; + if (d) { + d->callback_ud = spec ? spec->ud : NULL; + if (!yasl_events_open(d)) { + int error = errno; + ops->display_close(d); + errno = error; + return NULL; + } + } return d; } @@ -70,6 +80,7 @@ void yasl_display_close(struct yasl_display *d) { if (!d || !d->ops->display_close) return; + yasl_events_close(d); d->ops->display_close(d); } @@ -90,8 +101,13 @@ int yasl_display_dispatch(struct yasl_display *d, int timeout_ms) errno = ENOTSUP; return -1; } + int queue_error = yasl_events_error(d); + if (queue_error) { errno = queue_error; return -1; } + if (yasl_events_pending(d)) timeout_ms = 0; errno = 0; int result = d->ops->display_dispatch_timeout(d, timeout_ms); + queue_error = yasl_events_error(d); + if (queue_error) { errno = queue_error; return -1; } if (result < 0 && !errno) errno = EIO; return result; } @@ -175,7 +191,14 @@ struct yasl_surface *yasl_create_toplevel(struct yasl_display *d, errno = 0; struct yasl_surface *s = d->ops->surface_create_toplevel(d, spec, graphics_device); if (!s && !errno) errno = ENODEV; + if (s && !yasl_events_register(s)) { + int error = errno; + d->ops->surface_destroy(s); + errno = error; + return NULL; + } if (s) { + s->userdata = spec->userdata; /* Native creation may dispatch events. Attach this window's callbacks * only after it finishes, so callers can store the returned surface. */ if (d->app_runner) @@ -218,8 +241,16 @@ struct yasl_surface *yasl_surface_create_popup(struct yasl_surface *parent, cons errno = 0; struct yasl_surface *s = parent->display->ops->surface_create_popup(parent, spec); if (!s && !errno) errno = ENODEV; - if (s && parent->display->app_runner) { - app_runner_register_surface(parent->display->app_runner, s); + if (s) { + s->popup = true; + if (!yasl_events_register(s)) { + int error = errno; + parent->display->ops->surface_destroy(s); + errno = error; + return NULL; + } + if (parent->display->app_runner) + app_runner_register_surface(parent->display->app_runner, s); } return s; } @@ -248,6 +279,7 @@ void yasl_surface_destroy(struct yasl_surface *s) if (s->app_surface && s->display->app_runner) { app_runner_unregister_surface(s->display->app_runner, s); } + yasl_events_unregister(s); yasl_surface_clear_timers(s); input_queue_clear(&s->input); s->display->ops->surface_destroy(s); @@ -258,14 +290,18 @@ struct yasl_display *yasl_surface_display(const struct yasl_surface *s) return s ? s->display : NULL; } -void yasl_surface_set_callbacks(struct yasl_surface *s, const struct yasl_window_cbs *cbs, void *ud) +bool yasl_surface_set_callbacks(struct yasl_surface *s, const struct yasl_window_cbs *cbs, void *ud) { - if (!s) - return; + if (!s) { errno = EINVAL; return false; } + if (s->display && s->display->app_runner && s->app_surface) { + errno = EBUSY; + return false; + } s->callbacks = cbs ? *cbs : (struct yasl_window_cbs){0}; s->ud = ud; if (s->display && s->display->ops && s->display->ops->surface_set_callbacks) s->display->ops->surface_set_callbacks(s, cbs, ud); + return true; } void yasl_surface_set_title(struct yasl_surface *s, const char *title) @@ -317,6 +353,8 @@ bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev) { if (!s || !ev) return false; + if (s->display && s->display->events) + return yasl_events_poll_surface(s, ev); if (s->display && s->display->ops && s->display->ops->surface_poll_event) return s->display->ops->surface_poll_event(s, ev); return input_queue_pop(&s->input, ev); @@ -525,3 +563,177 @@ struct yasl_loop *yasl_display_loop(struct yasl_display *d) { return d ? d->loop : NULL; } + +bool yasl_frame_present(struct yasl_frame *frame, const struct yasl_rect2i *rects, size_t count) +{ + if (!frame || !frame->surface || (!rects && count)) { errno = EINVAL; return false; } + struct yasl_surface *s = frame->surface; + if (!s->drawing || s->active_frame != frame) { errno = EINVAL; return false; } + if (s->frame_presented) { errno = EALREADY; return false; } + for (size_t i = 0; i < count; i++) { + if (rects[i].width < 0 || rects[i].height < 0) { errno = EINVAL; return false; } + } + struct yasl_damage_list damage = {0}; + if (!rects || count > YASL_DAMAGE_MAX) damage.n = -1; + else { + damage.n = (int)count; + for (size_t i = 0; i < count; i++) damage.rects[i] = rects[i]; + } + errno = 0; + bool ok = yasl_surface_present(s, &frame->target, &damage); + if (ok) s->frame_presented = true; + if (!ok && !errno) errno = EIO; + return ok; +} + +void yasl_surface_request_frame(struct yasl_surface *s) +{ + if (!s) return; + yasl_surface_schedule_redraw(s); + if (s->display->loop) yasl_loop_kick(s->display->loop); +} + +static uint64_t pacing_tick(void *ud, yasl_timer_id id, uint64_t interval) +{ + (void)id; (void)interval; + struct yasl_surface *s = ud; + yasl_surface_request_frame(s); + double refresh = yasl_surface_timing(s).refresh_ns; + return refresh >= 1000000.0 && refresh <= 1000000000.0 + ? (uint64_t)refresh : 16666667; +} + +bool yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode) +{ + if (!s || (mode != YASL_PACING_ON_DEMAND && mode != YASL_PACING_CONTINUOUS)) { + errno = EINVAL; + return false; + } + if (s->pacing_timer) { + yasl_surface_del_timer(s, s->pacing_timer); + s->pacing_timer = 0; + } + if (mode == YASL_PACING_CONTINUOUS && !(yasl_display_caps(s->display) & YASL_CAP_FRAME_PACING)) { + s->pacing_timer = yasl_surface_add_timer(s, 16666667, pacing_tick, s); + if (!s->pacing_timer) { + s->pacing = YASL_PACING_ON_DEMAND; + errno = ENOMEM; + return false; + } + } + s->pacing = mode; + if (mode == YASL_PACING_CONTINUOUS) yasl_surface_request_frame(s); + return true; +} + +void yasl_surface_draw(struct yasl_surface *s, struct yasl_frame *frame) +{ + if (!s || !frame || !s->callbacks.draw) return; + struct yasl_display *d = s->display; + uint64_t id = s->id; + frame->surface = s; + frame->target.stride_bytes = frame->target.size.x * (int)sizeof(uint32_t); + frame->scale_120 = s->scale_120; + frame->frame_index = s->draw_index++; + frame->target_time_ns = yasl_surface_frame_time_ns(s, yasl_loop_now_ns()); + s->drawing = true; + s->frame_presented = false; + s->active_frame = frame; + s->callbacks.draw(s, frame, s->ud); + /* The callback may destroy s. Lookup by identity before accessing it. */ + struct yasl_surface *live = id ? yasl_events_find(d, id) : NULL; + if (live) { + live->drawing = false; + live->active_frame = NULL; + if (live->pacing == YASL_PACING_CONTINUOUS && + (yasl_display_caps(d) & YASL_CAP_FRAME_PACING)) + yasl_surface_schedule_redraw(live); + } +} + +static bool window_valid(struct yasl_surface *s) +{ + if (!s || s->popup) { errno = EINVAL; return false; } + return true; +} + +bool yasl_surface_set_size(struct yasl_surface *s, struct yasl_vector2i size) +{ + if (!window_valid(s)) return false; + if (size.x <= 0 || size.y <= 0 || size.x > 32767 || size.y > 32767 || + (int64_t)size.x * size.y * 4 > INT32_MAX) { errno = EINVAL; return false; } + if (!s->display->ops->surface_set_size) { errno = ENOTSUP; return false; } + errno = 0; + bool ok = s->display->ops->surface_set_size(s, size); + if (!ok && !errno) errno = EIO; + return ok; +} + +bool yasl_surface_set_fullscreen(struct yasl_surface *s, bool enabled) +{ + if (!window_valid(s)) return false; + if (!s->display->ops->surface_set_fullscreen) { errno = ENOTSUP; return false; } + errno = 0; + bool ok = s->display->ops->surface_set_fullscreen(s, enabled); + if (!ok && !errno) errno = EIO; + return ok; +} + +bool yasl_surface_set_maximized(struct yasl_surface *s, bool enabled) +{ + if (!window_valid(s)) return false; + if (!s->display->ops->surface_set_maximized) { errno = ENOTSUP; return false; } + errno = 0; + bool ok = s->display->ops->surface_set_maximized(s, enabled); + if (!ok && !errno) errno = EIO; + return ok; +} + +bool yasl_surface_minimize(struct yasl_surface *s) +{ + if (!window_valid(s)) return false; + if (!s->display->ops->surface_minimize) { errno = ENOTSUP; return false; } + errno = 0; + bool ok = s->display->ops->surface_minimize(s); + if (!ok && !errno) errno = EIO; + return ok; +} + +static bool selection_valid(struct yasl_display *d, enum yasl_selection selection) +{ + if (!d || (selection != YASL_CLIPBOARD && selection != YASL_PRIMARY)) { + errno = EINVAL; return false; + } + if (selection == YASL_PRIMARY && !(yasl_display_caps(d) & YASL_CAP_PRIMARY_SELECTION)) { + errno = ENOTSUP; return false; + } + return true; +} + +bool yasl_clipboard_set(struct yasl_display *d, enum yasl_selection selection, const char *text) +{ + if (!selection_valid(d, selection)) return false; + if (!text) { errno = EINVAL; return false; } + if (!yasl_utf8_validate(text, strlen(text))) { errno = EILSEQ; return false; } + if (!d->ops->display_copy_text) { errno = ENOTSUP; return false; } + errno = 0; + bool ok = d->ops->display_copy_text(d, selection, text); + if (!ok && !errno) errno = EIO; + return ok; +} + +uint64_t yasl_clipboard_request(struct yasl_surface *s, enum yasl_selection selection) +{ + if (!s) { errno = EINVAL; return 0; } + struct yasl_display *d = s->display; + if (!selection_valid(d, selection)) return 0; + if (!d->ops->display_request_paste) { errno = ENOTSUP; return 0; } + uint64_t id = yasl_events_request_id(d); + if (!id) return 0; + errno = 0; + if (!d->ops->display_request_paste(d, selection, s, id)) { + if (!errno) errno = EIO; + return 0; + } + return id; +} diff --git a/src/common/backend.h b/src/common/backend.h index 13ce9b5..876a4ca 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -10,6 +10,7 @@ #include struct yasl_backend_ops; +struct yasl_event_state; struct yasl_display { const struct yasl_backend_ops *ops; @@ -19,6 +20,9 @@ struct yasl_display { void *app_runner; /* Backend's copy of yasl_display_spec.app_id, or NULL; for desktop services. */ const char *app_id; + void *callback_ud; + struct yasl_event_state *events; + struct yasl_surface *surfaces; }; struct yasl_surface { @@ -39,8 +43,41 @@ struct yasl_surface { int timer_count; int timer_capacity; void *app_surface; + struct yasl_surface *event_next; + uint64_t id; + void *userdata; + bool popup; + bool drawing; + bool frame_presented; + uint32_t reported_scale; + uint64_t draw_index; + struct yasl_frame *active_frame; + yasl_pacing_mode pacing; + yasl_timer_id pacing_timer; }; +bool yasl_events_open(struct yasl_display *d); +void yasl_events_close(struct yasl_display *d); +int yasl_events_error(struct yasl_display *d); +bool yasl_events_pending(struct yasl_display *d); +struct yasl_surface *yasl_events_find(struct yasl_display *d, uint64_t id); +bool yasl_events_register(struct yasl_surface *s); +void yasl_events_unregister(struct yasl_surface *s); +bool yasl_events_push(struct yasl_display *d, struct yasl_surface *queue_owner, + const struct yasl_event *event); +bool yasl_events_poll_surface(struct yasl_surface *s, struct yasl_event *event); +void yasl_surface_notify_configure(struct yasl_surface *s, struct yasl_geometry geometry); +void yasl_surface_notify_close(struct yasl_surface *s); +void yasl_surface_notify_state(struct yasl_surface *s, uint32_t state); +void yasl_surface_notify_drag(struct yasl_surface *s, enum yasl_drag_event ev, struct yasl_vector2i pos); +void yasl_surface_notify_drop(struct yasl_surface *s, enum yasl_drop_event ev, struct yasl_vector2i pos); +void yasl_surface_notify_composition(struct yasl_surface *s, const char *text, int begin, int end); +void yasl_surface_draw(struct yasl_surface *s, struct yasl_frame *frame); +void yasl_display_notify_event(struct yasl_display *d, enum yasl_event_type type); +void yasl_surface_complete_paste(struct yasl_surface *s, uint64_t request_id, + enum yasl_transfer_status status, const char *text, size_t len); +uint64_t yasl_events_request_id(struct yasl_display *d); + void yasl_surface_clear_timers(struct yasl_surface *s); struct yasl_surface *yasl_create_toplevel(struct yasl_display *d, const struct yasl_window_spec *spec, void *graphics_device); @@ -74,6 +111,9 @@ struct yasl_backend_ops { int (*display_double_click_ms)(const struct yasl_display *d); void (*display_copy)(struct yasl_display *d, enum yasl_selection sel, const char *text); void (*display_paste)(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target); + bool (*display_copy_text)(struct yasl_display *d, enum yasl_selection sel, const char *text); + bool (*display_request_paste)(struct yasl_display *d, enum yasl_selection sel, + struct yasl_surface *target, uint64_t request_id); bool (*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); void (*display_drag_cancel)(struct yasl_display *d); /* Create with callbacks disabled; common code installs them after return. */ @@ -82,6 +122,11 @@ struct yasl_backend_ops { struct yasl_surface *(*surface_create_popup)(struct yasl_surface *parent, const struct yasl_popup_spec *spec); bool (*surface_popup_reposition)(struct yasl_surface *popup, const struct yasl_popup_spec *spec); void (*surface_destroy)(struct yasl_surface *s); + struct input_queue *(*surface_input_queue)(struct yasl_surface *s); + bool (*surface_set_size)(struct yasl_surface *s, struct yasl_vector2i size_logical); + bool (*surface_set_fullscreen)(struct yasl_surface *s, bool enabled); + bool (*surface_set_maximized)(struct yasl_surface *s, bool enabled); + bool (*surface_minimize)(struct yasl_surface *s); struct yasl_display *(*surface_display)(const struct yasl_surface *s); /* Copy only; must not invoke application callbacks. */ void (*surface_set_callbacks)(struct yasl_surface *s, const struct yasl_window_cbs *cbs, void *ud); diff --git a/src/common/events.c b/src/common/events.c new file mode 100644 index 0000000..a9bbcaf --- /dev/null +++ b/src/common/events.c @@ -0,0 +1,372 @@ +#include "backend.h" +#include "platform/sync.h" + +#include +#include +#include + +struct event_node { + struct yasl_event event; + uint64_t queue_id; + char *text; + void (*release)(void *); + struct event_node *next; +}; + +struct yasl_event_state { + yasl_mutex mutex; + struct event_node *head, *tail, *last; + uint64_t sequence, identity, request; + int error; +}; + +static void dispose(struct event_node *node) +{ + if (!node) return; + if (node->release) node->release(node->event.user.payload); + free(node->text); + free(node); +} + +static void dispose_list(struct event_node *nodes) +{ + while (nodes) { + struct event_node *next = nodes->next; + dispose(nodes); + nodes = next; + } +} + +bool yasl_events_open(struct yasl_display *d) +{ + struct yasl_event_state *state = calloc(1, sizeof(*state)); + if (!state) { errno = ENOMEM; return false; } + int error = yasl_mutex_init(&state->mutex); + if (error) { free(state); errno = error; return false; } + d->events = state; + return true; +} + +void yasl_events_close(struct yasl_display *d) +{ + struct yasl_event_state *state = d->events; + if (!state) return; + /* Producers must already have stopped. Detach before callbacks so cleanup + * can make harmless failed posts without touching a dying queue. */ + d->events = NULL; + dispose_list(state->head); + dispose(state->last); + yasl_mutex_destroy(&state->mutex); + free(state); +} + +int yasl_events_error(struct yasl_display *d) +{ + if (!d->events) return 0; + yasl_mutex_lock(&d->events->mutex); + int error = d->events->error; + yasl_mutex_unlock(&d->events->mutex); + return error; +} + +bool yasl_events_pending(struct yasl_display *d) +{ + if (!d->events) return false; + yasl_mutex_lock(&d->events->mutex); + bool pending = d->events->head != NULL; + yasl_mutex_unlock(&d->events->mutex); + return pending; +} + +struct yasl_surface *yasl_events_find(struct yasl_display *d, uint64_t id) +{ + for (struct yasl_surface *s = d->surfaces; s; s = s->event_next) + if (s->id == id) return s; + return NULL; +} + +static bool publish_input(const struct yasl_event *event, void *ud) +{ + struct yasl_surface *owner = ud; + return yasl_events_push(owner->display, owner, event); +} + +bool yasl_events_register(struct yasl_surface *s) +{ + struct yasl_display *d = s->display; + if (!d->events) return true; /* Internal test doubles need no queue. */ + yasl_mutex_lock(&d->events->mutex); + if (d->events->identity == UINT64_MAX) { + yasl_mutex_unlock(&d->events->mutex); + errno = EOVERFLOW; + return false; + } + s->reported_scale = s->scale_120; + s->id = ++d->events->identity; + s->event_next = d->surfaces; + d->surfaces = s; + yasl_mutex_unlock(&d->events->mutex); + struct input_queue *q = d->ops->surface_input_queue + ? d->ops->surface_input_queue(s) : &s->input; + if (q) { + /* Preserve input generated during native creation. */ + struct yasl_event event; + while (input_queue_pop(q, &event)) + yasl_events_push(d, s, &event); + q->publish = publish_input; + q->publish_ud = s; + } + return true; +} + +void yasl_events_unregister(struct yasl_surface *s) +{ + struct yasl_display *d = s->display; + if (!d->events) return; + struct event_node *discarded = NULL; + yasl_mutex_lock(&d->events->mutex); + struct yasl_surface **surface = &d->surfaces; + while (*surface && *surface != s) surface = &(*surface)->event_next; + if (*surface) *surface = s->event_next; + struct event_node **entry = &d->events->head; + d->events->tail = NULL; + while (*entry) { + struct event_node *node = *entry; + if (node->event.surface_id == s->id || node->queue_id == s->id) { + *entry = node->next; + node->next = discarded; + discarded = node; + } else { + d->events->tail = node; + entry = &node->next; + } + } + s->event_next = NULL; + yasl_mutex_unlock(&d->events->mutex); + dispose_list(discarded); +} + +static bool append(struct yasl_display *d, struct event_node *node) +{ + struct yasl_event_state *state = d->events; + if (state->sequence == UINT64_MAX) { errno = EOVERFLOW; return false; } + node->event.sequence = ++state->sequence; + if (!node->event.time_ns) node->event.time_ns = yasl_loop_now_ns(); + if (state->tail) state->tail->next = node; + else state->head = node; + state->tail = node; + return true; +} + +static bool string_event(enum yasl_event_type type) +{ + return type == YASL_EVENT_TEXT || type == YASL_EVENT_PASTE || + type == YASL_EVENT_DROP || type == YASL_EVENT_COMPOSITION; +} + +bool yasl_events_push(struct yasl_display *d, struct yasl_surface *queue_owner, + const struct yasl_event *event) +{ + if (!d || !d->events || (event->surface && !event->surface->id)) return true; + struct event_node *node = calloc(1, sizeof(*node)); + if (!node) goto failed; + node->event = *event; + node->event.surface_id = event->surface ? event->surface->id : 0; + node->queue_id = queue_owner ? queue_owner->id : node->event.surface_id; + if (string_event(event->type)) { + if (event->text.len == SIZE_MAX || (!event->text.data && event->text.len)) { + free(node); + goto failed; + } + node->text = malloc(event->text.len + 1); + if (!node->text) { free(node); goto failed; } + if (event->text.len) memcpy(node->text, event->text.data, event->text.len); + node->text[event->text.len] = '\0'; + if (event->type == YASL_EVENT_PASTE) node->event.paste.data = node->text; + else if (event->type == YASL_EVENT_COMPOSITION) node->event.composition.data = node->text; + else node->event.text.data = node->text; + } + yasl_mutex_lock(&d->events->mutex); + if ((node->event.surface_id && !yasl_events_find(d, node->event.surface_id)) || + (node->queue_id && !yasl_events_find(d, node->queue_id))) { + yasl_mutex_unlock(&d->events->mutex); + dispose(node); + return true; /* A native teardown notification for a canceled recipient. */ + } + bool ok = append(d, node); + if (!ok) d->events->error = errno; + yasl_mutex_unlock(&d->events->mutex); + if (!ok) dispose(node); + return ok; +failed: + yasl_mutex_lock(&d->events->mutex); + d->events->error = ENOMEM; + yasl_mutex_unlock(&d->events->mutex); + errno = ENOMEM; + return false; +} + +bool yasl_display_post(struct yasl_display *d, const struct yasl_message *message) +{ + if (!d || !d->events || !message) { errno = EINVAL; return false; } + struct event_node *node = calloc(1, sizeof(*node)); + if (!node) { errno = ENOMEM; return false; } + node->event = (struct yasl_event){ + .type = YASL_EVENT_USER, .surface_id = message->target_id, + .user = {.code = message->code, .payload = message->payload, .size = message->size}, + }; + node->queue_id = message->target_id; + node->release = message->release; + yasl_mutex_lock(&d->events->mutex); + bool ok = !message->target_id || yasl_events_find(d, message->target_id); + if (!ok) errno = ENOENT; + else ok = append(d, node); + yasl_mutex_unlock(&d->events->mutex); + if (!ok) { free(node); return false; } + if (d->loop) yasl_loop_kick(d->loop); + return true; +} + +static bool poll_event(struct yasl_display *d, uint64_t queue_id, struct yasl_event *event) +{ + if (!d || !d->events || !event) return false; + struct yasl_event_state *state = d->events; + yasl_mutex_lock(&state->mutex); + struct event_node *discarded = NULL; + struct event_node **entry = &state->head; + struct event_node *previous = NULL; + if (queue_id) { + /* The input-only migration adapter uses legacy callbacks for lifecycle. + * Retaining those notifications would keep dispatch permanently ready. + * Mixing this adapter with display polling is deliberately unsupported. */ + while (*entry) { + struct event_node *candidate = *entry; + if (candidate->event.type < YASL_EVENT_KEY || + candidate->event.type > YASL_EVENT_INPUT_RESET) { + *entry = candidate->next; + if (state->tail == candidate) state->tail = previous; + candidate->next = discarded; + discarded = candidate; + } else { + previous = candidate; + entry = &candidate->next; + } + } + entry = &state->head; + previous = NULL; + while (*entry && (*entry)->queue_id != queue_id) { + previous = *entry; + entry = &(*entry)->next; + } + } + struct event_node *node = *entry; + if (!node) { + yasl_mutex_unlock(&state->mutex); + dispose_list(discarded); + return false; + } + *entry = node->next; + if (state->tail == node) state->tail = previous; + node->next = NULL; + if (node->event.surface_id) + node->event.surface = yasl_events_find(d, node->event.surface_id); + struct event_node *last = state->last; + state->last = node; + *event = node->event; + yasl_mutex_unlock(&state->mutex); + dispose(last); + dispose_list(discarded); + return true; +} + +bool yasl_display_poll_event(struct yasl_display *d, struct yasl_event *event) +{ + return poll_event(d, 0, event); +} + +bool yasl_events_poll_surface(struct yasl_surface *s, struct yasl_event *event) +{ + return s && s->id ? poll_event(s->display, s->id, event) : false; +} + +uint64_t yasl_events_request_id(struct yasl_display *d) +{ + if (!d->events || d->events->request == UINT64_MAX) { errno = EOVERFLOW; return 0; } + return ++d->events->request; +} + +uint64_t yasl_surface_id(const struct yasl_surface *s) { return s ? s->id : 0; } +void yasl_surface_set_userdata(struct yasl_surface *s, void *ud) { if (s) s->userdata = ud; } +void *yasl_surface_userdata(const struct yasl_surface *s) { return s ? s->userdata : NULL; } + +void yasl_surface_notify_configure(struct yasl_surface *s, struct yasl_geometry geometry) +{ + uint32_t previous_scale = s->reported_scale; + s->reported_scale = geometry.scale_120; + s->size = geometry.size; + s->scale_120 = geometry.scale_120; + struct yasl_event event = {.type = YASL_EVENT_WINDOW_RESIZE, .surface = s, + .resize = {.geometry = geometry, .scale_changed = previous_scale != geometry.scale_120}}; + yasl_events_push(s->display, s, &event); + if (s->display) yasl_surface_request_frame(s); + if (s->callbacks.configure) s->callbacks.configure(s, geometry, s->ud); +} + +void yasl_surface_notify_close(struct yasl_surface *s) +{ + struct yasl_event event = {.type = YASL_EVENT_WINDOW_CLOSE, .surface = s}; + yasl_events_push(s->display, s, &event); + if (s->callbacks.close) s->callbacks.close(s, s->ud); +} + +void yasl_surface_notify_state(struct yasl_surface *s, uint32_t flags) +{ + s->state = flags; + struct yasl_event event = {.type = YASL_EVENT_WINDOW_STATE, .surface = s, .state.flags = flags}; + yasl_events_push(s->display, s, &event); + if (s->callbacks.state) s->callbacks.state(s, flags, s->ud); +} + +void yasl_surface_notify_drag(struct yasl_surface *s, enum yasl_drag_event action, struct yasl_vector2i pos) +{ + struct yasl_event event = {.type = YASL_EVENT_DRAG, .surface = s, .drag = {action, pos}}; + yasl_events_push(s->display, s, &event); + if (s->callbacks.drag) s->callbacks.drag(s, action, pos, s->ud); +} + +void yasl_surface_notify_drop(struct yasl_surface *s, enum yasl_drop_event action, struct yasl_vector2i pos) +{ + struct yasl_event event = {.type = YASL_EVENT_DROP_HOVER, .surface = s, .drop = {action, pos}}; + yasl_events_push(s->display, s, &event); + if (s->callbacks.drop) s->callbacks.drop(s, action, pos, s->ud); +} + +void yasl_surface_notify_composition(struct yasl_surface *s, const char *text, int begin, int end) +{ + struct yasl_event event = {.type = YASL_EVENT_COMPOSITION, .surface = s, + .composition = {.data = text ? text : "", .len = text ? strlen(text) : 0, + .begin = begin, .end = end, .active = text && text[0]}}; + yasl_events_push(s->display, s, &event); +} + +void yasl_surface_complete_paste(struct yasl_surface *s, uint64_t id, + enum yasl_transfer_status status, const char *text, size_t len) +{ + struct yasl_event event = {.type = YASL_EVENT_PASTE, .surface = s, + .paste = {.data = text ? text : "", .len = text ? len : 0, .request_id = id, .status = status}}; + if (!s->display->events) { + struct input_queue *q = s->display->ops->surface_input_queue + ? s->display->ops->surface_input_queue(s) : &s->input; + input_queue_push(q, &event); + } else yasl_events_push(s->display, s, &event); +} + +void yasl_display_notify_event(struct yasl_display *d, enum yasl_event_type type) +{ + struct yasl_event event = {.type = type}; + yasl_events_push(d, NULL, &event); + void (*callback)(void *) = type == YASL_EVENT_DISPLAY_QUIT ? d->callbacks.quit + : type == YASL_EVENT_DISPLAY_REOPEN ? d->callbacks.reopen + : type == YASL_EVENT_DISPLAY_LOST ? d->callbacks.lost : NULL; + if (callback) callback(d->callback_ud); +} diff --git a/src/common/input.c b/src/common/input.c index ce3219c..511cd0a 100644 --- a/src/common/input.c +++ b/src/common/input.c @@ -24,14 +24,15 @@ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev) struct yasl_event copy = *ev; char *owned = NULL; if (!copy.time_ns) copy.time_ns = yasl_loop_now_ns(); - if (has_text(ev)) { + if (has_text(ev) && !q->publish) { size_t len = ev->text.len; if ((!ev->text.data && len) || len == SIZE_MAX) goto failed; owned = malloc(len + 1); if (!owned) goto failed; if (len) memcpy(owned, ev->text.data, len); owned[len] = '\0'; - copy.text.data = owned; + if (ev->type == YASL_EVENT_PASTE) copy.paste.data = owned; + else copy.text.data = owned; } uint32_t buttons = q->buttons; switch (ev->type) { @@ -67,6 +68,10 @@ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev) } if (ev->type >= YASL_EVENT_POINTER_DOWN && ev->type <= YASL_EVENT_POINTER_LEAVE) copy.pointer.buttons = buttons; + if (q->publish) { + q->buttons = buttons; + return q->publish(©, q->publish_ud); + } if (q->count == q->capacity) { size_t capacity = q->capacity ? q->capacity * 2 : 32; if (capacity < q->capacity || capacity > SIZE_MAX / sizeof(*q->events)) @@ -127,8 +132,12 @@ bool input_queue_pop(struct input_queue *q, struct yasl_event *ev) void input_queue_clear(struct input_queue *q) { + bool (*publish)(const struct yasl_event *, void *) = q->publish; + void *publish_ud = q->publish_ud; discard_pending(q); free(q->last_text); free(q->events); *q = (struct input_queue){0}; + q->publish = publish; + q->publish_ud = publish_ud; } diff --git a/src/common/input.h b/src/common/input.h index aad5c9a..8980c55 100644 --- a/src/common/input.h +++ b/src/common/input.h @@ -19,6 +19,10 @@ struct input_queue { uint32_t buttons; /* Set by backends whose pointer leave ends any press: no release follows. */ bool leave_ends_press; + /* Installed by common code for display-wide publication. Standalone queues + * retain their normal storage/pop semantics when publish is NULL. */ + bool (*publish)(const struct yasl_event *ev, void *ud); + void *publish_ud; }; /* Copies text payloads and stamps time_ns when zero. Unless q->relative is set, diff --git a/src/common/meson.build b/src/common/meson.build index 9a30f52..1577132 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1 +1 @@ -sources += files('app.c', 'backend.c', 'desktop.c', 'input.c', 'native.c', 'timing.c', 'uri.c', 'utf8.c') +sources += files('app.c', 'backend.c', 'desktop.c', 'events.c', 'input.c', 'native.c', 'timing.c', 'uri.c', 'utf8.c') diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m index 2f9f8e5..60b20b0 100644 --- a/src/platform/cocoa/window.m +++ b/src/platform/cocoa/window.m @@ -12,6 +12,7 @@ #include "keymap.h" #include +#include #include #include #include @@ -172,11 +173,13 @@ static void drag_end(struct cocoa_display *d, enum yasl_drag_end how) [d->drag.image release]; d->drag.image = nil; - if (hover && !hover->destroyed && hover->cbs && hover->cbs->drag) { + if (hover && !hover->destroyed) { + darwin_ctx.depth++; if (how == YASL_DRAG_DROPPED) - hover->cbs->drag(&hover->base, YASL_DRAG_DROP, pos, hover->ud); + yasl_surface_notify_drag(&hover->base, YASL_DRAG_DROP, pos); else - hover->cbs->drag(&hover->base, YASL_DRAG_LEAVE, pos, hover->ud); + yasl_surface_notify_drag(&hover->base, YASL_DRAG_LEAVE, pos); + darwin_ctx.depth--; } if (l.end) l.end(ud, how); @@ -187,15 +190,14 @@ static void reap_dead(struct cocoa_display *d); static void set_state(struct cocoa_surface *w, uint32_t bit, bool on) { - if (!w || w->closed) + if (!w || w->destroyed || w->closed) return; uint32_t next = on ? (w->base.state | bit) : (w->base.state & ~bit); if (next == w->base.state) return; w->base.state = next; darwin_ctx.depth++; - if (w->cbs->state) - w->cbs->state(&w->base, next, w->ud); + yasl_surface_notify_state(&w->base, next); darwin_ctx.depth--; } @@ -227,13 +229,14 @@ static void push_focus(struct cocoa_surface *w, bool focused) input_queue_push(&w->input, &ev); darwin_ctx.depth++; set_state(w, YASL_STATE_ACTIVATED, focused); - cocoa_surface_schedule_redraw(w); + if (!w->destroyed && !w->closed) + cocoa_surface_schedule_redraw(w); darwin_ctx.depth--; } static void sync_size(struct cocoa_surface *w, bool force) { - if (!w || !w->layer || w->closed) + if (!w || w->destroyed || !w->layer || w->closed) return; uint64_t start = yasl_loop_now_ns(); NSRect bounds = w->view.bounds; @@ -254,14 +257,15 @@ static void sync_size(struct cocoa_surface *w, bool force) w->scale_120 = scale_120; w->layer.contentsScale = scale; w->layer.drawableSize = CGSizeMake(size.x, size.y); - if (w->cbs->configure) - w->cbs->configure(&w->base, (struct yasl_geometry){ + /* Common configure notification already requests a frame before invoking + * the legacy callback. Finish local bookkeeping before that callback can + * destroy this surface. */ + yasl_clock_note_resize(&w->clock, yasl_loop_now_ns() - start); + yasl_surface_notify_configure(&w->base, (struct yasl_geometry){ .size = size, .scale_120 = scale_120, - }, w->ud); - cocoa_surface_schedule_redraw(w); - if (w->view.inLiveResize) + }); + if (!w->destroyed && !w->closed && w->view.inLiveResize) render_and_commit(w); - yasl_clock_note_resize(&w->clock, yasl_loop_now_ns() - start); darwin_ctx.depth--; } @@ -427,19 +431,26 @@ static void update_drag_hover(struct cocoa_display *d, NSPoint screenPoint) struct cocoa_surface *old = d->drag.hover; struct yasl_vector2i old_pos = d->drag.hover_pos; d->drag.hover = NULL; - if (old->cbs && old->cbs->drag && !old->destroyed) - old->cbs->drag(&old->base, YASL_DRAG_LEAVE, old_pos, old->ud); + if (!old->destroyed) { + darwin_ctx.depth++; + yasl_surface_notify_drag(&old->base, YASL_DRAG_LEAVE, old_pos); + darwin_ctx.depth--; + } if (!d->drag.active) return; } if (target && target->destroyed) target = NULL; d->drag.hover = target; d->drag.hover_pos = pos; - if (target && !target->destroyed && target->cbs && target->cbs->drag) - target->cbs->drag(&target->base, YASL_DRAG_OVER, pos, target->ud); + if (target && !target->destroyed) { + darwin_ctx.depth++; + yasl_surface_notify_drag(&target->base, YASL_DRAG_OVER, pos); + darwin_ctx.depth--; + } } else if (target && !target->destroyed) { d->drag.hover_pos = pos; - if (target->cbs && target->cbs->drag) - target->cbs->drag(&target->base, YASL_DRAG_OVER, pos, target->ud); + darwin_ctx.depth++; + yasl_surface_notify_drag(&target->base, YASL_DRAG_OVER, pos); + darwin_ctx.depth--; } } @@ -559,15 +570,14 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor) - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { (void)sender; - if (darwin_ctx.cbs && darwin_ctx.cbs->quit) - darwin_ctx.cbs->quit(darwin_ctx.ud); + yasl_display_notify_event(&darwin_ctx.base, YASL_EVENT_DISPLAY_QUIT); return NSTerminateCancel; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)visible { (void)sender; - if (!visible && darwin_ctx.cbs && darwin_ctx.cbs->reopen) - darwin_ctx.cbs->reopen(darwin_ctx.ud); + if (!visible) + yasl_display_notify_event(&darwin_ctx.base, YASL_EVENT_DISPLAY_REOPEN); return YES; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender @@ -608,9 +618,11 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id send NSPasteboard *pb = [sender draggingPasteboard]; if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]]) { struct cocoa_surface *w = self.surface; - if (w && w->cbs && w->cbs->drop) { + if (w && !w->closed) { struct yasl_vector2i pos = dragging_pos(self, sender); - w->cbs->drop(&w->base, YASL_DROP_OVER, pos, w->ud); + darwin_ctx.depth++; + yasl_surface_notify_drop(&w->base, YASL_DROP_OVER, pos); + darwin_ctx.depth--; } return NSDragOperationCopy; } @@ -627,9 +639,11 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id send NSPasteboard *pb = [sender draggingPasteboard]; if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]]) { struct cocoa_surface *w = self.surface; - if (w && w->cbs && w->cbs->drop) { + if (w && !w->closed) { struct yasl_vector2i pos = dragging_pos(self, sender); - w->cbs->drop(&w->base, YASL_DROP_LEAVE, pos, w->ud); + darwin_ctx.depth++; + yasl_surface_notify_drop(&w->base, YASL_DROP_LEAVE, pos); + darwin_ctx.depth--; } } } @@ -662,8 +676,11 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id send ok = YES; } } - if (w->cbs && w->cbs->drop) - w->cbs->drop(&w->base, YASL_DROP_LEAVE, pos, w->ud); + if (!w->destroyed) { + darwin_ctx.depth++; + yasl_surface_notify_drop(&w->base, YASL_DROP_LEAVE, pos); + darwin_ctx.depth--; + } return ok; } @@ -869,13 +886,19 @@ static NSString *plain_string(id string) { (void)replacementRange; struct cocoa_surface *w = self.surface; - if (!w) return; + if (!w || w->destroyed || w->closed) return; const char *utf8 = plain_string(string).UTF8String; size_t len = utf8 ? strlen(utf8) : 0; + bool had_preedit = w->preedit != NULL; free(w->preedit); w->preedit = NULL; + darwin_ctx.depth++; on_text_input(w, utf8, len); - cocoa_surface_schedule_redraw(w); + if (had_preedit) + yasl_surface_notify_composition(&w->base, NULL, -1, -1); + if (!w->destroyed && !w->closed) + cocoa_surface_schedule_redraw(w); + darwin_ctx.depth--; } - (void)doCommandBySelector:(SEL)selector { (void)selector; } @@ -883,24 +906,43 @@ static NSString *plain_string(id string) - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { - (void)selectedRange; (void)replacementRange; struct cocoa_surface *w = self.surface; - if (!w) return; + if (!w || w->destroyed || w->closed) return; NSString *str = plain_string(string); const char *utf8 = str.length ? str.UTF8String : NULL; free(w->preedit); w->preedit = utf8 ? strdup(utf8) : NULL; - cocoa_surface_schedule_redraw(w); + int begin = -1, end = -1; + if (w->preedit && selectedRange.location != NSNotFound && + selectedRange.location <= str.length && selectedRange.length <= str.length - selectedRange.location) { + NSString *prefix = [str substringToIndex:selectedRange.location]; + NSString *selection = [str substringWithRange:selectedRange]; + const char *prefix_utf8 = prefix.UTF8String; + const char *selection_utf8 = selection.UTF8String; + if (prefix_utf8 && selection_utf8) { + begin = (int)strlen(prefix_utf8); + end = begin + (int)strlen(selection_utf8); + } + } + darwin_ctx.depth++; + yasl_surface_notify_composition(&w->base, w->preedit, begin, end); + if (!w->destroyed && !w->closed) + cocoa_surface_schedule_redraw(w); + darwin_ctx.depth--; } - (void)unmarkText { struct cocoa_surface *w = self.surface; - if (!w || !w->preedit) return; + if (!w || w->destroyed || w->closed || !w->preedit) return; free(w->preedit); w->preedit = NULL; - cocoa_surface_schedule_redraw(w); + darwin_ctx.depth++; + yasl_surface_notify_composition(&w->base, NULL, -1, -1); + if (!w->destroyed && !w->closed) + cocoa_surface_schedule_redraw(w); + darwin_ctx.depth--; } - (NSRange)selectedRange { return NSMakeRange(NSNotFound, 0); } @@ -971,10 +1013,12 @@ static NSString *plain_string(id string) bool visible = (self.window.occlusionState & NSWindowOcclusionStateVisible) != 0; darwin_ctx.depth++; set_state(w, YASL_STATE_SUSPENDED, !visible); - if (visible) - cocoa_surface_schedule_redraw(w); - else - w->link.paused = YES; + if (!w->destroyed && !w->closed) { + if (visible) + cocoa_surface_schedule_redraw(w); + else + w->link.paused = YES; + } darwin_ctx.depth--; } @@ -1031,8 +1075,7 @@ static NSString *plain_string(id string) darwin_ctx.depth++; w->closed = true; [w->link invalidate]; - if (w->cbs->close) - w->cbs->close(&w->base, w->ud); + yasl_surface_notify_close(&w->base); darwin_ctx.depth--; } @@ -1081,7 +1124,7 @@ bool cocoa_surface_present(struct cocoa_surface *w, const struct yasl_framebuffe static void render_and_commit(struct cocoa_surface *w) { - if (w->closed || !w->cbs->draw || w->rendering || w->size.x <= 0 || w->size.y <= 0 || + if (w->destroyed || w->closed || !w->cbs->draw || w->rendering || w->size.x <= 0 || w->size.y <= 0 || (w->base.state & YASL_STATE_SUSPENDED)) return; @@ -1110,8 +1153,8 @@ static void render_and_commit(struct cocoa_surface *w) }, }; if (drawable) - w->cbs->draw(&w->base, &w->frame, w->ud); - if (!w->closed && !w->did_present) { + yasl_surface_draw(&w->base, &w->frame); + if (!w->destroyed && !w->closed && !w->did_present) { w->want_draw = true; w->frames_skipped++; } @@ -1122,7 +1165,7 @@ static void render_and_commit(struct cocoa_surface *w) static void pump_window(struct cocoa_surface *w) { - if (!w || w->closed || !w->want_draw || !w->cbs->draw) + if (!w || w->destroyed || w->closed || !w->want_draw || !w->cbs->draw) return; if (w->link && !w->frame_due && !w->unpaced) return; @@ -1368,6 +1411,7 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya darwin_ctx.base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; darwin_ctx.cbs = &darwin_ctx.base.callbacks; darwin_ctx.ud = spec->ud; + darwin_ctx.base.callback_ud = spec->ud; if (spec->app_id) darwin_ctx.app_id = strdup(spec->app_id); darwin_ctx.base.app_id = darwin_ctx.app_id; } @@ -1585,6 +1629,57 @@ void cocoa_surface_set_title(struct cocoa_surface *w, const char *title) w->window.title = [NSString stringWithUTF8String:w->title] ?: @"YASL"; } +static bool impl_surface_set_size(struct yasl_surface *surface, struct yasl_vector2i size) +{ + struct cocoa_surface *w = (struct cocoa_surface *)surface; + if (!w || w->closed || w->is_popup || size.x <= 0 || size.y <= 0) { + errno = EINVAL; + return false; + } + [w->window setContentSize:NSMakeSize(size.x, size.y)]; + errno = 0; + return true; +} + +static bool impl_surface_set_fullscreen(struct yasl_surface *surface, bool enabled) +{ + struct cocoa_surface *w = (struct cocoa_surface *)surface; + if (!w || w->closed || w->is_popup || !w->window) { + errno = EINVAL; + return false; + } + bool fullscreen = (w->window.styleMask & NSWindowStyleMaskFullScreen) != 0; + if (fullscreen != enabled) + [w->window toggleFullScreen:nil]; + errno = 0; + return true; +} + +static bool impl_surface_set_maximized(struct yasl_surface *surface, bool enabled) +{ + struct cocoa_surface *w = (struct cocoa_surface *)surface; + if (!w || w->closed || w->is_popup || !w->window) { + errno = EINVAL; + return false; + } + if (w->window.isZoomed != enabled) + [w->window zoom:nil]; + errno = 0; + return true; +} + +static bool impl_surface_minimize(struct yasl_surface *surface) +{ + struct cocoa_surface *w = (struct cocoa_surface *)surface; + if (!w || w->closed || w->is_popup || !w->window) { + errno = EINVAL; + return false; + } + [w->window miniaturize:nil]; + errno = 0; + return true; +} + void cocoa_surface_set_background_color(struct cocoa_surface *w, uint32_t bg) { if (!w || w->closed) return; @@ -1644,34 +1739,54 @@ static NSPasteboard *pasteboard_for_selection(enum yasl_selection sel) } } +static bool cocoa_display_copy_text(struct cocoa_display *d, enum yasl_selection sel, const char *text); + void cocoa_display_copy(struct cocoa_display *d, enum yasl_selection sel, const char *text) { - (void)d; - if (!text) return; + (void)cocoa_display_copy_text(d, sel, text); +} + +static bool cocoa_display_copy_text(struct cocoa_display *d, enum yasl_selection sel, const char *text) +{ + assert([NSThread isMainThread]); + if (!d || !text) { errno = EINVAL; return false; } NSPasteboard *pb = pasteboard_for_selection(sel); - if (!pb) return; + if (!pb) { errno = EINVAL; return false; } NSString *s = [NSString stringWithUTF8String:text]; - if (!s) return; + if (!s) { errno = EINVAL; return false; } [pb clearContents]; - [pb setString:s forType:NSPasteboardTypeString]; + if (![pb setString:s forType:NSPasteboardTypeString]) { + errno = EIO; + return false; + } + errno = 0; + return true; } -void cocoa_display_paste(struct cocoa_display *d, enum yasl_selection sel, struct cocoa_surface *target) +static bool cocoa_display_request_paste(struct cocoa_display *d, enum yasl_selection sel, + struct cocoa_surface *target, uint64_t id) { - (void)d; - if (!target) return; + assert([NSThread isMainThread]); + if (!d || !target || target->destroyed || target->closed || + target->base.display != &d->base) { + errno = EINVAL; + return false; + } NSPasteboard *pb = pasteboard_for_selection(sel); - if (!pb) return; + if (!pb) { errno = EINVAL; return false; } NSString *s = [pb stringForType:NSPasteboardTypeString]; const char *utf8 = s ? s.UTF8String : NULL; - if (utf8) { - struct yasl_event ev = { - .type = YASL_EVENT_PASTE, - .surface = &target->base, - .text = { .data = utf8, .len = strlen(utf8) }, - }; - input_queue_push(&target->input, &ev); - } + enum yasl_transfer_status status = !s ? YASL_TRANSFER_UNAVAILABLE : + utf8 ? YASL_TRANSFER_OK : YASL_TRANSFER_FAILED; + yasl_surface_complete_paste(&target->base, id, status, + utf8 ? utf8 : NULL, utf8 ? strlen(utf8) : 0); + errno = 0; + return true; +} + +void cocoa_display_paste(struct cocoa_display *d, enum yasl_selection sel, struct cocoa_surface *target) +{ + (void)cocoa_display_request_paste(d, sel, target, 0); } int cocoa_display_modifiers(const struct cocoa_display *d) @@ -1990,6 +2105,7 @@ void cocoa_surface_set_callbacks(struct cocoa_surface *w, { if (!w || w->closed) return; w->base.callbacks = cbs ? *cbs : (struct yasl_window_cbs){0}; + w->base.ud = ud; w->cbs = &w->base.callbacks; w->ud = ud; } @@ -2071,6 +2187,18 @@ static void impl_display_paste(struct yasl_display *d, enum yasl_selection sel, cocoa_display_paste((struct cocoa_display *)d, sel, (struct cocoa_surface *)target); } +static bool impl_display_copy_text(struct yasl_display *d, enum yasl_selection sel, const char *text) +{ + return cocoa_display_copy_text((struct cocoa_display *)d, sel, text); +} + +static bool impl_display_request_paste(struct yasl_display *d, enum yasl_selection sel, + struct yasl_surface *target, uint64_t request_id) +{ + return cocoa_display_request_paste((struct cocoa_display *)d, sel, + (struct cocoa_surface *)target, request_id); +} + static bool impl_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) { return cocoa_display_drag_start((struct cocoa_display *)d, (struct cocoa_surface *)origin, image, l, ud); @@ -2104,6 +2232,12 @@ static void impl_surface_destroy(struct yasl_surface *s) cocoa_surface_destroy((struct cocoa_surface *)s); } +static struct input_queue *impl_surface_input_queue(struct yasl_surface *s) +{ + struct cocoa_surface *w = (struct cocoa_surface *)s; + return w ? &w->input : NULL; +} + static struct yasl_display *impl_surface_display(const struct yasl_surface *s) { return s ? &darwin_ctx.base : NULL; @@ -2129,11 +2263,6 @@ static bool impl_surface_present(struct yasl_surface *s, const struct yasl_frame return cocoa_surface_present((struct cocoa_surface *)s, fb, damage); } -static bool impl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev) -{ - return cocoa_surface_poll_event((struct cocoa_surface *)s, ev); -} - static struct yasl_geometry impl_surface_geometry(const struct yasl_surface *s) { return cocoa_surface_geometry((const struct cocoa_surface *)s); @@ -2254,18 +2383,24 @@ const struct yasl_backend_ops yasl_cocoa_backend = { .display_double_click_ms = impl_display_double_click_ms, .display_copy = impl_display_copy, .display_paste = impl_display_paste, + .display_copy_text = impl_display_copy_text, + .display_request_paste = impl_display_request_paste, .display_drag_start = impl_display_drag_start, .display_drag_cancel = impl_display_drag_cancel, .surface_create_toplevel = impl_surface_create_toplevel, .surface_create_popup = impl_surface_create_popup, .surface_popup_reposition = impl_surface_popup_reposition, .surface_destroy = impl_surface_destroy, + .surface_input_queue = impl_surface_input_queue, + .surface_set_size = impl_surface_set_size, + .surface_set_fullscreen = impl_surface_set_fullscreen, + .surface_set_maximized = impl_surface_set_maximized, + .surface_minimize = impl_surface_minimize, .surface_display = impl_surface_display, .surface_set_callbacks = impl_surface_set_callbacks, .surface_set_title = impl_surface_set_title, .surface_schedule_redraw = impl_surface_schedule_redraw, .surface_present = impl_surface_present, - .surface_poll_event = impl_surface_poll_event, .surface_geometry = impl_surface_geometry, .surface_timing = impl_surface_timing, .surface_frame_time_ns = impl_surface_frame_time_ns, diff --git a/src/platform/wayland/api.h b/src/platform/wayland/api.h index 9f290f5..0f935b2 100644 --- a/src/platform/wayland/api.h +++ b/src/platform/wayland/api.h @@ -14,6 +14,9 @@ void wayland_display_flush(struct wayland_display *d); int wayland_display_modifiers(const struct wayland_display *d); void wayland_display_copy(struct wayland_display *d, enum yasl_selection sel, const char *text); void wayland_display_paste(struct wayland_display *d, enum yasl_selection sel, struct wayland_surface *target); +bool wayland_display_copy_text(struct wayland_display *d, enum yasl_selection sel, const char *text); +bool wayland_display_request_paste(struct wayland_display *d, enum yasl_selection sel, + struct wayland_surface *target, uint64_t request_id); bool wayland_display_drag_start(struct wayland_display *d, struct wayland_surface *origin, const struct yasl_drag_image *image, const struct yasl_drag_listener *l, void *ud); void wayland_display_drag_cancel(struct wayland_display *d); struct wayland_surface *wayland_surface_create_toplevel(struct wayland_display *d, diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c index 4585cd6..7026a8c 100644 --- a/src/platform/wayland/backend.c +++ b/src/platform/wayland/backend.c @@ -1,5 +1,6 @@ #include "platform/wayland/internal.h" #include "platform/freedesktop/dbus.h" +#include static struct yasl_display *impl_display_open(struct yasl_loop *loop, const struct yasl_display_spec *spec) { @@ -44,11 +45,28 @@ static void impl_display_copy(struct yasl_display *d, enum yasl_selection sel, c wayland_display_copy((struct wayland_display *)d, sel, text); } +static bool impl_display_copy_text(struct yasl_display *d, enum yasl_selection sel, + const char *text) +{ + bool ok = wayland_display_copy_text((struct wayland_display *)d, sel, text); + if (!ok && errno == 0) errno = ENOTSUP; + return ok; +} + static void impl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target) { wayland_display_paste((struct wayland_display *)d, sel, (struct wayland_surface *)target); } +static bool impl_display_request_paste(struct yasl_display *d, enum yasl_selection sel, + struct yasl_surface *target, uint64_t request_id) +{ + bool ok = wayland_display_request_paste((struct wayland_display *)d, sel, + (struct wayland_surface *)target, request_id); + if (!ok && errno == 0) errno = ENOTSUP; + return ok; +} + static bool impl_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) { return wayland_display_drag_start((struct wayland_display *)d, (struct wayland_surface *)origin, image, l, ud); @@ -82,6 +100,59 @@ static void impl_surface_destroy(struct yasl_surface *s) wayland_surface_destroy((struct wayland_surface *)s); } +static struct input_queue *impl_surface_input_queue(struct yasl_surface *s) +{ + struct wayland_surface *w = (struct wayland_surface *)s; + return w ? &w->input : NULL; +} + +static bool impl_surface_set_size(struct yasl_surface *s, struct yasl_vector2i size) +{ + struct wayland_surface *w = (struct wayland_surface *)s; + if (!w || w->closed || w->destroyed || !w->toplevel || + size.x <= 0 || size.y <= 0) { + errno = EINVAL; + return false; + } + errno = ENOTSUP; + return false; +} + +static bool impl_surface_set_fullscreen(struct yasl_surface *s, bool enabled) +{ + struct wayland_surface *w = (struct wayland_surface *)s; + if (!w || w->closed || w->destroyed || !w->toplevel) { + errno = EINVAL; + return false; + } + if (enabled) xdg_toplevel_set_fullscreen(w->toplevel, NULL); + else xdg_toplevel_unset_fullscreen(w->toplevel); + return true; +} + +static bool impl_surface_set_maximized(struct yasl_surface *s, bool enabled) +{ + struct wayland_surface *w = (struct wayland_surface *)s; + if (!w || w->closed || w->destroyed || !w->toplevel) { + errno = EINVAL; + return false; + } + if (enabled) xdg_toplevel_set_maximized(w->toplevel); + else xdg_toplevel_unset_maximized(w->toplevel); + return true; +} + +static bool impl_surface_minimize(struct yasl_surface *s) +{ + struct wayland_surface *w = (struct wayland_surface *)s; + if (!w || w->closed || w->destroyed || !w->toplevel) { + errno = EINVAL; + return false; + } + xdg_toplevel_set_minimized(w->toplevel); + return true; +} + static struct yasl_display *impl_surface_display(const struct yasl_surface *s) { const struct wayland_surface *w = (const struct wayland_surface *)s; @@ -219,12 +290,19 @@ const struct yasl_backend_ops yasl_wayland_backend = { .display_double_click_ms = impl_display_double_click_ms, .display_copy = impl_display_copy, .display_paste = impl_display_paste, + .display_copy_text = impl_display_copy_text, + .display_request_paste = impl_display_request_paste, .display_drag_start = impl_display_drag_start, .display_drag_cancel = impl_display_drag_cancel, .surface_create_toplevel = impl_surface_create_toplevel, .surface_create_popup = impl_surface_create_popup, .surface_popup_reposition = impl_surface_popup_reposition, .surface_destroy = impl_surface_destroy, + .surface_input_queue = impl_surface_input_queue, + .surface_set_size = impl_surface_set_size, + .surface_set_fullscreen = impl_surface_set_fullscreen, + .surface_set_maximized = impl_surface_set_maximized, + .surface_minimize = impl_surface_minimize, .surface_display = impl_surface_display, .surface_set_callbacks = impl_surface_set_callbacks, .surface_set_title = impl_surface_set_title, diff --git a/src/platform/wayland/buffer.c b/src/platform/wayland/buffer.c index b906f4f..874cc76 100644 --- a/src/platform/wayland/buffer.c +++ b/src/platform/wayland/buffer.c @@ -182,10 +182,13 @@ int apply_pending_geometry(struct wayland_surface *w) w->buffers[i].presented = false; } publish_surface_scale(w); - if (w->cbs->configure && (resized || rescaled)) - w->cbs->configure(&w->base, (struct yasl_geometry){ + if (resized || rescaled) { + yasl_surface_notify_configure(&w->base, (struct yasl_geometry){ .size = { .x = width, .y = height }, .scale_120 = w->scale_120, - }, w->ud); + }); + if (w->closed || w->destroyed) + return -1; + } return 0; } @@ -525,9 +528,11 @@ void surface_draw_frame(struct wayland_surface *w) }; w->frame_active = true; w->frame_presented = false; - w->cbs->draw(&w->base, &w->frame, w->ud); + yasl_surface_draw(&w->base, &w->frame); + if (w->closed || w->destroyed) + return; w->frame_active = false; - if (!w->closed && !w->frame_presented) + if (!w->frame_presented) w->frames_skipped++; } diff --git a/src/platform/wayland/clipboard.c b/src/platform/wayland/clipboard.c index 353e993..8ca9b93 100644 --- a/src/platform/wayland/clipboard.c +++ b/src/platform/wayland/clipboard.c @@ -5,6 +5,7 @@ #include #define CLIPBOARD_TEXT_MAX (256u * 1024) +#define CLIPBOARD_TRANSFER_TIMEOUT_NS (30ull * 1000000000ull) struct offer { struct offer *next; @@ -24,10 +25,14 @@ struct transfer { struct offer *drop; struct wl_surface *drop_surface; int fd, id; + yasl_timer_id timeout; char *text; size_t len, offset, capacity; + uint64_t request_id; bool sending; bool uri_list; + bool paste; + bool cancelled; }; struct clipboard { struct wl_display *display; @@ -82,20 +87,48 @@ static void drop_event(struct clipboard *c, enum yasl_drop_event ev) static void free_offer(struct offer *o); +static void transfer_end(struct transfer *t, bool complete); + +static uint64_t transfer_timeout(void *ud, yasl_timer_id id, uint64_t interval) +{ + (void)interval; + struct transfer *t = ud; + if (t->timeout != id) + return 0; + t->timeout = 0; + transfer_end(t, false); + return 0; +} + static void transfer_end(struct transfer *t, bool complete) { - struct transfer **p = &t->owner->transfers; + struct clipboard *owner = t->owner; + struct transfer **p = &owner->transfers; while (*p && *p != t) p = &(*p)->next; if (*p) *p = t->next; - yasl_loop_del_fd(t->owner->loop, t->id); + yasl_loop_del_fd(owner->loop, t->id); + if (t->timeout) + yasl_loop_del_timer(owner->loop, t->timeout); close(t->fd); if (t->drop) { if (complete && wl_data_offer_get_version(t->drop->proxy) >= WL_DATA_OFFER_FINISH_SINCE_VERSION) wl_data_offer_finish(t->drop->proxy); free_offer(t->drop); - wl_display_flush(t->owner->display); + wl_display_flush(owner->display); } + bool notify = owner->receive && !t->cancelled && + ((!t->sending && complete) || (t->paste && !complete)); + clipboard_receive_fn receive = owner->receive; + void *receive_ud = owner->ud; + struct wl_surface *surface = t->drop_surface; + uint64_t request_id = t->request_id; + bool uri_list = t->uri_list; + enum yasl_transfer_status status = complete ? YASL_TRANSFER_OK : YASL_TRANSFER_FAILED; + const char *text = complete ? (t->text ? t->text : "") : NULL; + size_t len = complete ? t->len : 0; + if (notify) + receive(text, len, uri_list, surface, request_id, status, receive_ud); free(t->text); free(t); } @@ -124,9 +157,6 @@ static int transfer_io(int fd, short events, void *ud) t->len += (size_t)n; } else if (n == 0) { 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_end(t, false); } @@ -148,10 +178,31 @@ static struct transfer *start_transfer_text(struct clipboard *c, int fd, bool se } t->id = yasl_loop_add_fd(c->loop, fd, sending ? POLLOUT : POLLIN, transfer_io, t); if (t->id < 0) { close(fd); free(t->text); free(t); return NULL; } + t->timeout = yasl_loop_add_timer(c->loop, CLIPBOARD_TRANSFER_TIMEOUT_NS, + transfer_timeout, t); + if (!t->timeout) { + yasl_loop_del_fd(c->loop, t->id); + close(fd); + free(t->text); + free(t); + return NULL; + } t->next = c->transfers; c->transfers = t; return t; } +static struct transfer *start_transfer_paste(struct clipboard *c, int fd, + struct wl_surface *target, uint64_t request_id) +{ + struct transfer *t = start_transfer_text(c, fd, false, false, NULL); + if (t) { + t->drop_surface = target; + t->request_id = request_id; + t->paste = true; + } + return t; +} + static const char *match_text_mime(const char *mime) { if (unlikely(!mime)) return NULL; @@ -528,15 +579,17 @@ bool clipboard_set_seat(struct clipboard *c, struct wl_seat *seat) return true; } -void clipboard_copy(struct clipboard *c, const char *text, uint32_t serial) +bool clipboard_copy(struct clipboard *c, const char *text, uint32_t serial) { - if (!c || !c->device || !text || !serial) return; + if (!c || !c->device) { errno = ENOTSUP; return false; } + if (!text) { errno = EINVAL; return false; } + if (!serial) { errno = EAGAIN; return false; } char *copy = strdup(text); - if (!copy) return; + if (!copy) { errno = ENOMEM; return false; } if (c->source) wl_data_source_destroy(c->source); free(c->text); c->text = copy; c->source = wl_data_device_manager_create_data_source(c->manager); - if (!c->source) return; + if (!c->source) { errno = ENOMEM; return false; } wl_data_source_add_listener(c->source, &source_listener, c); wl_data_source_offer(c->source, "text/plain;charset=utf-8"); wl_data_source_offer(c->source, "text/plain"); @@ -544,30 +597,26 @@ void clipboard_copy(struct clipboard *c, const char *text, uint32_t serial) wl_data_source_offer(c->source, "STRING"); wl_data_source_offer(c->source, "TEXT"); wl_data_device_set_selection(c->device, c->source, serial); + return true; } void clipboard_paste(struct clipboard *c, struct wl_surface *target) { - if (!c || !c->device || !c->selection || !c->selection->mime) return; - int fds[2]; - if (pipe2(fds, O_CLOEXEC) < 0) return; - wl_data_offer_receive(c->selection->proxy, c->selection->mime, fds[1]); - close(fds[1]); - struct transfer *t = start_transfer_text(c, fds[0], false, false, c->text); - if (t) t->drop_surface = target; - wl_display_flush(c->display); + if (c && target) + (void)clipboard_request_paste(c, YASL_CLIPBOARD, target, 0); } -void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t serial) +bool clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t serial) { - if (!c || !text || !serial) return; - if (!c->primary_device || !c->primary_manager) return; + if (!c || !c->primary_device || !c->primary_manager) { errno = ENOTSUP; return false; } + if (!text) { errno = EINVAL; return false; } + if (!serial) { errno = EAGAIN; return false; } char *copy = strdup(text); - if (!copy) return; + if (!copy) { errno = ENOMEM; return false; } if (c->primary_source) zwp_primary_selection_source_v1_destroy(c->primary_source); free(c->primary_text); c->primary_text = copy; c->primary_source = zwp_primary_selection_device_manager_v1_create_source(c->primary_manager); - if (!c->primary_source) return; + if (!c->primary_source) { errno = ENOMEM; return false; } zwp_primary_selection_source_v1_add_listener(c->primary_source, &primary_source_listener, c); zwp_primary_selection_source_v1_offer(c->primary_source, "text/plain;charset=utf-8"); zwp_primary_selection_source_v1_offer(c->primary_source, "text/plain"); @@ -575,22 +624,58 @@ void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t seri zwp_primary_selection_source_v1_offer(c->primary_source, "STRING"); zwp_primary_selection_source_v1_offer(c->primary_source, "TEXT"); zwp_primary_selection_device_v1_set_selection(c->primary_device, c->primary_source, serial); + return true; } void clipboard_paste_primary(struct clipboard *c, struct wl_surface *target) { - if (!c) return; - if (c->primary_device && c->primary_selection && c->primary_selection->mime) { - int fds[2]; - if (pipe2(fds, O_CLOEXEC) < 0) return; + if (c && target) + (void)clipboard_request_paste(c, YASL_PRIMARY, target, 0); +} + +bool clipboard_request_paste(struct clipboard *c, enum yasl_selection selection, + struct wl_surface *target, uint64_t request_id) +{ + if (!c || !target || !c->receive) + return false; + int fds[2]; + if (selection == YASL_PRIMARY) { + if (!c->primary_device || !c->primary_selection || !c->primary_selection->mime) { + c->receive(NULL, 0, false, target, request_id, + YASL_TRANSFER_UNAVAILABLE, c->ud); + return true; + } + if (pipe2(fds, O_CLOEXEC) < 0) { + c->receive(NULL, 0, false, target, request_id, + YASL_TRANSFER_FAILED, c->ud); + return true; + } zwp_primary_selection_offer_v1_receive(c->primary_selection->proxy, c->primary_selection->mime, fds[1]); close(fds[1]); - struct transfer *t = start_transfer_text(c, fds[0], false, false, c->text); - if (t) t->drop_surface = target; - wl_display_flush(c->display); - return; + } else { + if (!c->device || !c->selection || !c->selection->mime) { + c->receive(NULL, 0, false, target, request_id, + YASL_TRANSFER_UNAVAILABLE, c->ud); + return true; + } + if (pipe2(fds, O_CLOEXEC) < 0) { + c->receive(NULL, 0, false, target, request_id, + YASL_TRANSFER_FAILED, c->ud); + return true; + } + wl_data_offer_receive(c->selection->proxy, c->selection->mime, fds[1]); + close(fds[1]); + } + struct transfer *transfer = start_transfer_paste(c, fds[0], target, request_id); + if (!transfer) { + c->receive(NULL, 0, false, target, request_id, + YASL_TRANSFER_FAILED, c->ud); + return true; } + if (wl_display_flush(c->display) < 0 && errno != EAGAIN) + transfer_end(transfer, false); + return true; } void clipboard_forget_surface(struct clipboard *c, struct wl_surface *surface) @@ -598,7 +683,10 @@ void clipboard_forget_surface(struct clipboard *c, struct wl_surface *surface) if (!c || !surface) return; for (struct transfer *t = c->transfers, *next; t; t = next) { next = t->next; - if (t->drop_surface == surface) transfer_end(t, false); + if (t->drop_surface == surface) { + t->cancelled = true; + transfer_end(t, false); + } } if (c->drag_surface == surface) c->drag_surface = NULL; } diff --git a/src/platform/wayland/clipboard.h b/src/platform/wayland/clipboard.h index 9fefa8e..133cc14 100644 --- a/src/platform/wayland/clipboard.h +++ b/src/platform/wayland/clipboard.h @@ -15,7 +15,8 @@ struct clipboard; struct zwp_primary_selection_device_manager_v1; typedef void (*clipboard_receive_fn)(const char *, size_t, bool uri_list, - struct wl_surface *on, void *); + struct wl_surface *on, uint64_t request_id, + enum yasl_transfer_status, void *); struct clipboard *clipboard_create(struct wl_display *, struct wl_data_device_manager *, struct wl_seat *, struct yasl_loop *, @@ -23,10 +24,12 @@ struct clipboard *clipboard_create(struct wl_display *, struct wl_data_device_ma void clipboard_destroy(struct clipboard *); bool clipboard_set_seat(struct clipboard *, struct wl_seat *); void clipboard_set_primary_manager(struct clipboard *, struct zwp_primary_selection_device_manager_v1 *); -void clipboard_copy(struct clipboard *, const char *, uint32_t); +bool clipboard_copy(struct clipboard *, const char *, uint32_t); void clipboard_paste(struct clipboard *, struct wl_surface *target); +bool clipboard_request_paste(struct clipboard *, enum yasl_selection, + struct wl_surface *target, uint64_t request_id); void clipboard_forget_surface(struct clipboard *, struct wl_surface *); -void clipboard_copy_primary(struct clipboard *, const char *, uint32_t); +bool clipboard_copy_primary(struct clipboard *, const char *, uint32_t); void clipboard_paste_primary(struct clipboard *, struct wl_surface *target); typedef void (*clipboard_drag_fn)(enum yasl_drag_event, struct wl_surface *on, diff --git a/src/platform/wayland/drag.c b/src/platform/wayland/drag.c index 2cfd432..1f4a78b 100644 --- a/src/platform/wayland/drag.c +++ b/src/platform/wayland/drag.c @@ -228,9 +228,9 @@ void conn_drop_target(struct wayland_display *c, enum yasl_drop_event ev, (void)c; struct wayland_surface *w = on ? (struct wayland_surface *)wl_surface_get_user_data(on) : NULL; - if (!w || w->parent || !w->cbs->drop) + if (!w || w->parent || w->closed) return; - w->cbs->drop(&w->base, ev, drag_device_pos(w, x, y), w->ud); + yasl_surface_notify_drop(&w->base, ev, drag_device_pos(w, x, y)); } void conn_drag_target(struct wayland_display *c, enum yasl_drag_event ev, @@ -239,7 +239,7 @@ void conn_drag_target(struct wayland_display *c, enum yasl_drag_event ev, (void)c; struct wayland_surface *w = on ? (struct wayland_surface *)wl_surface_get_user_data(on) : NULL; - if (!w || w->parent || !w->cbs->drag) + if (!w || w->parent || w->closed) return; - w->cbs->drag(&w->base, ev, drag_device_pos(w, x, y), w->ud); + yasl_surface_notify_drag(&w->base, ev, drag_device_pos(w, x, y)); } diff --git a/src/platform/wayland/ime.c b/src/platform/wayland/ime.c index 9f1b27d..4a091d8 100644 --- a/src/platform/wayland/ime.c +++ b/src/platform/wayland/ime.c @@ -11,8 +11,11 @@ static void show_preedit(struct wayland_surface *w, char *text, int begin, int e w->ime_shown = text; w->ime_shown_begin = begin; w->ime_shown_end = end; - if (changed) + if (changed) { + yasl_surface_notify_composition(&w->base, w->ime_shown, + w->ime_shown_begin, w->ime_shown_end); wayland_surface_schedule_redraw(w); + } } void ime_reset(struct wayland_surface *w) diff --git a/src/platform/wayland/keyboard.c b/src/platform/wayland/keyboard.c index 038ba78..7f8274d 100644 --- a/src/platform/wayland/keyboard.c +++ b/src/platform/wayland/keyboard.c @@ -1,7 +1,7 @@ #include "internal.h" +#include #include "common/keyboard.h" #include "keymap.h" -#include #define XKB_EVDEV_OFFSET 8 #define KEYMAP_MAX_BYTES (8u * 1024 * 1024) @@ -426,28 +426,66 @@ int wayland_display_modifiers(const struct wayland_display *c) } void wayland_display_copy(struct wayland_display *c, enum yasl_selection sel, const char *text) +{ + (void)wayland_display_copy_text(c, sel, text); +} + +bool wayland_display_copy_text(struct wayland_display *c, enum yasl_selection sel, + const char *text) { struct seat *s = c ? (c->active_seat ? c->active_seat : c->seats) : NULL; - if (!s || !s->clipboard) - return; - if (sel == YASL_PRIMARY) - clipboard_copy_primary(s->clipboard, text, s->input_serial); - else + if (!text) { + errno = EINVAL; + return false; + } + if (!s || !s->clipboard) { + errno = ENOTSUP; + return false; + } + if (!s->input_serial) { + errno = EAGAIN; + return false; + } + errno = 0; + bool ok = sel == YASL_PRIMARY ? + clipboard_copy_primary(s->clipboard, text, s->input_serial) : clipboard_copy(s->clipboard, text, s->input_serial); + return ok; } void wayland_display_paste(struct wayland_display *c, enum yasl_selection sel, struct wayland_surface *target) { struct seat *s = c ? (c->active_seat ? c->active_seat : c->seats) : NULL; - if (!s || !s->clipboard) + if (!target || target->closed || target->destroyed) return; + if (!s || !s->clipboard) { + yasl_surface_complete_paste(&target->base, 0, + YASL_TRANSFER_UNAVAILABLE, NULL, 0); return; - if (!target || target->closed) return; + } if (sel == YASL_PRIMARY) clipboard_paste_primary(s->clipboard, target->surface); else clipboard_paste(s->clipboard, target->surface); } +bool wayland_display_request_paste(struct wayland_display *c, enum yasl_selection sel, + struct wayland_surface *target, uint64_t request_id) +{ + struct seat *s = c ? (c->active_seat ? c->active_seat : c->seats) : NULL; + if (!target || target->closed || target->destroyed || !request_id) { + errno = EINVAL; + return false; + } + if (!s || !s->clipboard) { + errno = ENOTSUP; + return false; + } + errno = 0; + bool ok = clipboard_request_paste(s->clipboard, sel, target->surface, request_id); + if (!ok) errno = ENOMEM; + return ok; +} + bool wayland_surface_poll_event(struct wayland_surface *w, struct yasl_event *ev) { return w && ev && input_queue_pop(&w->input, ev); diff --git a/src/platform/wayland/pointer.c b/src/platform/wayland/pointer.c index cdc5780..da666da 100644 --- a/src/platform/wayland/pointer.c +++ b/src/platform/wayland/pointer.c @@ -1034,13 +1034,19 @@ static const struct zwp_tablet_seat_v2_listener tablet_seat_listener = { }; static void clipboard_received(const char *text, size_t len, bool uri_list, - struct wl_surface *on, void *ud) + struct wl_surface *on, uint64_t request_id, + enum yasl_transfer_status status, void *ud) { (void)ud; struct wayland_surface *w = on ? (struct wayland_surface *)wl_surface_get_user_data(on) : NULL; if (uri_list) w = surface_root(w); - if (!w || w->closed) return; + if (!w || w->closed || w->destroyed) return; + if (!uri_list) { + yasl_surface_complete_paste(&w->base, request_id, status, text, len); + return; + } + if (status != YASL_TRANSFER_OK) return; struct yasl_event ev = { .surface = &w->base, .type = uri_list ? YASL_EVENT_DROP : YASL_EVENT_PASTE, diff --git a/src/platform/wayland/shell.c b/src/platform/wayland/shell.c index 8795989..fc2ce2e 100644 --- a/src/platform/wayland/shell.c +++ b/src/platform/wayland/shell.c @@ -277,8 +277,7 @@ static void set_state(struct wayland_surface *w, uint32_t state) { if (state == w->base.state) return; w->base.state = state; - if (w->cbs && w->cbs->state) - w->cbs->state(&w->base, state, w->ud); + yasl_surface_notify_state(&w->base, state); } static void toplevel_configure(void *data, struct xdg_toplevel *toplevel, @@ -316,8 +315,8 @@ static void toplevel_close(void *data, struct xdg_toplevel *toplevel) { (void)toplevel; struct wayland_surface *w = data; - if (w && w->cbs && w->cbs->close) - w->cbs->close(&w->base, w->ud); + if (w && !w->closed) + yasl_surface_notify_close(&w->base); } static void toplevel_configure_bounds(void *data, struct xdg_toplevel *toplevel, diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c index cf1877d..2e85e2b 100644 --- a/src/platform/wayland/surface.c +++ b/src/platform/wayland/surface.c @@ -52,8 +52,7 @@ static void display_lose(struct wayland_display *c) surface_fail(w); } display_leave(c); - if (c->cbs && c->cbs->lost) - c->cbs->lost(c->ud); + yasl_display_notify_event(&c->base, YASL_EVENT_DISPLAY_LOST); } void surface_fail(struct wayland_surface *w) @@ -61,8 +60,7 @@ void surface_fail(struct wayland_surface *w) if (w->closed) return; w->closed = true; - if (w->cbs->close) - w->cbs->close(&w->base, w->ud); + yasl_surface_notify_close(&w->base); } static int display_ready(int fd, short revents, void *ud) @@ -395,6 +393,7 @@ void wayland_surface_set_callbacks(struct wayland_surface *w, const struct yasl_ { if (w && !w->closed) { w->base.callbacks = cbs ? *cbs : (struct yasl_window_cbs){0}; + w->base.ud = ud; w->cbs = &w->base.callbacks; w->ud = ud; } diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c index 765d83c..004d56c 100644 --- a/src/platform/x11/window.c +++ b/src/platform/x11/window.c @@ -32,8 +32,10 @@ #include #include #include +#include #define TEXT_LIMIT (16 * 1024 * 1024) +#define PASTE_TIMEOUT_NS YASL_MS_TO_NS(30000) #ifndef likely #define likely(x) __builtin_expect(!!(x), 1) @@ -72,8 +74,12 @@ struct x11_surface { struct paste_request { struct paste_request *next; + struct x11_display *display; struct x11_surface *target; Window window; + Atom selection; + uint64_t id; + yasl_timer_id timeout; }; struct tablet_axis { @@ -111,6 +117,7 @@ struct x11_display { bool xi2; bool has_shm; bool has_xkb; + bool lost; int xkb_event; enum yasl_key keys[256]; Cursor cursors[16]; @@ -150,6 +157,7 @@ static bool place(struct yasl_surface *base, struct yasl_vector2i at); static void drag_end(struct x11_display *d, enum yasl_drag_end how); static void drag_cancel(struct yasl_display *base); static void schedule_redraw(struct yasl_surface *base); +static uint64_t paste_timeout(void *ud, yasl_timer_id id, uint64_t interval_ns); static void leave(struct x11_display *d) { @@ -188,23 +196,23 @@ static void drag_set_hover(struct x11_display *d, struct x11_surface *s, struct struct x11_surface *old = d->drag.hover; struct yasl_vector2i old_pos = d->drag.hover_pos; d->drag.hover = NULL; - if (!old->destroyed && old->cbs && old->cbs->drag) - old->cbs->drag(&old->base, YASL_DRAG_LEAVE, old_pos, old->ud); - if (!d->drag.active) return; + if (!old->destroyed) + yasl_surface_notify_drag(&old->base, YASL_DRAG_LEAVE, old_pos); + if (!d->drag.active || (s && s->destroyed)) return; } if (s && s->destroyed) return; d->drag.hover = s; d->drag.hover_pos = pos; - if (s && s->cbs && s->cbs->drag) - s->cbs->drag(&s->base, YASL_DRAG_OVER, pos, s->ud); + if (s && !s->destroyed) + yasl_surface_notify_drag(&s->base, YASL_DRAG_OVER, pos); } static void drag_leave_hover(struct x11_display *d, struct x11_surface *s, struct yasl_vector2i pos) { if (!d->drag.active || d->drag.hover != s) return; d->drag.hover = NULL; - if (s && !s->destroyed && s->cbs && s->cbs->drag) - s->cbs->drag(&s->base, YASL_DRAG_LEAVE, pos, s->ud); + if (s && !s->destroyed) + yasl_surface_notify_drag(&s->base, YASL_DRAG_LEAVE, pos); } static void drag_drop_hover(struct x11_display *d) @@ -559,19 +567,41 @@ static void cancel_pastes(struct x11_display *d, struct x11_surface *target) { for (struct paste_request **p = &d->pastes; *p;) { struct paste_request *r = *p; - if (!target) { + if (target && r->target == target) { + /* Keep the requestor window alive until the selection owner replies. + * Some INCR owners write to it after the original surface is gone. */ + r->target = NULL; + r->id = 0; + p = &r->next; + } else if (!target) { *p = r->next; + if (r->timeout) yasl_loop_del_timer(d->loop, r->timeout); XDestroyWindow(d->native, r->window); free(r); } else { - /* Keep the requestor alive until SelectionNotify: the owner may - * still be writing to its property. Never retain a dead target. */ - if (r->target == target) r->target = NULL; p = &r->next; } } } +static uint64_t paste_timeout(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)id; + (void)interval_ns; + struct paste_request *r = ud; + struct x11_surface *s = r->target; + struct x11_display *d = r->display; + struct paste_request **p = &d->pastes; + while (*p && *p != r) p = &(*p)->next; + if (*p) *p = r->next; + r->timeout = 0; /* The loop retires this one-shot timer after the callback. */ + XDestroyWindow(d->native, r->window); + if (s && !s->destroyed) + yasl_surface_complete_paste(&s->base, r->id, YASL_TRANSFER_FAILED, NULL, 0); + free(r); + return 0; +} + static void surface_destroy(struct yasl_surface *base) { struct x11_surface *s = (struct x11_surface *)base; @@ -628,6 +658,7 @@ static void set_callbacks(struct yasl_surface *base, if (s->destroyed) return; s->base.callbacks = cbs ? *cbs : (struct yasl_window_cbs){0}; s->cbs = &s->base.callbacks; + s->base.ud = ud; s->ud = ud; } @@ -641,7 +672,7 @@ static void set_title(struct yasl_surface *base, const char *title) PropModeReplace, (const unsigned char *)title, (int)strlen(title)); } -static void net_message(struct x11_surface *s, Atom type, +static bool net_message(struct x11_surface *s, Atom type, long a, long b, long c, long e, long f) { struct x11_display *d = (struct x11_display *)s->base.display; @@ -650,8 +681,246 @@ static void net_message(struct x11_surface *s, Atom type, .type = ClientMessage, .display = d->native, .window = s->window, .message_type = type, .format = 32, .data.l = {a, b, c, e, f}, }; - XSendEvent(d->native, RootWindow(d->native, d->screen), False, - SubstructureRedirectMask | SubstructureNotifyMask, &event); + return XSendEvent(d->native, RootWindow(d->native, d->screen), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event) != 0; +} + +static bool surface_set_size(struct yasl_surface *base, struct yasl_vector2i size) +{ + struct x11_surface *s = (struct x11_surface *)base; + struct x11_display *d = (struct x11_display *)base->display; + if (s->destroyed || s->is_popup || size.x <= 0 || size.y <= 0 || + size.x > 32767 || size.y > 32767) { + errno = EINVAL; + return false; + } + if (!d->native || !s->window) { errno = ENODEV; return false; } + XResizeWindow(d->native, s->window, (unsigned)size.x, (unsigned)size.y); + XFlush(d->native); + return true; +} + +static bool surface_set_fullscreen(struct yasl_surface *base, bool enabled) +{ + struct x11_surface *s = (struct x11_surface *)base; + struct x11_display *d = (struct x11_display *)base->display; + if (s->destroyed || s->is_popup) { errno = EINVAL; return false; } + if (!d->native || !s->window) { errno = ENODEV; return false; } + if (!net_message(s, d->net_state, enabled ? 1 : 0, d->fullscreen, 0, 1, 0)) { + errno = ENOTSUP; + return false; + } + XFlush(d->native); + return true; +} + +static bool surface_set_maximized(struct yasl_surface *base, bool enabled) +{ + struct x11_surface *s = (struct x11_surface *)base; + struct x11_display *d = (struct x11_display *)base->display; + if (s->destroyed || s->is_popup) { errno = EINVAL; return false; } + if (!d->native || !s->window) { errno = ENODEV; return false; } + long action = enabled ? 1 : 0; + if (!net_message(s, d->net_state, action, d->maximized_v, d->maximized_h, 1, 0)) { + errno = ENOTSUP; + return false; + } + XFlush(d->native); + return true; +} + +static bool surface_minimize(struct yasl_surface *base) +{ + struct x11_surface *s = (struct x11_surface *)base; + struct x11_display *d = (struct x11_display *)base->display; + if (s->destroyed || s->is_popup) { errno = EINVAL; return false; } + if (!d->native || !s->window) { errno = ENODEV; return false; } + if (!XIconifyWindow(d->native, s->window, d->screen)) { errno = ENOTSUP; return false; } + XFlush(d->native); + return true; +} + +static size_t utf8_offset_for_chars(const char *text, size_t len, int chars) +{ + size_t at = 0; + while (at < len && chars-- > 0) { + unsigned char c = (unsigned char)text[at]; + size_t step = c < 0x80 ? 1 : (c < 0xe0 ? 2 : (c < 0xf0 ? 3 : 4)); + if (step > len - at) step = 1; + at += step; + } + return at; +} + +static size_t utf8_encode(char *out, uint32_t cp) +{ + if (!cp || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) cp = 0xfffd; + if (cp < 0x80) { out[0] = (char)cp; return 1; } + if (cp < 0x800) { + out[0] = (char)(0xc0 | (cp >> 6)); + out[1] = (char)(0x80 | (cp & 0x3f)); + return 2; + } + if (cp < 0x10000) { + out[0] = (char)(0xe0 | (cp >> 12)); + out[1] = (char)(0x80 | ((cp >> 6) & 0x3f)); + out[2] = (char)(0x80 | (cp & 0x3f)); + return 3; + } + out[0] = (char)(0xf0 | (cp >> 18)); + out[1] = (char)(0x80 | ((cp >> 12) & 0x3f)); + out[2] = (char)(0x80 | ((cp >> 6) & 0x3f)); + out[3] = (char)(0x80 | (cp & 0x3f)); + return 4; +} + +static char *xim_text_dup(const XIMText *text, size_t *len) +{ + *len = 0; + if (!text) return NULL; + const void *source = text->encoding_is_wchar + ? (const void *)text->string.wide_char + : (const void *)text->string.multi_byte; + if (!source) return NULL; + /* XIMText.length is an unsigned short, so four bytes per scalar cannot + * overflow size_t on the supported X11 targets. */ + size_t cap = (size_t)text->length * 4 + 1; + char *out = malloc(cap); + if (!out) return NULL; + size_t used = 0; + if (text->encoding_is_wchar) { + const wchar_t *src = text->string.wide_char; + for (unsigned i = 0; i < text->length; i++) { + uint32_t cp = (uint32_t)src[i]; + used += utf8_encode(out + used, cp); + } + } else { + const char *src = text->string.multi_byte; + mbstate_t state = {0}; + size_t consumed_bytes = 0; + size_t max_bytes = (size_t)text->length * MB_LEN_MAX; + unsigned converted = 0; + while (converted < text->length && consumed_bytes < max_bytes) { + wchar_t wc = 0; + size_t result = mbrtowc(&wc, src + consumed_bytes, 1, &state); + if (result == (size_t)-1) { free(out); return NULL; } + consumed_bytes++; + if (result == (size_t)-2) continue; + if (result == 0) { + /* The public UTF-8 event payload cannot contain an embedded NUL. */ + wc = (wchar_t)0xfffd; + state = (mbstate_t){0}; + } + used += utf8_encode(out + used, (uint32_t)wc); + converted++; + } + if (converted != text->length) { free(out); return NULL; } + } + out[used] = '\0'; + if (!yasl_utf8_validate(out, used)) { free(out); return NULL; } + *len = used; + return out; +} + +static void xim_preedit_done(XIM im, XPointer client_data, XPointer call_data) +{ + (void)im; + (void)call_data; + struct x11_surface *s = (struct x11_surface *)client_data; + if (s->destroyed) return; + free(s->ime_preedit); + s->ime_preedit = NULL; + s->ime_cursor_begin = s->ime_cursor_end = -1; + yasl_surface_notify_composition(&s->base, NULL, -1, -1); +} + +static int xim_preedit_start(XIM im, XPointer client_data, XPointer call_data) +{ + (void)im; + (void)client_data; + (void)call_data; + return -1; /* Accept preedit strings of any length. */ +} + +static void xim_preedit_draw(XIM im, XPointer client_data, XPointer call_data) +{ + (void)im; + struct x11_surface *s = (struct x11_surface *)client_data; + XIMPreeditDrawCallbackStruct *draw = (XIMPreeditDrawCallbackStruct *)call_data; + if (s->destroyed || !draw || draw->chg_first < 0 || draw->chg_length < 0) return; + size_t old_len = s->ime_preedit ? strlen(s->ime_preedit) : 0; + size_t new_len = old_len; + bool has_string = draw->text && (draw->text->encoding_is_wchar + ? draw->text->string.wide_char != NULL + : draw->text->string.multi_byte != NULL); + if (has_string) { + const char *old_text = s->ime_preedit ? s->ime_preedit : ""; + size_t first = utf8_offset_for_chars(old_text, old_len, draw->chg_first); + size_t after = first + utf8_offset_for_chars(old_text + first, + old_len - first, draw->chg_length); + size_t insert_len = 0; + char *insert = xim_text_dup(draw->text, &insert_len); + if (!insert) return; + if (insert_len > SIZE_MAX - (old_len - (after - first)) - 1) { + free(insert); + return; + } + new_len = old_len - (after - first) + insert_len; + char *updated = malloc(new_len + 1); + if (!updated) { free(insert); return; } + if (first) memcpy(updated, old_text, first); + if (insert_len) memcpy(updated + first, insert, insert_len); + if (old_len > after) memcpy(updated + first + insert_len, old_text + after, + old_len - after); + updated[new_len] = '\0'; + free(insert); + free(s->ime_preedit); + s->ime_preedit = new_len ? updated : (free(updated), NULL); + } + int caret = draw->caret < 0 ? -1 : (int)utf8_offset_for_chars( + s->ime_preedit ? s->ime_preedit : "", new_len, draw->caret); + s->ime_cursor_begin = s->ime_cursor_end = caret; + yasl_surface_notify_composition(&s->base, s->ime_preedit, caret, caret); +} + +static void xim_preedit_caret(XIM im, XPointer client_data, XPointer call_data) +{ + (void)im; + struct x11_surface *s = (struct x11_surface *)client_data; + XIMPreeditCaretCallbackStruct *caret = (XIMPreeditCaretCallbackStruct *)call_data; + if (s->destroyed || !caret || !s->ime_preedit) return; + int offset = caret->position < 0 ? -1 : (int)utf8_offset_for_chars( + s->ime_preedit, strlen(s->ime_preedit), caret->position); + s->ime_cursor_begin = s->ime_cursor_end = offset; + yasl_surface_notify_composition(&s->base, s->ime_preedit, offset, offset); +} + +static XIC create_ic(struct x11_display *d, struct x11_surface *s) +{ + if (!d->im) return NULL; + union { + int (*start)(XIM, XPointer, XPointer); + XIMProc proc; + } start_proc = {.start = xim_preedit_start}; + XIMCallback start = {.client_data = (XPointer)s, + .callback = start_proc.proc}; + XIMCallback done = {.client_data = (XPointer)s, .callback = xim_preedit_done}; + XIMCallback draw = {.client_data = (XPointer)s, .callback = xim_preedit_draw}; + XIMCallback caret = {.client_data = (XPointer)s, .callback = xim_preedit_caret}; + XVaNestedList preedit = XVaCreateNestedList(0, + XNPreeditStartCallback, &start, + XNPreeditDoneCallback, &done, + XNPreeditDrawCallback, &draw, + XNPreeditCaretCallback, &caret, NULL); + XIC ic = preedit ? XCreateIC(d->im, + XNInputStyle, XIMPreeditCallbacks | XIMStatusNothing, + XNPreeditAttributes, preedit, + XNClientWindow, s->window, XNFocusWindow, s->window, NULL) : NULL; + if (preedit) XFree(preedit); + if (!ic) + ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, s->window, XNFocusWindow, s->window, NULL); + return ic; } static struct yasl_surface *create_toplevel(struct yasl_display *base, @@ -691,10 +960,7 @@ static struct yasl_surface *create_toplevel(struct yasl_display *base, Atom xdnd_version = 5; XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); - if (d->im) { - s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, - XNClientWindow, s->window, XNFocusWindow, s->window, NULL); - } + s->ic = create_ic(d, s); s->next = d->surfaces; d->surfaces = s; set_title(&s->base, spec->title ? spec->title : "YASL"); @@ -851,10 +1117,7 @@ static struct yasl_surface *surface_create_popup(struct yasl_surface *parent_bas Atom xdnd_version = 5; XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); - if (d->im) { - s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, - XNClientWindow, s->window, XNFocusWindow, s->window, NULL); - } + s->ic = create_ic(d, s); XMapRaised(d->native, s->window); XFlush(d->native); } @@ -909,7 +1172,7 @@ static void update_state(struct x11_surface *s) if (data) XFree(data); if (state == s->base.state) return; s->base.state = state; - if (s->cbs && s->cbs->state) s->cbs->state(&s->base, state, s->ud); + yasl_surface_notify_state(&s->base, state); } static void selection_request(struct x11_display *d, XSelectionRequestEvent *request) @@ -1036,13 +1299,48 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event) while (*p && (*p)->window != event->requestor) p = &(*p)->next; if (!*p) return; struct paste_request *r = *p; - *p = r->next; struct x11_surface *s = r->target; - if (!s || event->property == None) { + if (!s) { + /* Surface destruction detached this accepted request. The reply means + * the owner has finished writing the initial property; retire safely. */ + *p = r->next; + if (r->timeout) yasl_loop_del_timer(d->loop, r->timeout); XDestroyWindow(d->native, r->window); free(r); return; } + if (event->property == None) { + *p = r->next; + if (r->timeout) yasl_loop_del_timer(d->loop, r->timeout); + enum yasl_transfer_status status = + XGetSelectionOwner(d->native, r->selection) == None + ? YASL_TRANSFER_UNAVAILABLE : YASL_TRANSFER_FAILED; + XDestroyWindow(d->native, r->window); + if (s && !s->destroyed) + yasl_surface_complete_paste(&s->base, r->id, status, NULL, 0); + free(r); + return; + } + /* Probe without deleting first. Deleting an INCR marker acknowledges it + * and invites the owner to write more properties; this backend rejects + * streaming, so leave it intact and retain a tombstone until timeout. */ + Atom probe_type = None; + int probe_format = 0; + unsigned long probe_count = 0, probe_left = 0; + unsigned char *probe_data = NULL; + int probe_result = XGetWindowProperty(d->native, r->window, event->property, + 0, 0, False, AnyPropertyType, &probe_type, &probe_format, + &probe_count, &probe_left, &probe_data); + if (probe_data) XFree(probe_data); + if (probe_result == Success && probe_type == d->incr) { + yasl_surface_complete_paste(&s->base, r->id, + YASL_TRANSFER_FAILED, NULL, 0); + r->target = NULL; + r->id = 0; + return; + } + *p = r->next; + if (r->timeout) yasl_loop_del_timer(d->loop, r->timeout); Atom actual = None; int format = 0; unsigned long count = 0, left = 0; @@ -1070,7 +1368,12 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event) if (data) { XFree(data); data = NULL; } break; } - char *nb = realloc(buf, total + count + 1); + if (count > TEXT_LIMIT - total) { + XFree(data); + data = NULL; + break; + } + char *nb = realloc(buf, total + (size_t)count + 1); if (!nb) { XFree(data); break; @@ -1079,7 +1382,7 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event) if (count) memcpy(buf + total, data, count); total += count; buf[total] = '\0'; - offset += count / 4; + offset += (count + 3) / 4; XFree(data); data = NULL; if (left == 0) { complete = true; break; } @@ -1091,8 +1394,12 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event) buf = utf8; if (buf) total = utf8_len; } + enum yasl_transfer_status status = YASL_TRANSFER_FAILED; if (complete && buf && yasl_utf8_validate(buf, total)) - push(s, (struct yasl_event){.type = YASL_EVENT_PASTE}, buf, total); + status = YASL_TRANSFER_OK; + yasl_surface_complete_paste(&s->base, r->id, status, + status == YASL_TRANSFER_OK ? buf : NULL, + status == YASL_TRANSFER_OK ? total : 0); free(buf); XDestroyWindow(d->native, r->window); free(r); @@ -1172,8 +1479,8 @@ static void handle_client_message(struct x11_display *d, struct x11_surface *s, const XClientMessageEvent *ev) { if (ev->message_type == d->protocols && - (Atom)ev->data.l[0] == d->delete_window && s->cbs && s->cbs->close) { - s->cbs->close(&s->base, s->ud); + (Atom)ev->data.l[0] == d->delete_window) { + yasl_surface_notify_close(&s->base); } else if (ev->message_type == d->xdnd_enter) { Window source = (Window)ev->data.l[0]; int version = (int)((unsigned long)ev->data.l[1] >> 24); @@ -1204,25 +1511,29 @@ static void handle_client_message(struct x11_display *d, struct x11_surface *s, d->xdnd.accepts = has_uri; } else if (ev->message_type == d->xdnd_position) { Window source = (Window)ev->data.l[0]; + Window target = s->window; int rx = (int)(int16_t)((ev->data.l[2] >> 16) & 0xFFFF); int ry = (int)(int16_t)(ev->data.l[2] & 0xFFFF); Time t = (Time)ev->data.l[3]; int wx = rx, wy = ry; if (d->native) { Window child; - XTranslateCoordinates(d->native, RootWindow(d->native, d->screen), s->window, + XTranslateCoordinates(d->native, RootWindow(d->native, d->screen), target, rx, ry, &wx, &wy, &child); } struct yasl_vector2i pos = {wx, wy}; if (d->xdnd.surface && d->xdnd.surface != s) { - if (d->xdnd.accepts && d->xdnd.surface->cbs && d->xdnd.surface->cbs->drop) - d->xdnd.surface->cbs->drop(&d->xdnd.surface->base, YASL_DROP_LEAVE, d->xdnd.pos, d->xdnd.surface->ud); + struct x11_surface *old = d->xdnd.surface; + struct yasl_vector2i old_pos = d->xdnd.pos; + d->xdnd.surface = NULL; + if (d->xdnd.accepts && !old->destroyed) + yasl_surface_notify_drop(&old->base, YASL_DROP_LEAVE, old_pos); } d->xdnd.surface = s; d->xdnd.pos = pos; d->xdnd.time = t; - if (d->xdnd.accepts && s->cbs && s->cbs->drop) - s->cbs->drop(&s->base, YASL_DROP_OVER, pos, s->ud); + if (d->xdnd.accepts && !s->destroyed) + yasl_surface_notify_drop(&s->base, YASL_DROP_OVER, pos); if (d->native) { XEvent reply = {0}; @@ -1233,37 +1544,38 @@ static void handle_client_message(struct x11_display *d, struct x11_surface *s, .message_type = d->xdnd_status, .format = 32, .data.l = { - s->window, - d->xdnd.accepts ? 1 : 0, + target, + d->xdnd.accepts && !s->destroyed ? 1 : 0, 0, 0, - d->xdnd.accepts ? d->xdnd_action_copy : None, + d->xdnd.accepts && !s->destroyed ? d->xdnd_action_copy : None, }, }; XSendEvent(d->native, source, False, NoEventMask, &reply); } } else if (ev->message_type == d->xdnd_leave) { if (d->xdnd.surface == s) { - if (d->xdnd.accepts && s->cbs && s->cbs->drop) - s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud); + if (d->xdnd.accepts && !s->destroyed) + yasl_surface_notify_drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos); } d->xdnd = (typeof(d->xdnd)){0}; } else if (ev->message_type == d->xdnd_drop) { Window source = (Window)ev->data.l[0]; + Window target = s->window; Time drop_time = (Time)ev->data.l[2]; bool was_accepting = d->xdnd.accepts && d->xdnd.has_uri_list && (d->xdnd.source == source); if (d->xdnd.surface == s) { - if (was_accepting && s->cbs && s->cbs->drop) - s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud); + if (was_accepting && !s->destroyed) + yasl_surface_notify_drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos); } - if (was_accepting) { + if (was_accepting && !s->destroyed) { d->pending_drop.active = true; d->pending_drop.source = source; d->pending_drop.surface = s; d->pending_drop.time = drop_time; if (d->native) { XConvertSelection(d->native, d->xdnd_selection, d->uri_list, - d->paste_property, s->window, drop_time); + d->paste_property, target, drop_time); } } else if (d->native) { XEvent reply = {0}; @@ -1273,7 +1585,7 @@ static void handle_client_message(struct x11_display *d, struct x11_surface *s, .window = source, .message_type = d->xdnd_finished, .format = 32, - .data.l = { s->window, 0, None }, + .data.l = { target, 0, None }, }; XSendEvent(d->native, source, False, NoEventMask, &reply); } @@ -1326,8 +1638,9 @@ static void dispatch(struct yasl_display *base) case ConfigureNotify: { struct yasl_vector2i size = {event.xconfigure.width, event.xconfigure.height}; if (size.x == s->size.x && size.y == s->size.y) break; - if (resize(s, size) && s->cbs && s->cbs->configure) - s->cbs->configure(&s->base, (struct yasl_geometry){size, YASL_SCALE_UNIT}, s->ud); + if (resize(s, size)) + yasl_surface_notify_configure(&s->base, + (struct yasl_geometry){size, YASL_SCALE_UNIT}); break; } case ClientMessage: @@ -1340,7 +1653,7 @@ static void dispatch(struct yasl_display *base) else s->base.state &= ~YASL_STATE_ACTIVATED; push(s, (struct yasl_event){ .type = YASL_EVENT_FOCUS, .focus.focused = focused}, NULL, 0); - if (s->cbs && s->cbs->state) s->cbs->state(&s->base, s->base.state, s->ud); + yasl_surface_notify_state(&s->base, s->base.state); break; } case PropertyNotify: @@ -1514,8 +1827,8 @@ static void flush(struct yasl_display *base) .target = {.generation = yasl_frame_begin(&s->base), .pixels = s->pixels, .size = s->size}, }; - s->cbs->draw(&s->base, &s->frame, s->ud); - s->drawing = false; + yasl_surface_draw(&s->base, &s->frame); + if (!s->destroyed) s->drawing = false; } XFlush(d->native); leave(d); @@ -1525,7 +1838,13 @@ static int ready(int fd, short events, void *ud) { (void)fd; struct yasl_display *base = ud; - if (events & (POLLIN | POLLERR | POLLHUP)) { + struct x11_display *d = (struct x11_display *)base; + if ((events & (POLLERR | POLLHUP)) && !d->lost) { + d->lost = true; + yasl_display_notify_event(base, YASL_EVENT_DISPLAY_LOST); + return 0; + } + if ((events & POLLIN) && !d->lost) { dispatch(base); flush(base); } @@ -1535,7 +1854,7 @@ static int ready(int fd, short events, void *ud) static int dispatch_timeout(struct yasl_display *base, int timeout_ms) { struct x11_display *d = (struct x11_display *)base; - if (unlikely(!d || !d->native)) return -1; + if (unlikely(!d || !d->native || d->lost)) return -1; dispatch(base); for (struct x11_surface *s = d->surfaces; s; s = s->next) { if (s->want_draw || s->input.count || s->input.reset_pending) { @@ -1692,46 +2011,96 @@ static uint32_t caps(const struct yasl_display *d) (((const struct x11_display *)d)->xi2 ? YASL_CAP_TABLET : 0); } -static void copy(struct yasl_display *base, enum yasl_selection sel, const char *text) +static bool copy_text(struct yasl_display *base, enum yasl_selection sel, const char *text) { - if (!text || (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY)) return; + if (!text || (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY)) { + errno = EINVAL; + return false; + } struct x11_display *d = (struct x11_display *)base; char *value = strdup(text); - if (!value) return; + if (!value) { errno = ENOMEM; return false; } free(d->selection[sel]); d->selection[sel] = value; XSetSelectionOwner(d->native, sel == YASL_PRIMARY ? XA_PRIMARY : d->clipboard, d->selection_window, CurrentTime); XFlush(d->native); + return true; } -static void paste(struct yasl_display *base, enum yasl_selection sel, struct yasl_surface *target) +static void copy(struct yasl_display *base, enum yasl_selection sel, const char *text) { - if (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) return; + (void)copy_text(base, sel, text); +} + +static bool request_paste(struct yasl_display *base, enum yasl_selection sel, + struct yasl_surface *target, uint64_t id) +{ + if (!target || (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY)) { + errno = EINVAL; + return false; + } struct x11_display *d = (struct x11_display *)base; struct x11_surface *s = (struct x11_surface *)target; + if (s->destroyed || target->display != base) { errno = EINVAL; return false; } Atom selection = sel == YASL_PRIMARY ? XA_PRIMARY : d->clipboard; - if (XGetSelectionOwner(d->native, selection) == d->selection_window) { + Window owner = XGetSelectionOwner(d->native, selection); + if (owner == None) { + yasl_surface_complete_paste(target, id, YASL_TRANSFER_UNAVAILABLE, NULL, 0); + return true; + } + if (owner == d->selection_window) { const char *text = d->selection[sel]; - if (text) push(s, (struct yasl_event){.type = YASL_EVENT_PASTE}, text, strlen(text)); + if (text) + yasl_surface_complete_paste(target, id, YASL_TRANSFER_OK, text, strlen(text)); + else + yasl_surface_complete_paste(target, id, YASL_TRANSFER_UNAVAILABLE, NULL, 0); + return true; } else { struct paste_request *r = calloc(1, sizeof(*r)); - if (!r) return; + if (!r) { errno = ENOMEM; return false; } r->window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen), 0, 0, 1, 1, 0, 0, 0); + if (!r->window) { free(r); errno = EIO; return false; } + r->display = d; r->target = s; + r->selection = selection; + r->id = id; + errno = 0; + r->timeout = yasl_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, + paste_timeout, r); + if (!r->timeout) { + XDestroyWindow(d->native, r->window); + free(r); + if (!errno) errno = ENOMEM; + return false; + } r->next = d->pastes; d->pastes = r; XConvertSelection(d->native, selection, d->utf8, d->paste_property, r->window, CurrentTime); XFlush(d->native); + return true; } } +static void paste(struct yasl_display *base, enum yasl_selection sel, + struct yasl_surface *target) +{ + (void)request_paste(base, sel, target, 0); +} + static bool input_poll(struct yasl_surface *base, struct yasl_event *event) { struct x11_surface *s = (struct x11_surface *)base; - return !s->destroyed && input_queue_pop(&s->input, event); + if (s->destroyed) return false; + if (base->display->events) return yasl_events_poll_surface(base, event); + return input_queue_pop(&s->input, event); +} + +static struct input_queue *surface_input_queue(struct yasl_surface *base) +{ + return &((struct x11_surface *)base)->input; } static void schedule_redraw(struct yasl_surface *base) @@ -1929,11 +2298,11 @@ static void drag_end(struct x11_display *d, enum yasl_drag_end how) d->drag.icon_pixmap = None; } - if (hover && !hover->destroyed && hover->cbs && hover->cbs->drag) { + if (hover && !hover->destroyed) { if (how == YASL_DRAG_DROPPED) - hover->cbs->drag(&hover->base, YASL_DRAG_DROP, hover_pos, hover->ud); + yasl_surface_notify_drag(&hover->base, YASL_DRAG_DROP, hover_pos); else - hover->cbs->drag(&hover->base, YASL_DRAG_LEAVE, hover_pos, hover->ud); + yasl_surface_notify_drag(&hover->base, YASL_DRAG_LEAVE, hover_pos); } if (l.end) @@ -2130,12 +2499,19 @@ const struct yasl_backend_ops yasl_x11_backend = { .display_modifiers = mods, .display_copy = copy, .display_paste = paste, + .display_copy_text = copy_text, + .display_request_paste = request_paste, .display_drag_start = drag_start, .display_drag_cancel = drag_cancel, .surface_create_toplevel = create_toplevel, .surface_create_popup = surface_create_popup, .surface_popup_reposition = surface_popup_reposition, .surface_destroy = surface_destroy, + .surface_input_queue = surface_input_queue, + .surface_set_size = surface_set_size, + .surface_set_fullscreen = surface_set_fullscreen, + .surface_set_maximized = surface_set_maximized, + .surface_minimize = surface_minimize, .surface_display = surface_display, .surface_set_callbacks = set_callbacks, .surface_set_title = set_title, diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m index 5fd5cbf..6d69907 100644 --- a/tests/cocoa/test_ownership.m +++ b/tests/cocoa/test_ownership.m @@ -3,6 +3,7 @@ #import #include "yasl/yasl.h" #include +#include #include #include @@ -20,13 +21,49 @@ int main(void) assert(yasl_cocoa_surface_get_view(s) == window.contentView); assert([window isKindOfClass:[NSWindow class]] && window.contentView); struct yasl_event event; - while (yasl_surface_poll_event(s, &event)) {} + while (yasl_display_poll_event(d, &event)) {} + + assert(yasl_surface_set_size(s, (struct yasl_vector2i){180, 110})); + assert(yasl_surface_set_fullscreen(s, false)); + assert(yasl_surface_set_maximized(s, false)); + assert(!yasl_surface_set_size(s, (struct yasl_vector2i){0, 100})); + assert(errno == EINVAL); + struct yasl_popup_spec popup_spec = { + .size = {80, 40}, .anchor = {10, 10, 2, 2}, + }; + struct yasl_surface *popup = yasl_surface_create_popup(s, &popup_spec); + assert(popup); + assert(!yasl_surface_set_size(popup, (struct yasl_vector2i){80, 40})); + assert(errno == EINVAL); + assert(!yasl_surface_set_fullscreen(popup, true) && errno == EINVAL); + assert(!yasl_surface_set_maximized(popup, true) && errno == EINVAL); + assert(!yasl_surface_minimize(popup) && errno == EINVAL); + yasl_surface_destroy(popup); + while (yasl_display_poll_event(d, &event)) {} + + id text_input = (id)yasl_cocoa_surface_get_view(s); + [text_input setMarkedText:@"é漢" selectedRange:NSMakeRange(1, 1) + replacementRange:NSMakeRange(NSNotFound, 0)]; + assert(strcmp(yasl_surface_ime_preedit(s), "é漢") == 0); + [text_input unmarkText]; + assert(yasl_surface_ime_preedit(s) == NULL); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_COMPOSITION && event.composition.active); + assert(strcmp(event.composition.data, "é漢") == 0); + assert(event.composition.begin == 2 && event.composition.end == 5); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_COMPOSITION && !event.composition.active); + assert(event.composition.begin == -1 && event.composition.end == -1); + while (yasl_display_poll_event(d, &event)) {} + + uint64_t request_ids[600]; @autoreleasepool { for (int i = 0; i < 600; i++) { char text[32]; snprintf(text, sizeof(text), "paste-%d", i); - yasl_display_copy(d, YASL_CLIPBOARD, text); - yasl_display_paste(d, YASL_CLIPBOARD, s); + assert(yasl_clipboard_set(d, YASL_CLIPBOARD, text)); + request_ids[i] = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(request_ids[i] != 0); memset(text, 'x', sizeof(text)); } } @@ -34,19 +71,31 @@ int main(void) for (int i = 0; i < 600; i++) { char text[32]; snprintf(text, sizeof(text), "paste-%d", i); - assert(yasl_surface_poll_event(s, &event)); - assert(event.type == YASL_EVENT_PASTE && strcmp(event.text.data, text) == 0); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == request_ids[i]); + assert(event.paste.status == YASL_TRANSFER_OK && strcmp(event.paste.data, text) == 0); } - const char *last = event.text.data; - assert(!yasl_surface_poll_event(s, &event)); + const char *last = event.paste.data; + assert(!yasl_display_poll_event(d, &event)); @autoreleasepool { yasl_display_copy(d, YASL_CLIPBOARD, "new selection"); } assert(strcmp(last, "paste-599") == 0); - @autoreleasepool { - yasl_display_copy(d, YASL_CLIPBOARD, ""); - yasl_display_paste(d, YASL_CLIPBOARD, s); - } - assert(yasl_surface_poll_event(s, &event)); - assert(event.type == YASL_EVENT_PASTE && !event.text.len && !event.text.data[0]); + assert(!yasl_clipboard_set(d, YASL_CLIPBOARD, "\xc3")); + assert(errno == EILSEQ); + assert(yasl_clipboard_set(d, YASL_CLIPBOARD, "")); + uint64_t empty_id = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(empty_id && yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == empty_id); + assert(event.paste.status == YASL_TRANSFER_OK && !event.paste.len && !event.paste.data[0]); + [NSPasteboard.generalPasteboard clearContents]; + uint64_t unavailable_id = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(unavailable_id && yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == unavailable_id); + assert(event.paste.status == YASL_TRANSFER_UNAVAILABLE && !event.paste.len); + yasl_display_copy(d, YASL_CLIPBOARD, "legacy paste"); + yasl_display_paste(d, YASL_CLIPBOARD, s); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == 0); + assert(event.paste.status == YASL_TRANSFER_OK && strcmp(event.text.data, "legacy paste") == 0); yasl_surface_destroy(s); yasl_display_close(d); return 0; diff --git a/tests/common/test_app.c b/tests/common/test_app.c index f61a957..214d8f2 100644 --- a/tests/common/test_app.c +++ b/tests/common/test_app.c @@ -2,6 +2,7 @@ #include "yasl/app.h" #include +#include #include #include #include @@ -16,13 +17,16 @@ struct test_state { bool quit_called; int frame_count; yasl_timer_id timer_id; + pthread_t worker; + uint64_t target_id; }; static void *worker_thread(void *arg) { struct test_state *state = arg; usleep(50000); /* 50ms */ - bool ok = yasl_post_event(state->display, state->surface, 42, (void *)0x1234, 0); + bool ok = yasl_display_post(state->display, &(struct yasl_message){ + .target_id = state->target_id, .code = 42, .payload = (void *)0x1234}); assert(ok); return NULL; } @@ -54,13 +58,13 @@ static enum yasl_app_result on_init(void *ud, struct yasl_display *d) yasl_surface_set_pointer_mode(state->surface, YASL_POINTER_RELATIVE); yasl_surface_set_pointer_mode(state->surface, YASL_POINTER_NORMAL); - state->timer_id = yasl_app_add_timer(d, YASL_MS_TO_NS(20), on_timer, state); + state->timer_id = yasl_loop_add_timer(yasl_display_loop(d), YASL_MS_TO_NS(20), on_timer, state); assert(state->timer_id > 0); - pthread_t thread; - int err = pthread_create(&thread, NULL, worker_thread, state); + state->target_id = yasl_surface_id(state->surface); + int err = pthread_create(&state->worker, NULL, worker_thread, state); assert(err == 0); - pthread_detach(thread); + yasl_surface_request_frame(state->surface); return YASL_APP_CONTINUE; @@ -80,7 +84,7 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) return YASL_APP_CONTINUE; } -static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { struct test_state *state = ud; assert(s == state->surface); @@ -88,12 +92,12 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context state->frame_called = true; state->frame_count++; - if (ctx->cpu.pixels) { - size_t count = (size_t)ctx->size.x * ctx->size.y; + if (ctx->target.pixels) { + size_t count = (size_t)ctx->target.size.x * ctx->target.size.y; for (size_t i = 0; i < count; i++) { - ctx->cpu.pixels[i] = 0xff204060; + ctx->target.pixels[i] = 0xff204060; } - bool ok = yasl_frame_present_cpu(ctx, NULL, 0); + bool ok = yasl_frame_present(ctx, NULL, 0); assert(ok); } @@ -106,14 +110,15 @@ static void on_quit(void *ud, struct yasl_display *d) { (void)d; struct test_state *state = ud; + assert(!pthread_join(state->worker, NULL)); state->quit_called = true; } -static void present_once(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void present_once(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { (*(int *)ud)++; - if (ctx->cpu.pixels) - assert(yasl_frame_present_cpu(ctx, NULL, 0)); + if (ctx->target.pixels) + assert(yasl_frame_present(ctx, NULL, 0)); yasl_surface_destroy(s); } @@ -132,6 +137,43 @@ static void test_window_without_init(void) assert(frames == 1); } + +static enum yasl_app_result fail_init(void *ud, struct yasl_display *d) +{ + (void)ud; (void)d; + return YASL_APP_ERROR; +} +static void count_quit(void *ud, struct yasl_display *d) +{ + (void)d; + (*(int *)ud)++; +} +static enum yasl_app_result inspect_initial_window(void *ud, struct yasl_display *d) +{ + struct yasl_surface *s = yasl_app_window(d); + assert(s && yasl_surface_userdata(s) == ud); + errno = 0; + assert(!yasl_surface_set_callbacks(s, NULL, NULL) && errno == EBUSY); + return YASL_APP_CONTINUE; +} +static void fail_frame(void *ud, struct yasl_surface *s, struct yasl_frame *frame) +{ + (void)ud; + assert(frame->surface == s && frame->target.stride_bytes == frame->target.size.x * 4); + yasl_app_exit(yasl_surface_display(s), YASL_APP_ERROR); +} +static void test_error_lifecycle(void) +{ + int quits = 0; + assert(yasl_run(&(struct yasl_app_desc){.userdata = &quits, + .init = fail_init, .quit = count_quit}) < 0); + assert(quits == 1 && errno == EIO); + struct yasl_window_spec window = {.size_logical = {160, 120}, .userdata = &quits}; + assert(yasl_run(&(struct yasl_app_desc){.userdata = &quits, .window = &window, + .init = inspect_initial_window, .frame = fail_frame, .quit = count_quit}) < 0); + assert(quits == 2 && errno == EIO); +} + int main(void) { struct test_state state = {0}; @@ -146,6 +188,9 @@ int main(void) }; int res = yasl_run(&desc); + if (res < 0 && !state.init_called && + (errno == EPERM || errno == EACCES || errno == ENOENT || + errno == ECONNREFUSED || errno == ENODEV)) return 77; assert(res == 0); assert(state.init_called); assert(state.frame_called); @@ -153,6 +198,7 @@ int main(void) assert(state.timer_called); assert(state.quit_called); test_window_without_init(); + test_error_lifecycle(); printf("App runner tests passed!\n"); return 0; } diff --git a/tests/common/test_events.c b/tests/common/test_events.c new file mode 100644 index 0000000..c8660cb --- /dev/null +++ b/tests/common/test_events.c @@ -0,0 +1,209 @@ +#include "common/backend.h" +#include "yasl/app.h" + +#include +#include +#include +#include +#include + +static struct input_queue *queue(struct yasl_surface *s) { return &s->input; } +static const struct yasl_backend_ops ops = {.surface_input_queue = queue}; +static int released; +static pthread_t owner; +static void release(void *payload) +{ + assert(pthread_equal(owner, pthread_self())); + assert(payload == &released); + released++; +} + +static void setup(struct yasl_display *d, struct yasl_surface *a, struct yasl_surface *b) +{ + *d = (struct yasl_display){.ops = &ops}; + *a = (struct yasl_surface){.display = d, .scale_120 = 120}; + *b = (struct yasl_surface){.display = d, .scale_120 = 120}; + assert(yasl_events_open(d)); + assert(yasl_events_register(a)); + assert(yasl_events_register(b)); +} + +static void teardown(struct yasl_display *d, struct yasl_surface *a, struct yasl_surface *b) +{ + yasl_events_unregister(a); + yasl_events_unregister(b); + input_queue_clear(&a->input); + input_queue_clear(&b->input); + yasl_events_close(d); +} + +static void test_order_and_lifetimes(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + uint64_t aid = yasl_surface_id(&a), bid = yasl_surface_id(&b); + assert(aid && bid && aid != bid); + yasl_surface_set_userdata(&a, &b); + assert(yasl_surface_userdata(&a) == &b); + char text[] = "text"; + assert(input_queue_push(&a.input, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {text, 4}})); + memset(text, 'x', 4); + yasl_surface_notify_state(&b, YASL_STATE_MAXIMIZED); + yasl_surface_notify_configure(&a, (struct yasl_geometry){{640, 480}, 180}); + yasl_surface_notify_drag(&b, YASL_DRAG_OVER, (struct yasl_vector2i){3, 4}); + yasl_surface_notify_composition(&a, "hello", 1, 3); + yasl_surface_notify_composition(&a, NULL, -1, -1); + yasl_surface_complete_paste(&b, 42, YASL_TRANSFER_UNAVAILABLE, NULL, 0); + const enum yasl_event_type expected[] = {YASL_EVENT_TEXT, YASL_EVENT_WINDOW_STATE, + YASL_EVENT_WINDOW_RESIZE, YASL_EVENT_DRAG, YASL_EVENT_COMPOSITION, + YASL_EVENT_COMPOSITION, YASL_EVENT_PASTE}; + struct yasl_event ev; + uint64_t sequence = 0; + for (size_t i = 0; i < sizeof(expected) / sizeof(*expected); i++) { + assert(yasl_display_poll_event(&d, &ev)); + assert(ev.type == expected[i] && ev.sequence > sequence && ev.time_ns); + sequence = ev.sequence; + if (i == 0) assert(!strcmp(ev.text.data, "text") && ev.surface_id == aid); + if (i == 2) assert(ev.resize.scale_changed && ev.resize.geometry.size.x == 640); + if (i == 4) assert(ev.composition.active && ev.composition.begin == 1 && !strcmp(ev.composition.data, "hello")); + if (i == 5) assert(!ev.composition.active && !ev.composition.len); + if (i == 6) assert(ev.paste.request_id == 42 && ev.paste.status == YASL_TRANSFER_UNAVAILABLE); + } + assert(!yasl_display_poll_event(&d, &ev)); + assert(input_queue_push(&a.input, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {"kept", 4}})); + assert(yasl_display_poll_event(&d, &ev)); + const char *borrowed = ev.text.data; + assert(input_queue_push(&a.input, &(struct yasl_event){.type = YASL_EVENT_KEY, .surface = &a})); + yasl_events_unregister(&a); + assert(!strcmp(borrowed, "kept")); + assert(!yasl_display_poll_event(&d, &ev)); + /* Reusing object memory must never reuse the identity. */ + assert(yasl_events_register(&a)); + assert(a.id != aid); + errno = 0; + assert(!yasl_display_post(&d, &(struct yasl_message){.target_id = aid})); + assert(errno == ENOENT); + teardown(&d, &a, &b); +} + +struct producer { struct yasl_display *display; uint64_t target; }; +static void *post_worker(void *ud) +{ + struct producer *producer = ud; + for (uint32_t i = 0; i < 32; i++) + assert(yasl_display_post(producer->display, &(struct yasl_message){ + .target_id = producer->target, .code = i, .payload = &released, .release = release})); + return NULL; +} + +static void test_post_cleanup(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + owner = pthread_self(); + released = 0; + pthread_t worker; + struct producer producer = {&d, a.id}; + assert(!pthread_create(&worker, NULL, post_worker, &producer)); + assert(!pthread_join(worker, NULL)); + struct yasl_event ev; + for (uint32_t i = 0; i < 8; i++) { + assert(yasl_display_poll_event(&d, &ev)); + assert(ev.user.code == i && ev.surface == &a); + } + assert(released == 7); /* Last event remains borrowed. */ + yasl_events_unregister(&a); + assert(released == 31); /* Undelivered target messages canceled. */ + assert(!yasl_display_poll_event(&d, &ev)); + assert(yasl_display_post(&d, &(struct yasl_message){.payload = &released, .release = release})); + yasl_events_close(&d); + assert(released == 33); /* Pending message and last consumed message. */ + errno = 0; + assert(!yasl_display_post(&d, &(struct yasl_message){.payload = &released, .release = release})); + assert(errno == EINVAL && released == 33); +} + +static int damage_count; +static bool present(struct yasl_surface *s, const struct yasl_framebuffer *target, + const struct yasl_damage_list *damage) +{ + (void)s; (void)target; + damage_count = damage->n; + return true; +} +static void test_damage(void) +{ + struct yasl_backend_ops present_ops = {.surface_present = present}; + struct yasl_display d = {.ops = &present_ops}; + struct yasl_surface s = {.display = &d, .frame_generation = 1, .drawing = true}; + struct yasl_frame frame = {.surface = &s, .target = {.generation = 1, .size = {8, 8}}}; + s.active_frame = &frame; + struct yasl_rect2i rects[YASL_DAMAGE_MAX + 1] = {{0}}; + assert(yasl_frame_present(&frame, NULL, 0) && damage_count == -1); + assert(!yasl_frame_present(&frame, NULL, 0) && errno == EALREADY); + s.frame_presented = false; + assert(yasl_frame_present(&frame, rects, 0) && damage_count == 0); + s.frame_presented = false; + assert(yasl_frame_present(&frame, rects, YASL_DAMAGE_MAX + 1) && damage_count == -1); + s.frame_presented = false; + rects[YASL_DAMAGE_MAX].width = -1; + errno = 0; + assert(!yasl_frame_present(&frame, rects, YASL_DAMAGE_MAX + 1) && errno == EINVAL); + assert(!yasl_frame_present(&frame, NULL, 1)); + s.drawing = false; + assert(!yasl_frame_present(&frame, NULL, 0)); +} + +static void test_failures(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + assert(!yasl_surface_set_size(&a, (struct yasl_vector2i){0, 10}) && errno == EINVAL); + assert(!yasl_surface_set_size(&a, (struct yasl_vector2i){10, 10}) && errno == ENOTSUP); + assert(!yasl_surface_set_fullscreen(&a, true) && errno == ENOTSUP); + assert(!yasl_surface_set_maximized(&a, true) && errno == ENOTSUP); + assert(!yasl_surface_minimize(&a) && errno == ENOTSUP); + a.popup = true; + assert(!yasl_surface_minimize(&a) && errno == EINVAL); + assert(!yasl_clipboard_set(&d, YASL_CLIPBOARD, "text") && errno == ENOTSUP); + assert(!yasl_clipboard_request(&a, YASL_CLIPBOARD) && errno == ENOTSUP); + /* Queued native event allocation/validation failure must be observable. */ + assert(!yasl_events_push(&d, &a, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {"x", SIZE_MAX}})); + assert(yasl_events_error(&d) == ENOMEM); + teardown(&d, &a, &b); +} + + +static void test_legacy_poll_does_not_spin(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + yasl_surface_notify_state(&a, YASL_STATE_ACTIVATED); + yasl_surface_notify_close(&b); + assert(yasl_events_pending(&d)); + struct yasl_event ev; + assert(!yasl_surface_poll_event(&a, &ev)); + assert(!yasl_events_pending(&d)); + assert(input_queue_push(&b.input, &(struct yasl_event){.type = YASL_EVENT_KEY, .surface = &b})); + assert(!yasl_surface_poll_event(&a, &ev)); + assert(yasl_surface_poll_event(&b, &ev) && ev.surface == &b); + assert(!yasl_events_pending(&d)); + teardown(&d, &a, &b); +} + +int main(void) +{ + test_order_and_lifetimes(); + test_post_cleanup(); + test_damage(); + test_failures(); + test_legacy_poll_does_not_spin(); + return 0; +} diff --git a/tests/common/test_hello_example.c b/tests/common/test_hello_example.c index 101e2d4..350b3bb 100644 --- a/tests/common/test_hello_example.c +++ b/tests/common/test_hello_example.c @@ -3,7 +3,7 @@ #include "../../examples/hello.c" #undef main -static void draw_once(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) +static void draw_once(void *ud, struct yasl_surface *surface, struct yasl_frame *frame) { draw(ud, surface, frame); if (!failed) yasl_surface_destroy(surface); diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py index 76fe8ab..af7e02d 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, hello, input_x11, interactive = sys.argv[1:] + weston, xwayland, test, timers, tablet, hello, input_x11, interactive, app, app_input = sys.argv[1:] servers = [] with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: root = Path(directory) @@ -82,6 +82,11 @@ def main(): 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) + print("Test: Wayland runner lifecycle and event destruction", flush=True) + subprocess.run([app], env=env, check=True, timeout=8) + subprocess.run([app_input], env=env, check=True, timeout=8) + print("Test: X11 runner lifecycle", flush=True) + subprocess.run([app], 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"], diff --git a/tests/meson.build b/tests/meson.build index 84b6847..cac62db 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -279,8 +279,13 @@ if get_option('integration_tests') test('backends', python, args: [files('helpers/run_backends.py'), weston, xwayland, backend_test, timer_test, tablet_example, hello_example_test, input_x11_test, - interactive_example], + interactive_example, app_test, app_input_test], timeout: 60, ) endif endif + +events_test = executable('yasl-events-test', 'common/test_events.c', + include_directories: inc, dependencies: [yasl_dep, dependency('threads')], +) +test('event-contracts', events_test) diff --git a/tests/test_api.cpp b/tests/test_api.cpp index bc54323..c5859cd 100644 --- a/tests/test_api.cpp +++ b/tests/test_api.cpp @@ -1,4 +1,5 @@ #include "yasl/native.h" +#include "yasl/app.h" #if YASL_HAS_WAYLAND #include "yasl/native/wayland.h" #endif @@ -39,6 +40,21 @@ int main() assert(yasl_loop_step(loop, 0) >= 0); yasl_loop_destroy(loop); + yasl_message message{}; + assert(!yasl_display_post(nullptr, &message)); + assert(!yasl_frame_present(nullptr, nullptr, 0)); + assert(!yasl_display_poll_event(nullptr, nullptr)); + assert(!yasl_surface_set_size(nullptr, {640, 480})); + assert(!yasl_surface_set_fullscreen(nullptr, true)); + assert(!yasl_surface_set_maximized(nullptr, true)); + assert(!yasl_surface_minimize(nullptr)); + assert(!yasl_surface_set_pacing(nullptr, YASL_PACING_CONTINUOUS)); + assert(!yasl_surface_set_callbacks(nullptr, nullptr, nullptr)); + assert(!yasl_clipboard_set(nullptr, YASL_CLIPBOARD, "")); + assert(!yasl_clipboard_request(nullptr, YASL_CLIPBOARD)); + assert(!yasl_app_window(nullptr)); + yasl_app_exit(nullptr, YASL_APP_ERROR); + yasl_event event{}; event.key.key = YASL_KEY_A; assert(event.key.key == YASL_KEY_A); diff --git a/tests/wayland/test_app_input.c b/tests/wayland/test_app_input.c index 88f9ead..7e0e9a0 100644 --- a/tests/wayland/test_app_input.c +++ b/tests/wayland/test_app_input.c @@ -5,9 +5,11 @@ #include "yasl/app.h" #include +#include #include struct test_state { + bool initialized; int keys; int buttons, motions, resets; int end; @@ -26,21 +28,20 @@ static void push_key(struct yasl_surface *s) static enum yasl_app_result on_init(void *ud, struct yasl_display *d) { struct test_state *state = ud; + state->initialized = true; if (state->end == 3) { /* With no event callback, a close request alone stops the runner. */ struct yasl_surface *s = yasl_surface_create_toplevel(d, &(struct yasl_window_spec){ .title = "App Close Test", .size = {64, 64} }); assert(s); - struct wayland_surface *w = (struct wayland_surface *)s; - w->cbs->close(s, w->ud); + yasl_surface_notify_close(s); return YASL_APP_CONTINUE; } if (state->end) { - struct wayland_display *display = (struct wayland_display *)d; if (state->end == 1) - display->cbs->quit(display->ud); + yasl_display_notify_event(d, YASL_EVENT_DISPLAY_QUIT); else - display->cbs->lost(display->ud); + yasl_display_notify_event(d, YASL_EVENT_DISPLAY_LOST); return YASL_APP_CONTINUE; } struct yasl_surface *s = yasl_surface_create_toplevel(d, @@ -106,6 +107,12 @@ int main(void) .init = on_init, .event = on_event, }); + /* A display variable may exist even when the test sandbox cannot connect + * to that compositor. Keep this as an integration test in that case. */ + if (res < 0 && !state.initialized && + (errno == EACCES || errno == EPERM || errno == ENOENT || + errno == ECONNREFUSED || errno == ENODEV)) + return 77; assert(res == 0); assert(state.keys == 1); assert(state.buttons == 1 && state.motions == 2 && state.resets == 1); diff --git a/tests/wayland/test_clipboard.c b/tests/wayland/test_clipboard.c index ff84209..1778ad2 100644 --- a/tests/wayland/test_clipboard.c +++ b/tests/wayland/test_clipboard.c @@ -1,24 +1,46 @@ #include "../../src/platform/wayland/clipboard.c" +#include "common/loop.h" #include -struct received { int count; size_t len; char text[32]; }; +struct received { + int count; + size_t len; + uint64_t request_id; + enum yasl_transfer_status status; + struct clipboard *clipboard; + bool forget_on_receive; + char text[32]; +}; static void receive(const char *text, size_t len, bool uri_list, - struct wl_surface *surface, void *ud) + struct wl_surface *surface, uint64_t request_id, + enum yasl_transfer_status status, void *ud) { (void)surface; struct received *r = ud; assert(!uri_list && len < sizeof(r->text)); - memcpy(r->text, text, len); - r->text[len] = '\0'; + if (status == YASL_TRANSFER_OK) { + assert(text); + memcpy(r->text, text, len); + r->text[len] = '\0'; + } else { + assert(!text && len == 0); + } r->len = len; + r->request_id = request_id; + r->status = status; r->count++; + if (r->forget_on_receive) { + r->forget_on_receive = false; + clipboard_forget_surface(r->clipboard, surface); + } } int main(void) { struct received received = {0}; struct clipboard c = {.loop = yasl_loop_create(), .receive = receive, .ud = &received}; + received.clipboard = &c; assert(c.loop); const struct { const char *bytes; size_t len; bool valid; } cases[] = { {"é字😀", sizeof("é字😀") - 1, true}, @@ -28,20 +50,61 @@ int main(void) }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { received = (struct received){0}; + received.clipboard = &c; int fds[2]; assert(pipe(fds) == 0); - assert(start_transfer_text(&c, fds[0], false, false, c.text)); + struct transfer *transfer = start_transfer_text(&c, fds[0], false, false, c.text); + assert(transfer); + yasl_timer_id timeout_id = transfer->timeout; + assert(timeout_id && yasl_loop_timer_armed(c.loop, timeout_id)); + transfer->paste = true; + transfer->request_id = 42; 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); + assert(!yasl_loop_timer_armed(c.loop, timeout_id)); + assert(received.count == 1); + assert(received.request_id == 42); + assert(received.status == (cases[i].valid ? YASL_TRANSFER_OK : YASL_TRANSFER_FAILED)); if (cases[i].valid) { assert(received.len == cases[i].len); assert(memcmp(received.text, cases[i].bytes, received.len) == 0); } } + received = (struct received){0}; + received.clipboard = &c; + int cancel_fds[2]; + assert(pipe(cancel_fds) == 0); + struct transfer *pending = start_transfer_text(&c, cancel_fds[0], false, false, NULL); + assert(pending); + yasl_timer_id cancel_timeout_id = pending->timeout; + assert(cancel_timeout_id && yasl_loop_timer_armed(c.loop, cancel_timeout_id)); + pending->paste = true; + pending->request_id = 99; + struct wl_surface *target = (struct wl_surface *)(uintptr_t)1; + pending->drop_surface = target; + clipboard_forget_surface(&c, target); + close(cancel_fds[1]); + assert(!c.transfers && received.count == 0); + assert(!yasl_loop_timer_armed(c.loop, cancel_timeout_id)); + + received.forget_on_receive = true; + int timeout_fds[2]; + assert(pipe(timeout_fds) == 0); + struct transfer *timed = start_transfer_text(&c, timeout_fds[0], false, false, NULL); + assert(timed && timed->timeout); + timed->paste = true; + timed->request_id = 100; + struct wl_surface *timeout_target = (struct wl_surface *)(uintptr_t)2; + timed->drop_surface = timeout_target; + yasl_timer_id timeout_id = timed->timeout; + assert(yasl_loop_del_timer(c.loop, timeout_id)); + (void)transfer_timeout(timed, timeout_id, CLIPBOARD_TRANSFER_TIMEOUT_NS); + close(timeout_fds[1]); + assert(!c.transfers && received.count == 1); + assert(received.request_id == 100 && received.status == YASL_TRANSFER_FAILED); yasl_loop_destroy(c.loop); return 0; } diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c index 95943d1..c2eca37 100644 --- a/tests/x11/test_input.c +++ b/tests/x11/test_input.c @@ -1,5 +1,7 @@ /* An isolated real X server supplies the keymap; disabling this surface's XIC * deliberately exercises text insertion when no input method is available. */ +#define _POSIX_C_SOURCE 200809L +#include #include "../../src/platform/x11/window.c" static void check_selection_encoding(struct x11_display *d, struct x11_surface *s) @@ -35,34 +37,72 @@ static void check_selection_encoding(struct x11_display *d, struct x11_surface * 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}, + /* Simulate legacy STRING, empty UTF-8, malformed UTF-8, and rejected INCR. */ + const struct { + Atom type; + const void *bytes; + size_t len; + int format; + const char *expected; + enum yasl_transfer_status status; + } cases[] = { + {XA_STRING, "caf\xe9 \xa3", 6, 8, "café £", YASL_TRANSFER_OK}, + {d->utf8, "", 0, 8, "", YASL_TRANSFER_OK}, + {d->utf8, "\xff", 1, 8, NULL, YASL_TRANSFER_FAILED}, + {d->incr, NULL, 1, 32, NULL, YASL_TRANSFER_FAILED}, }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { struct paste_request *pending = calloc(1, sizeof(*pending)); assert(pending); + pending->display = d; pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), 0, 0, 1, 1, 0, 0, 0); pending->target = s; + pending->id = (uint64_t)i + 1; + pending->timeout = yasl_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, + paste_timeout, pending); + assert(pending->timeout); 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); + unsigned long incr_bytes = 4096; + const void *bytes = cases[i].bytes; + if (cases[i].type == d->incr) bytes = &incr_bytes; + XChangeProperty(d->native, pending->window, property, cases[i].type, + cases[i].format, PropModeReplace, bytes, (int)cases[i].len); XSelectionEvent reply = {.requestor = pending->window, .property = property}; selection_notify(d, &reply); struct yasl_event event; - if (cases[i].expected) { - assert(yasl_surface_poll_event(&s->base, &event)); - assert(event.type == YASL_EVENT_PASTE); + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == i + 1); + assert(event.paste.status == cases[i].status); + if (cases[i].status == YASL_TRANSFER_OK) { assert(event.text.len == strlen(cases[i].expected)); assert(strcmp(event.text.data, cases[i].expected) == 0); } + if (cases[i].type == d->incr) { + assert(d->pastes == pending && !pending->target); + assert(yasl_loop_del_timer(d->loop, pending->timeout)); + pending->timeout = 0; + paste_timeout(pending, 0, PASTE_TIMEOUT_NS); + } assert(!yasl_surface_poll_event(&s->base, &event)); } + + struct paste_request *rejected = calloc(1, sizeof(*rejected)); + assert(rejected); + rejected->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + rejected->target = s; + rejected->selection = d->clipboard; + rejected->id = 99; + rejected->next = d->pastes; + d->pastes = rejected; + XSelectionEvent no_property = {.requestor = rejected->window, .property = None}; + selection_notify(d, &no_property); + struct yasl_event event; + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == 99); + assert(event.paste.status == YASL_TRANSFER_FAILED); } /* Own the XSETTINGS selection with one Net/DoubleClickTime value. */ @@ -96,6 +136,173 @@ static void check_double_click(struct x11_display *d) assert(yasl_display_double_click_ms(&d->base) == YASL_DOUBLE_CLICK_MS_DEFAULT); } +static struct yasl_event wait_paste(struct yasl_display *display, uint64_t id) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_PASTE && event.paste.request_id == id) + return event; + assert(!"expected paste completion was not queued"); + return (struct yasl_event){0}; +} + +static void check_request_completion(struct x11_display *d, struct x11_surface *s) +{ + assert(surface_input_queue(&s->base) == &s->input); + assert(copy_text(&d->base, YASL_CLIPBOARD, "")); + XSync(d->native, False); + assert(XGetSelectionOwner(d->native, d->clipboard) == d->selection_window); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 101)); + struct yasl_event event = wait_paste(&d->base, 101); + assert(event.paste.status == YASL_TRANSFER_OK && event.paste.len == 0); + assert(event.paste.data && !event.paste.data[0]); + + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 102)); + XSync(d->native, False); + dispatch(&d->base); + event = wait_paste(&d->base, 102); + assert(event.paste.status == YASL_TRANSFER_UNAVAILABLE); + + Window silent_owner = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + XSetSelectionOwner(d->native, d->clipboard, silent_owner, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 103)); + struct paste_request *timed = d->pastes; + assert(timed && timed->id == 103 && timed->timeout); + assert(yasl_loop_del_timer(d->loop, timed->timeout)); + timed->timeout = 0; + paste_timeout(timed, 0, PASTE_TIMEOUT_NS); + event = wait_paste(&d->base, 103); + assert(event.paste.status == YASL_TRANSFER_FAILED); + + struct yasl_window_spec spec = {.title = "Cancelled paste", .size = {80, 60}}; + struct yasl_surface *cancel_target = yasl_surface_create_toplevel(&d->base, &spec); + assert(cancel_target); + assert(request_paste(&d->base, YASL_CLIPBOARD, cancel_target, 104)); + assert(d->pastes && d->pastes->target == (struct x11_surface *)cancel_target); + yasl_timer_id timeout = d->pastes->timeout; + yasl_surface_destroy(cancel_target); + assert(d->pastes && !d->pastes->target && d->pastes->id == 0); + assert(yasl_loop_del_timer(d->loop, timeout)); + d->pastes->timeout = 0; + XSelectionEvent late_reply = { + .requestor = d->pastes->window, + .selection = d->pastes->selection, + .property = None, + }; + selection_notify(d, &late_reply); + assert(!d->pastes); + XDestroyWindow(d->native, silent_owner); + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); +} + +static struct yasl_event next_composition(struct yasl_display *display) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_COMPOSITION) + return event; + assert(!"expected composition snapshot was not queued"); + return (struct yasl_event){0}; +} + +static void drain_display_events(struct yasl_display *display) +{ + struct yasl_event event; + while (yasl_display_poll_event(display, &event)) {} +} + +static void check_xim_callbacks(struct yasl_display *display, struct x11_surface *s) +{ + const char *old_locale = setlocale(LC_CTYPE, NULL); + char *saved_locale = old_locale ? strdup(old_locale) : NULL; + const char *utf8_locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!utf8_locale) utf8_locale = setlocale(LC_CTYPE, "C.utf8"); + assert(utf8_locale); + + drain_display_events(display); + assert(xim_preedit_start(NULL, (XPointer)s, NULL) == -1); + + wchar_t initial[] = {L'A', (wchar_t)0x1f600, L'B'}; + XIMText wide = {.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + XIMPreeditDrawCallbackStruct draw = { + .caret = 2, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "A😀B") == 0); + struct yasl_event event = next_composition(display); + assert(event.composition.active && event.composition.len == 6); + assert(event.composition.begin == 5 && event.composition.end == 5); + + /* XIMText.length counts characters. The data need not have a terminator. */ + char multibyte[] = {(char)0xc3, (char)0xa9, 'Z', 'x'}; + XIMText mb = {.length = 2, .encoding_is_wchar = False, + .string.multi_byte = multibyte}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 3, .chg_first = 1, .chg_length = 1, .text = &mb, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 4); + assert(event.composition.end == 4); + + /* A NULL text string updates feedback/caret while preserving the snapshot. */ + XIMText feedback = {.length = 4, .encoding_is_wchar = False, + .string.multi_byte = NULL}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 4, .text = &feedback, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 1); + + char malformed[] = {(char)0xff, 'x'}; + XIMText invalid = {.length = 1, .encoding_is_wchar = False, + .string.multi_byte = malformed}; + draw.text = &invalid; + draw.chg_first = 0; + draw.chg_length = 0; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + struct yasl_event unused; + assert(!yasl_display_poll_event(display, &unused)); + + wchar_t empty[] = {0}; + XIMText deletion = {.length = 0, .encoding_is_wchar = True, + .string.wide_char = empty}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 0, .chg_first = 0, .chg_length = 4, .text = &deletion, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + event = next_composition(display); + assert(!event.composition.active && event.composition.len == 0); + assert(!s->ime_preedit); + + /* Done also publishes an explicit end snapshot. */ + wide = (XIMText){.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + (void)next_composition(display); + xim_preedit_done(NULL, (XPointer)s, NULL); + event = next_composition(display); + assert(!event.composition.active && !s->ime_preedit); + + if (saved_locale) { + setlocale(LC_CTYPE, saved_locale); + free(saved_locale); + } +} + int main(void) { struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; @@ -126,10 +333,13 @@ int main(void) XSync(d->native, False); dispatch(base); input_queue_clear(&s->input); + /* Native creation and keymap updates may already have queued focus or + * reset input on the shared display stream. Isolate this synthetic key. */ + struct yasl_event event; + while (yasl_surface_poll_event(surface, &event)) {} XKeyEvent native = {.type = KeyPress, .display = d->native, .window = s->window, .keycode = code}; key_event(s, &native, false); - struct yasl_event event; assert(yasl_surface_poll_event(surface, &event)); assert(event.type == YASL_EVENT_KEY && event.key.key == YASL_KEY_A); assert(event.key.codepoint == cases[i].label); @@ -149,6 +359,8 @@ int main(void) s->ic = saved_ic; check_selection_encoding(d, s); check_double_click(d); + check_request_completion(d, s); + check_xim_callbacks(base, s); /* Test CSD interactive actions, window menu, and IME caret */ assert(yasl_display_caps(base) & YASL_CAP_CSD); diff --git a/tests/x11/test_tablet_native.c b/tests/x11/test_tablet_native.c index b0d1366..706a5cc 100644 --- a/tests/x11/test_tablet_native.c +++ b/tests/x11/test_tablet_native.c @@ -57,14 +57,8 @@ static int connect_tool(const char *path) static void native_draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) { - struct yasl_frame_context context = { - .surface = surface, .size = frame->target.size, - .scale_120 = yasl_surface_geometry(surface).scale_120, - .cpu = {.pixels = frame->target.pixels, - .stride_bytes = frame->target.size.x * (int)sizeof(uint32_t)}, - .internal_handle = frame, - }; - draw(ud, surface, &context); + (void)surface; + draw(ud, surface, frame); } static void native_configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) -- cgit v1.2.3