summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 22:57:27 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 22:57:27 +0300
commite389c0fe03a53dd295fee138db7acf1861710370 (patch)
treea922aa05559770bef0b81f0de2f2aab634bfb35d /examples
parent47bd4dbe2e4a576a1f02507e9a87502d99df4f52 (diff)
feat(notepad): add file open/save, tab indent/unindent, clipboard, search, and drag-and-drop
Diffstat (limited to 'examples')
-rw-r--r--examples/notepad.c813
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",