diff options
Diffstat (limited to 'examples/notepad.c')
| -rw-r--r-- | examples/notepad.c | 579 |
1 files changed, 468 insertions, 111 deletions
diff --git a/examples/notepad.c b/examples/notepad.c index c06a4b9..2d20039 100644 --- a/examples/notepad.c +++ b/examples/notepad.c @@ -1,17 +1,21 @@ /** * @file notepad.c - * A full-featured, reactive text editor built on YASL Approach A. + * A full-featured desktop text editor built on YASL Approach A. * * 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() + * - New (Ctrl+N), Open (Ctrl+O, CLI arg, drag-and-drop), Save (Ctrl+S), Save As (Ctrl+Shift+S) + * - Tab key with 4-space tab stops; Shift+Tab unindent + * - Clipboard Copy (Ctrl+C), Cut (Ctrl+X), Paste (Ctrl+V) via YASL native clipboard + * - Undo (Ctrl+Z) & Redo (Ctrl+Y / Ctrl+Shift+Z) + * - Find (Ctrl+F), Find Next (F3), Find Prev (Shift+F3), Replace (Ctrl+H), Goto Line (Ctrl+G) + * - Insert Date/Time (F5) + * - Word navigation (Ctrl+Left/Right) & Word deletion (Ctrl+Backspace/Delete) + * - Word Wrap toggle (Ctrl+W) & Horizontal Scrolling + * - Zoom In / Out / Reset (Ctrl+=, Ctrl+-, Ctrl+0) + * - Mouse selection with click, drag, double-click (word), triple-click (line) + * - Unsaved changes confirmation on exit + * - Real-time asynchronous background stats via yasl_post_event() + * - 0% CPU consumption when idle (YASL_PACING_ON_DEMAND) */ #define _DEFAULT_SOURCE @@ -24,6 +28,7 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> #include <unistd.h> #define MAX_LINES 4096 @@ -136,6 +141,10 @@ typedef enum prompt_mode { PROMPT_OPEN, PROMPT_SAVE, PROMPT_FIND, + PROMPT_REPLACE_FIND, + PROMPT_REPLACE_WITH, + PROMPT_GOTO, + PROMPT_CONFIRM_QUIT, } prompt_mode; struct doc_stats { @@ -171,22 +180,34 @@ struct editor { bool has_selection; bool mouse_dragging; - /* Viewport */ + /* Double/Triple click tracking */ + uint64_t last_click_time; + struct yasl_vector2i last_click_pos; + int click_count; + + /* Viewport & Layout */ int scroll_y; + int scroll_x; + bool word_wrap; + int zoom_offset; /* -1, 0, 1, 2 */ bool caret_visible; /* Interactive mini-buffer prompt */ prompt_mode prompt; char prompt_buf[256]; int prompt_len; + char search_query[256]; + char replace_query[256]; /* Notification message in status bar */ char status_msg[128]; uint64_t status_msg_time; - /* Simple Undo stack */ + /* Undo & Redo stacks */ struct undo_snapshot undo_stack[MAX_UNDO]; int undo_count; + struct undo_snapshot redo_stack[MAX_UNDO]; + int redo_count; /* Background stats from async worker */ struct doc_stats stats; @@ -207,7 +228,6 @@ 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; @@ -229,6 +249,7 @@ static void editor_save_undo(struct editor *ed) for (int i = 0; i < ed->line_count; i++) { memcpy(s->lines[i], ed->lines[i], MAX_LINE_LEN); } + ed->redo_count = 0; /* New edit clears redo stack */ } static void editor_undo(struct editor *ed) @@ -237,6 +258,17 @@ static void editor_undo(struct editor *ed) editor_set_status(ed, "Already at oldest change"); return; } + /* Push current state to redo */ + if (ed->redo_count < MAX_UNDO) { + struct undo_snapshot *rs = &ed->redo_stack[ed->redo_count++]; + rs->line_count = ed->line_count; + rs->cursor_row = ed->cursor_row; + rs->cursor_col = ed->cursor_col; + for (int i = 0; i < ed->line_count; i++) { + memcpy(rs->lines[i], ed->lines[i], MAX_LINE_LEN); + } + } + struct undo_snapshot *s = &ed->undo_stack[--ed->undo_count]; ed->line_count = s->line_count; ed->cursor_row = s->cursor_row; @@ -250,6 +282,36 @@ static void editor_undo(struct editor *ed) editor_set_status(ed, "Undo"); } +static void editor_redo(struct editor *ed) +{ + if (ed->redo_count <= 0) { + editor_set_status(ed, "Already at newest change"); + return; + } + /* Push current state to undo */ + if (ed->undo_count < MAX_UNDO) { + struct undo_snapshot *us = &ed->undo_stack[ed->undo_count++]; + us->line_count = ed->line_count; + us->cursor_row = ed->cursor_row; + us->cursor_col = ed->cursor_col; + for (int i = 0; i < ed->line_count; i++) { + memcpy(us->lines[i], ed->lines[i], MAX_LINE_LEN); + } + } + + struct undo_snapshot *s = &ed->redo_stack[--ed->redo_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, "Redo"); +} + static void editor_notify_worker(struct editor *ed) { pthread_mutex_lock(&ed->worker_mutex); @@ -344,8 +406,10 @@ static bool editor_open_file(struct editor *ed, const char *path) ed->cursor_row = 0; ed->cursor_col = 0; ed->scroll_y = 0; + ed->scroll_x = 0; ed->has_selection = false; ed->undo_count = 0; + ed->redo_count = 0; editor_update_title(ed); char msg[128]; @@ -381,12 +445,29 @@ static bool editor_save_file(struct editor *ed, const char *path) return true; } -/* Decode file:// URL to filesystem path */ +static void editor_new_file(struct editor *ed) +{ + ed->lines[0][0] = '\0'; + ed->line_count = 1; + ed->has_file = false; + ed->file_path[0] = '\0'; + ed->is_modified = false; + ed->cursor_row = 0; + ed->cursor_col = 0; + ed->scroll_y = 0; + ed->scroll_x = 0; + ed->has_selection = false; + ed->undo_count = 0; + ed->redo_count = 0; + editor_update_title(ed); + editor_set_status(ed, "New file created"); + editor_notify_worker(ed); +} + 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; } @@ -470,7 +551,7 @@ static void editor_delete_selection(struct editor *ed) /* Clipboard operations */ static void editor_copy(struct editor *ed) { - char *buf = malloc(MAX_LINES * MAX_LINE_LEN); + char *buf = malloc((size_t)MAX_LINES * MAX_LINE_LEN); if (!buf) return; buf[0] = '\0'; @@ -493,8 +574,7 @@ static void editor_copy(struct editor *ed) 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]); + snprintf(buf, (size_t)MAX_LINES * MAX_LINE_LEN, "%s\n", ed->lines[ed->cursor_row]); } yasl_display_copy(ed->display, YASL_CLIPBOARD, buf); @@ -524,33 +604,89 @@ static void editor_cut(struct editor *ed) } } -/* Search / Find */ -static void editor_find_next(struct editor *ed, const char *query) +/* Search & Replace */ +static void editor_find_next(struct editor *ed, const char *query, bool forward) { if (!query || !*query) return; int qlen = (int)strlen(query); + snprintf(ed->search_query, sizeof(ed->search_query), "%s", query); - for (int i = 0; i < ed->line_count; i++) { - int r = (ed->cursor_row + i) % ed->line_count; + int start_r = ed->cursor_row; + int dir = forward ? 1 : -1; + + for (int step = 0; step < ed->line_count; step++) { + int r = (start_r + step * dir + ed->line_count) % 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; + int len = (int)strlen(line); + + if (forward) { + int start_c = (r == ed->cursor_row && step == 0) ? ed->cursor_col : 0; + if (start_c < len) { + const char *match = strstr(line + start_c, 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; + } + } + } else { + /* Backward search on line */ + int limit = (r == ed->cursor_row && step == 0) ? ed->cursor_col - qlen : len - qlen; + for (int c = limit; c >= 0; c--) { + if (strncmp(line + c, query, (size_t)qlen) == 0) { + 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"); } +static void editor_replace_current(struct editor *ed, const char *with) +{ + if (!ed->has_selection) return; + editor_delete_selection(ed); + size_t wlen = strlen(with); + for (size_t i = 0; i < wlen; i++) { + char *line = ed->lines[ed->cursor_row]; + int len = (int)strlen(line); + if (len < MAX_LINE_LEN - 2) { + memmove(line + ed->cursor_col + 1, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1)); + line[ed->cursor_col++] = with[i]; + } + } + ed->is_modified = true; + editor_update_title(ed); + editor_set_status(ed, "Replaced match"); +} + +/* Word navigation */ +static int find_word_prev(const char *line, int col) +{ + while (col > 0 && isspace((unsigned char)line[col - 1])) col--; + while (col > 0 && !isspace((unsigned char)line[col - 1])) col--; + return col; +} + +static int find_word_next(const char *line, int col) +{ + int len = (int)strlen(line); + while (col < len && !isspace((unsigned char)line[col])) col++; + while (col < len && isspace((unsigned char)line[col])) col++; + return col; +} + /* Drawing primitives */ static void fill_rect(uint32_t *pixels, int w, int h, int rx, int ry, int rw, int rh, uint32_t col) { @@ -628,8 +764,10 @@ static void insert_text(struct editor *ed, const char *text, size_t len) 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, ' '); + /* Tab stop: align to next multiple of 4 */ + int spaces = 4 - (ed->cursor_col % 4); + if (spaces == 0) spaces = 4; + for (int k = 0; k < spaces; k++) insert_char_at_cursor(ed, ' '); } else if (text[i] >= 32 && text[i] <= 126) { insert_char_at_cursor(ed, text[i]); } @@ -644,9 +782,10 @@ 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); + int spaces = 4 - (ed->cursor_col % 4); + if (spaces == 0) spaces = 4; + for (int k = 0; k < spaces; k++) insert_char_at_cursor(ed, ' '); } else { int r0, c0, r1, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); @@ -663,7 +802,6 @@ static void handle_tab(struct editor *ed, bool unindent) 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; @@ -683,7 +821,7 @@ static void handle_tab(struct editor *ed, bool unindent) } } -static void handle_backspace(struct editor *ed) +static void handle_backspace(struct editor *ed, bool ctrl) { if (ed->has_selection) { editor_delete_selection(ed); @@ -691,21 +829,32 @@ static void handle_backspace(struct editor *ed) } 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) { - 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; + if (ctrl) { + /* Delete previous word */ + int prev_c = find_word_prev(line, ed->cursor_col); + int deleted = ed->cursor_col - prev_c; + if (deleted > 0) { + int len = (int)strlen(line); + memmove(line + prev_c, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1)); + ed->cursor_col = prev_c; + } + } else { + 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) { + 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->is_modified = true; @@ -714,7 +863,7 @@ static void handle_backspace(struct editor *ed) editor_notify_worker(ed); } -static void handle_delete(struct editor *ed) +static void handle_delete(struct editor *ed, bool ctrl) { if (ed->has_selection) { editor_delete_selection(ed); @@ -723,16 +872,25 @@ static void handle_delete(struct editor *ed) 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) { - 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--; + if (ctrl) { + /* Delete next word */ + int next_c = find_word_next(line, ed->cursor_col); + int deleted = next_c - ed->cursor_col; + if (deleted > 0) { + memmove(line + ed->cursor_col, line + next_c, (size_t)(len - next_c + 1)); + } + } else { + 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) { + 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->is_modified = true; @@ -765,23 +923,67 @@ static void handle_enter(struct editor *ed) editor_notify_worker(ed); } +static void insert_date_time(struct editor *ed) +{ + time_t now = time(NULL); + struct tm *tm = localtime(&now); + char buf[64]; + strftime(buf, sizeof(buf), "%I:%M %p %m/%d/%Y", tm); + insert_text(ed, buf, strlen(buf)); +} + /* Prompt actions */ static void prompt_execute(struct editor *ed) { switch (ed->prompt) { case PROMPT_OPEN: editor_open_file(ed, ed->prompt_buf); + ed->prompt = PROMPT_NONE; break; case PROMPT_SAVE: editor_save_file(ed, ed->prompt_buf); + ed->prompt = PROMPT_NONE; break; case PROMPT_FIND: - editor_find_next(ed, ed->prompt_buf); + editor_find_next(ed, ed->prompt_buf, true); + ed->prompt = PROMPT_NONE; + break; + case PROMPT_REPLACE_FIND: + snprintf(ed->search_query, sizeof(ed->search_query), "%s", ed->prompt_buf); + ed->prompt = PROMPT_REPLACE_WITH; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + return; + case PROMPT_REPLACE_WITH: + editor_find_next(ed, ed->search_query, true); + editor_replace_current(ed, ed->prompt_buf); + ed->prompt = PROMPT_NONE; + break; + case PROMPT_GOTO: { + int target = atoi(ed->prompt_buf); + if (target > 0) { + if (target > ed->line_count) target = ed->line_count; + ed->cursor_row = target - 1; + ed->cursor_col = 0; + char msg[64]; + snprintf(msg, sizeof(msg), "Jumped to line %d", target); + editor_set_status(ed, msg); + } + ed->prompt = PROMPT_NONE; + break; + } + case PROMPT_CONFIRM_QUIT: + if (tolower((unsigned char)ed->prompt_buf[0]) == 'y') { + if (ed->has_file) { + editor_save_file(ed, ed->file_path); + } + } + ed->prompt = PROMPT_NONE; break; default: + ed->prompt = PROMPT_NONE; break; } - ed->prompt = PROMPT_NONE; } /* YASL App callbacks */ @@ -792,7 +994,7 @@ static int on_init(void *ud, struct yasl_display *d) struct yasl_surface_spec spec = { .title = "Untitled - YASL Notepad", - .size = {900, 600}, + .size = {960, 640}, }; ed->surface = yasl_surface_create_toplevel(d, &spec); if (!ed->surface) return YASL_APP_QUIT; @@ -871,9 +1073,24 @@ static int on_event(void *ud, const struct yasl_event *ev) break; } + /* Function Keys */ + if (ev->key.key == YASL_KEY_F3) { + editor_find_next(ed, ed->search_query, !shift); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + } else if (ev->key.key == YASL_KEY_F5) { + insert_date_time(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + } + /* Shortcuts */ if (ctrl) { switch (ev->key.key) { + case YASL_KEY_N: + editor_new_file(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; case YASL_KEY_S: if (shift || !ed->has_file) { ed->prompt = PROMPT_SAVE; @@ -896,6 +1113,38 @@ static int on_event(void *ud, const struct yasl_event *ev) ed->prompt_buf[0] = '\0'; yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; + case YASL_KEY_H: + ed->prompt = PROMPT_REPLACE_FIND; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_G: + ed->prompt = PROMPT_GOTO; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_W: + ed->word_wrap = !ed->word_wrap; + editor_set_status(ed, ed->word_wrap ? "Word Wrap: ON" : "Word Wrap: OFF"); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_EQUAL: /* Zoom In */ + if (ed->zoom_offset < 2) ed->zoom_offset++; + editor_set_status(ed, "Zoom In"); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_MINUS: /* Zoom Out */ + if (ed->zoom_offset > -1) ed->zoom_offset--; + editor_set_status(ed, "Zoom Out"); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_0: /* Zoom Reset */ + ed->zoom_offset = 0; + editor_set_status(ed, "Zoom Reset"); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; case YASL_KEY_A: ed->sel_anchor_row = 0; ed->sel_anchor_col = 0; @@ -916,7 +1165,20 @@ static int on_event(void *ud, const struct yasl_event *ev) yasl_display_paste(ed->display, YASL_CLIPBOARD, ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_Z: - editor_undo(ed); + if (shift) editor_redo(ed); + else editor_undo(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_Y: + editor_redo(ed); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_BACKSPACE: + handle_backspace(ed, true); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + case YASL_KEY_DELETE: + handle_delete(ed, true); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; default: @@ -941,16 +1203,18 @@ static int on_event(void *ud, const struct yasl_event *ev) switch (ev->key.key) { case YASL_KEY_BACKSPACE: - handle_backspace(ed); + handle_backspace(ed, false); break; case YASL_KEY_DELETE: - handle_delete(ed); + handle_delete(ed, false); break; case YASL_KEY_ENTER: handle_enter(ed); break; case YASL_KEY_LEFT: - if (ed->cursor_col > 0) { + if (ctrl) { + ed->cursor_col = find_word_prev(ed->lines[ed->cursor_row], ed->cursor_col); + } else if (ed->cursor_col > 0) { ed->cursor_col--; } else if (ed->cursor_row > 0) { ed->cursor_row--; @@ -960,7 +1224,9 @@ static int on_event(void *ud, const struct yasl_event *ev) break; case YASL_KEY_RIGHT: { int len = (int)strlen(ed->lines[ed->cursor_row]); - if (ed->cursor_col < len) { + if (ctrl) { + ed->cursor_col = find_word_next(ed->lines[ed->cursor_row], ed->cursor_col); + } else if (ed->cursor_col < len) { ed->cursor_col++; } else if (ed->cursor_row < ed->line_count - 1) { ed->cursor_row++; @@ -993,6 +1259,18 @@ static int on_event(void *ud, const struct yasl_event *ev) ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]); if (!shift) ed->has_selection = false; break; + case YASL_KEY_PAGE_UP: + ed->scroll_y -= 15; + if (ed->scroll_y < 0) ed->scroll_y = 0; + ed->cursor_row = ed->scroll_y; + if (!shift) ed->has_selection = false; + break; + case YASL_KEY_PAGE_DOWN: + ed->scroll_y += 15; + if (ed->scroll_y >= ed->line_count) ed->scroll_y = ed->line_count - 1; + ed->cursor_row = ed->scroll_y; + if (!shift) ed->has_selection = false; + break; case YASL_KEY_ESCAPE: ed->has_selection = false; break; @@ -1006,14 +1284,15 @@ static int on_event(void *ud, const struct yasl_event *ev) case YASL_EVENT_MOUSE_BUTTON: { struct yasl_geometry geom = yasl_surface_geometry(ed->surface); - int scale = geom.scale_120 >= 240 ? 2 : 1; + int scale = (geom.scale_120 >= 240 ? 2 : 1) + ed->zoom_offset; + if (scale < 1) scale = 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; + int col = (ev->mouse_button.pos.x - gutter_w) / char_w + (ed->word_wrap ? 0 : ed->scroll_x); if (row < 0) row = 0; if (row >= ed->line_count) row = ed->line_count - 1; int len = (int)strlen(ed->lines[row]); @@ -1021,12 +1300,39 @@ static int on_event(void *ud, const struct yasl_event *ev) if (col > len) col = len; if (ev->mouse_button.pressed && ev->mouse_button.button == 0) { - 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; + uint64_t now = yasl_loop_now_ns(); + if (now - ed->last_click_time < 350000000ull && + abs(ev->mouse_button.pos.x - ed->last_click_pos.x) < 8 && + abs(ev->mouse_button.pos.y - ed->last_click_pos.y) < 8) { + ed->click_count++; + } else { + ed->click_count = 1; + } + ed->last_click_time = now; + ed->last_click_pos = ev->mouse_button.pos; + + if (ed->click_count == 2) { + /* Double click: select word */ + const char *line = ed->lines[row]; + int c0 = col, c1 = col; + while (c0 > 0 && !isspace((unsigned char)line[c0 - 1])) c0--; + while (line[c1] && !isspace((unsigned char)line[c1])) c1++; + ed->sel_anchor_row = row; ed->sel_anchor_col = c0; + ed->cursor_row = row; ed->cursor_col = c1; + ed->has_selection = true; + } else if (ed->click_count >= 3) { + /* Triple click: select whole line */ + ed->sel_anchor_row = row; ed->sel_anchor_col = 0; + ed->cursor_row = row; ed->cursor_col = (int)strlen(ed->lines[row]); + ed->has_selection = true; + } else { + 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) { @@ -1038,14 +1344,15 @@ static int on_event(void *ud, const struct yasl_event *ev) 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 scale = (geom.scale_120 >= 240 ? 2 : 1) + ed->zoom_offset; + if (scale < 1) scale = 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_motion.pos.y - header_h) / line_h + ed->scroll_y; - int col = (ev->mouse_motion.pos.x - gutter_w) / char_w; + int col = (ev->mouse_motion.pos.x - gutter_w) / char_w + (ed->word_wrap ? 0 : ed->scroll_x); if (row < 0) row = 0; if (row >= ed->line_count) row = ed->line_count - 1; int len = (int)strlen(ed->lines[row]); @@ -1060,9 +1367,20 @@ static int on_event(void *ud, const struct yasl_event *ev) 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; + if (ev->mouse_scroll.mods & (YASL_MOD_CTRL | YASL_MOD_SUPER)) { + /* Ctrl + Wheel = Zoom */ + if (ev->mouse_scroll.scroll.y > 0 && ed->zoom_offset < 2) ed->zoom_offset++; + else if (ev->mouse_scroll.scroll.y < 0 && ed->zoom_offset > -1) ed->zoom_offset--; + } else if (ev->mouse_scroll.mods & YASL_MOD_SHIFT) { + /* Shift + Wheel = Horizontal scroll */ + ed->scroll_x -= (int)(ev->mouse_scroll.scroll.y * 3.0); + if (ed->scroll_x < 0) ed->scroll_x = 0; + } else { + /* Normal vertical 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; @@ -1076,6 +1394,14 @@ static int on_event(void *ud, const struct yasl_event *ev) break; case YASL_EVENT_WINDOW_CLOSE_REQUEST: + if (ed->is_modified && ed->prompt != PROMPT_CONFIRM_QUIT) { + ed->prompt = PROMPT_CONFIRM_QUIT; + ed->prompt_len = 0; + ed->prompt_buf[0] = '\0'; + editor_set_status(ed, "Save changes before exit? (y/n/esc)"); + yasl_surface_request_frame(ed->surface); + return YASL_APP_CONTINUE; + } return YASL_APP_QUIT; default: @@ -1094,7 +1420,8 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context uint32_t *pix = ctx->cpu.pixels; if (!pix || w <= 0 || h <= 0) return; - int scale = ctx->scale_120 >= 240 ? 2 : 1; + int scale = (ctx->scale_120 >= 240 ? 2 : 1) + ed->zoom_offset; + if (scale < 1) scale = 1; int char_w = 8 * scale; int line_h = 16 * scale; int gutter_w = 48 * scale; @@ -1121,14 +1448,15 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context /* 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"); + char header_str[256]; + snprintf(header_str, sizeof(header_str), + "^N New ^O Open ^S Save ^F Find ^H Repl ^G Goto ^Z Undo F5 Time ^W Wrap"); 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); - /* 4. Ensure cursor is in visible bounds */ + /* 4. Ensure cursor is in visible vertical and horizontal bounds */ int visible_lines = (h - header_h - status_h) / line_h; if (ed->cursor_row < ed->scroll_y) { ed->scroll_y = ed->cursor_row; @@ -1136,6 +1464,18 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context ed->scroll_y = ed->cursor_row - visible_lines + 1; } + int visible_cols = (w - gutter_w - 16 * scale) / char_w; + if (visible_cols < 1) visible_cols = 1; + if (!ed->word_wrap) { + if (ed->cursor_col < ed->scroll_x) { + ed->scroll_x = ed->cursor_col; + } else if (ed->cursor_col >= ed->scroll_x + visible_cols) { + ed->scroll_x = ed->cursor_col - visible_cols + 1; + } + } else { + ed->scroll_x = 0; + } + /* 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) { @@ -1157,10 +1497,10 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context /* Text and selection highlighting */ const char *line_text = ed->lines[line_idx]; int text_x = gutter_w + 8 * scale; - int col = 0; + int col = ed->scroll_x; - while (line_text[col]) { - int char_x = text_x + col * char_w; + while (line_text[col] && col - ed->scroll_x < visible_cols + 1) { + int char_x = text_x + (col - ed->scroll_x) * 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); } @@ -1171,9 +1511,11 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context /* 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); + int caret_x = gutter_w + 8 * scale + (ed->cursor_col - ed->scroll_x) * char_w; + if (caret_x >= gutter_w && caret_x < w) { + int caret_y = active_line_y + 3 * scale; + fill_rect(pix, w, h, caret_x, caret_y, 2 * scale, 10 * scale, caret_col); + } } /* 8. Bottom Status bar or Mini-buffer Prompt */ @@ -1181,8 +1523,13 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context 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]; + ed->prompt == PROMPT_SAVE ? "Save As: " : + ed->prompt == PROMPT_FIND ? "Find: " : + ed->prompt == PROMPT_REPLACE_FIND ? "Replace (Find): " : + ed->prompt == PROMPT_REPLACE_WITH ? "Replace with: " : + ed->prompt == PROMPT_GOTO ? "Go to Line: " : + "Save changes before exit? (y/n/esc): "; + char prompt_disp[350]; 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 { @@ -1197,8 +1544,10 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context } 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), "%s [0%% CPU Idle]", ed->is_modified ? "[Modified]" : "[Saved]"); + char status_right[96]; + snprintf(status_right, sizeof(status_right), "%s [Wrap:%s] [0%% CPU]", + ed->is_modified ? "[Modified]" : "[Saved]", + ed->word_wrap ? "ON" : "OFF"); 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); @@ -1240,17 +1589,25 @@ int main(int argc, char **argv) " 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", + "Comprehensive Features & Shortcuts:", + " - New Document: Ctrl+N", + " - Open File: Ctrl+O (or drag-and-drop file onto window)", + " - Save / Save As: Ctrl+S / Ctrl+Shift+S", + " - Find / Find Next: Ctrl+F / F3 (Shift+F3 for prev)", + " - Replace: Ctrl+H", + " - Go to Line: Ctrl+G", + " - Insert Date/Time: F5", + " - Word Wrap Toggle: Ctrl+W", + " - Zoom In/Out/Reset: Ctrl+= / Ctrl+- / Ctrl+0 (or Ctrl+Wheel)", + " - Copy / Cut / Paste: Ctrl+C / Ctrl+X / Ctrl+V", + " - Undo / Redo: Ctrl+Z / Ctrl+Y (Ctrl+Shift+Z)", + " - Select All: Ctrl+A", + " - Mouse Selection: Click+Drag, Double-click (word), Triple-click (line)", + " - Word Navigation: Ctrl+Left / Ctrl+Right", + " - Word Deletion: Ctrl+Backspace / Ctrl+Delete", + " - Tab / Shift+Tab: Align 4-space tab stops / unindent", "", - "Try editing or opening a file!" + "Try editing, typing, or opening files!" }; ed.line_count = sizeof(initial_content) / sizeof(initial_content[0]); for (int i = 0; i < ed.line_count; i++) { |
