/* * main.cpp * Spreadsheet app — global state, hit testing, entry point * Copyright (c) 2026 Daniel Hammer */ #include "spreadsheet.h" #include // ============================================================================ // 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; bool g_col_resizing = false; int g_col_resize_idx = -1; int g_col_resize_start_x = 0; int g_col_resize_start_w = 0; UndoEntry* g_undo[UNDO_MAX + 1]; int g_undo_count = 0; int g_undo_pos = 0; // Formula autocomplete bool g_ac_open = false; int g_ac_sel = 0; int g_ac_count = 0; char g_ac_matches[AC_MAX_MATCHES][16]; char g_ac_hints[AC_MAX_MATCHES][32]; // Fill handle bool g_fill_dragging = false; int g_fill_target_row = 0; // Mouse drag selection bool g_mouse_selecting = false; // Double-click detection uint64_t g_last_click_ms = 0; int g_last_click_col = -1; int g_last_click_row = -1; // ============================================================================ // 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 hit_fill_handle(int mx, int my) { if (g_editing) return false; int c0, r0, c1, r1; sel_range(&c0, &r0, &c1, &r1); int pbh = g_pathbar_open ? PATHBAR_H : 0; int grid_y = TOOLBAR_H + pbh + FORMULA_BAR_H + COL_HEADER_H; int sx = col_x(c0) - g_scroll_x; int sw = 0; for (int c = c0; c <= c1; c++) sw += g_col_widths[c]; int sy = grid_y + r0 * ROW_H - g_scroll_y; int sh = (r1 - r0 + 1) * ROW_H; int fhx = sx + sw - FILL_HANDLE_SIZE / 2; int fhy = sy + sh - FILL_HANDLE_SIZE / 2; return mx >= fhx - 3 && mx <= fhx + FILL_HANDLE_SIZE + 3 && my >= fhy - 3 && my <= fhy + FILL_HANDLE_SIZE + 3; } 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]) { load_file(args); } // 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); } WsWindow win; if (!win.create(title, INIT_W, INIT_H)) montauk::exit(1); uint32_t* pixels = win.pixels; render(pixels); win.present(); while (true) { Montauk::WinEvent ev; int r = win.poll(&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 = win.width; g_win_h = win.height; pixels = win.pixels; clamp_scroll(); render(pixels); win.present(); 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; } // Autocomplete interaction (when editing with autocomplete open) if (g_ac_open && g_editing) { if (key.scancode == 0x48) { // Up g_ac_sel = (g_ac_sel - 1 + g_ac_count) % g_ac_count; redraw = true; goto done_keys; } if (key.scancode == 0x50) { // Down g_ac_sel = (g_ac_sel + 1) % g_ac_count; redraw = true; goto done_keys; } if (key.ascii == '\t') { // Tab: accept autocomplete accept_autocomplete(); update_autocomplete(); redraw = true; goto done_keys; } if (key.ascii == '\n' || key.ascii == '\r') { // Enter: accept autocomplete then commit accept_autocomplete(); g_ac_open = false; commit_edit(); if (g_sel_row < MAX_ROWS - 1) g_sel_row++; ensure_sel_visible(); redraw = true; goto done_keys; } if (key.scancode == 0x01) { // Escape: close autocomplete only g_ac_open = false; redraw = true; goto done_keys; } } // Escape: cancel edit or quit if (key.scancode == 0x01) { if (g_editing) { cancel_edit(); g_ac_open = false; redraw = true; } else break; } // Enter: commit edit and move down else if (key.ascii == '\n' || key.ascii == '\r') { if (g_editing) { commit_edit(); g_ac_open = false; } if (g_sel_row < MAX_ROWS - 1) g_sel_row++; ensure_sel_visible(); redraw = true; } // Tab: commit and move right (or accept autocomplete above) else if (key.ascii == '\t') { if (g_editing) { commit_edit(); g_ac_open = false; } 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'; update_autocomplete(); } 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'; update_autocomplete(); } 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--; update_autocomplete(); redraw = true; } else if (key.scancode == 0x4D && g_editing) { if (g_edit_cursor < g_edit_len) g_edit_cursor++; update_autocomplete(); redraw = true; } // Home key else if (key.scancode == 0x47) { if (g_editing) { g_edit_cursor = 0; update_autocomplete(); } else { g_sel_col = 0; clear_selection(); ensure_sel_visible(); } redraw = true; } // End key else if (key.scancode == 0x4F) { if (g_editing) { g_edit_cursor = g_edit_len; update_autocomplete(); } else { // Jump to last non-empty column in current row int last = 0; for (int c = 0; c < MAX_COLS; c++) if (g_cells[g_sel_row][c].input[0]) last = c; g_sel_col = last; clear_selection(); ensure_sel_visible(); } 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'; } update_autocomplete(); 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); bool left_held = (btns & 1); bool left_released = !(btns & 1) && (prev & 1); int pbh = g_pathbar_open ? PATHBAR_H : 0; int header_y = TOOLBAR_H + pbh + FORMULA_BAR_H; // Update cursor style { int cursor = 0; // arrow if (g_col_resizing) { cursor = 1; // resize_h while dragging } else if (g_fill_dragging) { cursor = 2; // crosshair during fill } else if (hit_fill_handle(mx, my)) { cursor = 2; // crosshair on fill handle } else if (my >= header_y && my < header_y + COL_HEADER_H) { for (int c = 0; c < MAX_COLS; c++) { int cx = col_x(c) - g_scroll_x + g_col_widths[c] - 1; if (mx >= cx - COL_RESIZE_GRAB && mx <= cx + COL_RESIZE_GRAB) { cursor = 1; // resize_h break; } } } win.set_cursor(cursor); } // Fill handle: drag in progress if (g_fill_dragging) { if (left_held) { int col, row; if (hit_cell(mx, my, &col, &row)) { int c0, r0, c1, r1; sel_range(&c0, &r0, &c1, &r1); if (row > r1 && row != g_fill_target_row) { g_fill_target_row = row; redraw = true; } } } if (left_released) { int c0, r0, c1, r1; sel_range(&c0, &r0, &c1, &r1); if (g_fill_target_row > r1) { fill_down(r0, c0, r1, c1, g_fill_target_row); g_sel_row = g_fill_target_row; g_has_selection = true; } g_fill_dragging = false; redraw = true; } goto done_mouse; } // Column resize: drag in progress if (g_col_resizing) { if (left_held) { int delta = mx - g_col_resize_start_x; int new_w = g_col_resize_start_w + delta; if (new_w < MIN_COL_W) new_w = MIN_COL_W; g_col_widths[g_col_resize_idx] = new_w; clamp_scroll(); redraw = true; } if (left_released) { g_col_resizing = false; } goto done_mouse; } // Mouse drag selection: extend while dragging if (g_mouse_selecting && left_held && !clicked) { int col, row; if (hit_cell(mx, my, &col, &row)) { if (col != g_sel_col || row != g_sel_row) { g_sel_col = col; g_sel_row = row; g_has_selection = (col != g_anchor_col || row != g_anchor_row); redraw = true; } } } if (left_released) { g_mouse_selecting = false; } // Column resize or auto-fit: click/double-click near column header border if (clicked && my >= header_y && my < header_y + COL_HEADER_H) { for (int c = 0; c < MAX_COLS; c++) { int cx = col_x(c) - g_scroll_x + g_col_widths[c] - 1; if (mx >= cx - COL_RESIZE_GRAB && mx <= cx + COL_RESIZE_GRAB) { // Double-click: auto-fit column width uint64_t now = montauk::get_milliseconds(); if (g_last_click_col == c && (now - g_last_click_ms) < DBLCLICK_MS) { auto_fit_column(c); clamp_scroll(); g_last_click_ms = 0; redraw = true; goto done_mouse; } g_last_click_ms = now; g_last_click_col = c; g_col_resizing = true; g_col_resize_idx = c; g_col_resize_start_x = mx; g_col_resize_start_w = g_col_widths[c]; goto done_mouse; } } } if (clicked) { if (handle_fmt_dropdown_click(mx, my)) { redraw = true; } else if (handle_toolbar_click(mx, my)) { redraw = true; } else { // Check fill handle first if (hit_fill_handle(mx, my)) { int c0, r0, c1, r1; sel_range(&c0, &r0, &c1, &r1); g_fill_dragging = true; g_fill_target_row = r1; goto done_mouse; } int col, row; if (hit_cell(mx, my, &col, &row)) { if (g_editing) { commit_edit(); g_ac_open = false; } // Double-click cell: start editing uint64_t now = montauk::get_milliseconds(); if (col == g_last_click_col && row == g_last_click_row && (now - g_last_click_ms) < DBLCLICK_MS) { g_sel_col = col; g_sel_row = row; clear_selection(); start_editing(); g_last_click_ms = 0; redraw = true; goto done_mouse; } g_last_click_ms = now; g_last_click_col = col; g_last_click_row = row; g_sel_col = col; g_sel_row = row; g_anchor_col = col; g_anchor_row = row; g_has_selection = false; g_mouse_selecting = true; g_fmt_dropdown_open = false; redraw = true; } } } if (ev.mouse.scroll != 0) { g_scroll_y -= ev.mouse.scroll * SCROLL_STEP; clamp_scroll(); redraw = true; } } done_mouse: if (redraw) { render(pixels); win.present(); } } win.destroy(); montauk::exit(0); }