summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--docs/api-policy.md2
-rw-r--r--docs/api-redesign.md147
-rw-r--r--docs/getting-started.md21
-rw-r--r--docs/guide.md149
-rw-r--r--docs/migrating.md45
-rw-r--r--examples/game.c18
-rw-r--r--examples/hello.c12
-rw-r--r--examples/notepad.c66
-rw-r--r--examples/simple_window.c17
-rw-r--r--examples/tablet.c16
-rw-r--r--include/yasl/app.h87
-rw-r--r--include/yasl/yasl.h186
-rw-r--r--meson.build21
-rw-r--r--src/common/app.c468
-rw-r--r--src/common/backend.c222
-rw-r--r--src/common/backend.h45
-rw-r--r--src/common/events.c372
-rw-r--r--src/common/input.c13
-rw-r--r--src/common/input.h4
-rw-r--r--src/common/meson.build2
-rw-r--r--src/platform/cocoa/window.m275
-rw-r--r--src/platform/wayland/api.h3
-rw-r--r--src/platform/wayland/backend.c78
-rw-r--r--src/platform/wayland/buffer.c15
-rw-r--r--src/platform/wayland/clipboard.c152
-rw-r--r--src/platform/wayland/clipboard.h9
-rw-r--r--src/platform/wayland/drag.c8
-rw-r--r--src/platform/wayland/ime.c5
-rw-r--r--src/platform/wayland/keyboard.c54
-rw-r--r--src/platform/wayland/pointer.c10
-rw-r--r--src/platform/wayland/shell.c7
-rw-r--r--src/platform/wayland/surface.c7
-rw-r--r--src/platform/x11/window.c504
-rw-r--r--tests/cocoa/test_ownership.m75
-rw-r--r--tests/common/test_app.c72
-rw-r--r--tests/common/test_events.c209
-rw-r--r--tests/common/test_hello_example.c2
-rw-r--r--tests/helpers/run_backends.py7
-rw-r--r--tests/meson.build7
-rw-r--r--tests/test_api.cpp16
-rw-r--r--tests/wayland/test_app_input.c17
-rw-r--r--tests/wayland/test_clipboard.c75
-rw-r--r--tests/x11/test_input.c236
-rw-r--r--tests/x11/test_tablet_native.c10
45 files changed, 2902 insertions, 868 deletions
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 `<yasl/yasl.h>` 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 `<yasl/native.h>`;
+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 `<yasl/app.h>` 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 `<yasl/desktop.h>` 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 `<yasl/app.h>` 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/yasl.h>`. `yasl_surface_poll_event()` returns the same events that
-`yasl_app_desc.event` receives, so input handling code works under both.
+`<yasl/yasl.h>`. `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 <stdio.h>
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 = &target;
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 <errno.h>
-#include <stdlib.h>
-#include <string.h>
-
-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 <errno.h>
#include <stdlib.h>
@@ -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 <string.h>
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 <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+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(&copy, 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 <assert.h>
+#include <errno.h>
#include <math.h>
#include <poll.h>
#include <stdio.h>
@@ -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<NSDraggingInfo> 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<NSDraggingInfo> 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<NSDraggingInfo> 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 <errno.h>
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 <errno.h>
#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 <errno.h>
#include "common/keyboard.h"
#include "keymap.h"
-#include <errno.h>
#define XKB_EVDEV_OFFSET 8
#define KEYMAP_MAX_BYTES (8u * 1024 * 1024)
@@ -427,27 +427,65 @@ 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 <stdlib.h>
#include <string.h>
#include <strings.h>
+#include <wchar.h>
#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 <AppKit/AppKit.h>
#include "yasl/yasl.h"
#include <assert.h>
+#include <errno.h>
#include <stdio.h>
#include <string.h>
@@ -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<NSTextInputClient> text_input = (id<NSTextInputClient>)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 <assert.h>
+#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
@@ -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 <assert.h>
+#include <errno.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+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 <assert.h>
+#include <errno.h>
#include <stdlib.h>
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 <assert.h>
-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 <locale.h>
#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)