summaryrefslogtreecommitdiff
path: root/examples/notepad.c
diff options
context:
space:
mode:
Diffstat (limited to 'examples/notepad.c')
-rw-r--r--examples/notepad.c675
1 files changed, 675 insertions, 0 deletions
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);
+}