feat: add word processor, spreadsheet, kernel and userspace memory improvements
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user