From d26bd78644544151b42600c5358d0fecc84b14f1 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 22:43:23 +0300 Subject: feat: implement modern Approach A app runner, unified events, notepad, and game examples --- examples/notepad.c | 675 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 675 insertions(+) create mode 100644 examples/notepad.c (limited to 'examples/notepad.c') 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 + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ", + "", + "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); +} -- cgit v1.2.3