diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 15:05:06 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 15:05:06 +0300 |
| commit | 98f0d51157fa2a2b8ccaae54ed4850a88d223d01 (patch) | |
| tree | 90b03d0930986620d95eb0f45c07b96e1331783c | |
| parent | 339e1ad7adb8340732ee856d0f6f0d2c7f1455d1 (diff) | |
Simplify surface state, examples, and library installation
47 files changed, 722 insertions, 491 deletions
diff --git a/ARCHITECTURE_PLAN.md b/ARCHITECTURE_PLAN.md index 5c866e5..f2ec38c 100644 --- a/ARCHITECTURE_PLAN.md +++ b/ARCHITECTURE_PLAN.md @@ -1,4 +1,4 @@ -# YASL Modern Architecture Plan (Approach A) +# YASL Modern Architecture Plan This document outlines the transition of YASL to **Approach A: Inverted Lifecycle & Unified Event Architecture**, designed specifically to serve both high-performance game development and reactive desktop applications (IDEs and editors like `examples/notepad.c`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 939f4e4..30432ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ configurations checked. Cocoa changes need a macOS desktop build/test; Linux success alone does not validate them. Keep changes focused and describe any platforms that were not exercised. -Document public behavior in the installed header first: argument validity, +Document public behavior in the public header first: argument validity, units, ownership/lifetime, return/error behavior, thread requirements, callback rules, and unsupported results. Update guides/examples when the workflow changes. Use zero-initialized specs and check fallible operations in examples. @@ -47,28 +47,27 @@ one with `-Dwayland=enabled` or `-Dx11=enabled`; omit one with `disabled`. Runti selection chooses the native session and allows fallback, or you can request an explicit backend with `yasl_display_spec.backend`. -Include `<yasl/yasl.h>` for the core. The complete [hello.c](examples/hello.c) -handles creation failures, drawing, dispatch, input draining, and cleanup. -Its drawing callback fills every pixel before presenting: +Include `<yasl/app.h>` and pass application callbacks to `yasl_run()`. +The complete [hello.c](examples/hello.c) creates its window in `init`, handles +close requests in `event`, and fills every pixel in its frame callback: ```c -static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) +static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) { (void)ud; - size_t count = (size_t)frame->target.size.x * frame->target.size.y; + size_t count = (size_t)frame->size.x * frame->size.y; for (size_t i = 0; i < count; i++) - frame->target.pixels[i] = 0xff306090; - struct yasl_damage_list damage = {.n = -1}; - if (!yasl_surface_present(surface, &frame->target, &damage)) { - /* Arrange application error handling after returning. */ - } + frame->cpu.pixels[i] = 0xff306090; + if (!yasl_frame_present_cpu(frame, NULL, 0)) + yasl_surface_destroy(surface); } ``` Toplevel creation sizes are logical pixels; drawing and input use device pixels. -Targets are borrowed for one draw callback. Keep durable content in application -memory, dispatch events, drain input queues, then flush scheduled drawing. These -rules are explained with examples in the application guide. +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. ## License diff --git a/docs/api-policy.md b/docs/api-policy.md index 0c8f040..dd22ca7 100644 --- a/docs/api-policy.md +++ b/docs/api-policy.md @@ -1,16 +1,17 @@ # Public API policy -YASL 0.1 is pre-release. The installed headers are the public API; implementation +YASL 0.1 is pre-release. The headers under `include/yasl/` are the public API; implementation headers under `src/` are private even when their symbols appear in a static -archive. The shared library exports only declarations from installed headers. -Native backend headers and accessors are available only for built backends. +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. There is currently no stable ABI promise and no versioned shared-library SONAME. Rebuild consumers against the exact headers/library pair they ship. Source and binary compatibility can change before 1.0. Do not assume struct layout is stable across releases, mix headers and binaries from different revisions, or persist -public structs as a file/network format. The package version is available through -`pkg-config --modversion yasl`. +public structs as a file/network format. The release version is declared in +`meson.build`. See [migration notes](migrating.md) for window creation, portable keyboard, and fractional scroll API changes. @@ -28,7 +29,7 @@ Every public addition or behavior change needs: - An update to the guide and its backend limits when observable behavior changes. - A runnable example when the new workflow cannot be understood from an isolated call, and meaningful regression coverage for ownership or behavior changes. -- A check that shared exports and installed-package consumers still work. +- A check that shared exports and consumers of the installed library still work. Functions documenting errno set it on failure. Other booleans report acceptance or availability without a guaranteed errno; void setters are best effort. diff --git a/docs/getting-started.md b/docs/getting-started.md index d21c9bd..caa25eb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -39,79 +39,68 @@ CPU pixel targets are presented through the system's Metal device; applications do not need to create or manage that device themselves. The complete beginner example is [hello.c](../examples/hello.c). It checks -construction failures, fills every pixel, presents inside `draw`, drains input, -responds to quit/close/lost, and destroys resources in order. The +construction failures, fills every pixel, and presents inside a frame callback. +`yasl_run()` handles input delivery, display notifications, and cleanup. The [interactive window](../examples/simple_window.c) adds pointer input; the [tablet example](../examples/tablet.c) demonstrates retained application content, pressure, erasers, resizing, and scale changes. ## Link an application -Install the library and public headers into your chosen prefix: +Install the shared and static libraries into your chosen prefix. Headers, +examples, documentation, and pkg-config metadata remain in the source tree: ```sh -meson setup build --prefix=/usr/local +meson setup build --prefix=/usr/local --libdir=lib meson compile -C build meson install -C build -cc -std=c23 examples/hello.c -o hello $(pkg-config --cflags --libs yasl) +cc -std=c23 -Iinclude examples/hello.c -o hello -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lyasl ./hello ``` -The prefix must be writable, and your compiler, pkg-config, and runtime loader -must search it. For a custom prefix, set `PKG_CONFIG_PATH` to its pkg-config -directory and configure the loader for its library directory. Static linking -uses `pkg-config --static --libs yasl` to include backend dependencies. +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. -A Meson consumer uses the installed dependency: +A Meson application can instead vendor the source under `subprojects/yasl`: ```meson project('my-app', 'c', default_options: ['c_std=c23']) -yasl = dependency('yasl', version: '>=0.1.0') +yasl = subproject('yasl').get_variable('yasl_dep') executable('my-app', 'main.c', dependencies: yasl) ``` -YASL also declares `meson.override_dependency('yasl', ...)`, so the same consumer -can use a vendored Meson subproject through a fallback dependency. Public headers -can be included from C++11 or newer; the library itself builds as C23. +The 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`. ## The application cycle -1. Open a display with `yasl_display_open(NULL, &spec)`. NULL creates a loop owned - by the display. Supply your own loop only when you need shared loop ownership. -2. Create a toplevel with `yasl_window_spec`: supply a positive **logical-pixel** - size, a `yasl_window_cbs` table in `cbs`, and callback userdata in `ud`. - Zero-initialize every option and callback struct. Callback tables are copied; - userdata must remain alive. The new window's callbacks start after creation - returns. Read initial geometry with `yasl_surface_geometry()`. -3. Schedule the initial redraw. `draw` receives a borrowed target. Fill every - pixel with premultiplied `0xAARRGGBB`, then present that exact target, once. -4. Dispatch, drain input for every live surface queue, update application state, - and flush. Flush runs scheduled drawing and sends native requests. Dispatch - may also invoke callbacks, including drawing during native live resize. -5. Destroy popups before parents, surfaces before displays, and displays before - any supplied loop. NULL destroy/close calls do nothing; freed pointers are - invalid for every operation. +1. Include `<yasl/app.h>` and initialize a `yasl_app_desc` with your userdata and + `init`, `event`, and `frame` callbacks. Call `yasl_run(&desc)`. +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)`. +5. Let the runner close remaining surfaces and the display. Release your own + resources in the optional `quit` callback or after `yasl_run()` returns. ```c -while (running) { - if (yasl_display_dispatch(display, -1) < 0) { - perror("yasl_display_dispatch"); - break; - } - struct yasl_input_event event; - while (yasl_surface_input_poll(surface, &event)) { - /* Read the union member selected by event.kind. */ - } - yasl_display_flush(display); -} +struct yasl_app_desc desc = { + .app_id = "org.example.app", + .userdata = &app, + .init = init, + .event = event, + .frame = draw, +}; +return yasl_run(&desc); ``` -Zero timeout polls, positive timeout bounds the wait in milliseconds, and a -negative timeout waits for work. Undrained input makes dispatch return without -waiting. Use the return value only to detect failure, not to count input events. -If callbacks destroy surfaces, update your application registry immediately and -only poll surfaces that are still alive. Arrange display/loop cleanup after the -cycle ends; destroying them inside a callback is unsupported. +The default pacing mode sleeps until input, resize, or a requested redraw. +Use `YASL_PACING_CONTINUOUS` for an animation or game. The core API still supports +manual dispatch/poll/flush when an application owns its event loop; see the guide. See the [application guide](guide.md) for drawing, input, popups, timers, and platform limits, or the comments in the [public header](../include/yasl/yasl.h) diff --git a/docs/guide.md b/docs/guide.md index adcacc8..1bd1aef 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -39,6 +39,22 @@ it advertises CSD. Check each result and provide an application fallback. ## Window creation and callbacks +All examples include `<yasl/app.h>` and call `yasl_run()` with a +`yasl_app_desc`. Create surfaces in `init`, handle `YASL_EVENT_*` notifications +in `event`, and render into `frame->cpu.pixels` in `frame`. Present with +`yasl_frame_present_cpu(frame, NULL, 0)` and return `YASL_APP_QUIT` from an event +handler to exit. The runner handles dispatch, input polling, resize redraws, +and remaining surface/display cleanup. Display quit stops the runner; connection +loss and dispatch failures return an error after cleanup. + +Unified pointer button and motion events retain `device` and `pen` axes, +including erasers and pressure-only motion. Cancel held input or active strokes +on `YASL_EVENT_INPUT_RESET`, pointer leave, or focus loss. Reset also starts the +next motion delta from zero. [hello.c](../examples/hello.c) shows the minimal +lifecycle; [tablet.c](../examples/tablet.c) demonstrates device-aware drawing. + +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 @@ -55,6 +71,14 @@ 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`. +Read window state with `yasl_surface_state()`. Its `YASL_STATE_*` flags can +coexist, and the `state` callback receives the same current bitmask. Wayland +also reports `YASL_STATE_TILED` when any window edge is tiled; X11 and Cocoa +currently leave that flag unset. Unsupported flags are absent. On Wayland, an +ordinary floating toplevel has fullscreen, maximized, and tiled all absent. +`YASL_STATE_SUSPENDED` describes repaint suspension rather than minimization; +there is no portable minimized flag. + ## Geometry and scaling | Value | Units | @@ -301,7 +325,7 @@ signals the loop pipe but is not a portable cross-thread event-posting API. Portable CPU drawing needs no native headers. Opt into `<yasl/native.h>` and `<yasl/native/wayland.h>`, `<yasl/native/x11.h>`, or `<yasl/native/cocoa.h>` when -integrating a platform API. Typed headers and functions are installed only for +integrating a platform API. Typed native functions are exported only for built backends. Accessors return borrowed handles and zero/NULL for the wrong backend. Do not pass freed objects, destroy borrowed handles, independently dispatch native connections, or change surface roles and buffer ownership. diff --git a/docs/migrating.md b/docs/migrating.md index da4e591..395072e 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -1,9 +1,22 @@ # Migrating pre-release applications YASL 0.1 has no stable source or binary compatibility promise. Rebuild all -consumers against the matching installed headers and library after updating. +consumers against matching public headers and the library after updating. See the [application guide](guide.md) for the full window and input workflow. +## Application callbacks + +All examples now use `<yasl/app.h>` and `yasl_run()`. Move window creation to +`yasl_app_desc.init`, 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. + +Unified pointer button and motion events retain `device` and tablet axes. +Handle `YASL_EVENT_INPUT_RESET` by clearing held input and active strokes, just +as for the core API's `YASL_INPUT_RESET`. Window state remains a `YASL_STATE_*` +bitmask; `YASL_STATE_TILED` is reported for tiled Wayland toplevels. + ## Window creation Rename `struct yasl_surface_spec` to `struct yasl_window_spec` and diff --git a/docs/testing.md b/docs/testing.md index d01b82c..bd3f2f0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -24,7 +24,7 @@ meson test -C build-sanitize --print-errorlogs Default tests need no desktop connection. C++ header coverage is built when a C++ compiler is available. On Linux and macOS, the shared-export test ensures -only installed API declarations are exported. Test disabled-backend configurations separately with +only public API declarations are exported. Test disabled-backend configurations separately with `-Dwayland=disabled` or `-Dx11=disabled`; the combined integration harness needs both backends. @@ -94,13 +94,13 @@ 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 +[`examples/tablet.c`](../examples/tablet.c) includes `<yasl/app.h>` and the C +standard library. It uses `yasl_run()`, frame callbacks, and unified input events +on every backend. Tablet button and motion events retain `device`, which is +`YASL_PTR_DEV_PEN` or `YASL_PTR_DEV_ERASER`; their `pen` member 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. +buttons do not paint, and focus loss, leave, or `YASL_EVENT_INPUT_RESET` cancels a stroke. Stationary pressure changes arrive as motion events and update the brush too. The example uses portable physical keys: C and Space clear, Escape exits. diff --git a/examples/hello.c b/examples/hello.c index 0c474c2..4153394 100644 --- a/examples/hello.c +++ b/examples/hello.c @@ -1,104 +1,56 @@ /* A complete CPU-rendered window. Build instructions: docs/getting-started.md. */ -#include <yasl/yasl.h> +#include <yasl/app.h> #include <stdio.h> -#include <string.h> struct app { - bool running; bool failed; - bool smoke_test; }; -static void quit(void *ud) +static int init(void *ud, struct yasl_display *display) { - ((struct app *)ud)->running = false; -} - -static void lost(void *ud) -{ - struct app *app = ud; - app->failed = true; - app->running = false; -} - -static void close_window(struct yasl_surface *surface, void *ud) -{ - (void)surface; - quit(ud); + struct yasl_window_spec spec = {.title = "Hello YASL", .size = {640, 480}}; + struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec); + if (!surface) { + perror("yasl_surface_create_toplevel"); + ((struct app *)ud)->failed = true; + return YASL_APP_QUIT; + } + yasl_surface_request_frame(surface); + return YASL_APP_CONTINUE; } -static void configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) +static int event(void *ud, const struct yasl_event *event) { - (void)geometry; (void)ud; - yasl_surface_schedule_redraw(surface); + return event->type == YASL_EVENT_WINDOW_CLOSE_REQUEST ? YASL_APP_QUIT : YASL_APP_CONTINUE; } -static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) +static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) { struct app *app = ud; - size_t count = (size_t)frame->target.size.x * frame->target.size.y; + size_t count = (size_t)frame->size.x * frame->size.y; for (size_t i = 0; i < count; i++) - frame->target.pixels[i] = 0xff306090; /* Opaque premultiplied ARGB. */ + frame->cpu.pixels[i] = 0xff306090; /* Opaque premultiplied ARGB. */ - struct yasl_damage_list damage = {.n = -1}; - if (!yasl_surface_present(surface, &frame->target, &damage)) { + if (!yasl_frame_present_cpu(frame, NULL, 0)) { fprintf(stderr, "Could not present the draw target\n"); app->failed = true; - app->running = false; } - if (app->smoke_test) app->running = false; + if (app->failed) yasl_surface_destroy(surface); } -int main(int argc, char **argv) +int main(void) { - struct app app = {.running = true}; - if (argc == 2 && strcmp(argv[1], "--smoke-test") == 0) - app.smoke_test = true; - else if (argc != 1) { - fprintf(stderr, "Usage: %s [--smoke-test]\n", argv[0]); - return 1; - } - - struct yasl_display_cbs display_callbacks = {.quit = quit, .lost = lost}; - struct yasl_display_spec display_spec = { + struct app app = {0}; + struct yasl_app_desc desc = { .app_id = "org.example.hello", - .cbs = &display_callbacks, - .ud = &app, - }; - struct yasl_display *display = yasl_display_open(NULL, &display_spec); - if (!display) { - perror("yasl_display_open"); - return 1; - } - struct yasl_window_cbs callbacks = { - .draw = draw, .configure = configure, .close = close_window, - }; - struct yasl_window_spec spec = { - .title = "Hello YASL", .size = {640, 480}, - .cbs = &callbacks, .ud = &app, + .userdata = &app, + .init = init, + .event = event, + .frame = draw, }; - struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec); - if (!surface) { - perror("yasl_surface_create_toplevel"); - yasl_display_close(display); - return 1; - } - yasl_surface_schedule_redraw(surface); - - while (app.running) { - if (yasl_display_dispatch(display, -1) < 0) { - perror("yasl_display_dispatch"); - app.failed = true; - break; - } - /* Drain even unused input so queued events do not cause a busy loop. */ - struct yasl_input_event event; - while (yasl_surface_input_poll(surface, &event)) {} - if (app.running) yasl_display_flush(display); - } - yasl_surface_destroy(surface); - yasl_display_close(display); - return app.failed ? 1 : 0; + int result = yasl_run(&desc); + if (result) fprintf(stderr, "Could not run the application\n"); + return result || app.failed ? 1 : 0; } diff --git a/examples/simple_window.c b/examples/simple_window.c index 797de3b..e9e6545 100644 --- a/examples/simple_window.c +++ b/examples/simple_window.c @@ -1,9 +1,8 @@ -#include "yasl/yasl.h" +#include <yasl/app.h> #include <stdio.h> -#include <stdlib.h> -#include <stdbool.h> +#include <string.h> -static bool running = true; +static bool failed, smoke_test; static int mouse_x = -1; static int mouse_y = -1; static bool mouse_down = false; @@ -23,12 +22,12 @@ static const char *device_name(enum yasl_pointer_device dev) } } -static void on_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) +static void on_draw(void *ud, struct yasl_surface *s, struct yasl_frame_context *frame) { (void)ud; - int w = frame->target.size.x; - int h = frame->target.size.y; - uint32_t *pixels = frame->target.pixels; + int w = frame->size.x; + int h = frame->size.y; + uint32_t *pixels = frame->cpu.pixels; if (!pixels || w <= 0 || h <= 0) return; @@ -81,63 +80,28 @@ static void on_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) } } - struct yasl_damage_list dmg = { .n = -1 }; - yasl_surface_present(s, &frame->target, &dmg); -} - -static void on_configure(struct yasl_surface *s, struct yasl_geometry geo, void *ud) -{ - (void)ud; - printf("[YASL] Resized to %dx%d (scale: %.2fx)\n", - geo.size.x, geo.size.y, (double)geo.scale_120 / YASL_SCALE_UNIT); - yasl_surface_schedule_redraw(s); -} - -static void on_close(struct yasl_surface *s, void *ud) -{ - (void)s; - (void)ud; - printf("[YASL] Window close requested\n"); - running = false; + if (!yasl_frame_present_cpu(frame, NULL, 0)) { + fprintf(stderr, "Failed to present frame\n"); + failed = true; + } + if (failed || smoke_test) yasl_surface_destroy(s); } -static void on_quit(void *ud) +static int on_init(void *ud, struct yasl_display *display) { (void)ud; - running = false; -} - -int main(void) -{ - struct yasl_display_cbs disp_cbs = { .quit = on_quit }; - struct yasl_display_spec disp_spec = { - .app_id = "org.yasl.example", - .cbs = &disp_cbs, - }; - struct yasl_display *d = yasl_display_open(NULL, &disp_spec); - if (!d) { - fprintf(stderr, "Failed to open display\n"); - return 1; - } - - struct yasl_window_cbs window_cbs = { - .draw = on_draw, - .configure = on_configure, - .close = on_close, - }; struct yasl_window_spec window_spec = { .title = "YASL Interactive Window (Pen & Touch Enabled)", .size = { .x = 640, .y = 480 }, - .cbs = &window_cbs, }; - struct yasl_surface *s = yasl_surface_create_toplevel(d, &window_spec); + struct yasl_surface *s = yasl_surface_create_toplevel(display, &window_spec); if (!s) { fprintf(stderr, "Failed to create surface\n"); - yasl_display_close(d); - return 1; + failed = true; + return YASL_APP_QUIT; } - yasl_surface_schedule_redraw(s); + yasl_surface_request_frame(s); printf("=== YASL Demo Running ===\n"); printf(" - Stylus/Pen: full pressure (0..65535) and tilt detection\n"); @@ -145,56 +109,88 @@ int main(void) printf(" - Space: cycle background themes\n"); printf(" - Escape: exit\n\n"); - 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) { - if (ev.key.action == YASL_KEY_PRESS) { - if (ev.key.key == YASL_KEY_ESCAPE) { /* Escape */ - running = false; - } else if (ev.key.key == YASL_KEY_SPACE || ev.key.key == YASL_KEY_ENTER) { /* Space or Return */ - color_theme++; - yasl_surface_schedule_redraw(s); - } - } - } else if (ev.kind == YASL_INPUT_MOUSE) { - mouse_x = ev.mouse.pos.x; - mouse_y = ev.mouse.pos.y; - pen_pressure = ev.mouse.pen.pressure; - pen_tilt_x = ev.mouse.pen.tilt_x; - pen_tilt_y = ev.mouse.pen.tilt_y; - pointer_device = ev.mouse.device; - - if (ev.mouse.kind == YASL_PTR_PRESS) { - mouse_down = true; - if (pen_pressure > 0 || pointer_device == YASL_PTR_DEV_PEN) { - printf("[Pen Down] Pressure: %5u/65535 (%.1f%%) | Tilt: (%+3d°, %+3d°) | Device: %s\n", - pen_pressure, (pen_pressure * 100.0) / 65535.0, - pen_tilt_x, pen_tilt_y, device_name(pointer_device)); - } - } else if (ev.mouse.kind == YASL_PTR_RELEASE) { - mouse_down = false; - pen_pressure = 0; - } else if (ev.mouse.kind == YASL_PTR_MOTION && pen_pressure > 0) { - printf("[Pen Move] Pressure: %5u/65535 (%.1f%%) | Tilt: (%+3d°, %+3d°) | Device: %s\n", - pen_pressure, (pen_pressure * 100.0) / 65535.0, - pen_tilt_x, pen_tilt_y, device_name(pointer_device)); - } else if (ev.mouse.kind == YASL_PTR_LEAVE) { - mouse_x = -1; - mouse_y = -1; - mouse_down = false; - pen_pressure = 0; - } - yasl_surface_schedule_redraw(s); - } - } + return YASL_APP_CONTINUE; +} - yasl_display_flush(d); +static void pointer_sample(struct yasl_vector2i pos, enum yasl_pointer_device device, + struct yasl_pointer_pen pen) +{ + mouse_x = pos.x; + mouse_y = pos.y; + pointer_device = device; + pen_pressure = pen.pressure; + pen_tilt_x = pen.tilt_x; + pen_tilt_y = pen.tilt_y; +} + +static int on_event(void *ud, const struct yasl_event *ev) +{ + (void)ud; + switch (ev->type) { + case YASL_EVENT_WINDOW_CLOSE_REQUEST: + return YASL_APP_QUIT; + case YASL_EVENT_WINDOW_RESIZE: { + uint32_t flags = yasl_surface_state(ev->surface); + const char *mode = flags & YASL_STATE_FULLSCREEN ? "fullscreen" : + flags & YASL_STATE_MAXIMIZED ? "maximized" : + flags & YASL_STATE_TILED ? "tiled" : "windowed"; + printf("[YASL] Resized to %dx%d (scale: %.2fx, %s)\n", ev->resize.size.x, + ev->resize.size.y, (double)ev->resize.scale_120 / YASL_SCALE_UNIT, mode); + break; } + case YASL_EVENT_KEY: + if (ev->key.action != YASL_KEY_PRESS) return YASL_APP_CONTINUE; + if (ev->key.key == YASL_KEY_ESCAPE) return YASL_APP_QUIT; + if (ev->key.key == YASL_KEY_SPACE || ev->key.key == YASL_KEY_ENTER) + color_theme++; + break; + case YASL_EVENT_MOUSE_BUTTON: + pointer_sample(ev->mouse_button.pos, ev->mouse_button.device, ev->mouse_button.pen); + if (ev->mouse_button.button == 0) { + mouse_down = ev->mouse_button.pressed; + if (!mouse_down) pen_pressure = 0; + } + break; + case YASL_EVENT_MOUSE_MOTION: + pointer_sample(ev->mouse_motion.pos, ev->mouse_motion.device, ev->mouse_motion.pen); + break; + case YASL_EVENT_WINDOW_FOCUS: + if (ev->focus.focused) return YASL_APP_CONTINUE; + /* Fall through to cancel held input on focus loss. */ + [[fallthrough]]; + case YASL_EVENT_INPUT_RESET: + case YASL_EVENT_MOUSE_LEAVE: + mouse_x = mouse_y = -1; + mouse_down = false; + pen_pressure = 0; + break; + default: + return YASL_APP_CONTINUE; + } + if (pen_pressure > 0) { + printf("[Pen] Pressure: %5u/65535 (%.1f%%) | Tilt: (%+3d°, %+3d°) | Device: %s\n", + pen_pressure, pen_pressure * 100.0 / 65535.0, + pen_tilt_x, pen_tilt_y, device_name(pointer_device)); + } + yasl_surface_request_frame(ev->surface); + return YASL_APP_CONTINUE; +} - yasl_surface_destroy(s); - yasl_display_close(d); - return 0; +int main(int argc, char **argv) +{ + if (argc == 2 && strcmp(argv[1], "--smoke-test") == 0) + smoke_test = true; + else if (argc != 1) { + fprintf(stderr, "Usage: %s [--smoke-test]\n", argv[0]); + return 1; + } + struct yasl_app_desc desc = { + .app_id = "org.yasl.example", + .init = on_init, + .event = on_event, + .frame = on_draw, + }; + int result = yasl_run(&desc); + if (result) perror("yasl_run"); + return result || failed ? 1 : 0; } diff --git a/examples/tablet.c b/examples/tablet.c index 8a1af7e..f392cdc 100644 --- a/examples/tablet.c +++ b/examples/tablet.c @@ -1,5 +1,5 @@ /* Pressure-sensitive drawing using only YASL's portable API. */ -#include <yasl/yasl.h> +#include <yasl/app.h> #include <math.h> #include <stdio.h> @@ -14,10 +14,15 @@ #define HEADER 104 struct app { - bool running, smoke_test, failed, tablet_supported; + bool smoke_test, failed, tablet_supported; bool down, visible, previous; + struct yasl_surface *surface; enum yasl_backend backend; - struct yasl_mouse_event pointer; + struct { + struct yasl_vector2i pos; + enum yasl_pointer_device device; + struct yasl_pointer_pen pen; + } pointer; double last_x, last_y, last_radius; uint32_t *canvas; struct yasl_geometry geometry; @@ -135,31 +140,49 @@ static void stroke(struct app *app) app->previous = true; } -static void input(struct app *app, const struct yasl_input_event *event) +static int input(void *ud, const struct yasl_event *event) { - if (event->kind == YASL_INPUT_RESET || - (event->kind == YASL_INPUT_FOCUS && !event->focus.focused)) { + struct app *app = ud; + if (event->type == YASL_EVENT_WINDOW_CLOSE_REQUEST) + return YASL_APP_QUIT; + if (event->type == YASL_EVENT_WINDOW_RESIZE) { + if (!resize_canvas(app, (struct yasl_geometry){event->resize.size, event->resize.scale_120})) { + app->failed = true; + return YASL_APP_QUIT; + } + } else if (event->type == YASL_EVENT_INPUT_RESET || event->type == YASL_EVENT_MOUSE_LEAVE || + (event->type == YASL_EVENT_WINDOW_FOCUS && !event->focus.focused)) { cancel_stroke(app); - } else if (event->kind == YASL_INPUT_KEY && event->key.action == YASL_KEY_PRESS) { - if (event->key.key == YASL_KEY_ESCAPE) app->running = false; + } else if (event->type == YASL_EVENT_KEY && event->key.action == YASL_KEY_PRESS) { + if (event->key.key == YASL_KEY_ESCAPE) return YASL_APP_QUIT; if (event->key.key == YASL_KEY_C || event->key.key == YASL_KEY_SPACE) clear_canvas(app); - } else if (event->kind == YASL_INPUT_MOUSE) { - const struct yasl_mouse_event *pointer = &event->mouse; - if (pointer->kind == YASL_PTR_SCROLL) return; - if (app->pointer.device != pointer->device) cancel_stroke(app); - app->pointer = *pointer; - app->visible = pointer->kind != YASL_PTR_LEAVE; - if (pointer->kind == YASL_PTR_LEAVE) cancel_stroke(app); - else if (pointer->kind == YASL_PTR_PRESS && pointer->button == 0) { + } else if (event->type == YASL_EVENT_MOUSE_BUTTON) { + if (app->pointer.device != event->mouse_button.device) cancel_stroke(app); + app->pointer.pos = event->mouse_button.pos; + app->pointer.device = event->mouse_button.device; + app->pointer.pen = event->mouse_button.pen; + app->visible = true; + if (event->mouse_button.pressed && event->mouse_button.button == 0) { app->down = true; app->previous = false; stroke(app); - } else if (pointer->kind == YASL_PTR_RELEASE && pointer->button == 0) { + } else if (!event->mouse_button.pressed && event->mouse_button.button == 0) { app->down = app->previous = false; app->pointer.pen.pressure = 0; - } else if (pointer->kind == YASL_PTR_MOTION && app->down) stroke(app); + } + } else if (event->type == YASL_EVENT_MOUSE_MOTION) { + if (app->pointer.device != event->mouse_motion.device) cancel_stroke(app); + app->pointer.pos = event->mouse_motion.pos; + app->pointer.device = event->mouse_motion.device; + app->pointer.pen = event->mouse_motion.pen; + app->visible = true; + if (app->down) stroke(app); + } else { + return YASL_APP_CONTINUE; } + yasl_surface_request_frame(event->surface); + return YASL_APP_CONTINUE; } /* Tiny built-in 5x7 font keeps this example independent of a text library. */ @@ -214,18 +237,19 @@ static void text(struct yasl_framebuffer *fb, double dpi, int x, int y, } } -static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) +static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) { struct app *app = ud; - struct yasl_geometry geometry = yasl_surface_geometry(surface); - geometry.size = frame->target.size; + struct yasl_geometry geometry = {frame->size, frame->scale_120}; if (!resize_canvas(app, geometry)) { fprintf(stderr, "Cannot allocate drawing canvas\n"); - app->running = false; app->failed = true; + yasl_surface_destroy(surface); + app->surface = NULL; return; } - struct yasl_framebuffer *fb = &frame->target; + struct yasl_framebuffer target = {.size = frame->size, .pixels = frame->cpu.pixels}; + struct yasl_framebuffer *fb = ⌖ memcpy(fb->pixels, app->canvas, (size_t)geometry.size.x * geometry.size.y * sizeof(uint32_t)); double dpi = (double)app->geometry.scale_120 / YASL_SCALE_UNIT; rect(fb, 0, 0, fb->size.x, (int)lround(HEADER * dpi), PANEL); @@ -260,41 +284,42 @@ static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *u for (int i = 0; i <= 64; i++) rect(fb, (int)lround(cx + tx * i / 64), (int)lround(cy + ty * i / 64), 2, 2, ACCENT); } - struct yasl_damage_list damage = {.n = -1}; - if (!yasl_surface_present(surface, fb, &damage)) { + if (!yasl_frame_present_cpu(frame, NULL, 0)) { app->failed = true; - app->running = false; } - if (app->smoke_test) app->running = false; + if (app->failed || app->smoke_test) { + yasl_surface_destroy(surface); + app->surface = NULL; + } } -static void configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) +static int init(void *ud, struct yasl_display *display) { struct app *app = ud; - if (!resize_canvas(app, geometry)) { + 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}, + }; + 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; - app->running = false; + return YASL_APP_QUIT; } - yasl_surface_schedule_redraw(surface); -} - -static void close_window(struct yasl_surface *surface, void *ud) -{ - (void)surface; - ((struct app *)ud)->running = false; -} - -static void quit(void *ud) { ((struct app *)ud)->running = false; } -static void lost(void *ud) -{ - struct app *app = ud; - app->failed = true; - app->running = false; + yasl_surface_set_mouse_mode(app->surface, YASL_MOUSE_HIDDEN); + yasl_surface_request_frame(app->surface); + printf("YASL tablet on %s: pressure changes brush width; eraser removes ink.\n" + "Mouse: fixed-width drawing. C/Space: clear. Escape: quit.\n", + backend_name(app->backend)); + if (!app->tablet_supported) + printf("This display server does not expose tablet input.\n"); + return YASL_APP_CONTINUE; } int main(int argc, char **argv) { - struct app app = {.running = true}; + struct app app = {0}; enum yasl_backend backend = YASL_BACKEND_AUTO; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--smoke-test")) app.smoke_test = true; @@ -312,39 +337,12 @@ int main(int argc, char **argv) return help ? 0 : 1; } } - struct yasl_display_cbs display_cbs = {.quit = quit, .lost = lost}; - struct yasl_display_spec display_spec = { - .app_id = "org.yasl.tablet", .backend = backend, .cbs = &display_cbs, .ud = &app, + struct yasl_app_desc desc = { + .app_id = "org.yasl.tablet", .backend = backend, .userdata = &app, + .init = init, .event = input, .frame = draw, }; - struct yasl_display *display = yasl_display_open(NULL, &display_spec); - if (!display) { perror("Cannot open display"); return 1; } - app.backend = yasl_display_backend(display); - app.tablet_supported = (yasl_display_caps(display) & YASL_CAP_TABLET) != 0; - struct yasl_window_cbs callbacks = {.draw = draw, .configure = configure, .close = close_window}; - struct yasl_window_spec spec = { - .title = "YASL Tablet - pressure and tilt", .size = {900, 640}, - .cbs = &callbacks, .ud = &app, - }; - struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec); - if (!surface) { perror("Cannot create surface"); yasl_display_close(display); return 1; } - yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN); - yasl_surface_schedule_redraw(surface); - printf("YASL tablet on %s: pressure changes brush width; eraser removes ink.\n" - "Mouse: fixed-width drawing. C/Space: clear. Escape: quit.\n", - backend_name(app.backend)); - if (!app.tablet_supported) - printf("This display server does not expose tablet input.\n"); - while (app.running) { - if (yasl_display_dispatch(display, -1) < 0) { app.failed = true; break; } - struct yasl_input_event event; - while (yasl_surface_input_poll(surface, &event)) { - input(&app, &event); - yasl_surface_schedule_redraw(surface); - } - yasl_display_flush(display); - } + int result = yasl_run(&desc); + if (result) perror("yasl_run"); free(app.canvas); - yasl_surface_destroy(surface); - yasl_display_close(display); - return app.failed ? 1 : 0; + return result || app.failed ? 1 : 0; } diff --git a/include/yasl/app.h b/include/yasl/app.h index 27523d5..112140d 100644 --- a/include/yasl/app.h +++ b/include/yasl/app.h @@ -70,6 +70,7 @@ typedef enum yasl_event_type { YASL_EVENT_PASTE, YASL_EVENT_DROP_FILES, YASL_EVENT_USER, + YASL_EVENT_INPUT_RESET, /**< Input interrupted; clear held keys/buttons and composition. */ } yasl_event_type; /** Unified event payload delivered to yasl_app_desc.event(). */ @@ -95,6 +96,7 @@ struct yasl_event { struct { struct yasl_vector2i pos; /**< Device-pixel position relative to surface. */ + enum yasl_pointer_device device; /**< Mouse, touch, pen, or eraser. */ int16_t button; /**< 0 primary, 1 middle, 2 secondary. */ bool pressed; /**< true for press, false for release. */ uint32_t mods; @@ -104,6 +106,7 @@ struct yasl_event { struct { struct yasl_vector2i pos; /**< Device-pixel position relative to surface. */ struct yasl_vector2i delta; /**< Relative motion delta (dx, dy). */ + enum yasl_pointer_device device; /**< Mouse, touch, pen, or eraser. */ uint32_t mods; struct yasl_pointer_pen pen; } mouse_motion; @@ -271,6 +274,7 @@ struct yasl_app_desc { /** * Unified event handler called for input, resize, close, scale, and custom events. * 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. */ int (*event)(void *ud, const struct yasl_event *ev); @@ -293,6 +297,8 @@ struct yasl_app_desc { * Handles the display event loop, live-resizing, pacing, and cleanup. * @param desc Application callbacks and options. * @return 0 on orderly exit, or non-zero on error. + * Display quit notifications stop the runner; connection loss and dispatch + * failures return an error after application cleanup. */ YASL_API int yasl_run(const struct yasl_app_desc *desc); diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index e2c1d77..3b3b712 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -153,6 +153,7 @@ enum yasl_surface_state : uint32_t { YASL_STATE_ACTIVATED = 1 << 2, /**< Active window. */ YASL_STATE_SUSPENDED = 1 << 3, /**< Compositor reports suspension. */ YASL_STATE_CSD = 1 << 4, /**< Backend reports client-side decorations. */ + YASL_STATE_TILED = 1 << 5, /**< At least one edge is tiled; currently reported by Wayland. */ }; /** System cursor shapes. HIDDEN preserves pointer input; NONE is its compatibility alias. */ @@ -448,7 +449,7 @@ struct yasl_window_cbs { void (*configure)(struct yasl_surface *s, struct yasl_geometry geometry, void *ud); /** Window/popup close request; application is responsible for destruction. */ void (*close)(struct yasl_surface *s, void *ud); - /** Reported yasl_surface_state bitmask changed. */ + /** Reported yasl_surface_state bitmask changed; matches yasl_surface_state(s). */ void (*state)(struct yasl_surface *s, uint32_t state, void *ud); /** YASL surface-drag target hover, leave, or drop; pos is in device pixels. */ void (*drag)(struct yasl_surface *s, enum yasl_drag_event ev, @@ -746,6 +747,8 @@ YASL_API struct yasl_geometry yasl_surface_geometry(const struct yasl_surface *s * @param s Surface, or NULL. * @return A bitmask of yasl_surface_state values, or zero for NULL. * Flags are reported state, not requests; unsupported states are absent. + * Flags may coexist. Windowed and floating have no separate flags. Suspension + * does not imply minimization, and no minimized flag is reported. */ YASL_API uint32_t yasl_surface_state(const struct yasl_surface *s); /* Timing uses monotonic nanoseconds. Check source before treating timestamps diff --git a/meson.build b/meson.build index b0ea8e8..8e396d8 100644 --- a/meson.build +++ b/meson.build @@ -88,35 +88,6 @@ yasl_lib = both_libraries( install: true, ) -install_headers( - 'include/yasl/yasl.h', - 'include/yasl/app.h', - 'include/yasl/timing.h', - 'include/yasl/keycodes.h', - 'include/yasl/loop.h', - 'include/yasl/native.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 - -pkg = import('pkgconfig') -pkg.generate( - yasl_lib.get_shared_lib(), - name: 'yasl', - description: 'Yet Another Surface Library - minimal, modern desktop window surface abstraction', - version: meson.project_version(), - subdirs: '.', -) - yasl_dep = declare_dependency( link_with: yasl_lib.get_static_lib(), include_directories: include_directories('include'), @@ -131,7 +102,7 @@ hello_example = executable( link_with: yasl_lib.get_shared_lib(), ) -executable( +interactive_example = executable( 'yasl-example', 'examples/simple_window.c', include_directories: inc, @@ -158,11 +129,3 @@ game_example = executable( ) subdir('tests') - -doc_dir = get_option('datadir') / 'doc' / 'yasl' -install_data('README.md', 'CONTRIBUTING.md', 'IDE_INTEGRATION.md', 'Doxyfile', 'LICENSE', install_dir: doc_dir) -# Preserve source-relative links and keep the reference buildable offline. -install_subdir('docs', install_dir: doc_dir) -install_subdir('examples', install_dir: doc_dir) -install_subdir('include', install_dir: doc_dir) -install_data('LICENSE', install_dir: get_option('datadir') / 'licenses' / 'yasl') diff --git a/src/common/app.c b/src/common/app.c index da9747d..e1148f9 100644 --- a/src/common/app.c +++ b/src/common/app.c @@ -35,6 +35,7 @@ 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; @@ -380,6 +381,7 @@ static void app_runner_dispatch_input_events(struct app_runner *ar) if (ie.mouse.kind == YASL_PTR_PRESS || ie.mouse.kind == YASL_PTR_RELEASE) { ev.type = YASL_EVENT_MOUSE_BUTTON; ev.mouse_button.pos = ie.mouse.pos; + ev.mouse_button.device = ie.mouse.device; ev.mouse_button.button = ie.mouse.button; ev.mouse_button.pressed = (ie.mouse.kind == YASL_PTR_PRESS); ev.mouse_button.mods = ie.mouse.mods; @@ -391,6 +393,7 @@ static void app_runner_dispatch_input_events(struct app_runner *ar) } else if (ie.mouse.kind == YASL_PTR_MOTION) { ev.type = YASL_EVENT_MOUSE_MOTION; ev.mouse_motion.pos = ie.mouse.pos; + ev.mouse_motion.device = ie.mouse.device; if (as->has_last_mouse_pos) { ev.mouse_motion.delta.x = ie.mouse.pos.x - as->last_mouse_pos.x; ev.mouse_motion.delta.y = ie.mouse.pos.y - as->last_mouse_pos.y; @@ -439,6 +442,9 @@ static void app_runner_dispatch_input_events(struct app_runner *ar) break; case YASL_INPUT_RESET: as->has_last_mouse_pos = false; + ev.type = YASL_EVENT_INPUT_RESET; + if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT) + ar->running = false; break; default: break; @@ -447,27 +453,38 @@ static void app_runner_dispatch_input_events(struct app_runner *ar) } } +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; +} + int yasl_run(const struct yasl_app_desc *desc) { if (!desc || !desc->init) 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; - struct app_runner runner = { - .display = d, - .desc = desc, - .running = true, - .had_surfaces = false, - .surface_count = 0, - .surfaces = NULL, - }; + runner.display = d; int sync_error = yasl_mutex_init(&runner.user_event_mutex); if (sync_error) { yasl_display_close(d); @@ -503,6 +520,7 @@ int yasl_run(const struct yasl_app_desc *desc) int res = yasl_display_dispatch(d, timeout_ms); if (res < 0) { + runner.failed = true; break; } @@ -546,5 +564,5 @@ int yasl_run(const struct yasl_app_desc *desc) yasl_mutex_destroy(&runner.user_event_mutex); yasl_display_close(d); - return 0; + return runner.failed ? -1 : 0; } diff --git a/src/common/app_internal.h b/src/common/app_internal.h index 64fa34c..143df41 100644 --- a/src/common/app_internal.h +++ b/src/common/app_internal.h @@ -1,8 +1,7 @@ #ifndef YASL_APP_INTERNAL_H #define YASL_APP_INTERNAL_H -#include "yasl/app.h" -#include "common/backend.h" +struct yasl_surface; void app_runner_register_surface(void *runner, struct yasl_surface *s); void app_runner_unregister_surface(void *runner, struct yasl_surface *s); diff --git a/src/common/backend.c b/src/common/backend.c index be06136..3f2143d 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -328,9 +328,7 @@ struct yasl_geometry yasl_surface_geometry(const struct yasl_surface *s) uint32_t yasl_surface_state(const struct yasl_surface *s) { - if (!s || !s->display->ops->surface_state) - return 0; - return s->display->ops->surface_state(s); + return s ? s->state : 0; } struct yasl_timing yasl_surface_timing(const struct yasl_surface *s) diff --git a/src/common/backend.h b/src/common/backend.h index 37fa167..17d781e 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -22,7 +22,7 @@ struct yasl_surface { void *native_handle; struct yasl_vector2i size; uint32_t scale_120; - uint32_t state; + uint32_t state; /* Authoritative YASL_STATE_* bitmask, maintained by the backend. */ uint32_t bg_color; struct yasl_window_cbs callbacks; void *ud; @@ -72,7 +72,6 @@ struct yasl_backend_ops { bool (*surface_present)(struct yasl_surface *s, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage); bool (*surface_input_poll)(struct yasl_surface *s, struct yasl_input_event *ev); struct yasl_geometry (*surface_geometry)(const struct yasl_surface *s); - uint32_t (*surface_state)(const struct yasl_surface *s); struct yasl_timing (*surface_timing)(const struct yasl_surface *s); uint64_t (*surface_frame_time_ns)(struct yasl_surface *s, uint64_t now); void (*surface_set_cursor)(struct yasl_surface *s, enum yasl_cursor cursor); diff --git a/src/common/keyboard.h b/src/common/keyboard.h index 77c6380..d8b8bbc 100644 --- a/src/common/keyboard.h +++ b/src/common/keyboard.h @@ -1,7 +1,7 @@ #ifndef YASL_COMMON_KEYBOARD_H #define YASL_COMMON_KEYBOARD_H -#include "yasl/yasl.h" +#include <stdint.h> /* Layout labels omit controls, invalid Unicode, and Cocoa function-key PUA. */ static inline uint32_t yasl_key_label(uint32_t cp) diff --git a/src/platform/cocoa/api.h b/src/platform/cocoa/api.h index 89b71f3..0f705dd 100644 --- a/src/platform/cocoa/api.h +++ b/src/platform/cocoa/api.h @@ -1,7 +1,7 @@ #ifndef YASL_COCOA_API_H #define YASL_COCOA_API_H -#include "common/backend.h" +#include "yasl/yasl.h" struct cocoa_display; struct cocoa_surface; diff --git a/src/platform/cocoa/keymap.h b/src/platform/cocoa/keymap.h index 1aba70b..a5394e2 100644 --- a/src/platform/cocoa/keymap.h +++ b/src/platform/cocoa/keymap.h @@ -1,7 +1,7 @@ #ifndef YASL_COCOA_KEYMAP_H #define YASL_COCOA_KEYMAP_H -#include "yasl/yasl.h" +#include "yasl/keycodes.h" enum yasl_key yasl_key_from_cocoa(uint32_t code); diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m index dc6175d..aa9fa48 100644 --- a/src/platform/cocoa/window.m +++ b/src/platform/cocoa/window.m @@ -3,6 +3,7 @@ #import <QuartzCore/QuartzCore.h> #include "platform/cocoa/api.h" +#include "common/backend.h" #include "yasl/native/cocoa.h" #include "common/timing.h" #include "common/loop.h" @@ -93,7 +94,6 @@ struct cocoa_surface { struct input_queue input; struct yasl_vector2i pointer; double scroll_x; - uint32_t state; uint32_t mods; uint32_t buttons; enum yasl_cursor cursor; @@ -180,13 +180,13 @@ static void set_state(struct cocoa_surface *w, uint32_t bit, bool on) { if (!w || w->closed) return; - uint32_t next = on ? (w->state | bit) : (w->state & ~bit); - if (next == w->state) + uint32_t next = on ? (w->base.state | bit) : (w->base.state & ~bit); + if (next == w->base.state) return; - w->state = next; + w->base.state = next; darwin_ctx.depth++; if (w->cbs->state) - w->cbs->state(&w->base, w->state, w->ud); + w->cbs->state(&w->base, next, w->ud); darwin_ctx.depth--; } @@ -713,7 +713,7 @@ static struct yasl_vector2i dragging_pos(YaslView *view, id<NSDraggingInfo> send if (period > 0.0 && period < 0.25) w->clock.refresh_ns = period * 1e9; w->frame_due = true; - if (!w->want_draw || (w->state & YASL_STATE_SUSPENDED)) { + if (!w->want_draw || (w->base.state & YASL_STATE_SUSPENDED)) { if (++w->idle_frames >= LINK_IDLE_FRAMES) link.paused = YES; return; @@ -1069,7 +1069,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 || - (w->state & YASL_STATE_SUSPENDED)) + (w->base.state & YASL_STATE_SUSPENDED)) return; size_t count = (size_t)w->size.x * (size_t)w->size.y; @@ -2042,12 +2042,6 @@ static struct yasl_geometry impl_surface_geometry(const struct yasl_surface *s) return cocoa_surface_geometry((const struct cocoa_surface *)s); } -static uint32_t impl_surface_state(const struct yasl_surface *s) -{ - const struct cocoa_surface *w = (const struct cocoa_surface *)s; - return w ? w->state : 0; -} - static struct yasl_timing impl_surface_timing(const struct yasl_surface *s) { return cocoa_surface_timing((const struct cocoa_surface *)s); @@ -2156,7 +2150,6 @@ const struct yasl_backend_ops yasl_cocoa_backend = { .surface_present = impl_surface_present, .surface_input_poll = impl_surface_input_poll, .surface_geometry = impl_surface_geometry, - .surface_state = impl_surface_state, .surface_timing = impl_surface_timing, .surface_frame_time_ns = impl_surface_frame_time_ns, .surface_set_cursor = impl_surface_set_cursor, diff --git a/src/platform/sync.h b/src/platform/sync.h index 1dcf28f..43b4a34 100644 --- a/src/platform/sync.h +++ b/src/platform/sync.h @@ -5,6 +5,6 @@ * yasl_mutex_init/destroy/lock/unlock operations. Init returns zero on success * or an errno value on failure. The other operations require an initialized * mutex. Native types and calls stay in the selected implementation header. */ -#include "yasl_platform_sync.h" +#include "yasl_platform_sync.h" // IWYU pragma: export #endif diff --git a/src/platform/wayland/api.h b/src/platform/wayland/api.h index 04a6bf1..bbd20df 100644 --- a/src/platform/wayland/api.h +++ b/src/platform/wayland/api.h @@ -1,8 +1,7 @@ #ifndef YASL_WAYLAND_API_H #define YASL_WAYLAND_API_H -#include "common/backend.h" -#include "yasl/native/wayland.h" +#include "yasl/yasl.h" struct wayland_display; struct wayland_surface; @@ -28,7 +27,6 @@ void wayland_surface_schedule_redraw(struct wayland_surface *s); bool wayland_surface_present(struct wayland_surface *s, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage); bool wayland_surface_input_poll(struct wayland_surface *s, struct yasl_input_event *ev); struct yasl_geometry wayland_surface_geometry(const struct wayland_surface *s); -uint32_t wayland_surface_state(const struct wayland_surface *s); struct yasl_timing wayland_surface_timing(const struct wayland_surface *s); void wayland_surface_set_cursor(struct wayland_surface *s, enum yasl_cursor cursor); void wayland_surface_set_background_color(struct wayland_surface *s, uint32_t bg); diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c index 3eada38..cc96934 100644 --- a/src/platform/wayland/backend.c +++ b/src/platform/wayland/backend.c @@ -111,11 +111,6 @@ static struct yasl_geometry impl_surface_geometry(const struct yasl_surface *s) return wayland_surface_geometry((const struct wayland_surface *)s); } -static uint32_t impl_surface_state(const struct yasl_surface *s) -{ - return wayland_surface_state((const struct wayland_surface *)s); -} - static struct yasl_timing impl_surface_timing(const struct yasl_surface *s) { return wayland_surface_timing((const struct wayland_surface *)s); @@ -212,7 +207,6 @@ const struct yasl_backend_ops yasl_wayland_backend = { .surface_present = impl_surface_present, .surface_input_poll = impl_surface_input_poll, .surface_geometry = impl_surface_geometry, - .surface_state = impl_surface_state, .surface_timing = impl_surface_timing, .surface_frame_time_ns = impl_surface_frame_time_ns, .surface_set_cursor = impl_surface_set_cursor, diff --git a/src/platform/wayland/buffer.c b/src/platform/wayland/buffer.c index f4243d1..f5a2c0f 100644 --- a/src/platform/wayland/buffer.c +++ b/src/platform/wayland/buffer.c @@ -1,4 +1,6 @@ #include "internal.h" +#include "common/utf8.h" +#include <errno.h> #include <sys/resource.h> static long minor_faults(void) @@ -134,7 +136,8 @@ int apply_pending_geometry(struct wayland_surface *w) w->size_pending = false; return 0; } - bool floating = !w->parent && !w->maximized && !w->fullscreen && !w->tiled; + bool floating = !w->parent && !(w->base.state & + (YASL_STATE_MAXIMIZED | YASL_STATE_FULLSCREEN | YASL_STATE_TILED)); int free_w = floating && w->floating_width > 0 ? w->floating_width : w->logical_width; int free_h = floating && w->floating_height > 0 ? w->floating_height : w->logical_height; int logical_w = !w->parent && w->req_width > 0 ? w->req_width : free_w; @@ -143,7 +146,7 @@ int apply_pending_geometry(struct wayland_surface *w) if (logical_w < YASL_MIN_LOGICAL_PX) logical_w = YASL_MIN_LOGICAL_PX; if (logical_h < YASL_MIN_LOGICAL_PX) logical_h = YASL_MIN_LOGICAL_PX; } - if (!w->fullscreen) { + if (!(w->base.state & YASL_STATE_FULLSCREEN)) { if (w->bound_width > 0 && logical_w > w->bound_width) logical_w = w->bound_width; if (w->bound_height > 0 && logical_h > w->bound_height) @@ -539,16 +542,6 @@ void wayland_surface_set_title(struct wayland_surface *w, const char *title) free(copy); } -uint32_t wayland_surface_state(const struct wayland_surface *w) -{ - if (unlikely(!w)) return 0; - return ((uint32_t)w->fullscreen * YASL_STATE_FULLSCREEN) | - ((uint32_t)w->maximized * YASL_STATE_MAXIMIZED) | - ((uint32_t)w->activated * YASL_STATE_ACTIVATED) | - ((uint32_t)w->suspended * YASL_STATE_SUSPENDED) | - ((uint32_t)!w->server_decorated * YASL_STATE_CSD); -} - struct yasl_timing wayland_surface_timing(const struct wayland_surface *w) { struct yasl_timing t = { diff --git a/src/platform/wayland/clipboard.c b/src/platform/wayland/clipboard.c index 24dee97..353e993 100644 --- a/src/platform/wayland/clipboard.c +++ b/src/platform/wayland/clipboard.c @@ -1,6 +1,8 @@ #define _GNU_SOURCE #include "internal.h" #include "clipboard.h" +#include "common/utf8.h" +#include <errno.h> #define CLIPBOARD_TEXT_MAX (256u * 1024) diff --git a/src/platform/wayland/internal.h b/src/platform/wayland/internal.h index f777674..d619dba 100644 --- a/src/platform/wayland/internal.h +++ b/src/platform/wayland/internal.h @@ -4,8 +4,8 @@ #define _GNU_SOURCE #include "api.h" +#include "common/backend.h" #include "common/timing.h" -#include "common/utf8.h" #include "clipboard.h" #include <wayland-client.h> @@ -24,7 +24,6 @@ #include "protocols/xdg-toplevel-drag-v1-client-protocol.h" #include "protocols/primary-selection-unstable-v1-client-protocol.h" -#include <errno.h> #include <fcntl.h> #include <limits.h> #include <poll.h> @@ -307,10 +306,8 @@ struct wayland_surface { uint32_t pending_serial; /* 1-byte booleans packed tightly */ - bool server_decorated; bool frame_active; bool frame_presented; - bool tiled; bool size_pending; bool sample_refresh; bool has_pending_serial; @@ -318,10 +315,6 @@ struct wayland_surface { bool closed; bool destroyed; bool opaque; - bool fullscreen; - bool maximized; - bool activated; - bool suspended; bool want_draw; }; diff --git a/src/platform/wayland/keyboard.c b/src/platform/wayland/keyboard.c index 0913079..ebd0fc7 100644 --- a/src/platform/wayland/keyboard.c +++ b/src/platform/wayland/keyboard.c @@ -1,6 +1,7 @@ #include "internal.h" #include "common/keyboard.h" #include "keymap.h" +#include <errno.h> #define XKB_EVDEV_OFFSET 8 #define KEYMAP_MAX_BYTES (8u * 1024 * 1024) diff --git a/src/platform/wayland/keymap.h b/src/platform/wayland/keymap.h index 26a5cd6..c0ff81e 100644 --- a/src/platform/wayland/keymap.h +++ b/src/platform/wayland/keymap.h @@ -1,7 +1,7 @@ #ifndef YASL_WAYLAND_KEYMAP_H #define YASL_WAYLAND_KEYMAP_H -#include "yasl/yasl.h" +#include "yasl/keycodes.h" enum yasl_key yasl_key_from_evdev(uint32_t code); diff --git a/src/platform/wayland/popup.c b/src/platform/wayland/popup.c index fd415c9..cb578d5 100644 --- a/src/platform/wayland/popup.c +++ b/src/platform/wayland/popup.c @@ -137,6 +137,7 @@ struct wayland_surface *wayland_surface_create_popup(struct wayland_surface *par if (!w) return NULL; w->base.display = &c->base; + w->base.state = YASL_STATE_CSD; w->conn = c; w->conn_next = c->surfaces; c->surfaces = w; diff --git a/src/platform/wayland/shell.c b/src/platform/wayland/shell.c index 4c1caad..92c89d1 100644 --- a/src/platform/wayland/shell.c +++ b/src/platform/wayland/shell.c @@ -265,10 +265,12 @@ static const struct xdg_wm_base_listener wm_base_listener = { .ping = wm_base_ping, }; -static void notify_state(struct wayland_surface *w) +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, wayland_surface_state(w), w->ud); + w->cbs->state(&w->base, state, w->ud); } static void toplevel_configure(void *data, struct xdg_toplevel *toplevel, @@ -277,35 +279,29 @@ static void toplevel_configure(void *data, struct xdg_toplevel *toplevel, (void)toplevel; struct wayland_surface *w = data; - uint32_t state_mask = 0; + uint32_t state = w->base.state & ~(YASL_STATE_FULLSCREEN | + YASL_STATE_MAXIMIZED | YASL_STATE_ACTIVATED | + YASL_STATE_SUSPENDED | YASL_STATE_TILED); uint32_t *s; wl_array_for_each(s, states) { - if (*s < 32) - state_mask |= (1u << *s); + switch (*s) { + case XDG_TOPLEVEL_STATE_FULLSCREEN: state |= YASL_STATE_FULLSCREEN; break; + case XDG_TOPLEVEL_STATE_MAXIMIZED: state |= YASL_STATE_MAXIMIZED; break; + case XDG_TOPLEVEL_STATE_ACTIVATED: state |= YASL_STATE_ACTIVATED; break; + case XDG_TOPLEVEL_STATE_SUSPENDED: state |= YASL_STATE_SUSPENDED; break; + case XDG_TOPLEVEL_STATE_TILED_LEFT: + case XDG_TOPLEVEL_STATE_TILED_RIGHT: + case XDG_TOPLEVEL_STATE_TILED_TOP: + case XDG_TOPLEVEL_STATE_TILED_BOTTOM: + state |= YASL_STATE_TILED; + break; + default: break; + } } - bool max = (state_mask & (1u << XDG_TOPLEVEL_STATE_MAXIMIZED)) != 0; - bool fs = (state_mask & (1u << XDG_TOPLEVEL_STATE_FULLSCREEN)) != 0; - bool act = (state_mask & (1u << XDG_TOPLEVEL_STATE_ACTIVATED)) != 0; - bool susp = (state_mask & (1u << XDG_TOPLEVEL_STATE_SUSPENDED)) != 0; - bool tiled = (state_mask & ( - (1u << XDG_TOPLEVEL_STATE_TILED_LEFT) | - (1u << XDG_TOPLEVEL_STATE_TILED_RIGHT) | - (1u << XDG_TOPLEVEL_STATE_TILED_TOP) | - (1u << XDG_TOPLEVEL_STATE_TILED_BOTTOM))) != 0; - - if ((fs ^ w->fullscreen) | (max ^ w->maximized) | - (act ^ w->activated) | (susp ^ w->suspended)) { - w->fullscreen = fs; - w->maximized = max; - w->activated = act; - w->suspended = susp; - notify_state(w); - } - - w->tiled = tiled; w->req_width = width; w->req_height = height; + set_state(w, state); } static void toplevel_close(void *data, struct xdg_toplevel *toplevel) @@ -363,10 +359,12 @@ static void toplevel_decoration_configure(void *data, { (void)toplevel_decor; struct wayland_surface *w = data; - bool was_server = w->server_decorated; - w->server_decorated = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); - if (was_server != w->server_decorated) - notify_state(w); + uint32_t state = w->base.state; + if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + state &= ~YASL_STATE_CSD; + else + state |= YASL_STATE_CSD; + set_state(w, state); } static const struct zxdg_toplevel_decoration_v1_listener toplevel_decoration_listener = { diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c index 7daa679..ad3a24c 100644 --- a/src/platform/wayland/surface.c +++ b/src/platform/wayland/surface.c @@ -1,5 +1,6 @@ #include "internal.h" #include <assert.h> +#include <errno.h> static void display_free(struct wayland_display *c); @@ -335,6 +336,7 @@ struct wayland_surface *wayland_surface_create_toplevel(struct wayland_display * if (!w) return NULL; c->depth++; conn_link(c, w); + w->base.state = YASL_STATE_CSD; wayland_surface_set_callbacks(w, NULL, NULL); w->cursor = YASL_CURSOR_TEXT; w->req_width = width; diff --git a/src/platform/x11/keymap.h b/src/platform/x11/keymap.h index 2ec4295..8114fb5 100644 --- a/src/platform/x11/keymap.h +++ b/src/platform/x11/keymap.h @@ -1,7 +1,7 @@ #ifndef YASL_X11_KEYMAP_H #define YASL_X11_KEYMAP_H -#include "yasl/yasl.h" +#include "yasl/keycodes.h" enum yasl_key yasl_key_from_xkb_name(const char name[4]); diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c index 347b4c2..3bab6e2 100644 --- a/src/platform/x11/window.c +++ b/src/platform/x11/window.c @@ -58,7 +58,6 @@ struct x11_surface { struct yasl_frame frame; struct yasl_damage_list damage; struct yasl_frame_clock clock; - uint32_t state; bool want_draw, drawing, presented, destroyed; struct x11_surface *parent; bool is_popup; @@ -700,7 +699,7 @@ static struct yasl_surface *create_toplevel(struct yasl_display *base, if (spec->fullscreen) { XChangeProperty(d->native, s->window, d->net_state, XA_ATOM, 32, PropModeReplace, (unsigned char *)&d->fullscreen, 1); - s->state |= YASL_STATE_FULLSCREEN; + s->base.state |= YASL_STATE_FULLSCREEN; } XMapWindow(d->native, s->window); XFlush(d->native); @@ -893,7 +892,7 @@ static void update_state(struct x11_surface *s) int format; unsigned long count, left; unsigned char *data = NULL; - uint32_t state = s->state & YASL_STATE_ACTIVATED; + uint32_t state = s->base.state & ~(YASL_STATE_FULLSCREEN | YASL_STATE_MAXIMIZED); if (XGetWindowProperty(d->native, s->window, d->net_state, 0, 64, False, XA_ATOM, &actual, &format, &count, &left, &data) == Success && format == 32) { Atom *atoms = (Atom *)data; @@ -906,8 +905,8 @@ static void update_state(struct x11_surface *s) if (h && v) state |= YASL_STATE_MAXIMIZED; } if (data) XFree(data); - if (state == s->state) return; - s->state = state; + if (state == s->base.state) return; + s->base.state = state; if (s->cbs && s->cbs->state) s->cbs->state(&s->base, state, s->ud); } @@ -1335,11 +1334,11 @@ static void dispatch(struct yasl_display *base) case FocusIn: case FocusOut: { bool focused = event.type == FocusIn; if (s->ic) { if (focused) XSetICFocus(s->ic); else XUnsetICFocus(s->ic); } - if (focused) s->state |= YASL_STATE_ACTIVATED; - else s->state &= ~YASL_STATE_ACTIVATED; + if (focused) s->base.state |= YASL_STATE_ACTIVATED; + else s->base.state &= ~YASL_STATE_ACTIVATED; push(s, (struct yasl_input_event){ .kind = YASL_INPUT_FOCUS, .focus.focused = focused}, NULL, 0); - if (s->cbs && s->cbs->state) s->cbs->state(&s->base, s->state, s->ud); + if (s->cbs && s->cbs->state) s->cbs->state(&s->base, s->base.state, s->ud); break; } case PropertyNotify: @@ -1746,7 +1745,6 @@ static struct yasl_geometry geometry(const struct yasl_surface *s) { return (struct yasl_geometry){((const struct x11_surface *)s)->size, YASL_SCALE_UNIT}; } -static uint32_t state(const struct yasl_surface *s) { return ((const struct x11_surface *)s)->state; } static int mods(const struct yasl_display *d) { return (int)((const struct x11_display *)d)->mods; } static void *native_display(struct yasl_display *d) { return ((struct x11_display *)d)->native; } static void *native_surface(struct yasl_surface *s) { return (void *)(uintptr_t)((struct x11_surface *)s)->window; } @@ -2042,7 +2040,6 @@ const struct yasl_backend_ops yasl_x11_backend = { .surface_present = present, .surface_input_poll = input_poll, .surface_geometry = geometry, - .surface_state = state, .surface_timing = timing, .surface_frame_time_ns = frame_time, .surface_set_cursor = cursor, diff --git a/tests/benchmark.c b/tests/benchmark.c index 8cea45c..7431715 100644 --- a/tests/benchmark.c +++ b/tests/benchmark.c @@ -10,7 +10,6 @@ #include "yasl/yasl.h" #include "yasl/loop.h" #include "yasl/timing.h" -#include "yasl/native.h" #include "common/input.h" #if YASL_HAS_WAYLAND #include "yasl/native/wayland.h" diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c index ac0065c..ee6fe61 100644 --- a/tests/common/test_contracts.c +++ b/tests/common/test_contracts.c @@ -25,6 +25,14 @@ static void test_timing_clock(void) assert(stats.max_us == 500.0); } +static void test_surface_state(void) +{ + /* Reading reported flags does not require a backend query or display. */ + struct yasl_surface surface = {.state = YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED}; + assert(!yasl_surface_state(NULL)); + assert(yasl_surface_state(&surface) == (YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED)); +} + static void test_queue(void) { struct input_queue q = {0}; @@ -326,6 +334,7 @@ static void test_errors(void) int main(void) { test_timing_clock(); + test_surface_state(); test_queue(); test_validation(); test_native(); diff --git a/tests/common/test_hello_example.c b/tests/common/test_hello_example.c new file mode 100644 index 0000000..6e0318f --- /dev/null +++ b/tests/common/test_hello_example.c @@ -0,0 +1,20 @@ +/* Exercise hello's real frame callback; automated exit belongs in the test. */ +#define main hello_example_main +#include "../../examples/hello.c" +#undef main + +static void draw_once(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) +{ + draw(ud, surface, frame); + if (!((struct app *)ud)->failed) yasl_surface_destroy(surface); +} + +int main(void) +{ + struct app app = {0}; + struct yasl_app_desc desc = { + .app_id = "org.yasl.test.hello", .userdata = &app, + .init = init, .event = event, .frame = draw_once, + }; + return yasl_run(&desc) || app.failed ? 1 : 0; +} diff --git a/tests/common/test_tablet_example.c b/tests/common/test_tablet_example.c index 15dde9c..77e79f6 100644 --- a/tests/common/test_tablet_example.c +++ b/tests/common/test_tablet_example.c @@ -6,8 +6,20 @@ static void pointer(struct app *app, enum yasl_pointer_kind kind, enum yasl_pointer_device device, int button, int x, int y, uint16_t pressure) { - struct yasl_input_event event = {.kind = YASL_INPUT_MOUSE, .mouse = { - .kind = kind, .device = device, .button = button, .pos = {x, y}, .pen.pressure = pressure}}; + struct yasl_event event = {0}; + if (kind == YASL_PTR_MOTION) { + event.type = YASL_EVENT_MOUSE_MOTION; + event.mouse_motion.pos = (struct yasl_vector2i){x, y}; + event.mouse_motion.device = device; + event.mouse_motion.pen.pressure = pressure; + } else { + event.type = YASL_EVENT_MOUSE_BUTTON; + event.mouse_button.pos = (struct yasl_vector2i){x, y}; + event.mouse_button.device = device; + event.mouse_button.button = button; + event.mouse_button.pressed = kind == YASL_PTR_PRESS; + event.mouse_button.pen.pressure = pressure; + } input(app, &event); } @@ -38,13 +50,13 @@ int main(void) pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_ERASER, 0, 80, 160, 65535); assert(app.canvas[(size_t)160 * app.geometry.size.x + 80] == PAPER && app.canvas[(size_t)160 * app.geometry.size.x + 130] == INK); - struct yasl_input_event reset = {.kind = YASL_INPUT_RESET}; + struct yasl_event reset = {.type = YASL_EVENT_INPUT_RESET}; input(&app, &reset); pointer(&app, YASL_PTR_MOTION, YASL_PTR_DEV_ERASER, 0, 130, 160, 65535); assert(!app.down && app.canvas[(size_t)160 * app.geometry.size.x + 130] == INK); pointer(&app, YASL_PTR_PRESS, YASL_PTR_DEV_MOUSE, 0, 300, 200, 0); assert(app.canvas[(size_t)200 * app.geometry.size.x + 300] == INK); - struct yasl_input_event blur = {.kind = YASL_INPUT_FOCUS, .focus.focused = false}; + struct yasl_event blur = {.type = YASL_EVENT_WINDOW_FOCUS, .focus.focused = false}; input(&app, &blur); assert(!app.down); assert(resize_canvas(&app, (struct yasl_geometry){{800, 600}, 240})); diff --git a/tests/common/test_timers.c b/tests/common/test_timers.c index f321b2c..8c92158 100644 --- a/tests/common/test_timers.c +++ b/tests/common/test_timers.c @@ -4,7 +4,6 @@ #include <assert.h> #include <stdio.h> -#include <stdlib.h> #include <string.h> struct test_ctx { diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py index 70c82fb..76fe8ab 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 = sys.argv[1:] + weston, xwayland, test, timers, tablet, hello, input_x11, interactive = sys.argv[1:] servers = [] with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: root = Path(directory) @@ -87,7 +87,11 @@ def main(): subprocess.run([tablet, "--backend", backend, "--smoke-test"], env=env, check=True, timeout=8) print("Test: hello example on", backend, flush=True) - subprocess.run([hello, "--smoke-test"], + subprocess.run([hello], + env=env if backend == "wayland" else x_env, + check=True, timeout=8) + print("Test: interactive example on", backend, flush=True) + subprocess.run([interactive, "--smoke-test"], env=env if backend == "wayland" else x_env, check=True, timeout=8) except BaseException: diff --git a/tests/meson.build b/tests/meson.build index 26bb5cb..951b30e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -138,6 +138,10 @@ tablet_example_test = executable('yasl-tablet-example-test', 'common/test_tablet test('tablet-example', tablet_example_test) if has_x11 + state_x11_test = executable('yasl-state-x11-test', 'x11/test_state.c', + include_directories: inc, dependencies: yasl_dep, + ) + test('state-x11', state_x11_test) tablet_x11_test = executable('yasl-tablet-x11-test', 'x11/test_tablet.c', include_directories: inc, dependencies: yasl_dep, ) @@ -156,6 +160,10 @@ elif get_option('x11_tablet_tests') endif if has_wayland + state_wayland_test = executable('yasl-state-wayland-test', 'wayland/test_state.c', + include_directories: inc, dependencies: yasl_dep, + ) + test('state-wayland', state_wayland_test) clipboard_wayland_test = executable('yasl-clipboard-wayland-test', 'wayland/test_clipboard.c', include_directories: inc, dependencies: yasl_dep, ) @@ -192,8 +200,12 @@ benchmark_exe = executable( benchmark('performance', benchmark_exe) if get_option('integration_tests') + hello_example_test = executable('yasl-hello-example-test', 'common/test_hello_example.c', + dependencies: shared_dep, + ) if has_cocoa - test('hello-cocoa-window', hello_example, + test('hello-cocoa-window', hello_example_test, timeout: 20) + test('interactive-cocoa-window', interactive_example, args: ['--smoke-test'], timeout: 20, ) native_test = executable('yasl-native-test', 'cocoa/test_native.c', @@ -235,7 +247,8 @@ if get_option('integration_tests') xwayland = find_program('Xwayland') test('backends', python, args: [files('helpers/run_backends.py'), weston, xwayland, - backend_test, timer_test, tablet_example, hello_example, input_x11_test], + backend_test, timer_test, tablet_example, hello_example_test, input_x11_test, + interactive_example], timeout: 60, ) endif diff --git a/tests/test_lifecycle.c b/tests/test_lifecycle.c index c54abd0..00d1264 100644 --- a/tests/test_lifecycle.c +++ b/tests/test_lifecycle.c @@ -1,5 +1,4 @@ #include "yasl/loop.h" -#include "yasl/timing.h" #include "yasl/yasl.h" #include <assert.h> #include <poll.h> diff --git a/tests/wayland/test_app_input.c b/tests/wayland/test_app_input.c index 2cacb5e..c8c5687 100644 --- a/tests/wayland/test_app_input.c +++ b/tests/wayland/test_app_input.c @@ -9,6 +9,8 @@ struct test_state { int keys; + int buttons, motions, resets; + int end; }; static void push_key(struct yasl_surface *s) @@ -23,10 +25,32 @@ static void push_key(struct yasl_surface *s) static int on_init(void *ud, struct yasl_display *d) { - (void)ud; + struct test_state *state = ud; + if (state->end) { + struct wayland_display *display = (struct wayland_display *)d; + if (state->end == 1) + display->cbs->quit(display->ud); + else + display->cbs->lost(display->ud); + return YASL_APP_CONTINUE; + } struct yasl_surface *s = yasl_surface_create_toplevel(d, &(struct yasl_window_spec){ .title = "App Input Test", .size = {64, 64} }); assert(s); + struct input_queue *queue = &((struct wayland_surface *)s)->input; + input_queue_clear(queue); + struct yasl_input_event event = {.surface = s, .kind = YASL_INPUT_MOUSE, + .mouse = {.kind = YASL_PTR_PRESS, .device = YASL_PTR_DEV_ERASER, + .pos = {12, 34}, .pen = {.pressure = 50000, .tilt_x = -20, .tilt_y = 30}}}; + assert(input_queue_push(queue, &event)); + event.mouse.kind = YASL_PTR_MOTION; + event.mouse.pos = (struct yasl_vector2i){15, 39}; + assert(input_queue_push(queue, &event)); + struct yasl_input_event reset = {.surface = s, .kind = YASL_INPUT_RESET}; + assert(input_queue_push(queue, &reset)); + event.mouse.device = YASL_PTR_DEV_PEN; + event.mouse.pos = (struct yasl_vector2i){100, 200}; + assert(input_queue_push(queue, &event)); push_key(s); push_key(s); return YASL_APP_CONTINUE; @@ -35,7 +59,26 @@ static int on_init(void *ud, struct yasl_display *d) static int on_event(void *ud, const struct yasl_event *ev) { struct test_state *state = ud; - if (ev->type == YASL_EVENT_KEY) { + if (ev->type == YASL_EVENT_MOUSE_BUTTON) { + assert(ev->mouse_button.device == YASL_PTR_DEV_ERASER); + assert(ev->mouse_button.pressed && ev->mouse_button.button == 0); + assert(ev->mouse_button.pos.x == 12 && ev->mouse_button.pos.y == 34); + assert(ev->mouse_button.pen.pressure == 50000); + assert(ev->mouse_button.pen.tilt_x == -20 && ev->mouse_button.pen.tilt_y == 30); + state->buttons++; + } else if (ev->type == YASL_EVENT_MOUSE_MOTION) { + if (!state->motions) { + assert(ev->mouse_motion.device == YASL_PTR_DEV_ERASER); + assert(ev->mouse_motion.delta.x == 3 && ev->mouse_motion.delta.y == 5); + } else { + assert(ev->mouse_motion.device == YASL_PTR_DEV_PEN); + assert(!ev->mouse_motion.delta.x && !ev->mouse_motion.delta.y); + } + assert(ev->mouse_motion.pen.pressure == 50000); + state->motions++; + } else if (ev->type == YASL_EVENT_INPUT_RESET) { + state->resets++; + } else if (ev->type == YASL_EVENT_KEY) { state->keys++; yasl_surface_destroy(ev->surface); } @@ -56,5 +99,13 @@ int main(void) }); assert(res == 0); assert(state.keys == 1); + assert(state.buttons == 1 && state.motions == 2 && state.resets == 1); + for (int end = 1; end <= 2; end++) { + state = (struct test_state){.end = end}; + res = yasl_run(&(struct yasl_app_desc){ + .backend = YASL_BACKEND_WAYLAND, .userdata = &state, .init = on_init, + }); + assert(end == 1 ? res == 0 : res < 0); + } return 0; } diff --git a/tests/wayland/test_state.c b/tests/wayland/test_state.c new file mode 100644 index 0000000..2da26e5 --- /dev/null +++ b/tests/wayland/test_state.c @@ -0,0 +1,75 @@ +#include "../../src/platform/wayland/shell.c" +#include <assert.h> + +struct notifications { + int count; + uint32_t state; +}; + +static void state_changed(struct yasl_surface *surface, uint32_t state, void *ud) +{ + struct notifications *notifications = ud; + assert(yasl_surface_state(surface) == state); + notifications->count++; + notifications->state = state; +} + +static void configure(struct wayland_surface *surface, uint32_t *states, size_t count) +{ + struct wl_array array = {.size = count * sizeof(*states), .data = states}; + toplevel_configure(surface, NULL, 640, 480, &array); + assert(surface->req_width == 640 && surface->req_height == 480); +} + +int main(void) +{ + struct notifications notifications = {0}; + struct wayland_surface surface = {.base.state = YASL_STATE_CSD}; + wayland_surface_set_callbacks(&surface, &(struct yasl_window_cbs){ + .state = state_changed}, ¬ifications); + assert(yasl_surface_state(&surface.base) == YASL_STATE_CSD); + + uint32_t fullscreen[] = {XDG_TOPLEVEL_STATE_FULLSCREEN, + XDG_TOPLEVEL_STATE_ACTIVATED, XDG_TOPLEVEL_STATE_FULLSCREEN, 31, UINT32_MAX}; + configure(&surface, fullscreen, sizeof(fullscreen) / sizeof(*fullscreen)); + assert(notifications.count == 1); + assert(notifications.state == (YASL_STATE_CSD | YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED)); + configure(&surface, fullscreen, sizeof(fullscreen) / sizeof(*fullscreen)); + assert(notifications.count == 1); + + /* Decoration changes preserve the other flags and do not notify twice. */ + toplevel_decoration_configure(&surface, NULL, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + assert(notifications.count == 2); + assert(notifications.state == (YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED)); + toplevel_decoration_configure(&surface, NULL, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + assert(notifications.count == 2); + + uint32_t maximized[] = {XDG_TOPLEVEL_STATE_MAXIMIZED, XDG_TOPLEVEL_STATE_SUSPENDED}; + configure(&surface, maximized, sizeof(maximized) / sizeof(*maximized)); + assert(notifications.count == 3); + assert(notifications.state == (YASL_STATE_MAXIMIZED | YASL_STATE_SUSPENDED)); + toplevel_decoration_configure(&surface, NULL, ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + assert(notifications.count == 4); + assert(notifications.state == (YASL_STATE_CSD | YASL_STATE_MAXIMIZED | YASL_STATE_SUSPENDED)); + + /* Each tiled edge maps to the same portable flag; changing edges alone + * leaves the reported state unchanged. */ + uint32_t edges[] = {XDG_TOPLEVEL_STATE_TILED_LEFT, XDG_TOPLEVEL_STATE_TILED_RIGHT, + XDG_TOPLEVEL_STATE_TILED_TOP, XDG_TOPLEVEL_STATE_TILED_BOTTOM}; + for (size_t i = 0; i < sizeof(edges) / sizeof(*edges); i++) { + configure(&surface, &edges[i], 1); + assert(notifications.count == 5); + assert(notifications.state == (YASL_STATE_CSD | YASL_STATE_TILED)); + } + configure(&surface, edges, 0); + assert(notifications.count == 6); + assert(notifications.state == YASL_STATE_CSD); + + /* State still advances when applications have not installed callbacks. */ + wayland_surface_set_callbacks(&surface, NULL, NULL); + configure(&surface, fullscreen, sizeof(fullscreen) / sizeof(*fullscreen)); + assert(yasl_surface_state(&surface.base) == + (YASL_STATE_CSD | YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED)); + assert(notifications.count == 6); + return 0; +} diff --git a/tests/x11/test_state.c b/tests/x11/test_state.c new file mode 100644 index 0000000..abdda95 --- /dev/null +++ b/tests/x11/test_state.c @@ -0,0 +1,77 @@ +/* Supply native window-state properties without requiring an X server. */ +#define XGetWindowProperty state_property +#include "../../src/platform/x11/window.c" +#undef XGetWindowProperty + +static Atom properties[3]; +static unsigned long property_count; +static int notifications; +static uint32_t reported_state; + +int state_property(Display *display, Window window, Atom property, + long offset, long length, Bool delete_property, Atom type, + Atom *actual, int *format, unsigned long *count, unsigned long *left, + unsigned char **data) +{ + assert(display && window == 42 && property == 10); + assert(!offset && length >= 3 && !delete_property && type == XA_ATOM); + *actual = XA_ATOM; + *format = 32; + *count = property_count; + *left = 0; + *data = NULL; + if (property_count) { + *data = malloc(property_count * sizeof(*properties)); + assert(*data); + memcpy(*data, properties, property_count * sizeof(*properties)); + } + return Success; +} + +static void state_changed(struct yasl_surface *surface, uint32_t state, void *ud) +{ + (void)ud; + assert(yasl_surface_state(surface) == state); + reported_state = state; + notifications++; +} + +int main(void) +{ + struct x11_display display = {.base.ops = &yasl_x11_backend, + .net_state = 10, .fullscreen = 20, .maximized_h = 21, .maximized_v = 22}; + display.native = (Display *)&display; + struct x11_surface surface = {.base = {.display = &display.base, + .state = YASL_STATE_ACTIVATED}, .window = 42}; + set_callbacks(&surface.base, &(struct yasl_window_cbs){.state = state_changed}, NULL); + + properties[0] = display.fullscreen; + property_count = 1; + update_state(&surface); + assert(notifications == 1); + assert(reported_state == (YASL_STATE_FULLSCREEN | YASL_STATE_ACTIVATED)); + update_state(&surface); + assert(notifications == 1); + + /* Both maximized axes are required; activation survives property changes. */ + properties[0] = display.maximized_h; + update_state(&surface); + assert(notifications == 2 && reported_state == YASL_STATE_ACTIVATED); + properties[1] = display.maximized_v; + property_count = 2; + update_state(&surface); + assert(notifications == 3); + assert(reported_state == (YASL_STATE_MAXIMIZED | YASL_STATE_ACTIVATED)); + properties[2] = display.fullscreen; + property_count = 3; + update_state(&surface); + assert(notifications == 4); + assert(reported_state == (YASL_STATE_FULLSCREEN | YASL_STATE_MAXIMIZED | YASL_STATE_ACTIVATED)); + + property_count = 0; + update_state(&surface); + assert(notifications == 5 && reported_state == YASL_STATE_ACTIVATED); + update_state(&surface); + assert(notifications == 5); + return 0; +} diff --git a/tests/x11/test_tablet_native.c b/tests/x11/test_tablet_native.c index f12ec7f..078606a 100644 --- a/tests/x11/test_tablet_native.c +++ b/tests/x11/test_tablet_native.c @@ -55,6 +55,49 @@ static int connect_tool(const char *path) return fd; } +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); +} + +static void native_configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) +{ + input(ud, &(struct yasl_event){.surface = surface, .type = YASL_EVENT_WINDOW_RESIZE, + .resize = {.size = geometry.size, .scale_120 = geometry.scale_120}}); +} + +static void native_input(struct app *app, const struct yasl_input_event *raw) +{ + struct yasl_event event = {.surface = raw->surface}; + if (raw->kind == YASL_INPUT_RESET) event.type = YASL_EVENT_INPUT_RESET; + else if (raw->kind == YASL_INPUT_FOCUS) { + event.type = YASL_EVENT_WINDOW_FOCUS; + event.focus.focused = raw->focus.focused; + } else if (raw->kind == YASL_INPUT_MOUSE) { + if (raw->mouse.kind == YASL_PTR_MOTION) { + event.type = YASL_EVENT_MOUSE_MOTION; + event.mouse_motion.pos = raw->mouse.pos; + event.mouse_motion.device = raw->mouse.device; + event.mouse_motion.pen = raw->mouse.pen; + } else if (raw->mouse.kind == YASL_PTR_PRESS || raw->mouse.kind == YASL_PTR_RELEASE) { + event.type = YASL_EVENT_MOUSE_BUTTON; + event.mouse_button.pos = raw->mouse.pos; + event.mouse_button.device = raw->mouse.device; + event.mouse_button.button = raw->mouse.button; + event.mouse_button.pressed = raw->mouse.kind == YASL_PTR_PRESS; + event.mouse_button.pen = raw->mouse.pen; + } else if (raw->mouse.kind == YASL_PTR_LEAVE) event.type = YASL_EVENT_MOUSE_LEAVE; + } + input(app, &event); +} + static void pump(int fd, struct yasl_display *display, struct yasl_surface *surface, struct app *app) { xf86ITEventWaitForSync sync = {{sizeof(sync), XF86IT_EVENT_WAIT_FOR_SYNC}}; @@ -68,7 +111,7 @@ static void pump(int fd, struct yasl_display *display, struct yasl_surface *surf assert(yasl_display_dispatch(display, 0) == 0); struct yasl_input_event event; while (yasl_surface_input_poll(surface, &event)) { - input(app, &event); + native_input(app, &event); yasl_surface_schedule_redraw(surface); } yasl_display_flush(display); @@ -99,14 +142,14 @@ static void set_eraser_property(Display *display) int main(int argc, char **argv) { assert(argc == 3); /* pen socket, eraser socket */ - struct app app = {.running = true, .tablet_supported = true, .backend = YASL_BACKEND_X11}; + struct app app = {.tablet_supported = true, .backend = YASL_BACKEND_X11}; struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; struct yasl_display *display = yasl_display_open(NULL, &ds); assert(display && (yasl_display_caps(display) & YASL_CAP_TABLET)); struct yasl_window_spec ss = {.title = "YASL native pressure test", .size = {500, 400}}; struct yasl_surface *surface = yasl_surface_create_toplevel(display, &ss); assert(surface); - struct yasl_window_cbs cbs = {.draw = draw, .configure = configure}; + struct yasl_window_cbs cbs = {.draw = native_draw, .configure = native_configure}; yasl_surface_set_callbacks(surface, &cbs, &app); yasl_surface_schedule_redraw(surface); Display *x = yasl_display_get_native_handle(display); |
