feat: add word processor, spreadsheet, kernel and userspace memory improvements

This commit is contained in:
2026-03-05 19:18:11 +01:00
parent fcb6f8e247
commit ead7d296f7
28 changed files with 4146 additions and 56 deletions
+538
View File
@@ -0,0 +1,538 @@
/*
* main.cpp
* Spreadsheet app — global state, hit testing, entry point
* Copyright (c) 2026 Daniel Hammer
*/
#include "spreadsheet.h"
// ============================================================================
// Global state definitions
// ============================================================================
int g_win_w = INIT_W;
int g_win_h = INIT_H;
Cell g_cells[MAX_ROWS][MAX_COLS];
int g_sel_col = 0;
int g_sel_row = 0;
int g_scroll_x = 0;
int g_scroll_y = 0;
int g_col_widths[MAX_COLS];
bool g_editing = false;
char g_edit_buf[CELL_TEXT_MAX];
int g_edit_len = 0;
int g_edit_cursor = 0;
bool g_has_selection = false;
int g_anchor_col = 0;
int g_anchor_row = 0;
ClipCell g_clipboard[CLIP_MAX_CELLS];
int g_clip_count = 0;
int g_clip_cols = 0;
int g_clip_rows = 0;
char g_filepath[256] = {};
bool g_modified = false;
bool g_pathbar_open = false;
bool g_pathbar_save = false;
char g_pathbar_text[256] = {};
int g_pathbar_len = 0;
int g_pathbar_cursor = 0;
TrueTypeFont* g_font = nullptr;
TrueTypeFont* g_font_bold = nullptr;
bool g_fmt_dropdown_open = false;
UndoEntry* g_undo[UNDO_MAX + 1];
int g_undo_count = 0;
int g_undo_pos = 0;
// ============================================================================
// Hit testing
// ============================================================================
bool hit_cell(int mx, int my, int* out_col, int* out_row) {
int pbh = g_pathbar_open ? PATHBAR_H : 0;
int grid_y = TOOLBAR_H + pbh + FORMULA_BAR_H + COL_HEADER_H;
if (mx < ROW_HEADER_W || my < grid_y) return false;
int content_x = mx + g_scroll_x;
int content_y = my - grid_y + g_scroll_y;
*out_row = content_y / ROW_H;
if (*out_row < 0) *out_row = 0;
if (*out_row >= MAX_ROWS) *out_row = MAX_ROWS - 1;
int x = ROW_HEADER_W;
for (int c = 0; c < MAX_COLS; c++) {
if (content_x >= x - ROW_HEADER_W && content_x < x - ROW_HEADER_W + g_col_widths[c]) {
*out_col = c;
return true;
}
x += g_col_widths[c];
}
*out_col = MAX_COLS - 1;
return true;
}
bool handle_toolbar_click(int mx, int my) {
if (my >= TOOLBAR_H || my < TB_BTN_Y || my >= TB_BTN_Y + TB_BTN_SIZE) return false;
if (g_fmt_dropdown_open && !(mx >= TB_FMT_X0 && mx < TB_FMT_X1)) {
g_fmt_dropdown_open = false;
}
if (mx >= TB_OPEN_X0 && mx < TB_OPEN_X1) {
if (g_editing) commit_edit();
g_pathbar_open = true;
g_pathbar_save = false;
g_pathbar_text[0] = '\0';
g_pathbar_len = 0;
g_pathbar_cursor = 0;
} else if (mx >= TB_SAVE_X0 && mx < TB_SAVE_X1) {
if (g_editing) commit_edit();
if (g_filepath[0]) {
save_file();
} else {
g_pathbar_open = true;
g_pathbar_save = true;
str_cpy(g_pathbar_text, "0:/", 256);
g_pathbar_len = str_len(g_pathbar_text);
g_pathbar_cursor = g_pathbar_len;
}
} else if (mx >= TB_CUT_X0 && mx < TB_CUT_X1) {
if (!g_editing) cut_selection();
} else if (mx >= TB_COPY_X0 && mx < TB_COPY_X1) {
if (!g_editing) copy_selection();
} else if (mx >= TB_PASTE_X0 && mx < TB_PASTE_X1) {
if (!g_editing) paste_at_cursor();
} else if (mx >= TB_BOLD_X0 && mx < TB_BOLD_X1) {
if (!g_editing) apply_bold_toggle();
} else if (mx >= TB_AL_X0 && mx < TB_AL_X1) {
if (!g_editing) apply_align(ALIGN_LEFT);
} else if (mx >= TB_AC_X0 && mx < TB_AC_X1) {
if (!g_editing) apply_align(ALIGN_CENTER);
} else if (mx >= TB_AR_X0 && mx < TB_AR_X1) {
if (!g_editing) apply_align(ALIGN_RIGHT);
} else if (mx >= TB_FMT_X0 && mx < TB_FMT_X1) {
g_fmt_dropdown_open = !g_fmt_dropdown_open;
} else if (mx >= TB_UNDO_X0 && mx < TB_UNDO_X1) {
undo_do();
} else if (mx >= TB_REDO_X0 && mx < TB_REDO_X1) {
redo_do();
} else {
return false;
}
return true;
}
bool handle_fmt_dropdown_click(int mx, int my) {
if (!g_fmt_dropdown_open) return false;
int dx = TB_FMT_X0;
int dy = TOOLBAR_H;
int dw = 80;
int item_h = 26;
int item_count = 4;
int dh = item_count * item_h + 4;
if (mx >= dx && mx < dx + dw && my >= dy && my < dy + dh) {
int idx = (my - dy - 2) / item_h;
if (idx >= 0 && idx < item_count) {
apply_format((NumFormat)idx);
}
g_fmt_dropdown_open = false;
return true;
}
g_fmt_dropdown_open = false;
return true;
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Initialize column widths
for (int i = 0; i < MAX_COLS; i++)
g_col_widths[i] = DEF_COL_W;
// Initialize cells
for (int r = 0; r < MAX_ROWS; r++)
for (int c = 0; c < MAX_COLS; c++) {
g_cells[r][c].input[0] = '\0';
g_cells[r][c].display[0] = '\0';
g_cells[r][c].value = 0;
g_cells[r][c].type = CT_EMPTY;
g_cells[r][c].align = ALIGN_AUTO;
g_cells[r][c].fmt = FMT_AUTO;
g_cells[r][c].bold = false;
}
// Initialize undo pointers
for (int i = 0; i <= UNDO_MAX; i++) g_undo[i] = nullptr;
g_undo_count = 0;
g_undo_pos = 0;
// Load font
auto load_font = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (!f) return nullptr;
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init(path)) { montauk::mfree(f); return nullptr; }
return f;
};
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
g_font_bold = load_font("0:/fonts/Roboto-Bold.ttf");
// Check for file argument
char args[512] = {};
int arglen = montauk::getargs(args, sizeof(args));
if (arglen > 0 && args[0]) {
str_cpy(g_filepath, args, 256);
load_file(g_filepath);
}
// Build window title
char title[64] = "Spreadsheet";
if (g_filepath[0]) {
const char* fname = g_filepath;
for (int i = 0; g_filepath[i]; i++)
if (g_filepath[i] == '/') fname = g_filepath + i + 1;
snprintf(title, 64, "%s - Spreadsheet", fname);
}
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
montauk::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
montauk::win_present(win_id);
while (true) {
Montauk::WinEvent ev;
int r = montauk::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) {
montauk::sleep_ms(16);
continue;
}
// Close
if (ev.type == 3) break;
// Resize
if (ev.type == 2) {
g_win_w = ev.resize.w;
g_win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
clamp_scroll();
render(pixels);
montauk::win_present(win_id);
continue;
}
bool redraw = false;
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
auto& key = ev.key;
// Path bar input (intercepts all keys when open)
if (g_pathbar_open) {
if (key.ascii == '\n' || key.ascii == '\r') {
if (g_pathbar_text[0]) {
if (g_pathbar_save) {
str_cpy(g_filepath, g_pathbar_text, 256);
save_file();
} else {
load_file(g_pathbar_text);
}
}
g_pathbar_open = false;
} else if (key.scancode == 0x01) {
g_pathbar_open = false;
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
if (g_pathbar_cursor > 0) {
for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++)
g_pathbar_text[i] = g_pathbar_text[i + 1];
g_pathbar_len--;
g_pathbar_cursor--;
g_pathbar_text[g_pathbar_len] = '\0';
}
} else if (key.scancode == 0x4B) {
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
} else if (key.scancode == 0x4D) {
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
g_pathbar_text[i] = g_pathbar_text[i - 1];
g_pathbar_text[g_pathbar_cursor] = key.ascii;
g_pathbar_cursor++;
g_pathbar_len++;
g_pathbar_text[g_pathbar_len] = '\0';
}
redraw = true;
goto done_keys;
}
// Escape: cancel edit or quit
if (key.scancode == 0x01) {
if (g_editing) { cancel_edit(); redraw = true; }
else break;
}
// Enter: commit edit and move down
else if (key.ascii == '\n' || key.ascii == '\r') {
if (g_editing) commit_edit();
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
ensure_sel_visible();
redraw = true;
}
// Tab: commit and move right
else if (key.ascii == '\t') {
if (g_editing) commit_edit();
if (key.shift) {
if (g_sel_col > 0) g_sel_col--;
} else {
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
}
ensure_sel_visible();
redraw = true;
}
// Arrow keys (with Shift = extend selection)
else if (key.scancode == 0x48 && !g_editing) { // Up
if (key.shift) {
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
if (g_sel_row > 0) g_sel_row--;
} else {
if (g_sel_row > 0) g_sel_row--;
clear_selection();
}
ensure_sel_visible();
redraw = true;
}
else if (key.scancode == 0x50 && !g_editing) { // Down
if (key.shift) {
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
} else {
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
clear_selection();
}
ensure_sel_visible();
redraw = true;
}
else if (key.scancode == 0x4B && !g_editing) { // Left
if (key.shift) {
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
if (g_sel_col > 0) g_sel_col--;
} else {
if (g_sel_col > 0) g_sel_col--;
clear_selection();
}
ensure_sel_visible();
redraw = true;
}
else if (key.scancode == 0x4D && !g_editing) { // Right
if (key.shift) {
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
} else {
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
clear_selection();
}
ensure_sel_visible();
redraw = true;
}
// Backspace
else if (key.ascii == '\b' || key.scancode == 0x0E) {
if (g_editing) {
if (g_edit_cursor > 0) {
for (int i = g_edit_cursor - 1; i < g_edit_len - 1; i++)
g_edit_buf[i] = g_edit_buf[i + 1];
g_edit_len--;
g_edit_cursor--;
g_edit_buf[g_edit_len] = '\0';
}
redraw = true;
} else {
undo_push();
int c0, r0, c1, r1;
sel_range(&c0, &r0, &c1, &r1);
for (int r = r0; r <= r1; r++)
for (int c = c0; c <= c1; c++)
g_cells[r][c].input[0] = '\0';
eval_all_cells();
g_modified = true;
redraw = true;
}
}
// Delete key
else if (key.scancode == 0x53) {
if (g_editing) {
if (g_edit_cursor < g_edit_len) {
for (int i = g_edit_cursor; i < g_edit_len - 1; i++)
g_edit_buf[i] = g_edit_buf[i + 1];
g_edit_len--;
g_edit_buf[g_edit_len] = '\0';
}
redraw = true;
} else {
undo_push();
int c0, r0, c1, r1;
sel_range(&c0, &r0, &c1, &r1);
for (int r = r0; r <= r1; r++)
for (int c = c0; c <= c1; c++)
g_cells[r][c].input[0] = '\0';
eval_all_cells();
g_modified = true;
redraw = true;
}
}
// Edit mode arrow keys (cursor movement within formula bar)
else if (key.scancode == 0x4B && g_editing) {
if (g_edit_cursor > 0) g_edit_cursor--;
redraw = true;
}
else if (key.scancode == 0x4D && g_editing) {
if (g_edit_cursor < g_edit_len) g_edit_cursor++;
redraw = true;
}
// Ctrl+B: bold toggle
else if (key.ctrl && (key.ascii == 'b' || key.ascii == 'B' || key.ascii == 2) && !g_editing) {
apply_bold_toggle();
redraw = true;
}
// Ctrl+Z: undo
else if (key.ctrl && (key.ascii == 'z' || key.ascii == 'Z' || key.ascii == 26) && !g_editing) {
undo_do();
redraw = true;
}
// Ctrl+Y: redo
else if (key.ctrl && (key.ascii == 'y' || key.ascii == 'Y' || key.ascii == 25) && !g_editing) {
redo_do();
redraw = true;
}
// Ctrl+C: copy
else if (key.ctrl && (key.ascii == 'c' || key.ascii == 'C' || key.ascii == 3) && !g_editing) {
copy_selection();
redraw = true;
}
// Ctrl+X: cut
else if (key.ctrl && (key.ascii == 'x' || key.ascii == 'X' || key.ascii == 24) && !g_editing) {
cut_selection();
redraw = true;
}
// Ctrl+V: paste
else if (key.ctrl && (key.ascii == 'v' || key.ascii == 'V' || key.ascii == 22) && !g_editing) {
paste_at_cursor();
redraw = true;
}
// Ctrl+S: save (or Save As if no path)
else if (key.ctrl && (key.ascii == 's' || key.ascii == 'S' || key.ascii == 19)) {
if (g_editing) commit_edit();
if (g_filepath[0]) {
save_file();
} else {
g_pathbar_open = true;
g_pathbar_save = true;
str_cpy(g_pathbar_text, "0:/", 256);
g_pathbar_len = str_len(g_pathbar_text);
g_pathbar_cursor = g_pathbar_len;
}
redraw = true;
}
// Ctrl+O: open
else if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O' || key.ascii == 15) && !g_editing) {
g_pathbar_open = true;
g_pathbar_save = false;
g_pathbar_text[0] = '\0';
g_pathbar_len = 0;
g_pathbar_cursor = 0;
redraw = true;
}
// Printable character: start editing or insert
else if (key.ascii >= 32 && key.ascii < 127 && !key.ctrl) {
if (!g_editing) {
clear_selection();
g_editing = true;
g_edit_buf[0] = key.ascii;
g_edit_buf[1] = '\0';
g_edit_len = 1;
g_edit_cursor = 1;
} else if (g_edit_len < CELL_TEXT_MAX - 1) {
for (int i = g_edit_len; i > g_edit_cursor; i--)
g_edit_buf[i] = g_edit_buf[i - 1];
g_edit_buf[g_edit_cursor] = key.ascii;
g_edit_len++;
g_edit_cursor++;
g_edit_buf[g_edit_len] = '\0';
}
redraw = true;
}
// F2: edit current cell content (append mode)
else if (key.scancode == 0x3C) {
if (!g_editing) start_editing();
redraw = true;
}
}
done_keys:
// Mouse
if (ev.type == 1) {
int mx = ev.mouse.x;
int my = ev.mouse.y;
uint8_t btns = ev.mouse.buttons;
uint8_t prev = ev.mouse.prev_buttons;
bool clicked = (btns & 1) && !(prev & 1);
if (clicked) {
if (handle_fmt_dropdown_click(mx, my)) {
redraw = true;
}
else if (handle_toolbar_click(mx, my)) {
redraw = true;
}
else {
int col, row;
if (hit_cell(mx, my, &col, &row)) {
if (g_editing) commit_edit();
g_sel_col = col;
g_sel_row = row;
clear_selection();
g_fmt_dropdown_open = false;
redraw = true;
}
}
}
if (ev.mouse.scroll != 0) {
g_scroll_y -= ev.mouse.scroll * SCROLL_STEP;
clamp_scroll();
redraw = true;
}
}
if (redraw) {
render(pixels);
montauk::win_present(win_id);
}
}
montauk::win_destroy(win_id);
montauk::exit(0);
}