1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
|
# YASL (Yet Another Surface Library)
YASL is a minimal, modern, and high-performance desktop window surface library written in C.
It provides window creation, frame pacing, high-DPI scaling, and input routing without bundling audio, physics, or game engine components.
## Features
- **Lean Core**: No audio, no font rendering, and no widget toolkit overhead.
- **Wayland Protocol Coverage**:
- `xdg-shell` toplevels and hierarchical popups (`xdg_popup` + `xdg_positioner`).
- Fractional scaling via `wp_fractional_scale_v1` and `wp_viewporter`.
- Presentation feedback and refresh rate timing via `wp_presentation` and `wp_fifo_v1`.
- Cursor shapes via `wp_cursor_shape_device_v1`.
- Graphics tablets and stylus input via `zwp_tablet_v2` (pressure, tilt, eraser).
- Text Input Method (IME) composition via `zwp_text_input_v3`.
- Primary selection and clipboard transfers via `wl_data_device` and `zwp_primary_selection_v1`.
- Interactive client-side window move and resize triggers (`xdg_toplevel_move`, `xdg_toplevel_resize`).
- **macOS Cocoa & Metal**:
- Native `NSWindow` and `CAMetalLayer` presentation.
- Vsync pacing via `CADisplayLink`.
- **Zero Framework Leaks**:
- Self-contained UTF-8 validation.
- Self-contained event queue.
- No terminal-specific telemetry or hardcoded application names.
## Public API
Include `<yasl/yasl.h>` for surface creation, drawing, input, and display dispatch.
`yasl_surface_geometry()` returns device-pixel size and scale together.
`yasl_display_dispatch(display, timeout_ms)` polls with zero, waits indefinitely
with a negative timeout, or waits up to a positive number of milliseconds.
Pending input or redraw work returns immediately; call `yasl_display_flush()`
after processing input and scheduling redraws.
Call `yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN)` to hide the system
mouse and tablet cursor over a surface while continuing to receive pointer
input. The choice persists across motion and re-entry; select another cursor,
such as `YASL_CURSOR_ARROW`, to show it again. `YASL_CURSOR_NONE` is a compatible
alias for `YASL_CURSOR_HIDDEN`.
Include `<yasl/loop.h>` to use the optional event loop, file watches, or timers.
Timers use one callback model and nanoseconds throughout; use `YASL_MS_TO_NS(5)`
for a five-millisecond interval. Add application timers to
`yasl_display_loop(display)`. Add surface timers with `yasl_surface_add_timer()`
when they should be canceled automatically on surface destruction.
Include `<yasl/native.h>` for platform handles and application-managed native
rendering. These extensions are separate from the portable CPU drawing API.
`<yasl/timing.h>` contains the public timing result types.
## Backend selection
YASL selects the native window backend at runtime.
Leave `yasl_display_spec.backend` at zero to use this mode.
`YASL_BACKEND_AUTO` has the same effect.
You can also pass NULL as the display spec.
On Linux, YASL tries Wayland first in a Wayland session.
It uses `WAYLAND_DISPLAY`, `WAYLAND_SOCKET`, and `XDG_SESSION_TYPE` to detect that session.
In other sessions, it tries X11 first.
If the first backend cannot open, it tries the other built backend.
On macOS, it uses Cocoa.
Set `backend` to request one backend:
```c
struct yasl_display_spec spec = {
.app_id = "org.example.app",
.backend = YASL_BACKEND_X11,
};
struct yasl_display *display = yasl_display_open(loop, &spec);
```
Use `YASL_BACKEND_WAYLAND` to request Wayland.
Use `YASL_BACKEND_COCOA` to request Cocoa.
An explicit request never falls back to another backend.
Open returns NULL if the requested backend cannot open.
It sets `errno` to `ENOSYS` for a backend that was not built.
It sets `errno` to `EINVAL` for an invalid backend value.
Call `yasl_display_backend(display)` to read the selected backend.
All surfaces on a display use that backend.
Open separate displays to use Wayland and X11 in one process.
The public API needs no window system headers.
Shared libraries export only the functions declared in the installed YASL headers.
Input queues, frame clocks, backend entry points, and protocol helpers are private implementation details.
Use `yasl_display_caps()` to check optional features.
X11 supports CPU pixel buffers, keyboard and pointer input, and clipboard transfer.
Tablet pressure, tilt, and erasers use XInput 2 when the server exposes it.
It supports the primary selection, window placement, and drag operations.
It does not yet support YASL popups or IME preedit text.
Those features return their normal unsupported result.
The surface spec needs no native graphics device for CPU drawing.
On Cocoa, YASL creates its own Metal device for this mode.
An app that renders with Metal includes `<yasl/native.h>` and calls
`yasl_surface_create_native(display, &spec, device)`. During its draw callback,
`yasl_surface_get_native_frame()` returns the current Metal drawable. The app
submits and presents it, then calls `yasl_surface_present()` to record submission.
Type-safe native handles are declared in `<yasl/native/wayland.h>`,
`<yasl/native/x11.h>`, and `<yasl/native/cocoa.h>`:
- Wayland: `wl_display *` and `wl_surface *` via `yasl_wayland_display_get_wl_display()` and `yasl_wayland_surface_get_wl_surface()`.
- X11: `Display *` and `uintptr_t` window ID via `yasl_x11_display_get_display()` and `yasl_x11_surface_get_window()`.
- Cocoa: `NSApplication *`, `NSWindow *`, and `NSView *` via `yasl_cocoa_display_get_application()`, `yasl_cocoa_surface_get_window()`, and `yasl_cocoa_surface_get_view()`.
Generic untyped borrowed handles (`void *`) remain accessible via `<yasl/native.h>`. All handles expire on object destruction.
## Ownership and backend contracts
- Calls and callbacks use the thread that opened the display (the main thread
on Cocoa). Supplied event loops are borrowed. Destroy popups before parents,
surfaces before displays, and displays before supplied loops. Surface
destruction inside surface callbacks is supported; display or loop destruction
during callbacks/dispatch is not.
- Callback tables, drag listeners, input strings, titles, clipboard text, and
drag pixels are copied. Userdata is borrowed. Cocoa retains a supplied Metal
device. Native handles are borrowed, not transferred to the caller.
- Text, paste, and file-drop event payloads remain valid until the next
**successful** input poll on that surface or surface destruction. Dispatch,
flush, selection replacement, and failed polls do not invalidate them.
Copy a payload if it must live longer.
- Input queues grow rather than silently dropping events at a fixed limit.
If allocation fails, pending input is discarded and `YASL_INPUT_RESET` is
delivered before later events. X11 also sends a reset when input devices are
removed or disabled. Apps must clear held keys/buttons and input composition
state on this event.
- Clipboard requests retain their original target. Destroying that target
cancels delivery, rather than redirecting the paste to a focused window.
Unsupported primary selections are no-ops, never aliases for the clipboard.
- CPU pixels are tightly packed premultiplied `0xAARRGGBB`. Creation sizes are
logical pixels; framebuffer sizes, pointer coordinates, caret and damage are
device pixels. Every draw target needs complete valid pixel contents; do not
assume a recycled framebuffer contains the previous frame.
- Present only the current draw callback's target, at most once. Targets expire
when the callback returns; preserve the opaque `generation` when copying a
target. Damage is required, clipped to the target, and validated uniformly. Backends may upload the whole frame despite damage hints.
- `yasl_surface_create_native()` requires `YASL_CAP_NATIVE_GRAPHICS` (currently
Cocoa) and a non-NULL graphics device; other backends return `ENOTSUP`. Native
drawable queries are valid only for the current draw target and expire on
successful present, callback return, or surface destruction.
- `YASL_CAP_FRAME_PACING` distinguishes compositor/display-driven draws from
X11's unpaced flush-driven draws. `yasl_timing.source` distinguishes submission
estimates, frame callbacks, and confirmed presentation feedback. Refresh and
next-presentation times are estimates, not universal vsync guarantees.
These are pre-release API changes. Applications must handle the new reset input
kind and should inspect timing provenance instead of assuming all timestamps
confirm scanout. See `include/yasl/yasl.h` for the complete contracts.
## Building
Use a C23 compiler and Meson 1.6 or later.
Build with Meson and Ninja:
```sh
meson setup build
ninja -C build
ninja -C build test
```
Linux builds include each backend whose dependencies are installed.
Wayland needs `wayland-client`, `wayland-cursor`, and `xkbcommon`.
X11 needs `x11`, `xi` (libXi), and `xkbcommon`.
Use `-Dwayland=enabled` or `-Dx11=enabled` to require a backend.
Use `-Dwayland=disabled` or `-Dx11=disabled` to omit a backend.
At least one backend must be built.
To run the Linux window tests, install Weston and Xwayland.
Then run:
```sh
meson setup build-test -Dintegration_tests=true
meson test -C build-test --print-errorlogs
```
The tests start isolated display servers.
They test explicit requests, automatic selection, and fallback.
They also test both backends in one process, callback-table copying, input
payload ownership, queue growth, concurrent clipboard targets, and consistent
frame/damage validation. Wayland IME ownership tests need no display server.
For memory and undefined-behavior checks, configure a separate build with
`-Db_sanitize=address,undefined`.
On macOS, the same option runs native Cocoa window tests.
These tests need a desktop session.
## Tablet drawing example
Build and run the same C application on Wayland, X11, or Cocoa:
```sh
meson setup build
meson compile -C build
./build/yasl-tablet
./build/yasl-tablet --backend wayland
./build/yasl-tablet --backend x11
# On macOS:
./build/yasl-tablet --backend cocoa
```
Draw with the pen tip: pressure controls brush width. Flip to the eraser to
remove ink. The window shows the selected backend, tool, contact state, raw
pressure (0..65535), percentage, and tilt in degrees, with a live pressure bar,
brush outline, and tilt vector. The system cursor is hidden so the brush overlay
serves as the drawing cursor. Mouse drawing uses a fixed width. Press C or
Space to clear and Escape to quit. Resizing and display scale changes preserve
the drawing. `--smoke-test` presents one frame and exits for automated checks.
[`examples/tablet.c`](examples/tablet.c) includes only `<yasl/yasl.h>` and the C
standard library. It uses CPU framebuffers and `yasl_surface_input_poll()` on
every backend. Tablet events arrive as `YASL_INPUT_MOUSE`, distinguished by
`mouse.device == YASL_PTR_DEV_PEN` or `YASL_PTR_DEV_ERASER`; `mouse.pen` contains
pressure and signed tilt. Button 0 is tip contact on every backend. Track tip
press/release events: zero pressure alone does not mean release. Hover and side
buttons do not paint, and focus loss, leave, or `YASL_INPUT_RESET` cancels a stroke.
Stationary pressure changes arrive as motion events and update the brush too.
`YASL_CAP_TABLET` indicates backend/protocol support, not whether a tablet is
plugged in or which axes it has. Missing axes are zero. Wayland requires the
compositor's `zwp_tablet_manager_v2`; X11 requires XInput 2 and driver-provided
`Abs Pressure`/`Abs Tilt X`/`Abs Tilt Y` valuators. Cocoa handles both tablet mouse
subtypes and native tablet point/proximity events. X11 identifies erasers by
the Wacom tool property or the driver's device name. Hardware and driver
support determine which readings are available. The example displays a message
when the server has no tablet protocol and still allows mouse drawing.
The test suite checks synthetic Wayland/XInput samples and the drawing behavior
without hardware, including pressure-only changes, sparse axes, eraser input,
hover, release, reset, and scale changes. Linux integration tests launch the
example on isolated Wayland and X11 servers; macOS integration tests also check
Cocoa tablet event translation. Physical tablet testing is still needed to
validate a particular device/driver combination.
For an end-to-end X11 pressure test without hardware, install Xorg's `dummy`
video driver and `inputtest` input driver, then run:
```sh
meson configure build -Dx11_tablet_tests=true
meson test -C build tablet-x11-native --print-errorlogs
```
This starts an isolated Xorg server with virtual pen and eraser devices, sends
real driver samples, checks YASL's shared events through the example, and verifies
its presented window pixels. It checks stationary pressure changes, release
without valuators, and eraser identification through the driver's tool property.
The test disables automatic physical input/GPU attachment and cleans up its
server. Set `YASL_XORG_MODULE_PATH` to a comma-separated module search path if
the test drivers are outside Xorg's default directories.
## Quick Example
```c
#include <yasl/yasl.h>
static bool running = true;
static void on_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud)
{
/* Present frame target */
struct yasl_damage_list dmg = { .n = -1 };
yasl_surface_present(s, &frame->target, &dmg);
}
static void on_close(struct yasl_surface *s, void *ud)
{
(void)s; (void)ud;
running = false;
}
int main(void)
{
struct yasl_display_spec dspec = { .app_id = "org.example.app" };
struct yasl_display *d = yasl_display_open(NULL, &dspec);
struct yasl_surface_spec sspec = {
.title = "YASL Application",
.size = { .x = 800, .y = 600 },
};
struct yasl_surface *s = yasl_surface_create_toplevel(d, &sspec);
struct yasl_surface_cbs cbs = {
.draw = on_draw,
.close = on_close,
};
yasl_surface_set_callbacks(s, &cbs, NULL);
yasl_surface_schedule_redraw(s);
while (running) {
yasl_display_dispatch(d, -1);
struct yasl_input_event ev;
while (yasl_surface_input_poll(s, &ev)) {
if (ev.kind == YASL_INPUT_KEY && ev.key.action == YASL_KEY_PRESS) {
if (ev.key.key == 0xff1b) /* Escape */
running = false;
}
}
yasl_display_flush(d);
}
yasl_surface_destroy(s);
yasl_display_close(d);
return 0;
}
```
## License
MIT License.
|