feat: Intel GPU fixes, add tcc compiler, spreadsheet improvements, paint app, file manager improvements
This commit is contained in:
@@ -213,6 +213,62 @@ void apply_format(NumFormat f) {
|
||||
// Scroll clamping
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// Fill down (fill handle)
|
||||
// ============================================================================
|
||||
|
||||
void fill_down(int src_r0, int src_c0, int src_r1, int src_c1, int dst_r1) {
|
||||
if (dst_r1 <= src_r1) return;
|
||||
if (dst_r1 >= MAX_ROWS) dst_r1 = MAX_ROWS - 1;
|
||||
|
||||
undo_push();
|
||||
|
||||
int src_rows = src_r1 - src_r0 + 1;
|
||||
|
||||
for (int r = src_r1 + 1; r <= dst_r1; r++) {
|
||||
int src_r = src_r0 + ((r - src_r0) % src_rows);
|
||||
int drow = r - src_r;
|
||||
for (int c = src_c0; c <= src_c1; c++) {
|
||||
Cell* src = &g_cells[src_r][c];
|
||||
Cell* dst = &g_cells[r][c];
|
||||
dst->align = src->align;
|
||||
dst->fmt = src->fmt;
|
||||
dst->bold = src->bold;
|
||||
|
||||
if (src->input[0] == '=') {
|
||||
// Formula: adjust cell references
|
||||
adjust_formula_refs(src->input, dst->input, CELL_TEXT_MAX, 0, drow);
|
||||
} else {
|
||||
str_cpy(dst->input, src->input, CELL_TEXT_MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eval_all_cells();
|
||||
g_modified = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Auto-fit column width
|
||||
// ============================================================================
|
||||
|
||||
void auto_fit_column(int col) {
|
||||
if (!g_font) return;
|
||||
int max_w = MIN_COL_W;
|
||||
for (int r = 0; r < MAX_ROWS; r++) {
|
||||
if (g_cells[r][col].display[0]) {
|
||||
TrueTypeFont* f = (g_cells[r][col].bold && g_font_bold) ? g_font_bold : g_font;
|
||||
int tw = f->measure_text(g_cells[r][col].display, FONT_SIZE) + 12;
|
||||
if (tw > max_w) max_w = tw;
|
||||
}
|
||||
}
|
||||
g_col_widths[col] = max_w;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scroll clamping
|
||||
// ============================================================================
|
||||
|
||||
void clamp_scroll() {
|
||||
int pbh = g_pathbar_open ? PATHBAR_H : 0;
|
||||
int max_x = content_width() - (g_win_w - ROW_HEADER_W);
|
||||
|
||||
@@ -6,6 +6,34 @@
|
||||
|
||||
#include "spreadsheet.h"
|
||||
|
||||
// ============================================================================
|
||||
// Formula name list (for autocomplete and parsing)
|
||||
// ============================================================================
|
||||
|
||||
struct FormulaInfo {
|
||||
const char* name;
|
||||
const char* hint;
|
||||
};
|
||||
|
||||
static const FormulaInfo g_formula_list[] = {
|
||||
{"SUM", "SUM(range)"},
|
||||
{"AVG", "AVG(range)"},
|
||||
{"MIN", "MIN(range)"},
|
||||
{"MAX", "MAX(range)"},
|
||||
{"COUNT", "COUNT(range)"},
|
||||
{"ABS", "ABS(value)"},
|
||||
{"SQRT", "SQRT(value)"},
|
||||
{"ROUND", "ROUND(val,n)"},
|
||||
{"INT", "INT(value)"},
|
||||
{"FLOOR", "FLOOR(value)"},
|
||||
{"CEIL", "CEIL(value)"},
|
||||
{"POW", "POW(base,exp)"},
|
||||
{"MOD", "MOD(a,b)"},
|
||||
{"PI", "PI()"},
|
||||
{"IF", "IF(cond,t,f)"},
|
||||
};
|
||||
static constexpr int FORMULA_COUNT = sizeof(g_formula_list) / sizeof(g_formula_list[0]);
|
||||
|
||||
// ============================================================================
|
||||
// Cell reference parsing
|
||||
// ============================================================================
|
||||
@@ -124,6 +152,7 @@ static double eval_primary(const char* s, int* pos, bool* ok) {
|
||||
}
|
||||
fname[fi] = '\0';
|
||||
|
||||
// Range functions: SUM, AVG, MIN, MAX, COUNT
|
||||
if (s[*pos] == '(' && (strcmp(fname, "SUM") == 0 ||
|
||||
strcmp(fname, "AVG") == 0 ||
|
||||
strcmp(fname, "MIN") == 0 ||
|
||||
@@ -132,6 +161,77 @@ static double eval_primary(const char* s, int* pos, bool* ok) {
|
||||
return eval_range_func(fname, s, pos, ok);
|
||||
}
|
||||
|
||||
// No-arg: PI()
|
||||
if (s[*pos] == '(' && strcmp(fname, "PI") == 0) {
|
||||
(*pos)++;
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] == ')') (*pos)++;
|
||||
return 3.14159265358979;
|
||||
}
|
||||
|
||||
// Single-arg functions
|
||||
if (s[*pos] == '(' && (strcmp(fname, "ABS") == 0 ||
|
||||
strcmp(fname, "SQRT") == 0 ||
|
||||
strcmp(fname, "INT") == 0 ||
|
||||
strcmp(fname, "FLOOR") == 0 ||
|
||||
strcmp(fname, "CEIL") == 0)) {
|
||||
(*pos)++;
|
||||
double arg = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] == ')') (*pos)++;
|
||||
if (!*ok) return 0;
|
||||
if (strcmp(fname, "ABS") == 0) return stb_fabs(arg);
|
||||
if (strcmp(fname, "SQRT") == 0) return stb_sqrt(arg);
|
||||
if (strcmp(fname, "INT") == 0) return (double)(long long)arg;
|
||||
if (strcmp(fname, "FLOOR") == 0) return stb_floor(arg);
|
||||
if (strcmp(fname, "CEIL") == 0) return stb_ceil(arg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Two-arg functions: ROUND, POW, MOD
|
||||
if (s[*pos] == '(' && (strcmp(fname, "ROUND") == 0 ||
|
||||
strcmp(fname, "POW") == 0 ||
|
||||
strcmp(fname, "MOD") == 0)) {
|
||||
(*pos)++;
|
||||
double a = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] != ',') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
double b = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] == ')') (*pos)++;
|
||||
if (!*ok) return 0;
|
||||
if (strcmp(fname, "POW") == 0) return stb_pow(a, b);
|
||||
if (strcmp(fname, "MOD") == 0) {
|
||||
if (b == 0) { *ok = false; return 0; }
|
||||
return stb_fmod(a, b);
|
||||
}
|
||||
if (strcmp(fname, "ROUND") == 0) {
|
||||
double factor = stb_pow(10.0, b);
|
||||
return stb_floor(a * factor + 0.5) / factor;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// IF(condition, true_value, false_value)
|
||||
if (s[*pos] == '(' && strcmp(fname, "IF") == 0) {
|
||||
(*pos)++;
|
||||
double cond = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] != ',') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
double tv = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] != ',') { *ok = false; return 0; }
|
||||
(*pos)++;
|
||||
double fv = eval_expr(s, pos, ok);
|
||||
skip_spaces(s, pos);
|
||||
if (s[*pos] == ')') (*pos)++;
|
||||
if (!*ok) return 0;
|
||||
return (cond != 0.0) ? tv : fv;
|
||||
}
|
||||
|
||||
// Fall back to cell reference
|
||||
*pos = save_pos;
|
||||
int col, row, consumed;
|
||||
if (parse_cell_ref(s + *pos, &col, &row, &consumed)) {
|
||||
@@ -306,3 +406,142 @@ void cell_name(char* buf, int col, int row) {
|
||||
else if (r >= 10) { buf[1] = '0' + r / 10; buf[2] = '0' + r % 10; buf[3] = '\0'; }
|
||||
else { buf[1] = '0' + r; buf[2] = '\0'; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formula autocomplete
|
||||
// ============================================================================
|
||||
|
||||
void update_autocomplete() {
|
||||
g_ac_open = false;
|
||||
g_ac_count = 0;
|
||||
|
||||
if (!g_editing || g_edit_buf[0] != '=') return;
|
||||
|
||||
// Find start of current alphabetic word at cursor
|
||||
int word_start = g_edit_cursor;
|
||||
while (word_start > 0 && is_alpha(g_edit_buf[word_start - 1]))
|
||||
word_start--;
|
||||
|
||||
int word_len = g_edit_cursor - word_start;
|
||||
if (word_len == 0 || word_len > 7) return;
|
||||
|
||||
// Don't autocomplete if cursor is right before more alpha chars (mid-word)
|
||||
if (g_edit_cursor < g_edit_len && is_alpha(g_edit_buf[g_edit_cursor])) return;
|
||||
|
||||
// Don't show if next char is '(' -- user already completed the name
|
||||
if (g_edit_cursor < g_edit_len && g_edit_buf[g_edit_cursor] == '(') return;
|
||||
|
||||
char partial[8];
|
||||
for (int i = 0; i < word_len; i++)
|
||||
partial[i] = to_upper(g_edit_buf[word_start + i]);
|
||||
partial[word_len] = '\0';
|
||||
|
||||
for (int i = 0; i < FORMULA_COUNT && g_ac_count < AC_MAX_MATCHES; i++) {
|
||||
const char* name = g_formula_list[i].name;
|
||||
bool match = true;
|
||||
for (int j = 0; j < word_len; j++) {
|
||||
if (name[j] == '\0' || to_upper(name[j]) != partial[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Don't show exact matches (already fully typed)
|
||||
if (match && name[word_len] != '\0') {
|
||||
str_cpy(g_ac_matches[g_ac_count], name, 16);
|
||||
str_cpy(g_ac_hints[g_ac_count], g_formula_list[i].hint, 32);
|
||||
g_ac_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_ac_count > 0) {
|
||||
g_ac_open = true;
|
||||
g_ac_sel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void accept_autocomplete() {
|
||||
if (!g_ac_open || g_ac_sel < 0 || g_ac_sel >= g_ac_count) return;
|
||||
|
||||
// Find word start
|
||||
int word_start = g_edit_cursor;
|
||||
while (word_start > 0 && is_alpha(g_edit_buf[word_start - 1]))
|
||||
word_start--;
|
||||
|
||||
const char* name = g_ac_matches[g_ac_sel];
|
||||
int name_len = str_len(name);
|
||||
|
||||
int old_len = g_edit_cursor - word_start;
|
||||
int new_len = name_len + 1; // +1 for '('
|
||||
int delta = new_len - old_len;
|
||||
|
||||
// Check buffer space
|
||||
if (g_edit_len + delta >= CELL_TEXT_MAX - 1) { g_ac_open = false; return; }
|
||||
|
||||
// Shift rest of buffer
|
||||
if (delta > 0) {
|
||||
for (int i = g_edit_len; i >= g_edit_cursor; i--)
|
||||
g_edit_buf[i + delta] = g_edit_buf[i];
|
||||
} else if (delta < 0) {
|
||||
for (int i = g_edit_cursor; i <= g_edit_len; i++)
|
||||
g_edit_buf[i + delta] = g_edit_buf[i];
|
||||
}
|
||||
|
||||
// Write function name + opening paren
|
||||
for (int i = 0; i < name_len; i++)
|
||||
g_edit_buf[word_start + i] = name[i];
|
||||
g_edit_buf[word_start + name_len] = '(';
|
||||
|
||||
g_edit_len += delta;
|
||||
g_edit_cursor = word_start + new_len;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
|
||||
g_ac_open = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formula reference adjustment (for fill handle)
|
||||
// ============================================================================
|
||||
|
||||
void adjust_formula_refs(const char* src, char* dst, int max, int dcol, int drow) {
|
||||
int si = 0, di = 0;
|
||||
while (src[si] && di < max - 1) {
|
||||
if (is_alpha(src[si])) {
|
||||
// Try to parse a cell reference
|
||||
int col, row, consumed;
|
||||
if (parse_cell_ref(src + si, &col, &row, &consumed)) {
|
||||
// Check this is actually a cell ref and not part of a function name
|
||||
// by looking backward: if the previous char is also alpha, it's a
|
||||
// function name, not a cell ref
|
||||
bool is_func_part = (si > 0 && is_alpha(src[si - 1]));
|
||||
if (is_func_part) {
|
||||
dst[di++] = src[si++];
|
||||
continue;
|
||||
}
|
||||
int nc = col + dcol;
|
||||
int nr = row + drow;
|
||||
if (nc < 0) nc = 0;
|
||||
if (nc >= MAX_COLS) nc = MAX_COLS - 1;
|
||||
if (nr < 0) nr = 0;
|
||||
if (nr >= MAX_ROWS) nr = MAX_ROWS - 1;
|
||||
dst[di++] = 'A' + nc;
|
||||
int rv = nr + 1;
|
||||
if (rv >= 100 && di < max - 3) {
|
||||
dst[di++] = '0' + rv / 100;
|
||||
dst[di++] = '0' + (rv / 10) % 10;
|
||||
dst[di++] = '0' + rv % 10;
|
||||
} else if (rv >= 10 && di < max - 2) {
|
||||
dst[di++] = '0' + rv / 10;
|
||||
dst[di++] = '0' + rv % 10;
|
||||
} else {
|
||||
dst[di++] = '0' + rv;
|
||||
}
|
||||
si += consumed;
|
||||
} else {
|
||||
dst[di++] = src[si++];
|
||||
}
|
||||
} else {
|
||||
dst[di++] = src[si++];
|
||||
}
|
||||
}
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
@@ -60,6 +60,25 @@ 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
|
||||
// ============================================================================
|
||||
@@ -89,6 +108,28 @@ bool hit_cell(int mx, int my, int* out_col, int* out_row) {
|
||||
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;
|
||||
|
||||
@@ -202,8 +243,7 @@ extern "C" void _start() {
|
||||
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);
|
||||
load_file(args);
|
||||
}
|
||||
|
||||
// Build window title
|
||||
@@ -295,21 +335,56 @@ extern "C" void _start() {
|
||||
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(); redraw = true; }
|
||||
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();
|
||||
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
|
||||
// Tab: commit and move right (or accept autocomplete above)
|
||||
else if (key.ascii == '\t') {
|
||||
if (g_editing) commit_edit();
|
||||
if (g_editing) { commit_edit(); g_ac_open = false; }
|
||||
if (key.shift) {
|
||||
if (g_sel_col > 0) g_sel_col--;
|
||||
} else {
|
||||
@@ -372,6 +447,7 @@ extern "C" void _start() {
|
||||
g_edit_len--;
|
||||
g_edit_cursor--;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
update_autocomplete();
|
||||
}
|
||||
redraw = true;
|
||||
} else {
|
||||
@@ -394,6 +470,7 @@ extern "C" void _start() {
|
||||
g_edit_buf[i] = g_edit_buf[i + 1];
|
||||
g_edit_len--;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
update_autocomplete();
|
||||
}
|
||||
redraw = true;
|
||||
} else {
|
||||
@@ -411,10 +488,40 @@ extern "C" void _start() {
|
||||
// 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
|
||||
@@ -487,6 +594,7 @@ extern "C" void _start() {
|
||||
g_edit_cursor++;
|
||||
g_edit_buf[g_edit_len] = '\0';
|
||||
}
|
||||
update_autocomplete();
|
||||
redraw = true;
|
||||
}
|
||||
// F2: edit current cell content (append mode)
|
||||
@@ -510,11 +618,15 @@ extern "C" void _start() {
|
||||
int pbh = g_pathbar_open ? PATHBAR_H : 0;
|
||||
int header_y = TOOLBAR_H + pbh + FORMULA_BAR_H;
|
||||
|
||||
// Update cursor style: resize_h when hovering near column border in header
|
||||
// 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;
|
||||
@@ -527,6 +639,33 @@ extern "C" void _start() {
|
||||
montauk::win_setcursor(win_id, 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) {
|
||||
@@ -543,11 +682,39 @@ extern "C" void _start() {
|
||||
goto done_mouse;
|
||||
}
|
||||
|
||||
// Column resize: start drag (click near right edge of column header)
|
||||
// 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;
|
||||
@@ -565,12 +732,41 @@ extern "C" void _start() {
|
||||
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();
|
||||
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;
|
||||
clear_selection();
|
||||
g_anchor_col = col;
|
||||
g_anchor_row = row;
|
||||
g_has_selection = false;
|
||||
g_mouse_selecting = true;
|
||||
g_fmt_dropdown_open = false;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ void render(uint32_t* pixels) {
|
||||
px_hline(pixels, g_win_w, g_win_h, ROW_HEADER_W, y + ROW_H - 1, g_win_w - ROW_HEADER_W, GRID_COLOR);
|
||||
}
|
||||
|
||||
// ---- Selection border ----
|
||||
// ---- Selection border + fill handle ----
|
||||
{
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
@@ -281,8 +281,27 @@ void render(uint32_t* pixels) {
|
||||
for (int c = c0; c <= c1; c++) sw += g_col_widths[c];
|
||||
int sh = (r1 - r0 + 1) * ROW_H;
|
||||
|
||||
// Fill drag preview
|
||||
if (g_fill_dragging && g_fill_target_row > r1) {
|
||||
int fill_sh = (g_fill_target_row - r0 + 1) * ROW_H;
|
||||
// Dashed border for fill preview area
|
||||
for (int dx = 0; dx < sw; dx += 6) {
|
||||
int seg = dx + 3 < sw ? 3 : sw - dx;
|
||||
px_hline(pixels, g_win_w, g_win_h, sx + dx, sy_sel + fill_sh - 1, seg, SELECT_BORDER);
|
||||
}
|
||||
px_vline(pixels, g_win_w, g_win_h, sx, sy_sel + sh, fill_sh - sh, SELECT_BORDER);
|
||||
px_vline(pixels, g_win_w, g_win_h, sx + sw - 1, sy_sel + sh, fill_sh - sh, SELECT_BORDER);
|
||||
}
|
||||
|
||||
px_rect(pixels, g_win_w, g_win_h, sx, sy_sel, sw, sh, SELECT_BORDER);
|
||||
px_rect(pixels, g_win_w, g_win_h, sx + 1, sy_sel + 1, sw - 2, sh - 2, SELECT_BORDER);
|
||||
|
||||
// Fill handle (small blue square at bottom-right corner)
|
||||
if (!g_editing) {
|
||||
int fhx = sx + sw - FILL_HANDLE_SIZE / 2;
|
||||
int fhy = sy_sel + sh - FILL_HANDLE_SIZE / 2;
|
||||
px_fill(pixels, g_win_w, g_win_h, fhx, fhy, FILL_HANDLE_SIZE, FILL_HANDLE_SIZE, SELECT_BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
@@ -302,15 +321,35 @@ void render(uint32_t* pixels) {
|
||||
int sty = sy + (STATUS_BAR_H - HEADER_FONT) / 2;
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h, 6, sty, status, STATUS_TEXT, HEADER_FONT);
|
||||
|
||||
char right[64];
|
||||
char right[128];
|
||||
if (g_has_selection) {
|
||||
int c0, r0, c1, r1;
|
||||
sel_range(&c0, &r0, &c1, &r1);
|
||||
char n0[8], n1[8];
|
||||
cell_name(n0, c0, r0);
|
||||
cell_name(n1, c1, r1);
|
||||
int ncells = (c1 - c0 + 1) * (r1 - r0 + 1);
|
||||
snprintf(right, 64, "%s:%s (%d cells) ", n0, n1, ncells);
|
||||
|
||||
// Calculate aggregates for numeric cells
|
||||
double sum = 0;
|
||||
int num_count = 0;
|
||||
for (int r = r0; r <= r1; r++) {
|
||||
for (int c = c0; c <= c1; c++) {
|
||||
if (g_cells[r][c].type == CT_NUMBER || g_cells[r][c].type == CT_FORMULA) {
|
||||
sum += g_cells[r][c].value;
|
||||
num_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (num_count > 0) {
|
||||
char sbuf[24], abuf[24];
|
||||
double_to_str(sbuf, 24, sum);
|
||||
double_to_str(abuf, 24, sum / num_count);
|
||||
snprintf(right, 128, "SUM=%s AVG=%s COUNT=%d ", sbuf, abuf, num_count);
|
||||
} else {
|
||||
char n0[8], n1[8];
|
||||
cell_name(n0, c0, r0);
|
||||
cell_name(n1, c1, r1);
|
||||
int ncells = (c1 - c0 + 1) * (r1 - r0 + 1);
|
||||
snprintf(right, 128, "%s:%s (%d cells) ", n0, n1, ncells);
|
||||
}
|
||||
} else {
|
||||
Cell* sel = &g_cells[g_sel_row][g_sel_col];
|
||||
char cname[8];
|
||||
@@ -318,9 +357,9 @@ void render(uint32_t* pixels) {
|
||||
if (sel->type == CT_FORMULA || sel->type == CT_NUMBER) {
|
||||
char vbuf[32];
|
||||
double_to_str(vbuf, 32, sel->value);
|
||||
snprintf(right, 64, "%s = %s ", cname, vbuf);
|
||||
snprintf(right, 128, "%s = %s ", cname, vbuf);
|
||||
} else {
|
||||
snprintf(right, 64, "%s ", cname);
|
||||
snprintf(right, 128, "%s ", cname);
|
||||
}
|
||||
}
|
||||
int rw = g_font->measure_text(right, HEADER_FONT);
|
||||
@@ -348,4 +387,48 @@ void render(uint32_t* pixels) {
|
||||
dx + 8, iy + (item_h - HEADER_FONT) / 2, items[i], CELL_TEXT, HEADER_FONT);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Autocomplete dropdown overlay ----
|
||||
if (g_ac_open && g_editing && g_font) {
|
||||
int fbar_y_ac = TOOLBAR_H + pathbar_h;
|
||||
int fx = sep_x + 10;
|
||||
|
||||
// Measure text up to the start of the current word
|
||||
int ws = g_edit_cursor;
|
||||
while (ws > 0 && is_alpha(g_edit_buf[ws - 1])) ws--;
|
||||
char word_prefix[CELL_TEXT_MAX];
|
||||
int wpi = 0;
|
||||
for (int i = 0; i < ws && wpi < CELL_TEXT_MAX - 1; i++)
|
||||
word_prefix[wpi++] = g_edit_buf[i];
|
||||
word_prefix[wpi] = '\0';
|
||||
|
||||
int ac_x = fx + g_font->measure_text(word_prefix, FONT_SIZE);
|
||||
int ac_y = fbar_y_ac + FORMULA_BAR_H;
|
||||
int ac_item_h = 24;
|
||||
int ac_w = 160;
|
||||
int ac_h = g_ac_count * ac_item_h + 4;
|
||||
|
||||
// Shadow
|
||||
px_fill(pixels, g_win_w, g_win_h, ac_x + 2, ac_y + 2, ac_w, ac_h,
|
||||
Color::from_rgb(0xAA, 0xAA, 0xAA));
|
||||
// Background
|
||||
px_fill(pixels, g_win_w, g_win_h, ac_x, ac_y, ac_w, ac_h, BG_COLOR);
|
||||
px_rect(pixels, g_win_w, g_win_h, ac_x, ac_y, ac_w, ac_h, GRID_COLOR);
|
||||
|
||||
for (int i = 0; i < g_ac_count; i++) {
|
||||
int iy = ac_y + 2 + i * ac_item_h;
|
||||
if (i == g_ac_sel)
|
||||
px_fill(pixels, g_win_w, g_win_h, ac_x + 2, iy, ac_w - 4, ac_item_h - 2, SELECT_FILL);
|
||||
|
||||
TrueTypeFont* bf = g_font_bold ? g_font_bold : g_font;
|
||||
bf->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
ac_x + 6, iy + (ac_item_h - HEADER_FONT) / 2,
|
||||
g_ac_matches[i], CELL_TEXT, HEADER_FONT);
|
||||
|
||||
int nw = bf->measure_text(g_ac_matches[i], HEADER_FONT);
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
ac_x + 12 + nw, iy + (ac_item_h - HEADER_FONT) / 2,
|
||||
g_ac_hints[i], HEADER_TEXT, HEADER_FONT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
@@ -41,6 +42,9 @@ static constexpr int DEF_COL_W = 100;
|
||||
static constexpr int MIN_COL_W = 30;
|
||||
static constexpr int ROW_H = 26;
|
||||
static constexpr int COL_RESIZE_GRAB = 5; // pixels from border edge for grab zone
|
||||
static constexpr int FILL_HANDLE_SIZE = 6;
|
||||
static constexpr int AC_MAX_MATCHES = 8;
|
||||
static constexpr int DBLCLICK_MS = 400;
|
||||
|
||||
static constexpr int CELL_TEXT_MAX = 128;
|
||||
static constexpr int FONT_SIZE = 18;
|
||||
@@ -174,6 +178,25 @@ extern UndoEntry* g_undo[UNDO_MAX + 1];
|
||||
extern int g_undo_count;
|
||||
extern int g_undo_pos;
|
||||
|
||||
// Formula autocomplete
|
||||
extern bool g_ac_open;
|
||||
extern int g_ac_sel;
|
||||
extern int g_ac_count;
|
||||
extern char g_ac_matches[AC_MAX_MATCHES][16];
|
||||
extern char g_ac_hints[AC_MAX_MATCHES][32];
|
||||
|
||||
// Fill handle
|
||||
extern bool g_fill_dragging;
|
||||
extern int g_fill_target_row;
|
||||
|
||||
// Mouse drag selection
|
||||
extern bool g_mouse_selecting;
|
||||
|
||||
// Double-click detection
|
||||
extern uint64_t g_last_click_ms;
|
||||
extern int g_last_click_col;
|
||||
extern int g_last_click_row;
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — helpers.cpp
|
||||
// ============================================================================
|
||||
@@ -203,6 +226,9 @@ int content_width();
|
||||
int content_height();
|
||||
void cell_name(char* buf, int col, int row);
|
||||
void format_value(char* buf, int max, double val, NumFormat fmt);
|
||||
void update_autocomplete();
|
||||
void accept_autocomplete();
|
||||
void adjust_formula_refs(const char* src, char* dst, int max, int dcol, int drow);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — fileio.cpp
|
||||
@@ -231,6 +257,8 @@ void apply_align(CellAlign a);
|
||||
void apply_format(NumFormat f);
|
||||
void clamp_scroll();
|
||||
void ensure_sel_visible();
|
||||
void fill_down(int src_r0, int src_c0, int src_r1, int src_c1, int dst_r1);
|
||||
void auto_fit_column(int col);
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations — render.cpp
|
||||
@@ -243,5 +271,6 @@ void render(uint32_t* pixels);
|
||||
// ============================================================================
|
||||
|
||||
bool hit_cell(int mx, int my, int* out_col, int* out_row);
|
||||
bool hit_fill_handle(int mx, int my);
|
||||
bool handle_toolbar_click(int mx, int my);
|
||||
bool handle_fmt_dropdown_click(int mx, int my);
|
||||
|
||||
Reference in New Issue
Block a user