792 lines
28 KiB
C++
792 lines
28 KiB
C++
/*
|
|
* main.cpp
|
|
* Spreadsheet app — global state, hit testing, entry point
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "spreadsheet.h"
|
|
#include <gui/standalone.hpp>
|
|
|
|
// ============================================================================
|
|
// 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);
|
|
}
|