539 lines
19 KiB
C++
539 lines
19 KiB
C++
/*
|
|
* 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);
|
|
}
|