feat: split desktop implementation into multiple source files, reorganize apps
This commit is contained in:
@@ -0,0 +1,759 @@
|
||||
/*
|
||||
* app_texteditor.cpp
|
||||
* MontaukOS Desktop - Text Editor application
|
||||
* Single-buffer text editor with line numbers, cursor, scrolling, file I/O
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Text Editor state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TE_TOOLBAR_H = 36;
|
||||
static constexpr int TE_PATHBAR_H = 32;
|
||||
static constexpr int TE_STATUS_H = 24;
|
||||
static constexpr int TE_LINE_NUM_W = 48;
|
||||
static constexpr int TE_INIT_CAP = 4096;
|
||||
static constexpr int TE_MAX_CAP = 262144; // 256KB
|
||||
static constexpr int TE_MAX_LINES = 16384;
|
||||
static constexpr int TE_TAB_WIDTH = 4;
|
||||
|
||||
struct TextEditorState {
|
||||
char* buffer;
|
||||
int buf_len;
|
||||
int buf_cap;
|
||||
int* line_offsets;
|
||||
int line_count;
|
||||
int cursor_pos; // byte position in buffer
|
||||
int cursor_line;
|
||||
int cursor_col;
|
||||
int scroll_y; // first visible line
|
||||
int scroll_x; // horizontal scroll in pixels
|
||||
bool modified;
|
||||
char filepath[256];
|
||||
char filename[64];
|
||||
DesktopState* desktop;
|
||||
|
||||
bool show_pathbar;
|
||||
char pathbar_text[256];
|
||||
int pathbar_cursor;
|
||||
int pathbar_len;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Line index management
|
||||
// ============================================================================
|
||||
|
||||
static void te_recompute_lines(TextEditorState* te) {
|
||||
if (!te->line_offsets) {
|
||||
te->line_offsets = (int*)montauk::malloc(TE_MAX_LINES * sizeof(int));
|
||||
}
|
||||
|
||||
te->line_count = 0;
|
||||
te->line_offsets[te->line_count++] = 0;
|
||||
|
||||
for (int i = 0; i < te->buf_len; i++) {
|
||||
if (te->buffer[i] == '\n' && te->line_count < TE_MAX_LINES) {
|
||||
te->line_offsets[te->line_count++] = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void te_update_cursor_pos(TextEditorState* te) {
|
||||
// Find which line the cursor is on
|
||||
te->cursor_line = 0;
|
||||
for (int i = te->line_count - 1; i >= 0; i--) {
|
||||
if (te->cursor_pos >= te->line_offsets[i]) {
|
||||
te->cursor_line = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
te->cursor_col = te->cursor_pos - te->line_offsets[te->cursor_line];
|
||||
}
|
||||
|
||||
static int te_line_length(TextEditorState* te, int line) {
|
||||
if (line < 0 || line >= te->line_count) return 0;
|
||||
int start = te->line_offsets[line];
|
||||
int end;
|
||||
if (line + 1 < te->line_count) {
|
||||
end = te->line_offsets[line + 1] - 1; // exclude newline
|
||||
} else {
|
||||
end = te->buf_len;
|
||||
}
|
||||
return end - start;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Buffer operations
|
||||
// ============================================================================
|
||||
|
||||
static void te_ensure_capacity(TextEditorState* te, int needed) {
|
||||
if (te->buf_len + needed <= te->buf_cap) return;
|
||||
int new_cap = te->buf_cap * 2;
|
||||
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
|
||||
if (new_cap < te->buf_len + needed) new_cap = te->buf_len + needed;
|
||||
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
|
||||
te->buf_cap = new_cap;
|
||||
}
|
||||
|
||||
static void te_insert_char(TextEditorState* te, char c) {
|
||||
if (te->buf_len >= TE_MAX_CAP - 1) return;
|
||||
te_ensure_capacity(te, 1);
|
||||
|
||||
// Shift everything after cursor right
|
||||
for (int i = te->buf_len; i > te->cursor_pos; i--) {
|
||||
te->buffer[i] = te->buffer[i - 1];
|
||||
}
|
||||
te->buffer[te->cursor_pos] = c;
|
||||
te->buf_len++;
|
||||
te->cursor_pos++;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_insert_string(TextEditorState* te, const char* s, int len) {
|
||||
if (te->buf_len + len >= TE_MAX_CAP) return;
|
||||
te_ensure_capacity(te, len);
|
||||
|
||||
for (int i = te->buf_len - 1; i >= te->cursor_pos; i--) {
|
||||
te->buffer[i + len] = te->buffer[i];
|
||||
}
|
||||
for (int i = 0; i < len; i++) {
|
||||
te->buffer[te->cursor_pos + i] = s[i];
|
||||
}
|
||||
te->buf_len += len;
|
||||
te->cursor_pos += len;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_backspace(TextEditorState* te) {
|
||||
if (te->cursor_pos <= 0) return;
|
||||
te->cursor_pos--;
|
||||
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
|
||||
te->buffer[i] = te->buffer[i + 1];
|
||||
}
|
||||
te->buf_len--;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_delete_char(TextEditorState* te) {
|
||||
if (te->cursor_pos >= te->buf_len) return;
|
||||
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
|
||||
te->buffer[i] = te->buffer[i + 1];
|
||||
}
|
||||
te->buf_len--;
|
||||
te->modified = true;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cursor movement
|
||||
// ============================================================================
|
||||
|
||||
static void te_move_up(TextEditorState* te) {
|
||||
if (te->cursor_line <= 0) return;
|
||||
int target_col = te->cursor_col;
|
||||
int prev_line = te->cursor_line - 1;
|
||||
int prev_len = te_line_length(te, prev_line);
|
||||
if (target_col > prev_len) target_col = prev_len;
|
||||
te->cursor_pos = te->line_offsets[prev_line] + target_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_down(TextEditorState* te) {
|
||||
if (te->cursor_line >= te->line_count - 1) return;
|
||||
int target_col = te->cursor_col;
|
||||
int next_line = te->cursor_line + 1;
|
||||
int next_len = te_line_length(te, next_line);
|
||||
if (target_col > next_len) target_col = next_len;
|
||||
te->cursor_pos = te->line_offsets[next_line] + target_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_left(TextEditorState* te) {
|
||||
if (te->cursor_pos > 0) {
|
||||
te->cursor_pos--;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
}
|
||||
|
||||
static void te_move_right(TextEditorState* te) {
|
||||
if (te->cursor_pos < te->buf_len) {
|
||||
te->cursor_pos++;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
}
|
||||
|
||||
static void te_move_home(TextEditorState* te) {
|
||||
te->cursor_pos = te->line_offsets[te->cursor_line];
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_move_end(TextEditorState* te) {
|
||||
te->cursor_pos = te->line_offsets[te->cursor_line] + te_line_length(te, te->cursor_line);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scrolling
|
||||
// ============================================================================
|
||||
|
||||
static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines, int content_w) {
|
||||
if (te->cursor_line < te->scroll_y) {
|
||||
te->scroll_y = te->cursor_line;
|
||||
}
|
||||
if (te->cursor_line >= te->scroll_y + visible_lines) {
|
||||
te->scroll_y = te->cursor_line - visible_lines + 1;
|
||||
}
|
||||
|
||||
// Horizontal scroll
|
||||
int cell_w = mono_cell_width();
|
||||
int cursor_px = te->cursor_col * cell_w;
|
||||
int view_w = content_w - TE_LINE_NUM_W;
|
||||
if (cursor_px - te->scroll_x > view_w - cell_w * 2) {
|
||||
te->scroll_x = cursor_px - view_w + cell_w * 4;
|
||||
}
|
||||
if (cursor_px < te->scroll_x) {
|
||||
te->scroll_x = cursor_px - cell_w * 2;
|
||||
if (te->scroll_x < 0) te->scroll_x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// File I/O
|
||||
// ============================================================================
|
||||
|
||||
static void te_load_file(TextEditorState* te, const char* path) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return;
|
||||
|
||||
uint64_t size = montauk::getsize(fd);
|
||||
if (size > TE_MAX_CAP) size = TE_MAX_CAP;
|
||||
|
||||
if ((int)size >= te->buf_cap) {
|
||||
int new_cap = (int)size + 1024;
|
||||
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
|
||||
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
|
||||
te->buf_cap = new_cap;
|
||||
}
|
||||
|
||||
montauk::read(fd, (uint8_t*)te->buffer, 0, size);
|
||||
montauk::close(fd);
|
||||
|
||||
te->buf_len = (int)size;
|
||||
te->cursor_pos = 0;
|
||||
te->scroll_y = 0;
|
||||
te->scroll_x = 0;
|
||||
te->modified = false;
|
||||
|
||||
montauk::strncpy(te->filepath, path, 255);
|
||||
|
||||
// Extract filename from path
|
||||
int last_slash = -1;
|
||||
for (int i = 0; path[i]; i++) {
|
||||
if (path[i] == '/') last_slash = i;
|
||||
}
|
||||
if (last_slash >= 0) {
|
||||
montauk::strncpy(te->filename, path + last_slash + 1, 63);
|
||||
} else {
|
||||
montauk::strncpy(te->filename, path, 63);
|
||||
}
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
static void te_save_file(TextEditorState* te) {
|
||||
if (te->filepath[0] == '\0') return;
|
||||
|
||||
int fd = montauk::fcreate(te->filepath);
|
||||
if (fd < 0) return;
|
||||
|
||||
montauk::fwrite(fd, (const uint8_t*)te->buffer, 0, te->buf_len);
|
||||
montauk::close(fd);
|
||||
|
||||
te->modified = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_draw(Window* win, Framebuffer& fb) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
int cell_w = mono_cell_width();
|
||||
int cell_h = mono_cell_height();
|
||||
|
||||
// ---- Toolbar (36px) ----
|
||||
c.fill_rect(0, 0, c.w, TE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
|
||||
// Open button
|
||||
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg);
|
||||
if (te->desktop && te->desktop->icon_folder.pixels)
|
||||
c.icon(8, 10, te->desktop->icon_folder);
|
||||
|
||||
// Save button
|
||||
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
|
||||
if (te->desktop && te->desktop->icon_save.pixels)
|
||||
c.icon(36, 10, te->desktop->icon_save);
|
||||
|
||||
// Vertical separator
|
||||
c.vline(60, 4, 28, colors::BORDER);
|
||||
|
||||
// Filename + modified flag
|
||||
char toolbar_label[128];
|
||||
if (te->filename[0]) {
|
||||
snprintf(toolbar_label, 128, "%s%s", te->filename, te->modified ? " [modified]" : "");
|
||||
} else {
|
||||
snprintf(toolbar_label, 128, "Untitled%s", te->modified ? " [modified]" : "");
|
||||
}
|
||||
int sfh = system_font_height();
|
||||
c.text(68, (TE_TOOLBAR_H - sfh) / 2, toolbar_label, colors::TEXT_COLOR);
|
||||
|
||||
// Toolbar bottom separator
|
||||
c.hline(0, TE_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// ---- Path bar (conditional, 32px) ----
|
||||
int editor_y_start = TE_TOOLBAR_H;
|
||||
|
||||
if (te->show_pathbar) {
|
||||
int pb_y = TE_TOOLBAR_H;
|
||||
c.fill_rect(0, pb_y, c.w, TE_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Text input box
|
||||
int inp_x = 8;
|
||||
int inp_y = pb_y + 4;
|
||||
int btn_w = 56;
|
||||
int inp_w = c.w - inp_x - btn_w - 12;
|
||||
int inp_h = 24;
|
||||
|
||||
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
|
||||
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
|
||||
|
||||
// Path text
|
||||
int text_y = inp_y + (inp_h - sfh) / 2;
|
||||
c.text(inp_x + 4, text_y, te->pathbar_text, colors::TEXT_COLOR);
|
||||
|
||||
// Blinking cursor in path input
|
||||
char prefix[256];
|
||||
int plen = te->pathbar_cursor;
|
||||
if (plen > 255) plen = 255;
|
||||
for (int i = 0; i < plen; i++) prefix[i] = te->pathbar_text[i];
|
||||
prefix[plen] = '\0';
|
||||
int cx = inp_x + 4 + text_width(prefix);
|
||||
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
|
||||
|
||||
// Open button
|
||||
int ob_x = inp_x + inp_w + 6;
|
||||
c.button(ob_x, inp_y, btn_w, inp_h, "Open", colors::ACCENT, colors::WHITE, 3);
|
||||
|
||||
// Path bar bottom separator
|
||||
c.hline(0, pb_y + TE_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
editor_y_start = TE_TOOLBAR_H + TE_PATHBAR_H;
|
||||
}
|
||||
|
||||
// ---- Editor area ----
|
||||
int text_area_h = c.h - editor_y_start - TE_STATUS_H;
|
||||
int visible_lines = text_area_h / cell_h;
|
||||
if (visible_lines < 1) visible_lines = 1;
|
||||
|
||||
te_ensure_cursor_visible(te, visible_lines, c.w);
|
||||
|
||||
// Line number gutter background
|
||||
c.fill_rect(0, editor_y_start, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
|
||||
// Gutter separator
|
||||
c.vline(TE_LINE_NUM_W, editor_y_start, text_area_h, colors::BORDER);
|
||||
|
||||
// Draw lines
|
||||
Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99);
|
||||
Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8);
|
||||
Color text_color = colors::TEXT_COLOR;
|
||||
|
||||
int text_start_x = TE_LINE_NUM_W + 4;
|
||||
|
||||
for (int vis = 0; vis < visible_lines + 1; vis++) {
|
||||
int line = te->scroll_y + vis;
|
||||
if (line >= te->line_count) break;
|
||||
|
||||
int py = editor_y_start + vis * cell_h;
|
||||
if (py >= editor_y_start + text_area_h) break;
|
||||
|
||||
// Cursor line highlighting
|
||||
if (line == te->cursor_line) {
|
||||
int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py);
|
||||
if (hl_h > 0)
|
||||
c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color);
|
||||
}
|
||||
|
||||
// Line number
|
||||
char num_str[8];
|
||||
snprintf(num_str, 8, "%4d", line + 1);
|
||||
c.text_mono(4, py, num_str, linenum_color);
|
||||
|
||||
// Line text (per-character rendering with horizontal scroll clipping)
|
||||
int line_start = te->line_offsets[line];
|
||||
int line_len = te_line_length(te, line);
|
||||
|
||||
for (int ci = 0; ci < line_len; ci++) {
|
||||
int px = text_start_x + ci * cell_w - te->scroll_x;
|
||||
if (px + cell_w <= TE_LINE_NUM_W + 1) continue;
|
||||
if (px >= c.w) break;
|
||||
|
||||
char ch = te->buffer[line_start + ci];
|
||||
if (ch >= 32 || ch < 0) {
|
||||
if (fonts::mono && fonts::mono->valid) {
|
||||
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
|
||||
fonts::mono->draw_char_to_buffer(
|
||||
c.pixels, c.w, c.h, px, py + gc->ascent,
|
||||
ch, text_color, gc);
|
||||
} else {
|
||||
// bitmap fallback
|
||||
uint32_t text_px = text_color.to_pixel();
|
||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
||||
for (int fy = 0; fy < FONT_HEIGHT && py + fy < editor_y_start + text_area_h; fy++) {
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = px + fx;
|
||||
int dy = py + fy;
|
||||
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < c.h)
|
||||
c.pixels[dy * c.w + dx] = text_px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if (line == te->cursor_line) {
|
||||
int cx = text_start_x + te->cursor_col * cell_w - te->scroll_x;
|
||||
if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) {
|
||||
int cur_h = gui_min(cell_h, editor_y_start + text_area_h - py);
|
||||
if (cur_h > 0)
|
||||
c.fill_rect(cx, py, 2, cur_h, colors::ACCENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
int status_y = c.h - TE_STATUS_H;
|
||||
c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
||||
|
||||
// Cursor position (right side)
|
||||
char status_right[32];
|
||||
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
|
||||
int sr_w = text_width(status_right);
|
||||
int status_text_y = status_y + (TE_STATUS_H - sfh) / 2;
|
||||
c.text(c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
|
||||
|
||||
// Filename + modified flag (left side)
|
||||
char status_left[128];
|
||||
if (te->filename[0]) {
|
||||
snprintf(status_left, 128, " %s%s", te->filename, te->modified ? " [modified]" : "");
|
||||
} else {
|
||||
snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : "");
|
||||
}
|
||||
c.text(4, status_text_y, status_left, colors::PANEL_TEXT);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mouse handling
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_mouse(Window* win, MouseEvent& ev) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int local_x = ev.x - cr.x;
|
||||
int local_y = ev.y - cr.y;
|
||||
|
||||
int cell_w = mono_cell_width();
|
||||
int cell_h = mono_cell_height();
|
||||
|
||||
int editor_y_start = TE_TOOLBAR_H + (te->show_pathbar ? TE_PATHBAR_H : 0);
|
||||
int text_area_h = cr.h - editor_y_start - TE_STATUS_H;
|
||||
|
||||
// ---- Toolbar clicks ----
|
||||
if (ev.left_pressed() && local_y < TE_TOOLBAR_H) {
|
||||
// Open button
|
||||
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
|
||||
te->show_pathbar = !te->show_pathbar;
|
||||
if (te->show_pathbar) {
|
||||
// Pre-fill with current filepath
|
||||
montauk::strncpy(te->pathbar_text, te->filepath, 255);
|
||||
te->pathbar_len = montauk::slen(te->pathbar_text);
|
||||
te->pathbar_cursor = te->pathbar_len;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Save button
|
||||
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
|
||||
te_save_file(te);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Path bar clicks ----
|
||||
if (te->show_pathbar && local_y >= TE_TOOLBAR_H && local_y < TE_TOOLBAR_H + TE_PATHBAR_H) {
|
||||
if (ev.left_pressed()) {
|
||||
// Check "Open" button region
|
||||
int btn_w = 56;
|
||||
int inp_w = cr.w - 8 - btn_w - 12;
|
||||
int ob_x = 8 + inp_w + 6;
|
||||
if (local_x >= ob_x && local_x < ob_x + btn_w) {
|
||||
if (te->pathbar_text[0]) {
|
||||
te_load_file(te, te->pathbar_text);
|
||||
// Update window title
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", te->filename);
|
||||
montauk::strncpy(win->title, title, 63);
|
||||
te->show_pathbar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Editor area clicks ----
|
||||
if (ev.left_pressed() && local_y >= editor_y_start && local_y < editor_y_start + text_area_h && local_x > TE_LINE_NUM_W) {
|
||||
int clicked_line = te->scroll_y + (local_y - editor_y_start) / cell_h;
|
||||
if (clicked_line >= te->line_count) clicked_line = te->line_count - 1;
|
||||
if (clicked_line < 0) clicked_line = 0;
|
||||
|
||||
int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + cell_w / 2) / cell_w;
|
||||
if (clicked_col < 0) clicked_col = 0;
|
||||
int line_len = te_line_length(te, clicked_line);
|
||||
if (clicked_col > line_len) clicked_col = line_len;
|
||||
|
||||
te->cursor_pos = te->line_offsets[clicked_line] + clicked_col;
|
||||
te_update_cursor_pos(te);
|
||||
}
|
||||
|
||||
// ---- Scroll ----
|
||||
if (ev.scroll != 0 && local_y >= editor_y_start && local_y < editor_y_start + text_area_h) {
|
||||
te->scroll_y -= ev.scroll * 3;
|
||||
if (te->scroll_y < 0) te->scroll_y = 0;
|
||||
int max_scroll = te->line_count - (text_area_h / cell_h) + 1;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (te->scroll_y > max_scroll) te->scroll_y = max_scroll;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard handling
|
||||
// ============================================================================
|
||||
|
||||
static void texteditor_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (!te || !key.pressed) return;
|
||||
|
||||
// ---- Path bar input mode ----
|
||||
if (te->show_pathbar) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (te->pathbar_text[0]) {
|
||||
te_load_file(te, te->pathbar_text);
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", te->filename);
|
||||
montauk::strncpy(win->title, title, 63);
|
||||
te->show_pathbar = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x01) { // Escape
|
||||
te->show_pathbar = false;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (te->pathbar_cursor > 0) {
|
||||
for (int i = te->pathbar_cursor - 1; i < te->pathbar_len - 1; i++)
|
||||
te->pathbar_text[i] = te->pathbar_text[i + 1];
|
||||
te->pathbar_len--;
|
||||
te->pathbar_cursor--;
|
||||
te->pathbar_text[te->pathbar_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4B) { // Left
|
||||
if (te->pathbar_cursor > 0) te->pathbar_cursor--;
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x4D) { // Right
|
||||
if (te->pathbar_cursor < te->pathbar_len) te->pathbar_cursor++;
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 32 && key.ascii < 127 && te->pathbar_len < 254) {
|
||||
for (int i = te->pathbar_len; i > te->pathbar_cursor; i--)
|
||||
te->pathbar_text[i] = te->pathbar_text[i - 1];
|
||||
te->pathbar_text[te->pathbar_cursor] = key.ascii;
|
||||
te->pathbar_cursor++;
|
||||
te->pathbar_len++;
|
||||
te->pathbar_text[te->pathbar_len] = '\0';
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Normal editor mode ----
|
||||
|
||||
// Ctrl+S: save
|
||||
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
|
||||
te_save_file(te);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+O: open
|
||||
if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) {
|
||||
te->show_pathbar = !te->show_pathbar;
|
||||
if (te->show_pathbar) {
|
||||
montauk::strncpy(te->pathbar_text, te->filepath, 255);
|
||||
te->pathbar_len = montauk::slen(te->pathbar_text);
|
||||
te->pathbar_cursor = te->pathbar_len;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
if (key.scancode == 0x48) { te_move_up(te); return; }
|
||||
if (key.scancode == 0x50) { te_move_down(te); return; }
|
||||
if (key.scancode == 0x4B) { te_move_left(te); return; }
|
||||
if (key.scancode == 0x4D) { te_move_right(te); return; }
|
||||
|
||||
// Home
|
||||
if (key.scancode == 0x47) { te_move_home(te); return; }
|
||||
// End
|
||||
if (key.scancode == 0x4F) { te_move_end(te); return; }
|
||||
// Delete
|
||||
if (key.scancode == 0x53) { te_delete_char(te); return; }
|
||||
|
||||
// Backspace
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
te_backspace(te);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
te_insert_char(te, '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab
|
||||
if (key.ascii == '\t') {
|
||||
for (int i = 0; i < TE_TAB_WIDTH; i++) {
|
||||
te_insert_char(te, ' ');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
te_insert_char(te, key.ascii);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void texteditor_on_close(Window* win) {
|
||||
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||
if (te) {
|
||||
if (te->buffer) montauk::mfree(te->buffer);
|
||||
if (te->line_offsets) montauk::mfree(te->line_offsets);
|
||||
montauk::mfree(te);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text Editor launchers
|
||||
// ============================================================================
|
||||
|
||||
void open_texteditor(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Text Editor", 180, 60, 600, 450);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
|
||||
montauk::memset(te, 0, sizeof(TextEditorState));
|
||||
|
||||
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
|
||||
te->buf_cap = TE_INIT_CAP;
|
||||
te->buf_len = 0;
|
||||
te->modified = false;
|
||||
te->desktop = ds;
|
||||
te->show_pathbar = false;
|
||||
te->pathbar_text[0] = '\0';
|
||||
te->pathbar_cursor = 0;
|
||||
te->pathbar_len = 0;
|
||||
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
|
||||
win->app_data = te;
|
||||
win->on_draw = texteditor_on_draw;
|
||||
win->on_mouse = texteditor_on_mouse;
|
||||
win->on_key = texteditor_on_key;
|
||||
win->on_close = texteditor_on_close;
|
||||
}
|
||||
|
||||
void open_texteditor_with_file(DesktopState* ds, const char* path) {
|
||||
// Extract filename for window title
|
||||
const char* name = path;
|
||||
for (int i = 0; path[i]; i++) {
|
||||
if (path[i] == '/') name = path + i + 1;
|
||||
}
|
||||
|
||||
char title[64];
|
||||
snprintf(title, 64, "%s - Editor", name);
|
||||
|
||||
int idx = desktop_create_window(ds, title, 180, 60, 600, 450);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
|
||||
montauk::memset(te, 0, sizeof(TextEditorState));
|
||||
|
||||
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
|
||||
te->buf_cap = TE_INIT_CAP;
|
||||
te->buf_len = 0;
|
||||
te->modified = false;
|
||||
te->desktop = ds;
|
||||
te->show_pathbar = false;
|
||||
te->pathbar_text[0] = '\0';
|
||||
te->pathbar_cursor = 0;
|
||||
te->pathbar_len = 0;
|
||||
|
||||
// Initialize line index for empty document first (ensures line_offsets
|
||||
// is non-null even if te_load_file fails to open the file)
|
||||
te_recompute_lines(te);
|
||||
te_update_cursor_pos(te);
|
||||
|
||||
te_load_file(te, path);
|
||||
|
||||
win->app_data = te;
|
||||
win->on_draw = texteditor_on_draw;
|
||||
win->on_mouse = texteditor_on_mouse;
|
||||
win->on_key = texteditor_on_key;
|
||||
win->on_close = texteditor_on_close;
|
||||
}
|
||||
Reference in New Issue
Block a user