summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 23:12:24 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 23:12:24 +0300
commit4ab6c6924f08fe64d594d099509924576d0f7ea2 (patch)
tree0c04283e635318033faee78f4c8d4498f4ccb28e
parent36b44f2a14571da4389d982c67c4467bc6dd8d5f (diff)
feat(notepad): add full notepad features: redo, search/replace, goto, date/time, word wrap, zoom, and mouse word/line selection
-rw-r--r--examples/notepad.c579
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++) {