diff options
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/notepad.c | 813 |
1 files changed, 705 insertions, 108 deletions
diff --git a/examples/notepad.c b/examples/notepad.c index cb2cd61..c06a4b9 100644 --- a/examples/notepad.c +++ b/examples/notepad.c @@ -1,16 +1,17 @@ /** * @file notepad.c - * A complete, reactive, software-rendered text editor built on YASL Approach A. + * A full-featured, reactive 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 + * Features: + * - Open & Save files (CLI argument `yasl-notepad [file]`, Ctrl+O, Ctrl+S) + * - Tab key inserts 4 spaces; Shift+Tab unindents + * - Clipboard Copy (Ctrl+C), Cut (Ctrl+X), Paste (Ctrl+V) via YASL clipboard API + * - Select All (Ctrl+A), text selection with mouse drag and Shift+Arrows + * - Undo support (Ctrl+Z) + * - Find / Search in document (Ctrl+F) + * - Drag-and-drop files directly into the window to open them + * - Dynamic HiDPI scale awareness & 0% CPU consumption when idle + * - Asynchronous background word & character analysis via yasl_post_event() */ #define _DEFAULT_SOURCE @@ -25,8 +26,9 @@ #include <string.h> #include <unistd.h> -#define MAX_LINES 2048 -#define MAX_LINE_LEN 512 +#define MAX_LINES 4096 +#define MAX_LINE_LEN 1024 +#define MAX_UNDO 32 /* Classic 8x8 font bitmap covering ASCII 32 (' ') to 126 ('~') */ static const uint8_t font8x8[95][8] = { @@ -129,23 +131,63 @@ static const uint8_t font8x8[95][8] = { #define MSG_STATS_READY 100 +typedef enum prompt_mode { + PROMPT_NONE = 0, + PROMPT_OPEN, + PROMPT_SAVE, + PROMPT_FIND, +} prompt_mode; + struct doc_stats { int words; int chars; }; +struct undo_snapshot { + char lines[MAX_LINES][MAX_LINE_LEN]; + int line_count; + int cursor_row; + int cursor_col; +}; + struct editor { struct yasl_display *display; struct yasl_surface *surface; + /* File tracking */ + char file_path[512]; + bool has_file; + bool is_modified; + + /* Text buffer */ char lines[MAX_LINES][MAX_LINE_LEN]; int line_count; + /* Cursor & Selection */ int cursor_row; int cursor_col; + int sel_anchor_row; + int sel_anchor_col; + bool has_selection; + bool mouse_dragging; + + /* Viewport */ int scroll_y; bool caret_visible; + /* Interactive mini-buffer prompt */ + prompt_mode prompt; + char prompt_buf[256]; + int prompt_len; + + /* Notification message in status bar */ + char status_msg[128]; + uint64_t status_msg_time; + + /* Simple Undo stack */ + struct undo_snapshot undo_stack[MAX_UNDO]; + int undo_count; + /* Background stats from async worker */ struct doc_stats stats; bool worker_running; @@ -155,6 +197,59 @@ struct editor { pthread_cond_t worker_cond; }; +static void editor_set_status(struct editor *ed, const char *msg) +{ + snprintf(ed->status_msg, sizeof(ed->status_msg), "%s", msg); + ed->status_msg_time = yasl_loop_now_ns(); +} + +static void editor_update_title(struct editor *ed) +{ + if (!ed->surface) return; + const char *name = ed->has_file ? ed->file_path : "Untitled"; + /* Extract filename component */ + const char *slash = strrchr(name, '/'); + if (slash) name = slash + 1; + + char title[540]; + snprintf(title, sizeof(title), "%s%s - YASL Notepad", ed->is_modified ? "*" : "", name); + yasl_surface_set_title(ed->surface, title); +} + +static void editor_save_undo(struct editor *ed) +{ + if (ed->undo_count >= MAX_UNDO) { + memmove(&ed->undo_stack[0], &ed->undo_stack[1], sizeof(struct undo_snapshot) * (MAX_UNDO - 1)); + ed->undo_count = MAX_UNDO - 1; + } + struct undo_snapshot *s = &ed->undo_stack[ed->undo_count++]; + s->line_count = ed->line_count; + s->cursor_row = ed->cursor_row; + s->cursor_col = ed->cursor_col; + for (int i = 0; i < ed->line_count; i++) { + memcpy(s->lines[i], ed->lines[i], MAX_LINE_LEN); + } +} + +static void editor_undo(struct editor *ed) +{ + if (ed->undo_count <= 0) { + editor_set_status(ed, "Already at oldest change"); + return; + } + struct undo_snapshot *s = &ed->undo_stack[--ed->undo_count]; + ed->line_count = s->line_count; + ed->cursor_row = s->cursor_row; + ed->cursor_col = s->cursor_col; + for (int i = 0; i < ed->line_count; i++) { + memcpy(ed->lines[i], s->lines[i], MAX_LINE_LEN); + } + ed->has_selection = false; + ed->is_modified = true; + editor_update_title(ed); + editor_set_status(ed, "Undo"); +} + static void editor_notify_worker(struct editor *ed) { pthread_mutex_lock(&ed->worker_mutex); @@ -178,7 +273,6 @@ static void *worker_thread(void *arg) ed->worker_dirty = false; pthread_mutex_unlock(&ed->worker_mutex); - /* Debounce / simulate asynchronous LSP analysis */ usleep(30000); int word_count = 0; @@ -214,7 +308,247 @@ static uint64_t on_blink_timer(void *ud, yasl_timer_id id, uint64_t interval_ns) struct editor *ed = ud; ed->caret_visible = !ed->caret_visible; yasl_surface_request_frame(ed->surface); - return interval_ns; /* Repeat every 500ms */ + return interval_ns; +} + +/* File I/O */ +static bool editor_open_file(struct editor *ed, const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) { + char err[128]; + snprintf(err, sizeof(err), "Could not open: %s", path); + editor_set_status(ed, err); + return false; + } + + ed->line_count = 0; + char buf[MAX_LINE_LEN]; + while (fgets(buf, sizeof(buf), f) && ed->line_count < MAX_LINES) { + size_t len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + snprintf(ed->lines[ed->line_count++], MAX_LINE_LEN, "%s", buf); + } + fclose(f); + + if (ed->line_count == 0) { + ed->lines[0][0] = '\0'; + ed->line_count = 1; + } + + snprintf(ed->file_path, sizeof(ed->file_path), "%s", path); + ed->has_file = true; + ed->is_modified = false; + ed->cursor_row = 0; + ed->cursor_col = 0; + ed->scroll_y = 0; + ed->has_selection = false; + ed->undo_count = 0; + + editor_update_title(ed); + char msg[128]; + snprintf(msg, sizeof(msg), "Opened %s (%d lines)", path, ed->line_count); + editor_set_status(ed, msg); + editor_notify_worker(ed); + return true; +} + +static bool editor_save_file(struct editor *ed, const char *path) +{ + FILE *f = fopen(path, "w"); + if (!f) { + char err[128]; + snprintf(err, sizeof(err), "Could not save: %s", path); + editor_set_status(ed, err); + return false; + } + + for (int i = 0; i < ed->line_count; i++) { + fprintf(f, "%s\n", ed->lines[i]); + } + fclose(f); + + snprintf(ed->file_path, sizeof(ed->file_path), "%s", path); + ed->has_file = true; + ed->is_modified = false; + editor_update_title(ed); + + char msg[128]; + snprintf(msg, sizeof(msg), "Saved to %s", path); + editor_set_status(ed, msg); + return true; +} + +/* Decode file:// URL to filesystem path */ +static void decode_uri_path(const char *uri, char *out, size_t max_out) +{ + if (strncmp(uri, "file://", 7) == 0) { + uri += 7; + /* Handle file://localhost/... */ + if (strncmp(uri, "localhost/", 10) == 0) { + uri += 9; + } + } + size_t out_len = 0; + while (*uri && *uri != '\r' && *uri != '\n' && out_len < max_out - 1) { + if (*uri == '%' && isxdigit((unsigned char)uri[1]) && isxdigit((unsigned char)uri[2])) { + char hex[3] = {uri[1], uri[2], '\0'}; + out[out_len++] = (char)strtol(hex, NULL, 16); + uri += 3; + } else { + out[out_len++] = *uri++; + } + } + out[out_len] = '\0'; +} + +/* Selection helper */ +static void get_selection_bounds(const struct editor *ed, int *r0, int *c0, int *r1, int *c1) +{ + if (!ed->has_selection) { + *r0 = *r1 = ed->cursor_row; + *c0 = *c1 = ed->cursor_col; + return; + } + if (ed->sel_anchor_row < ed->cursor_row || + (ed->sel_anchor_row == ed->cursor_row && ed->sel_anchor_col <= ed->cursor_col)) { + *r0 = ed->sel_anchor_row; *c0 = ed->sel_anchor_col; + *r1 = ed->cursor_row; *c1 = ed->cursor_col; + } else { + *r0 = ed->cursor_row; *c0 = ed->cursor_col; + *r1 = ed->sel_anchor_row; *c1 = ed->sel_anchor_col; + } +} + +static bool is_char_selected(const struct editor *ed, int r, int c) +{ + if (!ed->has_selection) return false; + int r0, c0, r1, c1; + get_selection_bounds(ed, &r0, &c0, &r1, &c1); + if (r < r0 || r > r1) return false; + if (r == r0 && r == r1) return c >= c0 && c < c1; + if (r == r0) return c >= c0; + if (r == r1) return c < c1; + return true; +} + +static void editor_delete_selection(struct editor *ed) +{ + if (!ed->has_selection) return; + editor_save_undo(ed); + + int r0, c0, r1, c1; + get_selection_bounds(ed, &r0, &c0, &r1, &c1); + + if (r0 == r1) { + char *line = ed->lines[r0]; + int len = (int)strlen(line); + memmove(line + c0, line + c1, (size_t)(len - c1 + 1)); + } else { + char *first = ed->lines[r0]; + char *last = ed->lines[r1]; + char temp[MAX_LINE_LEN]; + snprintf(temp, sizeof(temp), "%s", last + c1); + snprintf(first + c0, MAX_LINE_LEN - c0, "%s", temp); + + int lines_deleted = r1 - r0; + memmove(ed->lines[r0 + 1], ed->lines[r1 + 1], + (size_t)(ed->line_count - r1 - 1) * MAX_LINE_LEN); + ed->line_count -= lines_deleted; + } + + ed->cursor_row = r0; + ed->cursor_col = c0; + ed->has_selection = false; + ed->is_modified = true; + editor_update_title(ed); + editor_notify_worker(ed); +} + +/* Clipboard operations */ +static void editor_copy(struct editor *ed) +{ + char *buf = malloc(MAX_LINES * MAX_LINE_LEN); + if (!buf) return; + buf[0] = '\0'; + + if (ed->has_selection) { + int r0, c0, r1, c1; + get_selection_bounds(ed, &r0, &c0, &r1, &c1); + if (r0 == r1) { + int len = c1 - c0; + if (len > 0) { + strncpy(buf, ed->lines[r0] + c0, (size_t)len); + buf[len] = '\0'; + } + } else { + strcat(buf, ed->lines[r0] + c0); + strcat(buf, "\n"); + for (int r = r0 + 1; r < r1; r++) { + strcat(buf, ed->lines[r]); + strcat(buf, "\n"); + } + strncat(buf, ed->lines[r1], (size_t)c1); + } + } else { + /* Copy entire current line */ + snprintf(buf, MAX_LINES * MAX_LINE_LEN, "%s\n", ed->lines[ed->cursor_row]); + } + + yasl_display_copy(ed->display, YASL_CLIPBOARD, buf); + free(buf); + editor_set_status(ed, "Copied to clipboard"); +} + +static void editor_cut(struct editor *ed) +{ + editor_copy(ed); + if (ed->has_selection) { + editor_delete_selection(ed); + } else { + editor_save_undo(ed); + if (ed->line_count > 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--; + if (ed->cursor_row >= ed->line_count) ed->cursor_row = ed->line_count - 1; + ed->cursor_col = 0; + ed->is_modified = true; + editor_update_title(ed); + } else { + ed->lines[0][0] = '\0'; + ed->cursor_col = 0; + } + } +} + +/* Search / Find */ +static void editor_find_next(struct editor *ed, const char *query) +{ + if (!query || !*query) return; + int qlen = (int)strlen(query); + + for (int i = 0; i < ed->line_count; i++) { + int r = (ed->cursor_row + i) % ed->line_count; + const char *line = ed->lines[r]; + int start_col = (r == ed->cursor_row) ? ed->cursor_col + 1 : 0; + if (start_col < (int)strlen(line)) { + const char *match = strstr(line + start_col, query); + if (match) { + int c = (int)(match - line); + ed->cursor_row = r; + ed->cursor_col = c + qlen; + ed->sel_anchor_row = r; + ed->sel_anchor_col = c; + ed->has_selection = true; + editor_set_status(ed, "Found match"); + return; + } + } + } + editor_set_status(ed, "Not found"); } /* Drawing primitives */ @@ -235,8 +569,7 @@ static void fill_rect(uint32_t *pixels, int w, int h, int rx, int ry, int rw, in 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 = ' '; + if (c < 32 || c > 126) c = ' '; const uint8_t *glyph = font8x8[c - 32]; for (int y = 0; y < 8; y++) { @@ -258,28 +591,32 @@ static void draw_string(uint32_t *pixels, int w, int h, int px, int py, const ch } } -/* Text editor actions */ +/* Editing operations */ 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; + 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->is_modified = true; ed->caret_visible = true; + editor_update_title(ed); editor_notify_worker(ed); } static void insert_text(struct editor *ed, const char *text, size_t len) { + if (ed->has_selection) { + editor_delete_selection(ed); + } + editor_save_undo(ed); + for (size_t i = 0; i < len; i++) { if (text[i] == '\n') { - /* Split line */ - if (ed->line_count >= MAX_LINES - 1) - break; + 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); @@ -290,23 +627,75 @@ static void insert_text(struct editor *ed, const char *text, size_t len) snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder); ed->cursor_row++; ed->cursor_col = 0; + } else if (text[i] == '\t') { + /* Tab inserts 4 spaces */ + for (int k = 0; k < 4; k++) insert_char_at_cursor(ed, ' '); } else if (text[i] >= 32 && text[i] <= 126) { insert_char_at_cursor(ed, text[i]); } } + ed->is_modified = true; ed->caret_visible = true; + editor_update_title(ed); editor_notify_worker(ed); } +static void handle_tab(struct editor *ed, bool unindent) +{ + editor_save_undo(ed); + if (!unindent) { + /* Indent: insert 4 spaces */ + if (!ed->has_selection) { + insert_text(ed, " ", 4); + } else { + int r0, c0, r1, c1; + get_selection_bounds(ed, &r0, &c0, &r1, &c1); + for (int r = r0; r <= r1; r++) { + char *line = ed->lines[r]; + if (strlen(line) + 4 < MAX_LINE_LEN) { + memmove(line + 4, line, strlen(line) + 1); + memcpy(line, " ", 4); + } + } + ed->cursor_col += 4; + ed->sel_anchor_col += 4; + ed->is_modified = true; + editor_update_title(ed); + } + } else { + /* Unindent: remove up to 4 leading spaces */ + int r0 = ed->cursor_row, r1 = ed->cursor_row; + if (ed->has_selection) { + int c0, c1; + get_selection_bounds(ed, &r0, &c0, &r1, &c1); + } + for (int r = r0; r <= r1; r++) { + char *line = ed->lines[r]; + int spaces = 0; + while (spaces < 4 && line[spaces] == ' ') spaces++; + if (spaces > 0) { + memmove(line, line + spaces, strlen(line) - spaces + 1); + if (r == ed->cursor_row && ed->cursor_col >= spaces) ed->cursor_col -= spaces; + ed->is_modified = true; + editor_update_title(ed); + } + } + } +} + static void handle_backspace(struct editor *ed) { + if (ed->has_selection) { + editor_delete_selection(ed); + return; + } + editor_save_undo(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); @@ -319,18 +708,24 @@ static void handle_backspace(struct editor *ed) ed->cursor_col = prev_len; } } + ed->is_modified = true; ed->caret_visible = true; + editor_update_title(ed); editor_notify_worker(ed); } static void handle_delete(struct editor *ed) { + if (ed->has_selection) { + editor_delete_selection(ed); + return; + } + editor_save_undo(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) { @@ -340,14 +735,20 @@ static void handle_delete(struct editor *ed) ed->line_count--; } } + ed->is_modified = true; ed->caret_visible = true; + editor_update_title(ed); editor_notify_worker(ed); } static void handle_enter(struct editor *ed) { - if (ed->line_count >= MAX_LINES - 1) - return; + if (ed->has_selection) { + editor_delete_selection(ed); + } + editor_save_undo(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); @@ -358,10 +759,31 @@ static void handle_enter(struct editor *ed) snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder); ed->cursor_row++; ed->cursor_col = 0; + ed->is_modified = true; ed->caret_visible = true; + editor_update_title(ed); editor_notify_worker(ed); } +/* Prompt actions */ +static void prompt_execute(struct editor *ed) +{ + switch (ed->prompt) { + case PROMPT_OPEN: + editor_open_file(ed, ed->prompt_buf); + break; + case PROMPT_SAVE: + editor_save_file(ed, ed->prompt_buf); + break; + case PROMPT_FIND: + editor_find_next(ed, ed->prompt_buf); + break; + default: + break; + } + ed->prompt = PROMPT_NONE; +} + /* YASL App callbacks */ static int on_init(void *ud, struct yasl_display *d) { @@ -369,27 +791,26 @@ static int on_init(void *ud, struct yasl_display *d) ed->display = d; struct yasl_surface_spec spec = { - .title = "YASL Notepad (Approach A Example)", - .size = {860, 560}, + .title = "Untitled - YASL Notepad", + .size = {900, 600}, }; ed->surface = yasl_surface_create_toplevel(d, &spec); - if (!ed->surface) { - perror("yasl_surface_create_toplevel"); - return YASL_APP_QUIT; - } + if (!ed->surface) 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 */ + if (ed->has_file) { + editor_open_file(ed, ed->file_path); + } else { + editor_update_title(ed); + } + editor_notify_worker(ed); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; @@ -401,12 +822,123 @@ static int on_event(void *ud, const struct yasl_event *ev) switch (ev->type) { case YASL_EVENT_TEXT: - insert_text(ed, ev->text.text, ev->text.len); + if (ed->prompt != PROMPT_NONE) { + for (size_t i = 0; i < ev->text.len && ed->prompt_len < (int)sizeof(ed->prompt_buf) - 1; i++) { + if (ev->text.text[i] >= 32 && ev->text.text[i] <= 126) { + ed->prompt_buf[ed->prompt_len++] = ev->text.text[i]; + ed->prompt_buf[ed->prompt_len] = '\0'; + } + } + } else { + insert_text(ed, ev->text.text, ev->text.len); + } yasl_surface_request_frame(ed->surface); break; + case YASL_EVENT_PASTE: + if (ev->paste.text && ev->paste.len > 0) { + insert_text(ed, ev->paste.text, ev->paste.len); + yasl_surface_request_frame(ed->surface); + } + break; + + case YASL_EVENT_DROP_FILES: + if (ev->drop_files.files && ev->drop_files.len > 0) { + char path[512]; + decode_uri_path(ev->drop_files.files, path, sizeof(path)); + if (path[0]) { + editor_open_file(ed, path); + yasl_surface_request_frame(ed->surface); + } + } + break; + case YASL_EVENT_KEY: if (ev->key.action == YASL_KEY_PRESS || ev->key.action == YASL_KEY_REPEAT) { + bool ctrl = (ev->key.mods & (YASL_MOD_CTRL | YASL_MOD_SUPER)) != 0; + bool shift = (ev->key.mods & YASL_MOD_SHIFT) != 0; + + /* Interactive Prompt Mode */ + if (ed->prompt != PROMPT_NONE) { + if (ev->key.key == YASL_KEY_ESCAPE) { + ed->prompt = PROMPT_NONE; + } else if (ev->key.key == YASL_KEY_ENTER) { + prompt_execute(ed); + } else if (ev->key.key == YASL_KEY_BACKSPACE && ed->prompt_len > 0) { + ed->prompt_buf[--ed->prompt_len] = '\0'; + } + yasl_surface_request_frame(ed->surface); + break; + } + + /* Shortcuts */ + if (ctrl) { + switch (ev->key.key) { + case YASL_KEY_S: + if (shift || !ed->has_file) { + ed->prompt = PROMPT_SAVE; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + } else { + editor_save_file(ed, ed->file_path); + } + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_O: + ed->prompt = PROMPT_OPEN; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_F: + ed->prompt = PROMPT_FIND; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_A: + ed->sel_anchor_row = 0; + ed->sel_anchor_col = 0; + ed->cursor_row = ed->line_count - 1; + ed->cursor_col = (int)strlen(ed->lines[ed->line_count - 1]); + ed->has_selection = true; + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_C: + editor_copy(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_X: + editor_cut(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_V: + yasl_display_paste(ed->display, YASL_CLIPBOARD, ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_Z: + editor_undo(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + default: + break; + } + } + + /* Tab key */ + if (ev->key.key == YASL_KEY_TAB) { + handle_tab(ed, shift); + yasl_surface_request_frame(ed->surface); + break; + } + + /* Navigation with optional Shift selection */ + bool was_selecting = ed->has_selection; + if (shift && !was_selecting) { + ed->sel_anchor_row = ed->cursor_row; + ed->sel_anchor_col = ed->cursor_col; + ed->has_selection = true; + } + switch (ev->key.key) { case YASL_KEY_BACKSPACE: handle_backspace(ed); @@ -424,7 +956,7 @@ static int on_event(void *ud, const struct yasl_event *ev) ed->cursor_row--; ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]); } - ed->caret_visible = true; + if (!shift) ed->has_selection = false; break; case YASL_KEY_RIGHT: { int len = (int)strlen(ed->lines[ed->cursor_row]); @@ -434,7 +966,7 @@ static int on_event(void *ud, const struct yasl_event *ev) ed->cursor_row++; ed->cursor_col = 0; } - ed->caret_visible = true; + if (!shift) ed->has_selection = false; break; } case YASL_KEY_UP: @@ -443,7 +975,7 @@ static int on_event(void *ud, const struct yasl_event *ev) int len = (int)strlen(ed->lines[ed->cursor_row]); if (ed->cursor_col > len) ed->cursor_col = len; } - ed->caret_visible = true; + if (!shift) ed->has_selection = false; break; case YASL_KEY_DOWN: if (ed->cursor_row < ed->line_count - 1) { @@ -451,36 +983,69 @@ static int on_event(void *ud, const struct yasl_event *ev) int len = (int)strlen(ed->lines[ed->cursor_row]); if (ed->cursor_col > len) ed->cursor_col = len; } - ed->caret_visible = true; + if (!shift) ed->has_selection = false; break; case YASL_KEY_HOME: ed->cursor_col = 0; - ed->caret_visible = true; + if (!shift) ed->has_selection = false; break; case YASL_KEY_END: ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]); - ed->caret_visible = true; + if (!shift) ed->has_selection = false; + break; + case YASL_KEY_ESCAPE: + ed->has_selection = false; break; default: break; } + ed->caret_visible = true; yasl_surface_request_frame(ed->surface); } break; - case YASL_EVENT_MOUSE_BUTTON: + case YASL_EVENT_MOUSE_BUTTON: { + 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 = 24 * 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; + if (ev->mouse_button.pressed && ev->mouse_button.button == 0) { - /* Compute scale-dependent dimensions */ + ed->cursor_row = row; + ed->cursor_col = col; + ed->sel_anchor_row = row; + ed->sel_anchor_col = col; + ed->has_selection = false; + ed->mouse_dragging = true; + ed->caret_visible = true; + yasl_surface_request_frame(ed->surface); + } else if (!ev->mouse_button.pressed && ev->mouse_button.button == 0) { + ed->mouse_dragging = false; + } + break; + } + + case YASL_EVENT_MOUSE_MOTION: + if (ed->mouse_dragging) { 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; + int header_h = 24 * scale; + int row = (ev->mouse_motion.pos.y - header_h) / line_h + ed->scroll_y; + int col = (ev->mouse_motion.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]); @@ -489,18 +1054,17 @@ static int on_event(void *ud, const struct yasl_event *ev) ed->cursor_row = row; ed->cursor_col = col; - ed->caret_visible = true; + ed->has_selection = (ed->cursor_row != ed->sel_anchor_row || ed->cursor_col != ed->sel_anchor_col); yasl_surface_request_frame(ed->surface); } break; - case YASL_EVENT_MOUSE_SCROLL: { + 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) { @@ -528,23 +1092,26 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context int w = ctx->size.x; int h = ctx->size.y; uint32_t *pix = ctx->cpu.pixels; - if (!pix || w <= 0 || h <= 0) - return; + 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 header_h = 24 * scale; int status_h = 24 * scale; - /* Catppuccin-inspired modern dark theme */ + /* Theme colors */ uint32_t bg_main = 0xff1e1e2e; + uint32_t bg_header = 0xff181825; uint32_t bg_gutter = 0xff181825; uint32_t bg_active_line = 0xff252538; + uint32_t bg_selection = 0xff3b4261; uint32_t bg_status = 0xff11111b; + uint32_t bg_prompt = 0xff2a2b3d; uint32_t text_fg = 0xffcdd6f4; uint32_t text_gutter = 0xff6c7086; + uint32_t text_header = 0xff89b4fa; uint32_t text_status = 0xffa6adc8; uint32_t caret_col = 0xfff38ba8; uint32_t status_accent = 0xffa6e3a1; @@ -552,10 +1119,16 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context /* 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); + /* 2. Top Header Menu bar */ + fill_rect(pix, w, h, 0, 0, w, header_h, bg_header); + char header_str[128]; + snprintf(header_str, sizeof(header_str), "^S Save ^O Open ^F Find ^C Copy ^V Paste ^Z Undo Tab 4-Spaces"); + draw_string(pix, w, h, 12 * scale, 6 * scale, header_str, text_header, scale); + + /* 3. Gutter background */ + fill_rect(pix, w, h, 0, header_h, gutter_w, h - header_h - status_h, bg_gutter); - /* 3. Ensure cursor is within visible bounds */ + /* 4. Ensure cursor is in 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; @@ -563,13 +1136,13 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context ed->scroll_y = ed->cursor_row - visible_lines + 1; } - /* 4. Active line highlight */ + /* 5. 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 */ + /* 6. Render lines, gutter, and selection */ for (int i = 0; i < visible_lines; i++) { int line_idx = ed->scroll_y + i; if (line_idx >= ed->line_count) break; @@ -581,34 +1154,56 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context 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 */ + /* Text and selection highlighting */ const char *line_text = ed->lines[line_idx]; - draw_string(pix, w, h, gutter_w + 8 * scale, y, line_text, text_fg, scale); + int text_x = gutter_w + 8 * scale; + int col = 0; + + while (line_text[col]) { + int char_x = text_x + col * char_w; + if (is_char_selected(ed, line_idx, col)) { + fill_rect(pix, w, h, char_x, header_h + i * line_h, char_w, line_h, bg_selection); + } + draw_char(pix, w, h, char_x, y, line_text[col], text_fg, scale); + col++; + } } - /* 6. Render caret */ + /* 7. 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 */ + /* 8. Bottom Status bar or Mini-buffer Prompt */ 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); + if (ed->prompt != PROMPT_NONE) { + fill_rect(pix, w, h, 0, status_y, w, status_h, bg_prompt); + const char *prompt_label = ed->prompt == PROMPT_OPEN ? "Open File: " : + ed->prompt == PROMPT_SAVE ? "Save As: " : "Find: "; + char prompt_disp[300]; + snprintf(prompt_disp, sizeof(prompt_disp), "%s%s_", prompt_label, ed->prompt_buf); + draw_string(pix, w, h, 12 * scale, status_y + 6 * scale, prompt_disp, 0xfffab387, scale); + } else { + fill_rect(pix, w, h, 0, status_y, w, status_h, bg_status); + + char status_left[160]; + if (ed->status_msg[0] && (yasl_loop_now_ns() - ed->status_msg_time < 3000000000ull)) { + snprintf(status_left, sizeof(status_left), "%s", ed->status_msg); + } else { + 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); + char status_right[64]; + snprintf(status_right, sizeof(status_right), "%s [0%% CPU Idle]", ed->is_modified ? "[Modified]" : "[Saved]"); + 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, + ed->is_modified ? 0xfffab387 : status_accent, scale); + } - /* Present full surface */ yasl_frame_present_cpu(ctx, NULL, 0); } @@ -625,41 +1220,43 @@ static void on_quit(void *ud, struct yasl_display *d) pthread_cond_destroy(&ed->worker_cond); } -int main(void) +int main(int argc, char **argv) { 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]); + /* Check command-line file argument */ + if (argc > 1) { + snprintf(ed.file_path, sizeof(ed.file_path), "%s", argv[1]); + ed.has_file = true; + } else { + const char *initial_content[] = { + "// Welcome to YASL Notepad!", + "// A full-featured desktop text editor built on YASL Approach A.", + "", + "#include <stdio.h>", + "", + "int main(int argc, char **argv) {", + " printf(\"Hello, modern YASL!\\n\");", + " return 0;", + "}", + "", + "Shortcuts & Features:", + " - Open File: Ctrl+O (or drag-and-drop file here)", + " - Save / SaveAs: Ctrl+S", + " - Find / Search: Ctrl+F", + " - Copy / Cut: Ctrl+C / Ctrl+X", + " - Paste: Ctrl+V (using native clipboard API)", + " - Undo: Ctrl+Z", + " - Select All: Ctrl+A (or mouse click & drag)", + " - Tab / Shift+Tab: Indent / unindent 4 spaces", + "", + "Try editing or opening a file!" + }; + 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", |
