summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ARCHITECTURE_PLAN.md89
-rw-r--r--README.md4
-rw-r--r--examples/game.c358
-rw-r--r--examples/notepad.c675
-rw-r--r--include/yasl/app.h297
-rw-r--r--meson.build13
-rw-r--r--src/common/app.c529
-rw-r--r--src/common/app_internal.h10
-rw-r--r--src/common/backend.c10
-rw-r--r--src/common/backend.h2
-rw-r--r--src/common/meson.build2
-rw-r--r--tests/common/test_app.c134
-rw-r--r--tests/meson.build9
13 files changed, 2129 insertions, 3 deletions
diff --git a/ARCHITECTURE_PLAN.md b/ARCHITECTURE_PLAN.md
new file mode 100644
index 0000000..5c866e5
--- /dev/null
+++ b/ARCHITECTURE_PLAN.md
@@ -0,0 +1,89 @@
+# YASL Modern Architecture Plan (Approach A)
+
+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`).
+
+---
+
+## 1. Motivation & Core Goals
+
+### The Problem in YASL 0.1
+1. **Split-Brain Event Model**: Window lifecycle and drawing use callbacks, but input events are queued in a surface ring buffer requiring a manual `dispatch -> poll -> flush` pump.
+2. **Platform Live-Resize Quirks**: On macOS (`inLiveResize`) and Wayland (`xdg_surface.configure`), drawing must be strictly synchronized to the OS event to avoid freezing, visual stutter, or black borders.
+3. **Game vs. IDE Divergence**:
+ - Games need continuous, VSync-paced ticks (60/120/144+ Hz) and direct GPU presentation (Metal/Vulkan).
+ - IDEs need reactive, on-demand redraws (0% CPU when idle), rich text input (IME, UTF-8, autorepeat), multi-window support, and cross-thread communication (LSP/file watchers).
+4. **Stack Overhead**: Presenting a CPU frame currently requires passing a 1,044-byte stack struct (`yasl_damage_list`).
+
+---
+
+## 2. Architecture Overview
+
+```
+┌────────────────────────────────────────────────────────┐
+│ Layer 2: Specialized Helpers (Opt-in) │
+│ - File Descriptor Watcher (LSP, Sockets, Pipes) │
+│ - Cross-Thread Event Dispatcher (yasl_post_event) │
+│ - Mouse Relative Mode (Pointer Lock for 3D/Games) │
+├────────────────────────────────────────────────────────┤
+│ Layer 1: Unified App Runner (include/yasl/app.h) │
+│ - yasl_run(const struct yasl_app_desc *desc) │
+│ - Dual-mode pacing: YASL_PACING_CONTINUOUS vs ON_DEMAND│
+│ - Decoupled frame target (CPU software vs GPU native) │
+├────────────────────────────────────────────────────────┤
+│ Layer 0: Core Native Surfaces & Dispatchers │
+│ - Wayland / X11 / Cocoa display & surface backends │
+└────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 3. Phased Execution Plan
+
+### Phase 1: Define Public Interface (`include/yasl/app.h` & modernized types)
+1. **Unified Event Pipeline (`yasl_event`)**:
+ - Merge input and window events into a single enum:
+ - `YASL_EVENT_KEY`, `YASL_EVENT_TEXT`
+ - `YASL_EVENT_MOUSE_MOTION`, `YASL_EVENT_MOUSE_BUTTON`, `YASL_EVENT_MOUSE_SCROLL`
+ - `YASL_EVENT_WINDOW_RESIZE`, `YASL_EVENT_WINDOW_SCALE_CHANGED`, `YASL_EVENT_WINDOW_CLOSE_REQUEST`, `YASL_EVENT_WINDOW_FOCUS`
+ - `YASL_EVENT_USER` (custom payloads from background threads)
+ - Add `bool repeat` to `yasl_key_event` to distinguish game keypresses from editor autorepeat.
+2. **Dual-Target Frame Context (`yasl_frame_context`)**:
+ - Provide `ctx->cpu` (software buffer) and `ctx->gpu` (native layer/surface).
+ - Lightweight presentation: `yasl_frame_present_cpu(ctx, dirty_rects, count)` where `NULL` dirty rects indicate full presentation without a 1 KB stack structure.
+3. **Pacing Modes**:
+ - `YASL_PACING_CONTINUOUS`: Drives `frame()` every display refresh for games.
+ - `YASL_PACING_ON_DEMAND`: Sleeps at 0% CPU when idle; wakes on events, timers, or `yasl_surface_request_frame()`.
+4. **App Descriptor & Runner**:
+ - Define `struct yasl_app_desc` with `init`, `event`, `frame`, and `quit`.
+ - Declare `int yasl_run(const struct yasl_app_desc *desc);`.
+
+### Phase 2: Implement Core Runner & Cross-Thread Messaging (`src/common/app.c`)
+1. **The Lifecycle Pump (`yasl_run`)**:
+ - Initialize display and event loop, invoke `desc->init()`.
+ - Run the unified event loop:
+ - Drain native inputs and dispatch clean `const yasl_event *` into `desc->event()`.
+ - Automatically synchronize macOS live-resize (`inLiveResize`) and Wayland configure passes to call `desc->frame()`.
+ - Regulate pacing: continuous ticks for games vs. reactive idle sleep for desktop apps.
+2. **Cross-Thread Event Posting (`yasl_post_event`)**:
+ - Implement thread-safe event enqueueing backed by a mutex-protected ringbuffer and loop wake pipe (`yasl_loop_kick`).
+ - Allows background threads (LSP, compilers, physics, asset loaders) to post `YASL_EVENT_USER` safely to the main thread.
+
+### Phase 3: Add Key Capabilities for Games & Desktop UI
+1. **Relative Mouse Mode (`yasl_surface_set_mouse_mode`)**:
+ - Implement `YASL_MOUSE_RELATIVE` (pointer lock / virtualized motion deltas) for games and 3D viewports.
+2. **Background File Descriptor Watching (`yasl_app_watch_fd`)**:
+ - Register pipes/sockets (e.g., LSP stdin/stdout) directly with the main loop.
+
+### Phase 4: Verification, Examples & Testing
+1. **`examples/game.c`**:
+ - Continuous 60/144 FPS game loop, animated graphics, raw WASD input, and relative mouse control.
+2. **`examples/notepad.c`**:
+ - Comprehensive text editor example demonstrating YASL's capabilities:
+ - Reactive rendering (0% CPU at idle, redrawing only when dirty).
+ - UTF-8 text insertion, backspace, enter, and cursor navigation with autorepeat.
+ - Blinking text caret driven by a timer.
+ - Scale-aware text rendering and dynamic window resizing.
+ - Background async task (e.g., word count / syntax check / LSP task) posting results via `yasl_post_event`.
+3. **Automated Tests**:
+ - Add test suites in `tests/` verifying `yasl_run`, event propagation, cross-thread posting, and pacing modes.
+ - Verify all 18 existing backend tests pass without regressions.
diff --git a/README.md b/README.md
index 8300b74..35b12ed 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ for platform capabilities and limits.
- [Getting started](docs/getting-started.md): build, install, link, and run a window.
- [Application guide](docs/guide.md): geometry, drawing, input, ownership, and backend limits.
-- [Public reference](include/yasl/yasl.h), [loop](include/yasl/loop.h),
+- [Public reference](include/yasl/yasl.h), [app runner](include/yasl/app.h), [loop](include/yasl/loop.h),
[timing](include/yasl/timing.h), and [native extensions](include/yasl/native.h).
Run `doxygen Doxyfile` for a searchable HTML reference in `build-docs/html`.
- [API policy](docs/api-policy.md), [migration notes](docs/migrating.md), and
@@ -37,7 +37,7 @@ newer and the system SDK, with a Metal-capable device for window drawing.
```sh
meson setup build
meson compile -C build
-./build/yasl-hello
+./build/yasl-notepad
meson test -C build --print-errorlogs
```
diff --git a/examples/game.c b/examples/game.c
new file mode 100644
index 0000000..0a8bc18
--- /dev/null
+++ b/examples/game.c
@@ -0,0 +1,358 @@
+/**
+ * @file game.c
+ * A continuous 60/144+ FPS game loop demonstration built on YASL Approach A.
+ *
+ * Demonstrates:
+ * - Continuous display-synchronized ticks (YASL_PACING_CONTINUOUS)
+ * - 3D software rendering (rotating shaded cube + starfield)
+ * - WASD continuous player movement & key state tracking
+ * - Relative mouse mode (YASL_MOUSE_RELATIVE) for smooth first-person camera orbit
+ * - Spacebar toggles pointer lock (Relative vs Normal cursor)
+ * - Real-time FPS counter and frame latency display
+ * - Dynamic window resizing with automatic aspect ratio adjustment
+ */
+
+#define _DEFAULT_SOURCE
+#include <yasl/app.h>
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define NUM_STARS 256
+#define FOV_SCALE 320.0f
+
+struct star {
+ float x, y, z;
+};
+
+struct vec3 {
+ float x, y, z;
+};
+
+struct game_state {
+ struct yasl_display *display;
+ struct yasl_surface *surface;
+
+ /* Camera & controls */
+ float cam_x, cam_y, cam_z;
+ float cam_yaw, cam_pitch;
+ bool key_w, key_a, key_s, key_d;
+ bool key_q, key_e;
+ bool relative_mouse;
+
+ /* Simulation objects */
+ float cube_rot_x, cube_rot_y;
+ struct star stars[NUM_STARS];
+
+ /* FPS & timing stats */
+ uint64_t last_stat_time_ns;
+ int frame_count;
+ float current_fps;
+ float avg_frame_time_ms;
+};
+
+/* 8x8 bitmap font for overlay HUD */
+static const uint8_t font8x8_digits[10][8] = {
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, /* 0 */
+ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, /* 1 */
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, /* 2 */
+ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, /* 3 */
+ {0x0C,0x1C,0x34,0x64,0x7E,0x04,0x04,0x00}, /* 4 */
+ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, /* 5 */
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, /* 6 */
+ {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00}, /* 7 */
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, /* 8 */
+ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, /* 9 */
+};
+
+static void draw_digit(uint32_t *pix, int w, int h, int px, int py, int digit, uint32_t col, int s)
+{
+ if (digit < 0 || digit > 9) return;
+ const uint8_t *glyph = font8x8_digits[digit];
+ for (int y = 0; y < 8; y++) {
+ uint8_t row = glyph[y];
+ for (int x = 0; x < 8; x++) {
+ if (row & (1 << x)) {
+ for (int dy = 0; dy < s; dy++) {
+ int yy = py + y * s + dy;
+ if (yy < 0 || yy >= h) continue;
+ for (int dx = 0; dx < s; dx++) {
+ int xx = px + x * s + dx;
+ if (xx >= 0 && xx < w) pix[(size_t)yy * w + xx] = col;
+ }
+ }
+ }
+ }
+ }
+}
+
+static void draw_number(uint32_t *pix, int w, int h, int px, int py, int num, uint32_t col, int scale)
+{
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%d", num);
+ for (int i = 0; buf[i]; i++) {
+ draw_digit(pix, w, h, px + i * 8 * scale, py, buf[i] - '0', col, scale);
+ }
+}
+
+static void draw_line(uint32_t *pix, int w, int h, int x0, int y0, int x1, int y1, uint32_t col)
+{
+ int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+ int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+ int err = dx + dy, e2;
+
+ while (1) {
+ if (x0 >= 0 && x0 < w && y0 >= 0 && y0 < h) {
+ pix[(size_t)y0 * w + x0] = col;
+ }
+ if (x0 == x1 && y0 == y1) break;
+ e2 = 2 * err;
+ if (e2 >= dy) { err += dy; x0 += sx; }
+ if (e2 <= dx) { err += dx; y0 += sy; }
+ }
+}
+
+static struct vec3 rotate_vertex(struct vec3 v, float rx, float ry)
+{
+ /* Rotate around Y */
+ float cy = cosf(ry), sy = sinf(ry);
+ float x1 = v.x * cy + v.z * sy;
+ float z1 = -v.x * sy + v.z * cy;
+
+ /* Rotate around X */
+ float cx = cosf(rx), sx = sinf(rx);
+ float y2 = v.y * cx - z1 * sx;
+ float z2 = v.y * sx + z1 * cx;
+
+ return (struct vec3){x1, y2, z2};
+}
+
+static bool project_point(struct vec3 p, int w, int h, int *out_x, int *out_y)
+{
+ if (p.z <= 0.2f) return false;
+ float inv_z = FOV_SCALE / p.z;
+ *out_x = (int)(w * 0.5f + p.x * inv_z);
+ *out_y = (int)(h * 0.5f - p.y * inv_z);
+ return true;
+}
+
+static int on_init(void *ud, struct yasl_display *d)
+{
+ struct game_state *game = ud;
+ game->display = d;
+
+ struct yasl_surface_spec spec = {
+ .title = "YASL 3D Game Loop (Approach A)",
+ .size = {800, 600},
+ };
+ game->surface = yasl_surface_create_toplevel(d, &spec);
+ if (!game->surface) return YASL_APP_QUIT;
+
+ /* Drive continuous 60/144Hz ticks */
+ yasl_surface_set_pacing(game->surface, YASL_PACING_CONTINUOUS);
+
+ game->cam_z = 3.5f;
+ game->relative_mouse = false;
+
+ /* Initialize stars */
+ for (int i = 0; i < NUM_STARS; i++) {
+ game->stars[i].x = ((float)(rand() % 2000) - 1000.0f) / 100.0f;
+ game->stars[i].y = ((float)(rand() % 2000) - 1000.0f) / 100.0f;
+ game->stars[i].z = ((float)(rand() % 1000) + 10.0f) / 50.0f;
+ }
+
+ game->last_stat_time_ns = yasl_loop_now_ns();
+ return YASL_APP_CONTINUE;
+}
+
+static int on_event(void *ud, const struct yasl_event *ev)
+{
+ struct game_state *game = ud;
+
+ switch (ev->type) {
+ case YASL_EVENT_KEY: {
+ bool down = (ev->key.action == YASL_KEY_PRESS || ev->key.action == YASL_KEY_REPEAT);
+ if (ev->key.action == YASL_KEY_RELEASE) down = false;
+
+ switch (ev->key.key) {
+ case YASL_KEY_W: game->key_w = down; break;
+ case YASL_KEY_S: game->key_s = down; break;
+ case YASL_KEY_A: game->key_a = down; break;
+ case YASL_KEY_D: game->key_d = down; break;
+ case YASL_KEY_Q: game->key_q = down; break;
+ case YASL_KEY_E: game->key_e = down; break;
+ case YASL_KEY_SPACE:
+ if (ev->key.action == YASL_KEY_PRESS) {
+ /* Toggle relative mouse mode */
+ game->relative_mouse = !game->relative_mouse;
+ yasl_surface_set_mouse_mode(game->surface,
+ game->relative_mouse ? YASL_MOUSE_RELATIVE : YASL_MOUSE_NORMAL);
+ }
+ break;
+ case YASL_KEY_ESCAPE:
+ if (ev->key.action == YASL_KEY_PRESS) {
+ if (game->relative_mouse) {
+ game->relative_mouse = false;
+ yasl_surface_set_mouse_mode(game->surface, YASL_MOUSE_NORMAL);
+ } else {
+ return YASL_APP_QUIT;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+
+ case YASL_EVENT_MOUSE_MOTION:
+ if (game->relative_mouse) {
+ /* Smooth first-person camera orbit */
+ game->cam_yaw += (float)ev->mouse_motion.delta.x * 0.005f;
+ game->cam_pitch += (float)ev->mouse_motion.delta.y * 0.005f;
+ if (game->cam_pitch > 1.4f) game->cam_pitch = 1.4f;
+ if (game->cam_pitch < -1.4f) game->cam_pitch = -1.4f;
+ }
+ break;
+
+ case YASL_EVENT_MOUSE_BUTTON:
+ if (ev->mouse_button.pressed && !game->relative_mouse) {
+ /* Click window to capture cursor */
+ game->relative_mouse = true;
+ yasl_surface_set_mouse_mode(game->surface, YASL_MOUSE_RELATIVE);
+ }
+ break;
+
+ case YASL_EVENT_WINDOW_CLOSE_REQUEST:
+ return YASL_APP_QUIT;
+
+ default:
+ break;
+ }
+
+ return YASL_APP_CONTINUE;
+}
+
+static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx)
+{
+ (void)s;
+ struct game_state *game = ud;
+ int w = ctx->size.x;
+ int h = ctx->size.y;
+ uint32_t *pix = ctx->cpu.pixels;
+ if (!pix || w <= 0 || h <= 0) return;
+
+ /* Update game simulation */
+ float speed = 0.04f;
+ float forward_x = sinf(game->cam_yaw);
+ float forward_z = cosf(game->cam_yaw);
+ float right_x = cosf(game->cam_yaw);
+ float right_z = -sinf(game->cam_yaw);
+
+ if (game->key_w) { game->cam_x -= forward_x * speed; game->cam_z -= forward_z * speed; }
+ if (game->key_s) { game->cam_x += forward_x * speed; game->cam_z += forward_z * speed; }
+ if (game->key_a) { game->cam_x -= right_x * speed; game->cam_z -= right_z * speed; }
+ if (game->key_d) { game->cam_x += right_x * speed; game->cam_z += right_z * speed; }
+ if (game->key_q) { game->cam_y -= speed; }
+ if (game->key_e) { game->cam_y += speed; }
+
+ game->cube_rot_x += 0.015f;
+ game->cube_rot_y += 0.025f;
+
+ /* Clear screen to deep space dark blue */
+ memset(pix, 0x08, (size_t)w * h * sizeof(uint32_t));
+
+ /* 1. Render 3D Starfield */
+ for (int i = 0; i < NUM_STARS; i++) {
+ game->stars[i].z -= 0.05f;
+ if (game->stars[i].z <= 0.2f) game->stars[i].z += 20.0f;
+
+ struct vec3 sp = {game->stars[i].x - game->cam_x,
+ game->stars[i].y - game->cam_y,
+ game->stars[i].z};
+ int sx, sy;
+ if (project_point(sp, w, h, &sx, &sy) && sx >= 0 && sx < w && sy >= 0 && sy < h) {
+ uint8_t brightness = (uint8_t)(255.0f * (1.0f - sp.z / 20.0f));
+ uint32_t star_col = 0xff000000 | (brightness << 16) | (brightness << 8) | brightness;
+ pix[(size_t)sy * w + sx] = star_col;
+ }
+ }
+
+ /* 2. Render 3D Cube */
+ static const struct vec3 cube_vertices[8] = {
+ {-1.0f, -1.0f, -1.0f}, { 1.0f, -1.0f, -1.0f},
+ { 1.0f, 1.0f, -1.0f}, {-1.0f, 1.0f, -1.0f},
+ {-1.0f, -1.0f, 1.0f}, { 1.0f, -1.0f, 1.0f},
+ { 1.0f, 1.0f, 1.0f}, {-1.0f, 1.0f, 1.0f},
+ };
+
+ static const int cube_edges[12][2] = {
+ {0, 1}, {1, 2}, {2, 3}, {3, 0},
+ {4, 5}, {5, 6}, {6, 7}, {7, 4},
+ {0, 4}, {1, 5}, {2, 6}, {3, 7},
+ };
+
+ int proj_x[8], proj_y[8];
+ bool valid[8];
+
+ for (int i = 0; i < 8; i++) {
+ struct vec3 rot = rotate_vertex(cube_vertices[i], game->cube_rot_x, game->cube_rot_y);
+ rot.x -= game->cam_x;
+ rot.y -= game->cam_y;
+ rot.z += game->cam_z;
+ valid[i] = project_point(rot, w, h, &proj_x[i], &proj_y[i]);
+ }
+
+ uint32_t cyan = 0xff00ffff;
+ for (int i = 0; i < 12; i++) {
+ int u = cube_edges[i][0];
+ int v = cube_edges[i][1];
+ if (valid[u] && valid[v]) {
+ draw_line(pix, w, h, proj_x[u], proj_y[u], proj_x[v], proj_y[v], cyan);
+ }
+ }
+
+ /* 3. Center crosshair in relative mouse mode */
+ if (game->relative_mouse) {
+ int cx = w / 2, cy = h / 2;
+ draw_line(pix, w, h, cx - 6, cy, cx + 6, cy, 0xffffffff);
+ draw_line(pix, w, h, cx, cy - 6, cx, cy + 6, 0xffffffff);
+ }
+
+ /* 4. FPS counter calculation */
+ game->frame_count++;
+ uint64_t now = yasl_loop_now_ns();
+ if (now - game->last_stat_time_ns >= 1000000000ull) {
+ uint64_t elapsed = now - game->last_stat_time_ns;
+ game->current_fps = (float)game->frame_count * 1000000000.0f / (float)elapsed;
+ game->avg_frame_time_ms = 1000.0f / (game->current_fps > 0 ? game->current_fps : 1.0f);
+ game->frame_count = 0;
+ game->last_stat_time_ns = now;
+ }
+
+ /* Draw FPS overlay digits in top-left */
+ int scale = ctx->scale_120 >= 240 ? 2 : 1;
+ draw_number(pix, w, h, 16 * scale, 16 * scale, (int)game->current_fps, 0xff00ff00, scale);
+
+ /* Submit frame */
+ yasl_frame_present_cpu(ctx, NULL, 0);
+}
+
+int main(void)
+{
+ static struct game_state game;
+ struct yasl_app_desc desc = {
+ .app_id = "org.yasl.game",
+ .backend = YASL_BACKEND_AUTO,
+ .userdata = &game,
+ .init = on_init,
+ .event = on_event,
+ .frame = on_frame,
+ };
+
+ return yasl_run(&desc);
+}
diff --git a/examples/notepad.c b/examples/notepad.c
new file mode 100644
index 0000000..250be58
--- /dev/null
+++ b/examples/notepad.c
@@ -0,0 +1,675 @@
+/**
+ * @file notepad.c
+ * A complete, reactive, software-rendered text editor built on YASL Approach A.
+ *
+ * Demonstrates:
+ * - Reactive presentation (0% CPU when idle via YASL_PACING_ON_DEMAND)
+ * - Clean event callbacks (unified yasl_event)
+ * - Monospace text rendering, line numbering gutter, and status bar
+ * - Full text navigation, insertion, deletion, and line splitting
+ * - Blinking text caret driven by yasl_app_add_timer()
+ * - Mouse click cursor placement & smooth scroll wheel handling
+ * - Background async worker thread reporting stats via yasl_post_event()
+ * - HiDPI dynamic scale awareness
+ */
+
+#define _DEFAULT_SOURCE
+#include <yasl/app.h>
+
+#include <ctype.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAX_LINES 2048
+#define MAX_LINE_LEN 512
+
+/* Classic 8x8 font bitmap covering ASCII 32 (' ') to 126 ('~') */
+static const uint8_t font8x8[95][8] = {
+ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, /* */
+ {0x18,0x3C,0x3C,0x18,0x18,0x00,0x18,0x00}, /* ! */
+ {0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00}, /* " */
+ {0x6C,0x6C,0xFE,0x6C,0xFE,0x6C,0x6C,0x00}, /* # */
+ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00}, /* $ */
+ {0x00,0x63,0x66,0x0C,0x18,0x33,0x63,0x00}, /* % */
+ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00}, /* & */
+ {0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00}, /* ' */
+ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00}, /* ( */
+ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00}, /* ) */
+ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00}, /* * */
+ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00}, /* + */
+ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, /* , */
+ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00}, /* - */
+ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, /* . */
+ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00}, /* / */
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, /* 0 */
+ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, /* 1 */
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, /* 2 */
+ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, /* 3 */
+ {0x0C,0x1C,0x34,0x64,0x7E,0x04,0x04,0x00}, /* 4 */
+ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, /* 5 */
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, /* 6 */
+ {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00}, /* 7 */
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, /* 8 */
+ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, /* 9 */
+ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00}, /* : */
+ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30}, /* ; */
+ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00}, /* < */
+ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00}, /* = */
+ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00}, /* > */
+ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00}, /* ? */
+ {0x3C,0x66,0x6E,0x6A,0x6E,0x60,0x3C,0x00}, /* @ */
+ {0x18,0x3C,0x66,0x7E,0x66,0x66,0x66,0x00}, /* A */
+ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, /* B */
+ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, /* C */
+ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, /* D */
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, /* E */
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00}, /* F */
+ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00}, /* G */
+ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, /* H */
+ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* I */
+ {0x0E,0x06,0x06,0x06,0x66,0x66,0x3C,0x00}, /* J */
+ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, /* K */
+ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00}, /* L */
+ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, /* M */
+ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00}, /* N */
+ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* O */
+ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00}, /* P */
+ {0x3C,0x66,0x66,0x66,0x6E,0x3C,0x0E,0x00}, /* Q */
+ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00}, /* R */
+ {0x3C,0x66,0x30,0x18,0x0C,0x66,0x3C,0x00}, /* S */
+ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* T */
+ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* U */
+ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00}, /* V */
+ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, /* W */
+ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00}, /* X */
+ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, /* Y */
+ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00}, /* Z */
+ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00}, /* [ */
+ {0x80,0xC0,0x60,0x30,0x18,0x0C,0x06,0x00}, /* \ */
+ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00}, /* ] */
+ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00}, /* ^ */
+ {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00}, /* _ */
+ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00}, /* ` */
+ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3B,0x00}, /* a */
+ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00}, /* b */
+ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00}, /* c */
+ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00}, /* d */
+ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00}, /* e */
+ {0x1C,0x30,0x30,0x78,0x30,0x30,0x30,0x00}, /* f */
+ {0x00,0x00,0x3B,0x66,0x66,0x3E,0x06,0x3C}, /* g */
+ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00}, /* h */
+ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00}, /* i */
+ {0x06,0x00,0x0E,0x06,0x06,0x66,0x66,0x3C}, /* j */
+ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00}, /* k */
+ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* l */
+ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00}, /* m */
+ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00}, /* n */
+ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00}, /* o */
+ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60}, /* p */
+ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06}, /* q */
+ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00}, /* r */
+ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00}, /* s */
+ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00}, /* t */
+ {0x00,0x00,0x66,0x66,0x66,0x66,0x3B,0x00}, /* u */
+ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00}, /* v */
+ {0x00,0x00,0x63,0x6B,0x7F,0x3E,0x36,0x00}, /* w */
+ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00}, /* x */
+ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C}, /* y */
+ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00}, /* z */
+ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00}, /* { */
+ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* | */
+ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00}, /* } */
+ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00} /* ~ */
+};
+
+#define MSG_STATS_READY 100
+
+struct doc_stats {
+ int words;
+ int chars;
+};
+
+struct editor {
+ struct yasl_display *display;
+ struct yasl_surface *surface;
+
+ char lines[MAX_LINES][MAX_LINE_LEN];
+ int line_count;
+
+ int cursor_row;
+ int cursor_col;
+ int scroll_y;
+ bool caret_visible;
+
+ /* Background stats from async worker */
+ struct doc_stats stats;
+ bool worker_running;
+ bool worker_dirty;
+ pthread_t worker_tid;
+ pthread_mutex_t worker_mutex;
+ pthread_cond_t worker_cond;
+};
+
+static void editor_notify_worker(struct editor *ed)
+{
+ pthread_mutex_lock(&ed->worker_mutex);
+ ed->worker_dirty = true;
+ pthread_cond_signal(&ed->worker_cond);
+ pthread_mutex_unlock(&ed->worker_mutex);
+}
+
+static void *worker_thread(void *arg)
+{
+ struct editor *ed = arg;
+ while (ed->worker_running) {
+ pthread_mutex_lock(&ed->worker_mutex);
+ while (!ed->worker_dirty && ed->worker_running) {
+ pthread_cond_wait(&ed->worker_cond, &ed->worker_mutex);
+ }
+ if (!ed->worker_running) {
+ pthread_mutex_unlock(&ed->worker_mutex);
+ break;
+ }
+ ed->worker_dirty = false;
+ pthread_mutex_unlock(&ed->worker_mutex);
+
+ /* Debounce / simulate asynchronous LSP analysis */
+ usleep(30000);
+
+ int word_count = 0;
+ int char_count = 0;
+ for (int i = 0; i < ed->line_count; i++) {
+ const char *p = ed->lines[i];
+ bool in_word = false;
+ while (*p) {
+ char_count++;
+ if (isspace((unsigned char)*p)) {
+ in_word = false;
+ } else if (!in_word) {
+ in_word = true;
+ word_count++;
+ }
+ p++;
+ }
+ }
+
+ struct doc_stats *s = malloc(sizeof(*s));
+ if (s) {
+ s->words = word_count;
+ s->chars = char_count;
+ yasl_post_event(ed->display, ed->surface, MSG_STATS_READY, s, sizeof(*s));
+ }
+ }
+ return NULL;
+}
+
+static uint64_t on_blink_timer(void *ud, yasl_timer_id id, uint64_t interval_ns)
+{
+ (void)id;
+ struct editor *ed = ud;
+ ed->caret_visible = !ed->caret_visible;
+ yasl_surface_request_frame(ed->surface);
+ return interval_ns; /* Repeat every 500ms */
+}
+
+/* Drawing primitives */
+static void fill_rect(uint32_t *pixels, int w, int h, int rx, int ry, int rw, int rh, uint32_t col)
+{
+ int x0 = rx < 0 ? 0 : rx;
+ int y0 = ry < 0 ? 0 : ry;
+ int x1 = rx + rw > w ? w : rx + rw;
+ int y1 = ry + rh > h ? h : ry + rh;
+
+ for (int y = y0; y < y1; y++) {
+ uint32_t *row = pixels + (size_t)y * w;
+ for (int x = x0; x < x1; x++) {
+ row[x] = col;
+ }
+ }
+}
+
+static void draw_char(uint32_t *pixels, int w, int h, int px, int py, char c, uint32_t col, int scale)
+{
+ if (c < 32 || c > 126)
+ c = ' ';
+ const uint8_t *glyph = font8x8[c - 32];
+
+ for (int y = 0; y < 8; y++) {
+ uint8_t row = glyph[y];
+ for (int x = 0; x < 8; x++) {
+ if (row & (1 << x)) {
+ fill_rect(pixels, w, h, px + x * scale, py + y * scale, scale, scale, col);
+ }
+ }
+ }
+}
+
+static void draw_string(uint32_t *pixels, int w, int h, int px, int py, const char *str, uint32_t col, int scale)
+{
+ while (*str) {
+ draw_char(pixels, w, h, px, py, *str, col, scale);
+ px += 8 * scale;
+ str++;
+ }
+}
+
+/* Text editor actions */
+static void insert_char_at_cursor(struct editor *ed, char c)
+{
+ char *line = ed->lines[ed->cursor_row];
+ int len = (int)strlen(line);
+ if (len >= MAX_LINE_LEN - 2)
+ return;
+
+ memmove(line + ed->cursor_col + 1, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1));
+ line[ed->cursor_col] = c;
+ ed->cursor_col++;
+ ed->caret_visible = true;
+ editor_notify_worker(ed);
+}
+
+static void insert_text(struct editor *ed, const char *text, size_t len)
+{
+ for (size_t i = 0; i < len; i++) {
+ if (text[i] == '\n') {
+ /* Split line */
+ if (ed->line_count >= MAX_LINES - 1)
+ break;
+ char *curr = ed->lines[ed->cursor_row];
+ char remainder[MAX_LINE_LEN];
+ snprintf(remainder, sizeof(remainder), "%s", curr + ed->cursor_col);
+ curr[ed->cursor_col] = '\0';
+ memmove(ed->lines[ed->cursor_row + 2], ed->lines[ed->cursor_row + 1],
+ (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN);
+ ed->line_count++;
+ snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder);
+ ed->cursor_row++;
+ ed->cursor_col = 0;
+ } else if (text[i] >= 32 && text[i] <= 126) {
+ insert_char_at_cursor(ed, text[i]);
+ }
+ }
+ ed->caret_visible = true;
+ editor_notify_worker(ed);
+}
+
+static void handle_backspace(struct editor *ed)
+{
+ char *line = ed->lines[ed->cursor_row];
+ if (ed->cursor_col > 0) {
+ int len = (int)strlen(line);
+ memmove(line + ed->cursor_col - 1, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1));
+ ed->cursor_col--;
+ } else if (ed->cursor_row > 0) {
+ /* Merge with previous line */
+ char *prev = ed->lines[ed->cursor_row - 1];
+ int prev_len = (int)strlen(prev);
+ int curr_len = (int)strlen(line);
+ if (prev_len + curr_len < MAX_LINE_LEN - 1) {
+ strncat(prev, line, (size_t)(MAX_LINE_LEN - prev_len - 1));
+ memmove(ed->lines[ed->cursor_row], ed->lines[ed->cursor_row + 1],
+ (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN);
+ ed->line_count--;
+ ed->cursor_row--;
+ ed->cursor_col = prev_len;
+ }
+ }
+ ed->caret_visible = true;
+ editor_notify_worker(ed);
+}
+
+static void handle_delete(struct editor *ed)
+{
+ char *line = ed->lines[ed->cursor_row];
+ int len = (int)strlen(line);
+ if (ed->cursor_col < len) {
+ memmove(line + ed->cursor_col, line + ed->cursor_col + 1, (size_t)(len - ed->cursor_col));
+ } else if (ed->cursor_row < ed->line_count - 1) {
+ /* Merge with next line */
+ char *next = ed->lines[ed->cursor_row + 1];
+ int next_len = (int)strlen(next);
+ if (len + next_len < MAX_LINE_LEN - 1) {
+ strncat(line, next, (size_t)(MAX_LINE_LEN - len - 1));
+ memmove(ed->lines[ed->cursor_row + 1], ed->lines[ed->cursor_row + 2],
+ (size_t)(ed->line_count - ed->cursor_row - 2) * MAX_LINE_LEN);
+ ed->line_count--;
+ }
+ }
+ ed->caret_visible = true;
+ editor_notify_worker(ed);
+}
+
+static void handle_enter(struct editor *ed)
+{
+ if (ed->line_count >= MAX_LINES - 1)
+ return;
+ char *curr = ed->lines[ed->cursor_row];
+ char remainder[MAX_LINE_LEN];
+ snprintf(remainder, sizeof(remainder), "%s", curr + ed->cursor_col);
+ curr[ed->cursor_col] = '\0';
+ memmove(ed->lines[ed->cursor_row + 2], ed->lines[ed->cursor_row + 1],
+ (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN);
+ ed->line_count++;
+ snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder);
+ ed->cursor_row++;
+ ed->cursor_col = 0;
+ ed->caret_visible = true;
+ editor_notify_worker(ed);
+}
+
+/* YASL App callbacks */
+static int on_init(void *ud, struct yasl_display *d)
+{
+ struct editor *ed = ud;
+ ed->display = d;
+
+ struct yasl_surface_spec spec = {
+ .title = "YASL Notepad (Approach A Example)",
+ .size = {860, 560},
+ };
+ ed->surface = yasl_surface_create_toplevel(d, &spec);
+ if (!ed->surface) {
+ perror("yasl_surface_create_toplevel");
+ return YASL_APP_QUIT;
+ }
+
+ yasl_surface_set_pacing(ed->surface, YASL_PACING_ON_DEMAND);
+
+ /* Start cursor blink timer (500ms) */
+ yasl_app_add_timer(d, YASL_MS_TO_NS(500), on_blink_timer, ed);
+
+ /* Start background analysis thread */
+ ed->worker_running = true;
+ pthread_mutex_init(&ed->worker_mutex, NULL);
+ pthread_cond_init(&ed->worker_cond, NULL);
+ pthread_create(&ed->worker_tid, NULL, worker_thread, ed);
+
+ /* Trigger initial analysis and frame */
+ editor_notify_worker(ed);
+ yasl_surface_request_frame(ed->surface);
+ return YASL_APP_CONTINUE;
+}
+
+static int on_event(void *ud, const struct yasl_event *ev)
+{
+ struct editor *ed = ud;
+
+ switch (ev->type) {
+ case YASL_EVENT_TEXT:
+ insert_text(ed, ev->text.text, ev->text.len);
+ yasl_surface_request_frame(ed->surface);
+ break;
+
+ case YASL_EVENT_KEY:
+ if (ev->key.action == YASL_KEY_PRESS || ev->key.action == YASL_KEY_REPEAT) {
+ switch (ev->key.key) {
+ case YASL_KEY_BACKSPACE:
+ handle_backspace(ed);
+ break;
+ case YASL_KEY_DELETE:
+ handle_delete(ed);
+ break;
+ case YASL_KEY_ENTER:
+ handle_enter(ed);
+ break;
+ case YASL_KEY_LEFT:
+ if (ed->cursor_col > 0) {
+ ed->cursor_col--;
+ } else if (ed->cursor_row > 0) {
+ ed->cursor_row--;
+ ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]);
+ }
+ ed->caret_visible = true;
+ break;
+ case YASL_KEY_RIGHT: {
+ int len = (int)strlen(ed->lines[ed->cursor_row]);
+ if (ed->cursor_col < len) {
+ ed->cursor_col++;
+ } else if (ed->cursor_row < ed->line_count - 1) {
+ ed->cursor_row++;
+ ed->cursor_col = 0;
+ }
+ ed->caret_visible = true;
+ break;
+ }
+ case YASL_KEY_UP:
+ if (ed->cursor_row > 0) {
+ ed->cursor_row--;
+ int len = (int)strlen(ed->lines[ed->cursor_row]);
+ if (ed->cursor_col > len) ed->cursor_col = len;
+ }
+ ed->caret_visible = true;
+ break;
+ case YASL_KEY_DOWN:
+ if (ed->cursor_row < ed->line_count - 1) {
+ ed->cursor_row++;
+ int len = (int)strlen(ed->lines[ed->cursor_row]);
+ if (ed->cursor_col > len) ed->cursor_col = len;
+ }
+ ed->caret_visible = true;
+ break;
+ case YASL_KEY_HOME:
+ ed->cursor_col = 0;
+ ed->caret_visible = true;
+ break;
+ case YASL_KEY_END:
+ ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]);
+ ed->caret_visible = true;
+ break;
+ default:
+ break;
+ }
+ yasl_surface_request_frame(ed->surface);
+ }
+ break;
+
+ case YASL_EVENT_MOUSE_BUTTON:
+ if (ev->mouse_button.pressed && ev->mouse_button.button == 0) {
+ /* Compute scale-dependent dimensions */
+ struct yasl_geometry geom = yasl_surface_geometry(ed->surface);
+ int scale = geom.scale_120 >= 240 ? 2 : 1;
+ int char_w = 8 * scale;
+ int line_h = 16 * scale;
+ int gutter_w = 48 * scale;
+ int header_h = 8 * scale;
+
+ int row = (ev->mouse_button.pos.y - header_h) / line_h + ed->scroll_y;
+ int col = (ev->mouse_button.pos.x - gutter_w) / char_w;
+
+ if (row < 0) row = 0;
+ if (row >= ed->line_count) row = ed->line_count - 1;
+ int len = (int)strlen(ed->lines[row]);
+ if (col < 0) col = 0;
+ if (col > len) col = len;
+
+ ed->cursor_row = row;
+ ed->cursor_col = col;
+ ed->caret_visible = true;
+ yasl_surface_request_frame(ed->surface);
+ }
+ break;
+
+ case YASL_EVENT_MOUSE_SCROLL: {
+ ed->scroll_y -= (int)(ev->mouse_scroll.scroll.y * 3.0);
+ if (ed->scroll_y < 0) ed->scroll_y = 0;
+ if (ed->scroll_y >= ed->line_count) ed->scroll_y = ed->line_count - 1;
+ yasl_surface_request_frame(ed->surface);
+ break;
+ }
+
+ case YASL_EVENT_USER:
+ if (ev->user.type == MSG_STATS_READY && ev->user.payload) {
+ struct doc_stats *s = (struct doc_stats *)ev->user.payload;
+ ed->stats = *s;
+ free(s);
+ yasl_surface_request_frame(ed->surface);
+ }
+ break;
+
+ case YASL_EVENT_WINDOW_CLOSE_REQUEST:
+ return YASL_APP_QUIT;
+
+ default:
+ break;
+ }
+
+ return YASL_APP_CONTINUE;
+}
+
+static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx)
+{
+ (void)s;
+ struct editor *ed = ud;
+ int w = ctx->size.x;
+ int h = ctx->size.y;
+ uint32_t *pix = ctx->cpu.pixels;
+ if (!pix || w <= 0 || h <= 0)
+ return;
+
+ int scale = ctx->scale_120 >= 240 ? 2 : 1;
+ int char_w = 8 * scale;
+ int line_h = 16 * scale;
+ int gutter_w = 48 * scale;
+ int header_h = 8 * scale;
+ int status_h = 24 * scale;
+
+ /* Catppuccin-inspired modern dark theme */
+ uint32_t bg_main = 0xff1e1e2e;
+ uint32_t bg_gutter = 0xff181825;
+ uint32_t bg_active_line = 0xff252538;
+ uint32_t bg_status = 0xff11111b;
+ uint32_t text_fg = 0xffcdd6f4;
+ uint32_t text_gutter = 0xff6c7086;
+ uint32_t text_status = 0xffa6adc8;
+ uint32_t caret_col = 0xfff38ba8;
+ uint32_t status_accent = 0xffa6e3a1;
+
+ /* 1. Clear background */
+ fill_rect(pix, w, h, 0, 0, w, h, bg_main);
+
+ /* 2. Gutter background */
+ fill_rect(pix, w, h, 0, 0, gutter_w, h - status_h, bg_gutter);
+
+ /* 3. Ensure cursor is within visible bounds */
+ int visible_lines = (h - header_h - status_h) / line_h;
+ if (ed->cursor_row < ed->scroll_y) {
+ ed->scroll_y = ed->cursor_row;
+ } else if (ed->cursor_row >= ed->scroll_y + visible_lines) {
+ ed->scroll_y = ed->cursor_row - visible_lines + 1;
+ }
+
+ /* 4. Active line highlight */
+ int active_line_y = header_h + (ed->cursor_row - ed->scroll_y) * line_h;
+ if (active_line_y >= header_h && active_line_y < h - status_h) {
+ fill_rect(pix, w, h, gutter_w, active_line_y, w - gutter_w, line_h, bg_active_line);
+ }
+
+ /* 5. Render visible lines and gutter numbers */
+ for (int i = 0; i < visible_lines; i++) {
+ int line_idx = ed->scroll_y + i;
+ if (line_idx >= ed->line_count) break;
+
+ int y = header_h + i * line_h + 4 * scale;
+
+ /* Line number */
+ char num_buf[16];
+ snprintf(num_buf, sizeof(num_buf), "%3d", line_idx + 1);
+ draw_string(pix, w, h, 8 * scale, y, num_buf, text_gutter, scale);
+
+ /* Text content */
+ const char *line_text = ed->lines[line_idx];
+ draw_string(pix, w, h, gutter_w + 8 * scale, y, line_text, text_fg, scale);
+ }
+
+ /* 6. Render caret */
+ if (ed->caret_visible && active_line_y >= header_h && active_line_y < h - status_h) {
+ int caret_x = gutter_w + 8 * scale + ed->cursor_col * char_w;
+ int caret_y = active_line_y + 3 * scale;
+ fill_rect(pix, w, h, caret_x, caret_y, 2 * scale, 10 * scale, caret_col);
+ }
+
+ /* 7. Status bar */
+ int status_y = h - status_h;
+ fill_rect(pix, w, h, 0, status_y, w, status_h, bg_status);
+
+ char status_left[128];
+ snprintf(status_left, sizeof(status_left), "Ln %d, Col %d Lines: %d Words: %d Chars: %d",
+ ed->cursor_row + 1, ed->cursor_col + 1, ed->line_count, ed->stats.words, ed->stats.chars);
+ draw_string(pix, w, h, 12 * scale, status_y + 6 * scale, status_left, text_status, scale);
+
+ char status_right[64];
+ snprintf(status_right, sizeof(status_right), "[0%% CPU Idle] Scale: %.1fx",
+ (double)ctx->scale_120 / 120.0);
+ int right_w = (int)strlen(status_right) * char_w;
+ draw_string(pix, w, h, w - right_w - 12 * scale, status_y + 6 * scale, status_right, status_accent, scale);
+
+ /* Present full surface */
+ yasl_frame_present_cpu(ctx, NULL, 0);
+}
+
+static void on_quit(void *ud, struct yasl_display *d)
+{
+ (void)d;
+ struct editor *ed = ud;
+ ed->worker_running = false;
+ pthread_mutex_lock(&ed->worker_mutex);
+ pthread_cond_signal(&ed->worker_cond);
+ pthread_mutex_unlock(&ed->worker_mutex);
+ pthread_join(ed->worker_tid, NULL);
+ pthread_mutex_destroy(&ed->worker_mutex);
+ pthread_cond_destroy(&ed->worker_cond);
+}
+
+int main(void)
+{
+ static struct editor ed;
+
+ /* Initialize starter document */
+ const char *initial_content[] = {
+ "// Welcome to YASL Notepad!",
+ "// A reactive, modern desktop text editor built on YASL Approach A.",
+ "",
+ "#include <stdio.h>",
+ "",
+ "int main(int argc, char **argv) {",
+ " printf(\"Hello, modern YASL!\\n\");",
+ " return 0;",
+ "}",
+ "",
+ "Features demonstrated in this example:",
+ " [x] 0% CPU at idle (YASL_PACING_ON_DEMAND)",
+ " [x] Unified event callback (yasl_event)",
+ " [x] Smooth caret blink timer via yasl_app_add_timer()",
+ " [x] Asynchronous background linter/stats via yasl_post_event()",
+ " [x] Full keyboard autorepeat & text navigation",
+ " [x] Mouse click cursor positioning & scroll wheel support",
+ " [x] Dynamic HiDPI scale awareness",
+ "",
+ "Try typing here or pressing Arrow keys, Enter, Backspace, Home, End..."
+ };
+
+ ed.line_count = sizeof(initial_content) / sizeof(initial_content[0]);
+ for (int i = 0; i < ed.line_count; i++) {
+ snprintf(ed.lines[i], MAX_LINE_LEN, "%s", initial_content[i]);
+ }
+ ed.cursor_row = 6;
+ ed.cursor_col = 35;
+ ed.caret_visible = true;
+
+ struct yasl_app_desc desc = {
+ .app_id = "org.yasl.notepad",
+ .backend = YASL_BACKEND_AUTO,
+ .userdata = &ed,
+ .init = on_init,
+ .event = on_event,
+ .frame = on_frame,
+ .quit = on_quit,
+ };
+
+ return yasl_run(&desc);
+}
diff --git a/include/yasl/app.h b/include/yasl/app.h
new file mode 100644
index 0000000..91de019
--- /dev/null
+++ b/include/yasl/app.h
@@ -0,0 +1,297 @@
+/**
+ * @file
+ * Unified application runner, event pipeline, and presentation context.
+ *
+ * Implements the inverted lifecycle architecture for modern cross-platform
+ * applications (games, IDEs, and desktop tools). Instead of managing manual
+ * dispatch/poll/flush pumps, the application provides event and frame callbacks
+ * to yasl_run(), which drives platform runloops, display pacing, and live resizing.
+ */
+#ifndef YASL_APP_H
+#define YASL_APP_H
+
+#include "yasl/yasl.h"
+#include "yasl/loop.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Application callback return status. */
+typedef enum yasl_app_result {
+ YASL_APP_CONTINUE = 0, /**< Continue running the application. */
+ YASL_APP_QUIT, /**< Request graceful termination of yasl_run(). */
+} yasl_app_result;
+
+/** Pacing mode governing when frame() callbacks are invoked. */
+typedef enum yasl_pacing_mode {
+ /**
+ * Reactive / on-demand presentation (ideal for IDEs, text editors, tools).
+ * The application sleeps at 0% CPU when idle. frame() is only invoked when
+ * explicitly requested via yasl_surface_request_frame() or upon OS window
+ * expose/resize events.
+ */
+ YASL_PACING_ON_DEMAND = 0,
+
+ /**
+ * Continuous vsync-aligned tick (ideal for game engines and animations).
+ * frame() is driven continuously at the display's native refresh rate
+ * (e.g. 60/120/144 Hz) via DisplayLink, Wayland frame callbacks, or loop timers.
+ */
+ YASL_PACING_CONTINUOUS,
+} yasl_pacing_mode;
+
+/** Mouse capture and virtualization modes. */
+typedef enum yasl_mouse_mode {
+ YASL_MOUSE_NORMAL = 0, /**< Standard visible pointer. */
+ YASL_MOUSE_HIDDEN, /**< Hidden cursor, normal bounded window coordinates. */
+ YASL_MOUSE_RELATIVE, /**< Relative pointer lock: cursor hidden, unbounded deltas. */
+} yasl_mouse_mode;
+
+/** Unified event types delivered to the application event callback. */
+typedef enum yasl_event_type {
+ YASL_EVENT_NONE = 0,
+
+ /* Input events */
+ YASL_EVENT_KEY,
+ YASL_EVENT_TEXT,
+ YASL_EVENT_MOUSE_BUTTON,
+ YASL_EVENT_MOUSE_MOTION,
+ YASL_EVENT_MOUSE_SCROLL,
+ YASL_EVENT_MOUSE_LEAVE,
+
+ /* Window lifecycle events */
+ YASL_EVENT_WINDOW_RESIZE,
+ YASL_EVENT_WINDOW_SCALE_CHANGED,
+ YASL_EVENT_WINDOW_CLOSE_REQUEST,
+ YASL_EVENT_WINDOW_FOCUS,
+
+ /* System & external events */
+ YASL_EVENT_DROP_FILES,
+ YASL_EVENT_USER,
+} yasl_event_type;
+
+/** Unified event payload delivered to yasl_app_desc.event(). */
+struct yasl_event {
+ struct yasl_surface *surface; /**< Associated surface, or NULL for display-wide events. */
+ yasl_event_type type;
+
+ union {
+ struct {
+ enum yasl_key key;
+ enum yasl_key_action action;
+ uint32_t mods;
+ uint32_t codepoint;
+ uint32_t shifted;
+ uint32_t native_code;
+ bool repeat; /**< true if this press is generated by OS autorepeat. */
+ } key;
+
+ struct {
+ const char *text; /**< UTF-8 text string; borrowed for callback duration. */
+ size_t len; /**< Length in bytes excluding terminating NUL. */
+ } text;
+
+ struct {
+ struct yasl_vector2i pos; /**< Device-pixel position relative to surface. */
+ int16_t button; /**< 0 primary, 1 middle, 2 secondary. */
+ bool pressed; /**< true for press, false for release. */
+ uint32_t mods;
+ struct yasl_pointer_pen pen;
+ } mouse_button;
+
+ struct {
+ struct yasl_vector2i pos; /**< Device-pixel position relative to surface. */
+ struct yasl_vector2i delta; /**< Relative motion delta (dx, dy). */
+ uint32_t mods;
+ struct yasl_pointer_pen pen;
+ } mouse_motion;
+
+ struct {
+ struct yasl_vector2i pos;
+ struct yasl_vector2d scroll; /**< Fractional line units; positive right/down. */
+ uint32_t mods;
+ } mouse_scroll;
+
+ struct {
+ struct yasl_vector2i size; /**< Configured content extent in device pixels. */
+ uint32_t scale_120;
+ } resize;
+
+ struct {
+ uint32_t scale_120;
+ struct yasl_vector2i size;
+ } scale;
+
+ struct {
+ bool focused;
+ } focus;
+
+ struct {
+ const char *files; /**< NUL-terminated text/uri-list. */
+ size_t len;
+ } drop_files;
+
+ struct {
+ uint32_t type; /**< Application-defined message type. */
+ void *payload; /**< User-defined payload pointer. */
+ size_t size; /**< Optional payload size. */
+ } user;
+ };
+};
+
+/** Frame context provided to yasl_app_desc.frame() during presentation. */
+struct yasl_frame_context {
+ struct yasl_surface *surface;
+ uint64_t frame_index;
+ uint64_t target_time_ns; /**< Monotonic timestamp aligned to display refresh. */
+ struct yasl_vector2i size; /**< Surface content size in device pixels. */
+ uint32_t scale_120;
+
+ /** CPU software rendering target, if surface was created for CPU blitting. */
+ struct {
+ uint32_t *pixels; /**< Writable premultiplied 0xAARRGGBB device pixels. */
+ int stride_bytes; /**< Row stride in bytes. */
+ } cpu;
+
+ /** Native GPU rendering layer/surface, if surface was created for GPU rendering. */
+ struct {
+ void *native_layer; /**< CAMetalLayer* on Cocoa, wl_surface* on Wayland, Window on X11. */
+ } gpu;
+
+ /** Internal handle used by yasl_frame_present_cpu. Applications must not touch. */
+ void *internal_handle;
+};
+
+/**
+ * Present CPU software target pixels.
+ * @param ctx Active frame context passed to yasl_app_desc.frame().
+ * @param dirty_rects Optional array of damaged rectangles in device pixels.
+ * Pass NULL or count <= 0 to indicate full-surface damage.
+ * @param count Number of rectangles in dirty_rects.
+ * @return true if accepted, false on presentation failure.
+ */
+YASL_API bool yasl_frame_present_cpu(struct yasl_frame_context *ctx,
+ const struct yasl_rect2i *dirty_rects,
+ int count);
+
+/**
+ * Configure the presentation pacing mode for a surface.
+ * @param s Surface.
+ * @param mode YASL_PACING_ON_DEMAND for reactive UI, YASL_PACING_CONTINUOUS for games.
+ */
+YASL_API void yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode);
+
+/**
+ * Mark a surface as needing a redraw on the next frame pass.
+ * Wakes the event loop if sleeping under YASL_PACING_ON_DEMAND.
+ * @param s Surface.
+ */
+YASL_API void yasl_surface_request_frame(struct yasl_surface *s);
+
+/**
+ * Configure mouse pointer capture / virtualization mode.
+ * @param s Surface.
+ * @param mode Normal, hidden, or relative pointer lock.
+ */
+YASL_API void yasl_surface_set_mouse_mode(struct yasl_surface *s, yasl_mouse_mode mode);
+
+/**
+ * Safely post a custom user event to the main event loop from ANY thread.
+ * Wakes the display loop and delivers YASL_EVENT_USER to the event callback.
+ * @param d Display.
+ * @param s Optional target surface, or NULL.
+ * @param user_type Application-defined message type.
+ * @param payload Arbitrary pointer passed through to the event.
+ * @param size Optional payload size.
+ * @return true if queued successfully, false if queue is full or memory failed.
+ */
+YASL_API bool yasl_post_event(struct yasl_display *d, struct yasl_surface *s,
+ uint32_t user_type, void *payload, size_t size);
+
+/**
+ * Watch an open file descriptor or socket for poll-style readiness in the main loop.
+ * @param d Display.
+ * @param fd Open file descriptor (e.g. LSP stdout pipe or socket).
+ * @param poll_events Poll bitmask (e.g. POLLIN, POLLOUT).
+ * @param cb Readiness callback run on the main thread.
+ * @param ud Userdata.
+ * @return Positive watch ID, or -1 on failure.
+ */
+YASL_API int yasl_app_watch_fd(struct yasl_display *d, int fd, short poll_events,
+ int (*cb)(int fd, short revents, void *ud), void *ud);
+
+/**
+ * Unregister a file watch.
+ * @param d Display.
+ * @param watch_id Watch ID returned by yasl_app_watch_fd().
+ */
+YASL_API void yasl_app_unwatch_fd(struct yasl_display *d, int watch_id);
+
+/**
+ * Schedule a repeating or one-shot timer on the display's event loop.
+ * @param d Display.
+ * @param interval_ns Delay in monotonic nanoseconds.
+ * @param cb Callback returning next interval in nanoseconds, or 0 to stop.
+ * @param ud Userdata.
+ * @return Positive timer ID, or 0 on failure.
+ */
+YASL_API yasl_timer_id yasl_app_add_timer(struct yasl_display *d, uint64_t interval_ns,
+ yasl_timer_cb cb, void *ud);
+
+/**
+ * Cancel an active timer.
+ * @param d Display.
+ * @param id Timer ID returned by yasl_app_add_timer().
+ */
+YASL_API void yasl_app_del_timer(struct yasl_display *d, yasl_timer_id id);
+
+/** Application callbacks descriptor passed to yasl_run(). */
+struct yasl_app_desc {
+ /** Application identifier, e.g. "org.example.app". */
+ const char *app_id;
+ /** Desired backend; YASL_BACKEND_AUTO chooses native with fallback. */
+ enum yasl_backend backend;
+ /** User data passed to all callbacks. */
+ void *userdata;
+
+ /**
+ * Called once after display and event machinery initialization.
+ * Use this to create windows and initialize application resources.
+ * Return YASL_APP_CONTINUE or YASL_APP_QUIT.
+ */
+ int (*init)(void *ud, struct yasl_display *d);
+
+ /**
+ * 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.
+ * Return YASL_APP_CONTINUE or YASL_APP_QUIT.
+ */
+ int (*event)(void *ud, const struct yasl_event *ev);
+
+ /**
+ * Invoked when a surface needs to present a frame.
+ * Synchronized automatically with display pacing and live window resizing.
+ */
+ void (*frame)(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx);
+
+ /**
+ * Called right before the display is closed and yasl_run() exits.
+ * Destroy remaining surfaces and release application resources here.
+ */
+ void (*quit)(void *ud, struct yasl_display *d);
+};
+
+/**
+ * Run the application with the inverted lifecycle runner.
+ * 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.
+ */
+YASL_API int yasl_run(const struct yasl_app_desc *desc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YASL_APP_H */
diff --git a/meson.build b/meson.build
index 07f4390..b1fce24 100644
--- a/meson.build
+++ b/meson.build
@@ -91,6 +91,7 @@ yasl_lib = both_libraries(
install_headers(
'include/yasl/yasl.h',
+ 'include/yasl/app.h',
'include/yasl/timing.h',
'include/yasl/keycodes.h',
'include/yasl/loop.h',
@@ -145,6 +146,18 @@ tablet_example = executable(
dependencies: yasl_dep,
)
+notepad_example = executable(
+ 'yasl-notepad',
+ 'examples/notepad.c',
+ dependencies: yasl_dep,
+)
+
+game_example = executable(
+ 'yasl-game',
+ 'examples/game.c',
+ dependencies: yasl_dep,
+)
+
subdir('tests')
doc_dir = get_option('datadir') / 'doc' / 'yasl'
diff --git a/src/common/app.c b/src/common/app.c
new file mode 100644
index 0000000..15c2a3f
--- /dev/null
+++ b/src/common/app.c
@@ -0,0 +1,529 @@
+#include "yasl/app.h"
+#include "common/app_internal.h"
+#include "common/backend.h"
+
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct user_event_node {
+ struct yasl_surface *surface;
+ uint32_t type;
+ void *payload;
+ size_t size;
+ struct user_event_node *next;
+};
+
+struct app_surface {
+ struct yasl_surface *surface;
+ struct app_runner *runner;
+ yasl_pacing_mode pacing;
+ yasl_mouse_mode mouse_mode;
+ struct yasl_vector2i last_mouse_pos;
+ bool has_last_mouse_pos;
+ struct yasl_vector2i last_size;
+ uint32_t last_scale_120;
+ uint64_t frame_index;
+ bool frame_in_progress;
+ bool presented;
+ struct app_surface *next;
+};
+
+struct app_runner {
+ struct yasl_display *display;
+ const struct yasl_app_desc *desc;
+ bool running;
+ bool had_surfaces;
+ int surface_count;
+ struct app_surface *surfaces;
+
+ pthread_mutex_t user_event_mutex;
+ struct user_event_node *user_event_head;
+ struct user_event_node *user_event_tail;
+};
+
+static void app_runner_surface_draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud)
+{
+ struct app_surface *as = ud;
+ struct app_runner *ar = as ? as->runner : NULL;
+ if (!ar || !ar->desc || !ar->desc->frame)
+ return;
+
+ as->frame_in_progress = true;
+ as->presented = false;
+
+ struct yasl_frame_context ctx = {
+ .surface = s,
+ .frame_index = as->frame_index++,
+ .target_time_ns = yasl_surface_frame_time_ns(s, yasl_loop_now_ns()),
+ .size = frame->target.size,
+ .scale_120 = s->scale_120,
+ .cpu = {
+ .pixels = frame->target.pixels,
+ .stride_bytes = frame->target.size.x * (int)sizeof(uint32_t),
+ },
+ .gpu = {
+ .native_layer = NULL,
+ },
+ .internal_handle = frame,
+ };
+
+ struct yasl_native_frame nf;
+ if (yasl_surface_get_native_frame(s, &frame->target, &nf)) {
+ ctx.gpu.native_layer = nf.drawable;
+ }
+
+ ar->desc->frame(ar->desc->userdata, s, &ctx);
+
+ if (as->pacing == YASL_PACING_CONTINUOUS && ar->running) {
+ yasl_surface_schedule_redraw(s);
+ }
+
+ as->frame_in_progress = false;
+}
+
+static void app_runner_surface_configure(struct yasl_surface *s, struct yasl_geometry geom, void *ud)
+{
+ struct app_surface *as = ud;
+ struct app_runner *ar = as ? as->runner : NULL;
+ if (!ar || !ar->desc || !ar->desc->event)
+ return;
+
+ bool scale_changed = (as->last_scale_120 != geom.scale_120 && as->last_scale_120 != 0);
+ as->last_scale_120 = geom.scale_120;
+ as->last_size = geom.size;
+
+ if (scale_changed) {
+ struct yasl_event ev = {
+ .surface = s,
+ .type = YASL_EVENT_WINDOW_SCALE_CHANGED,
+ .scale = {
+ .scale_120 = geom.scale_120,
+ .size = geom.size,
+ },
+ };
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ }
+
+ struct yasl_event ev = {
+ .surface = s,
+ .type = YASL_EVENT_WINDOW_RESIZE,
+ .resize = {
+ .size = geom.size,
+ .scale_120 = geom.scale_120,
+ },
+ };
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+
+ yasl_surface_schedule_redraw(s);
+}
+
+static void app_runner_surface_close(struct yasl_surface *s, void *ud)
+{
+ struct app_surface *as = ud;
+ struct app_runner *ar = as ? as->runner : NULL;
+ if (!ar || !ar->desc || !ar->desc->event)
+ return;
+
+ struct yasl_event ev = {
+ .surface = s,
+ .type = YASL_EVENT_WINDOW_CLOSE_REQUEST,
+ };
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT) {
+ ar->running = false;
+ }
+}
+
+void app_runner_register_surface(void *runner, struct yasl_surface *s)
+{
+ if (!runner || !s)
+ return;
+ struct app_runner *ar = runner;
+ struct app_surface *as = calloc(1, sizeof(*as));
+ if (!as)
+ return;
+
+ as->surface = s;
+ as->runner = ar;
+ as->pacing = YASL_PACING_ON_DEMAND;
+ as->mouse_mode = YASL_MOUSE_NORMAL;
+ as->last_scale_120 = s->scale_120;
+ as->last_size = s->size;
+ s->app_surface = as;
+
+ as->next = ar->surfaces;
+ ar->surfaces = as;
+ ar->surface_count++;
+ ar->had_surfaces = true;
+
+ struct yasl_surface_cbs cbs = {
+ .draw = app_runner_surface_draw,
+ .configure = app_runner_surface_configure,
+ .close = app_runner_surface_close,
+ };
+ yasl_surface_set_callbacks(s, &cbs, as);
+}
+
+void app_runner_unregister_surface(void *runner, struct yasl_surface *s)
+{
+ if (!runner || !s || !s->app_surface)
+ return;
+ struct app_runner *ar = runner;
+ struct app_surface *as = s->app_surface;
+
+ struct app_surface **curr = &ar->surfaces;
+ while (*curr) {
+ if (*curr == as) {
+ *curr = as->next;
+ break;
+ }
+ curr = &(*curr)->next;
+ }
+
+ ar->surface_count--;
+ s->app_surface = NULL;
+ free(as);
+}
+
+bool yasl_frame_present_cpu(struct yasl_frame_context *ctx,
+ const struct yasl_rect2i *dirty_rects,
+ int count)
+{
+ if (!ctx || !ctx->surface || !ctx->internal_handle)
+ return false;
+
+ struct yasl_frame *frame = (struct yasl_frame *)ctx->internal_handle;
+ struct yasl_damage_list damage;
+ if (!dirty_rects || count <= 0) {
+ damage.n = -1;
+ damage.bounds = (struct yasl_rect2i){0};
+ } else {
+ damage.n = count > YASL_DAMAGE_MAX ? YASL_DAMAGE_MAX : count;
+ damage.bounds = (struct yasl_rect2i){0};
+ for (int i = 0; i < damage.n; i++) {
+ damage.rects[i] = dirty_rects[i];
+ }
+ }
+
+ return yasl_surface_present(ctx->surface, &frame->target, &damage);
+}
+
+void yasl_surface_set_pacing(struct yasl_surface *s, yasl_pacing_mode mode)
+{
+ if (!s || !s->app_surface)
+ return;
+ struct app_surface *as = s->app_surface;
+ as->pacing = mode;
+ if (mode == YASL_PACING_CONTINUOUS) {
+ yasl_surface_schedule_redraw(s);
+ }
+}
+
+void yasl_surface_request_frame(struct yasl_surface *s)
+{
+ if (!s)
+ return;
+ yasl_surface_schedule_redraw(s);
+ if (s->display && s->display->loop) {
+ yasl_loop_kick(s->display->loop);
+ }
+}
+
+void yasl_surface_set_mouse_mode(struct yasl_surface *s, yasl_mouse_mode mode)
+{
+ if (!s || !s->app_surface)
+ return;
+ struct app_surface *as = s->app_surface;
+ as->mouse_mode = mode;
+ if (mode == YASL_MOUSE_NORMAL) {
+ yasl_surface_set_cursor(s, YASL_CURSOR_ARROW);
+ } else {
+ yasl_surface_set_cursor(s, YASL_CURSOR_HIDDEN);
+ }
+}
+
+bool yasl_post_event(struct yasl_display *d, struct yasl_surface *s,
+ uint32_t user_type, void *payload, size_t size)
+{
+ if (!d || !d->app_runner)
+ return false;
+ struct app_runner *ar = d->app_runner;
+
+ struct user_event_node *node = malloc(sizeof(*node));
+ if (!node)
+ return false;
+ node->surface = s;
+ node->type = user_type;
+ node->payload = payload;
+ node->size = size;
+ node->next = NULL;
+
+ pthread_mutex_lock(&ar->user_event_mutex);
+ if (ar->user_event_tail) {
+ ar->user_event_tail->next = node;
+ ar->user_event_tail = node;
+ } else {
+ ar->user_event_head = node;
+ ar->user_event_tail = node;
+ }
+ pthread_mutex_unlock(&ar->user_event_mutex);
+
+ if (d->loop) {
+ yasl_loop_kick(d->loop);
+ }
+ return true;
+}
+
+int yasl_app_watch_fd(struct yasl_display *d, int fd, short poll_events,
+ int (*cb)(int fd, short revents, void *ud), void *ud)
+{
+ if (!d || !d->loop)
+ return -1;
+ return yasl_loop_add_fd(d->loop, fd, poll_events, cb, ud);
+}
+
+void yasl_app_unwatch_fd(struct yasl_display *d, int watch_id)
+{
+ if (!d || !d->loop)
+ return;
+ yasl_loop_del_fd(d->loop, watch_id);
+}
+
+yasl_timer_id yasl_app_add_timer(struct yasl_display *d, uint64_t interval_ns,
+ yasl_timer_cb cb, void *ud)
+{
+ if (!d || !d->loop)
+ return 0;
+ return yasl_loop_add_timer(d->loop, interval_ns, cb, ud);
+}
+
+void yasl_app_del_timer(struct yasl_display *d, yasl_timer_id id)
+{
+ if (!d || !d->loop)
+ return;
+ yasl_loop_del_timer(d->loop, id);
+}
+
+static void app_runner_dispatch_user_events(struct app_runner *ar)
+{
+ pthread_mutex_lock(&ar->user_event_mutex);
+ struct user_event_node *node = ar->user_event_head;
+ ar->user_event_head = NULL;
+ ar->user_event_tail = NULL;
+ pthread_mutex_unlock(&ar->user_event_mutex);
+
+ while (node && ar->running) {
+ struct user_event_node *next = node->next;
+ if (ar->desc->event) {
+ struct yasl_event ev = {
+ .surface = node->surface,
+ .type = YASL_EVENT_USER,
+ .user = {
+ .type = node->type,
+ .payload = node->payload,
+ .size = node->size,
+ },
+ };
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ }
+ free(node);
+ node = next;
+ }
+ while (node) {
+ struct user_event_node *next = node->next;
+ free(node);
+ node = next;
+ }
+}
+
+static void app_runner_dispatch_input_events(struct app_runner *ar)
+{
+ for (struct app_surface *as = ar->surfaces; as && ar->running; as = as->next) {
+ struct yasl_input_event ie;
+ while (yasl_surface_input_poll(as->surface, &ie) && ar->running) {
+ if (!ar->desc->event)
+ continue;
+
+ struct yasl_event ev = {0};
+ ev.surface = ie.surface ? ie.surface : as->surface;
+
+ switch (ie.kind) {
+ case YASL_INPUT_KEY:
+ ev.type = YASL_EVENT_KEY;
+ ev.key.key = ie.key.key;
+ ev.key.action = ie.key.action;
+ ev.key.mods = ie.key.mods;
+ ev.key.codepoint = ie.key.codepoint;
+ ev.key.shifted = ie.key.shifted;
+ ev.key.native_code = ie.key.native_code;
+ ev.key.repeat = (ie.key.action == YASL_KEY_REPEAT);
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ break;
+ case YASL_INPUT_TEXT:
+ ev.type = YASL_EVENT_TEXT;
+ ev.text.text = ie.text.text;
+ ev.text.len = ie.text.len;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ break;
+ case YASL_INPUT_MOUSE:
+ 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.button = ie.mouse.button;
+ ev.mouse_button.pressed = (ie.mouse.kind == YASL_PTR_PRESS);
+ ev.mouse_button.mods = ie.mouse.mods;
+ ev.mouse_button.pen = ie.mouse.pen;
+ as->last_mouse_pos = ie.mouse.pos;
+ as->has_last_mouse_pos = true;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ } else if (ie.mouse.kind == YASL_PTR_MOTION) {
+ ev.type = YASL_EVENT_MOUSE_MOTION;
+ ev.mouse_motion.pos = ie.mouse.pos;
+ 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;
+ } else {
+ ev.mouse_motion.delta = (struct yasl_vector2i){0, 0};
+ }
+ ev.mouse_motion.mods = ie.mouse.mods;
+ ev.mouse_motion.pen = ie.mouse.pen;
+ as->last_mouse_pos = ie.mouse.pos;
+ as->has_last_mouse_pos = true;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ } else if (ie.mouse.kind == YASL_PTR_SCROLL) {
+ ev.type = YASL_EVENT_MOUSE_SCROLL;
+ ev.mouse_scroll.pos = ie.mouse.pos;
+ ev.mouse_scroll.scroll = ie.mouse.scroll;
+ ev.mouse_scroll.mods = ie.mouse.mods;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ } else if (ie.mouse.kind == YASL_PTR_LEAVE) {
+ ev.type = YASL_EVENT_MOUSE_LEAVE;
+ as->has_last_mouse_pos = false;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ }
+ break;
+ case YASL_INPUT_FOCUS:
+ ev.type = YASL_EVENT_WINDOW_FOCUS;
+ ev.focus.focused = ie.focus.focused;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ break;
+ case YASL_INPUT_DROP_FILES:
+ ev.type = YASL_EVENT_DROP_FILES;
+ ev.drop_files.files = ie.drop_files.files;
+ ev.drop_files.len = ie.drop_files.len;
+ if (ar->desc->event(ar->desc->userdata, &ev) == YASL_APP_QUIT)
+ ar->running = false;
+ break;
+ case YASL_INPUT_RESET:
+ as->has_last_mouse_pos = false;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+int yasl_run(const struct yasl_app_desc *desc)
+{
+ if (!desc || !desc->init)
+ return -1;
+
+ struct yasl_display_spec dspec = {
+ .app_id = desc->app_id,
+ .backend = desc->backend,
+ };
+ 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,
+ };
+ pthread_mutex_init(&runner.user_event_mutex, NULL);
+ d->app_runner = &runner;
+
+ if (desc->init(desc->userdata, d) == YASL_APP_QUIT) {
+ runner.running = false;
+ }
+
+ uint32_t caps = yasl_display_caps(d);
+ bool has_frame_pacing = (caps & YASL_CAP_FRAME_PACING) != 0;
+
+ while (runner.running) {
+ if (runner.had_surfaces && runner.surface_count == 0) {
+ runner.running = false;
+ break;
+ }
+
+ bool any_continuous = false;
+ for (struct app_surface *as = runner.surfaces; as; as = as->next) {
+ if (as->pacing == YASL_PACING_CONTINUOUS)
+ any_continuous = true;
+ }
+
+ int timeout_ms = -1;
+ if (any_continuous) {
+ timeout_ms = has_frame_pacing ? -1 : 16;
+ }
+
+ int res = yasl_display_dispatch(d, timeout_ms);
+ if (res < 0) {
+ break;
+ }
+
+ app_runner_dispatch_user_events(&runner);
+ app_runner_dispatch_input_events(&runner);
+
+ if (runner.running) {
+ yasl_display_flush(d);
+ }
+
+ if (!has_frame_pacing && any_continuous && runner.running) {
+ for (struct app_surface *as = runner.surfaces; as; as = as->next) {
+ if (as->pacing == YASL_PACING_CONTINUOUS) {
+ yasl_surface_schedule_redraw(as->surface);
+ }
+ }
+ }
+ }
+
+ if (desc->quit) {
+ desc->quit(desc->userdata, d);
+ }
+
+ d->app_runner = NULL;
+ while (runner.surfaces) {
+ struct app_surface *next = runner.surfaces->next;
+ free(runner.surfaces);
+ runner.surfaces = next;
+ }
+
+ pthread_mutex_lock(&runner.user_event_mutex);
+ struct user_event_node *node = runner.user_event_head;
+ while (node) {
+ struct user_event_node *next = node->next;
+ free(node);
+ node = next;
+ }
+ pthread_mutex_unlock(&runner.user_event_mutex);
+ pthread_mutex_destroy(&runner.user_event_mutex);
+
+ yasl_display_close(d);
+ return 0;
+}
diff --git a/src/common/app_internal.h b/src/common/app_internal.h
new file mode 100644
index 0000000..64fa34c
--- /dev/null
+++ b/src/common/app_internal.h
@@ -0,0 +1,10 @@
+#ifndef YASL_APP_INTERNAL_H
+#define YASL_APP_INTERNAL_H
+
+#include "yasl/app.h"
+#include "common/backend.h"
+
+void app_runner_register_surface(void *runner, struct yasl_surface *s);
+void app_runner_unregister_surface(void *runner, struct yasl_surface *s);
+
+#endif
diff --git a/src/common/backend.c b/src/common/backend.c
index 23275d6..e690697 100644
--- a/src/common/backend.c
+++ b/src/common/backend.c
@@ -1,5 +1,6 @@
#include "backend.h"
#include "loop.h"
+#include "common/app_internal.h"
#include <errno.h>
#include <stdlib.h>
@@ -167,6 +168,9 @@ struct yasl_surface *yasl_create_toplevel(struct yasl_display *d,
errno = 0;
struct yasl_surface *s = d->ops->surface_create_toplevel(d, spec, graphics_device);
if (!s && !errno) errno = ENODEV;
+ if (s && d->app_runner) {
+ app_runner_register_surface(d->app_runner, s);
+ }
return s;
}
@@ -202,6 +206,9 @@ struct yasl_surface *yasl_surface_create_popup(struct yasl_surface *parent, cons
errno = 0;
struct yasl_surface *s = parent->display->ops->surface_create_popup(parent, spec);
if (!s && !errno) errno = ENODEV;
+ if (s && parent->display->app_runner) {
+ app_runner_register_surface(parent->display->app_runner, s);
+ }
return s;
}
@@ -226,6 +233,9 @@ void yasl_surface_destroy(struct yasl_surface *s)
{
if (!s || !s->display->ops->surface_destroy)
return;
+ if (s->app_surface && s->display->app_runner) {
+ app_runner_unregister_surface(s->display->app_runner, s);
+ }
yasl_surface_clear_timers(s);
input_queue_clear(&s->input);
s->display->ops->surface_destroy(s);
diff --git a/src/common/backend.h b/src/common/backend.h
index 6014cda..b93547a 100644
--- a/src/common/backend.h
+++ b/src/common/backend.h
@@ -14,6 +14,7 @@ struct yasl_display {
void *native_handle;
struct yasl_display_cbs callbacks;
struct yasl_loop *loop;
+ void *app_runner;
};
struct yasl_surface {
@@ -32,6 +33,7 @@ struct yasl_surface {
yasl_timer_id *timers;
int timer_count;
int timer_capacity;
+ void *app_surface;
};
void yasl_surface_clear_timers(struct yasl_surface *s);
diff --git a/src/common/meson.build b/src/common/meson.build
index 1247ff5..e3986cd 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -1 +1 @@
-sources += files('backend.c', 'input.c', 'keyboard.c', 'loop.c', 'native.c', 'timing.c', 'utf8.c')
+sources += files('app.c', 'backend.c', 'input.c', 'keyboard.c', 'loop.c', 'native.c', 'timing.c', 'utf8.c')
diff --git a/tests/common/test_app.c b/tests/common/test_app.c
new file mode 100644
index 0000000..661620c
--- /dev/null
+++ b/tests/common/test_app.c
@@ -0,0 +1,134 @@
+#define _DEFAULT_SOURCE
+#include "yasl/app.h"
+
+#include <assert.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <unistd.h>
+
+struct test_state {
+ struct yasl_display *display;
+ struct yasl_surface *surface;
+ bool init_called;
+ bool frame_called;
+ bool user_event_called;
+ bool timer_called;
+ bool quit_called;
+ int frame_count;
+ yasl_timer_id timer_id;
+};
+
+static void *worker_thread(void *arg)
+{
+ struct test_state *state = arg;
+ usleep(50000); /* 50ms */
+ bool ok = yasl_post_event(state->display, state->surface, 42, (void *)0x1234, 0);
+ assert(ok);
+ return NULL;
+}
+
+static uint64_t on_timer(void *ud, yasl_timer_id id, uint64_t interval_ns)
+{
+ (void)id;
+ (void)interval_ns;
+ struct test_state *state = ud;
+ state->timer_called = true;
+ yasl_surface_request_frame(state->surface);
+ return 0; /* one-shot */
+}
+
+static int on_init(void *ud, struct yasl_display *d)
+{
+ struct test_state *state = ud;
+ state->display = d;
+ state->init_called = true;
+
+ struct yasl_surface_spec spec = {
+ .title = "App Runner Test",
+ .size = {320, 240},
+ };
+ state->surface = yasl_surface_create_toplevel(d, &spec);
+ assert(state->surface != NULL);
+
+ yasl_surface_set_pacing(state->surface, YASL_PACING_ON_DEMAND);
+ yasl_surface_set_mouse_mode(state->surface, YASL_MOUSE_RELATIVE);
+ yasl_surface_set_mouse_mode(state->surface, YASL_MOUSE_NORMAL);
+
+ state->timer_id = yasl_app_add_timer(d, YASL_MS_TO_NS(20), on_timer, state);
+ assert(state->timer_id > 0);
+
+ pthread_t thread;
+ int err = pthread_create(&thread, NULL, worker_thread, state);
+ assert(err == 0);
+ pthread_detach(thread);
+
+ yasl_surface_request_frame(state->surface);
+ return YASL_APP_CONTINUE;
+}
+
+static int on_event(void *ud, const struct yasl_event *ev)
+{
+ struct test_state *state = ud;
+ if (ev->type == YASL_EVENT_USER) {
+ assert(ev->user.type == 42);
+ assert(ev->user.payload == (void *)0x1234);
+ state->user_event_called = true;
+ yasl_surface_request_frame(state->surface);
+ } else if (ev->type == YASL_EVENT_WINDOW_CLOSE_REQUEST) {
+ return YASL_APP_QUIT;
+ }
+ return YASL_APP_CONTINUE;
+}
+
+static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx)
+{
+ struct test_state *state = ud;
+ assert(s == state->surface);
+ assert(ctx->surface == s);
+ state->frame_called = true;
+ state->frame_count++;
+
+ if (ctx->cpu.pixels) {
+ size_t count = (size_t)ctx->size.x * ctx->size.y;
+ for (size_t i = 0; i < count; i++) {
+ ctx->cpu.pixels[i] = 0xff204060;
+ }
+ bool ok = yasl_frame_present_cpu(ctx, NULL, 0);
+ assert(ok);
+ }
+
+ if (state->user_event_called) {
+ yasl_surface_destroy(state->surface);
+ }
+}
+
+static void on_quit(void *ud, struct yasl_display *d)
+{
+ (void)d;
+ struct test_state *state = ud;
+ state->quit_called = true;
+}
+
+int main(void)
+{
+ struct test_state state = {0};
+ struct yasl_app_desc desc = {
+ .app_id = "org.yasl.test.app",
+ .backend = YASL_BACKEND_AUTO,
+ .userdata = &state,
+ .init = on_init,
+ .event = on_event,
+ .frame = on_frame,
+ .quit = on_quit,
+ };
+
+ int res = yasl_run(&desc);
+ assert(res == 0);
+ assert(state.init_called);
+ assert(state.frame_called);
+ assert(state.user_event_called);
+ assert(state.timer_called);
+ assert(state.quit_called);
+ printf("App runner tests passed!\n");
+ return 0;
+}
diff --git a/tests/meson.build b/tests/meson.build
index c6570d9..f74e3f8 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -51,6 +51,7 @@ if system == 'linux' or system == 'darwin'
endif
exported_headers = files(
'../include/yasl/yasl.h',
+ '../include/yasl/app.h',
'../include/yasl/timing.h',
'../include/yasl/loop.h',
'../include/yasl/native.h',
@@ -96,6 +97,14 @@ timer_test = executable(
)
test('timers', timer_test)
+app_test = executable(
+ 'yasl-app-test',
+ 'common/test_app.c',
+ include_directories: inc,
+ dependencies: yasl_dep,
+)
+test('app-runner', app_test)
+
input_alloc_test = executable(
'yasl-input-alloc-test',
['common/test_input_alloc.c', '../src/common/input.c'],