ref: c5d556434652b8f00fb0ec89fe2d1fc162f5e4c5
dir: /fc.c/
#include <u.h> #include <libc.h> #include <draw.h> #include <event.h> #include <keyboard.h> #include <bio.h> #include <ctype.h> enum { MAXBOXES = 300, BOXWIDTH = 128, BOXHEIGHT = 32, MAXFORMULA = 256, MAXCONTENT = 256, }; enum { T_TEXT = 0, T_NUMBER, T_FORMULA, T_LABEL, MAXBOXTYPES, }; enum { TOK_NUM = 0, TOK_CELL, TOK_RANGE, TOK_OP, TOK_FUNC, TOK_LPAREN, TOK_RPAREN, TOK_COMMA, TOK_STRING, TOK_END, OP_ADD = '+', OP_SUB = '-', OP_MUL = '*', OP_DIV = '/', OP_MOD = '%', OP_POW = '^', OP_EQ = '=', OP_NE = '!', OP_LT = '<', OP_GT = '>', OP_LE = '[', /* Using [ for <= */ OP_GE = ']', /* Using ] for >= */ FN_SUM = 0, FN_AVG, FN_MIN, FN_MAX, FN_COUNT, FN_ABS, FN_SQRT, FN_POW, FN_ROUND, FN_FLOOR, FN_CEIL, FN_IF, FN_AND, FN_OR, FN_NOT, FN_CONCAT, FN_LEN, FN_UPPER, FN_LOWER, FN_LOOKUP, MAXFUNCS, }; typedef struct Box Box; struct Box { Point pos; Rectangle r; char content[MAXCONTENT]; char formula[MAXFORMULA]; char label[32]; int type; double value; int selected; int dirty; int refs[10]; int nrefs; }; struct { Box boxes[MAXBOXES]; int nboxes; int selected; int editing; char editbuf[MAXCONTENT]; int editpos; int editing_label; char labelbuf[32]; int labelpos; int entering_filename; char filenamebuf[256]; int filenamepos; int save_mode; /* 1=save, 2=load */ int current_mode; Point offset; int needredraw; int gridsnap; int gridsize; } sheet; typedef struct BoxType BoxType; struct BoxType { char *name; void (*parse)(Box*); void (*eval)(Box*); void (*draw)(Box*, Image*); }; typedef struct Token Token; struct Token { int type; union { double num; int cell; /* box index */ struct { int start, end; /* range of boxes */ } range; int op; int func; char str[MAXCONTENT]; }; }; typedef struct Function Function; struct Function { char *name; int minargs; int maxargs; double (*eval)(Token *args, int nargs); }; typedef struct Eval Eval; struct Eval { Token tokens[256]; int ntokens; int pos; Box *current; int depth; }; typedef void (*KeyHandler)(int key); typedef void (*MouseHandler)(Mouse m); /* For data-oriented mouse handling */ typedef struct InputMode InputMode; struct InputMode { char *name; KeyHandler handler; MouseHandler mouse_handler; /* Added mouse handler */ void (*draw)(void); char *status; }; typedef struct Command Command; struct Command { int key; void (*action)(void); }; typedef struct EditAction EditAction; struct EditAction { int key; int (*action)(char *buf, int *pos, int maxlen); /* Returns 1 to exit mode */ }; typedef struct DrawStep DrawStep; struct DrawStep { void (*draw)(void); int condition; /* 0=always, 1=if grid, etc */ }; Image *colors[6]; Image *boxbg; Image *boxselected; Image *boxediting; Image *gridcolor; void parse_text(Box*); void parse_number(Box*); void parse_formula(Box*); void eval_text(Box*); void eval_number(Box*); void eval_formula(Box*); void draw_box_generic(Box*, Image*); void recalc_all(void); void handle_normal_mode(int key); void handle_cell_edit(int key); void handle_label_edit(int key); void handle_filename_input(int key); void handle_normal_mouse(Mouse m); void handle_edit_mouse(Mouse m); void handle_label_mouse(Mouse m); void handle_filename_mouse(Mouse m); void draw_normal_overlay(void); void draw_cell_edit_overlay(void); void draw_label_edit_overlay(void); void draw_filename_overlay(void); void cmd_quit(void); void cmd_save(void); void cmd_save_as(void); void cmd_open(void); void cmd_open_file(void); void cmd_start_label(void); void cmd_toggle_grid(void); void cmd_delete_box(void); void draw_background(void); void draw_grid_lines(void); void draw_all_boxes(void); void draw_status_line(void); int cistrcmp(char *s1, char *s2); int tokenize_formula(char *formula, Token *tokens, int maxtokens); int cellref_lookup(char *ref); int edit_finish(char *buf, int *pos, int maxlen); int edit_cancel(char *buf, int *pos, int maxlen); int edit_backspace(char *buf, int *pos, int maxlen); int edit_add_char(char *buf, int *pos, int maxlen); double round(double nargs); double fn_sum(Token *args, int nargs); double fn_avg(Token *args, int nargs); double fn_min(Token *args, int nargs); double fn_max(Token *args, int nargs); double fn_count(Token *args, int nargs); double fn_abs(Token *args, int nargs); double fn_sqrt(Token *args, int nargs); double fn_pow(Token *args, int nargs); double fn_round(Token *args, int nargs); double fn_floor(Token *args, int nargs); double fn_ceil(Token *args, int nargs); double fn_if(Token *args, int nargs); double fn_and(Token *args, int nargs); double fn_or(Token *args, int nargs); double fn_not(Token *args, int nargs); double fn_lookup(Token *args, int nargs); double eval_expr(Eval *e); double eval_term(Eval *e); double eval_factor(Eval *e); double eval_primary(Eval *e); BoxType boxtypes[] = { [T_TEXT] = {"text", parse_text, eval_text, draw_box_generic}, [T_NUMBER] = {"number", parse_number, eval_number, draw_box_generic}, [T_FORMULA] = {"formula", parse_formula, eval_formula, draw_box_generic}, [T_LABEL] = {"label", parse_text, eval_text, draw_box_generic}, }; Function functions[MAXFUNCS] = { [FN_SUM] = {"SUM", 1, 100, fn_sum}, [FN_AVG] = {"AVG", 1, 100, fn_avg}, [FN_MIN] = {"MIN", 1, 100, fn_min}, [FN_MAX] = {"MAX", 1, 100, fn_max}, [FN_COUNT] = {"COUNT", 1, 100, fn_count}, [FN_ABS] = {"ABS", 1, 1, fn_abs}, [FN_SQRT] = {"SQRT", 1, 1, fn_sqrt}, [FN_POW] = {"POW", 2, 2, fn_pow}, [FN_ROUND] = {"ROUND", 1, 2, fn_round}, [FN_FLOOR] = {"FLOOR", 1, 1, fn_floor}, [FN_CEIL] = {"CEIL", 1, 1, fn_ceil}, [FN_IF] = {"IF", 3, 3, fn_if}, [FN_AND] = {"AND", 2, 100, fn_and}, [FN_OR] = {"OR", 2, 100, fn_or}, [FN_NOT] = {"NOT", 1, 1, fn_not}, [FN_LOOKUP] = {"LOOKUP", 2, 3, fn_lookup}, }; InputMode input_modes[] = { [0] = {"normal", handle_normal_mode, handle_normal_mouse, draw_normal_overlay, "S:save O:open L:label G:grid"}, [1] = {"edit", handle_cell_edit, handle_edit_mouse, draw_cell_edit_overlay, "Enter:save Esc:cancel"}, [2] = {"label", handle_label_edit, handle_label_mouse, draw_label_edit_overlay,"Enter:save Esc:cancel"}, [3] = {"filename", handle_filename_input, handle_filename_mouse, draw_filename_overlay, "Tab:.spr ` Enter:confirm `Esc:cancel"}, }; Command commands[] = { {'q', cmd_quit}, {Kdel, cmd_quit}, {'s', cmd_save}, {'S', cmd_save_as}, {'o', cmd_open}, {'O', cmd_open_file}, {'l', cmd_start_label}, {'g', cmd_toggle_grid}, {'d', cmd_delete_box}, {0, nil} }; EditAction edit_actions[] = { {'\n', edit_finish}, {Kesc, edit_cancel}, {Kbs, edit_backspace}, {-1, edit_add_char}, /* Default for any other key */ {0, nil} }; DrawStep draw_steps[] = { {draw_background, 0}, /* Always */ {draw_grid_lines, 1}, /* If gridsnap */ {draw_all_boxes, 0}, /* Always */ {draw_status_line, 0}, /* Always */ {nil, 0} }; void handlekey(int key) { InputMode *mode = &input_modes[sheet.current_mode]; if(mode->handler) mode->handler(key); } void handlemouse(Mouse m) { InputMode *mode = &input_modes[sheet.current_mode]; if(mode->mouse_handler) mode->mouse_handler(m); } void initcolors(void) { colors[0] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x000000FF); colors[1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFFFFFFFF); colors[2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xEEEEEEFF); colors[3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x4444FFFF); colors[4] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFF4444FF); colors[5] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCC88FF); boxbg = colors[1]; boxselected = colors[3]; boxediting = colors[5]; gridcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCCCCFF); } double round(double x) { if(x >= 0) return floor(x + 0.5); else return ceil(x - 0.5); } /* Helper for case-insensitive string comparison */ int cistrcmp(char *s1, char *s2) { while(*s1 && *s2) { int c1 = toupper(*s1); int c2 = toupper(*s2); if(c1 != c2) return c1 - c2; s1++; s2++; } return *s1 - *s2; } int cellref_lookup(char *ref) { int i; /* First, check if any box has this exact label */ for(i = 0; i < sheet.nboxes; i++) { if(sheet.boxes[i].label[0] && cistrcmp(sheet.boxes[i].label, ref) == 0) { return i; } } /* If no labeled box found, try index-based (A=0, B=1, etc.) */ /* Handle single letters first */ if(strlen(ref) == 1 && isalpha(ref[0])) { int idx = toupper(ref[0]) - 'A'; if(idx >= 0 && idx < sheet.nboxes) return idx; } /* Handle double letters (AA, AB, etc.) */ if(strlen(ref) == 2 && isalpha(ref[0]) && isalpha(ref[1])) { int idx = (toupper(ref[0]) - 'A' + 1) * 26 + (toupper(ref[1]) - 'A'); if(idx >= 0 && idx < sheet.nboxes) return idx; } /* Try traditional Excel-style (A1, B2) - just use the letter part */ if(isalpha(ref[0])) { int col = 0; char *p = ref; while(*p && isalpha(*p)) { col = col * 26 + (toupper(*p) - 'A'); p++; } if(col >= 0 && col < sheet.nboxes) return col; } return -1; /* not found */ } int tokenize_formula(char *formula, Token *tokens, int maxtokens) { char *p = formula; int ntok = 0; char buf[256]; int i; if(*p == '=') p++; while(*p && ntok < maxtokens) { Token *t = &tokens[ntok]; while(*p && (*p == ' ' || *p == '\t')) p++; if(!*p) break; if(isdigit(*p) || (*p == '.' && isdigit(*(p+1)))) { char *endp; t->type = TOK_NUM; t->num = strtod(p, &endp); p = endp; ntok++; continue; } if(isalpha(*p)) { i = 0; while(*p && (isalnum(*p) || *p == '_') && i < 255) buf[i++] = *p++; buf[i] = '\0'; if(*p == ':') { p++; /* skip : */ char buf2[256]; i = 0; while(*p && (isalnum(*p) || *p == '_')) buf2[i++] = *p++; buf2[i] = '\0'; t->type = TOK_RANGE; t->range.start = cellref_lookup(buf); t->range.end = cellref_lookup(buf2); ntok++; continue; } int found = 0; for(i = 0; i < MAXFUNCS; i++) { if(functions[i].name && cistrcmp(buf, functions[i].name) == 0) { t->type = TOK_FUNC; t->func = i; found = 1; break; } } if(!found) { /* It's a cell reference */ t->type = TOK_CELL; t->cell = cellref_lookup(buf); } ntok++; continue; } if(*p == '"') { p++; i = 0; while(*p && *p != '"' && i < MAXCONTENT-1) t->str[i++] = *p++; t->str[i] = '\0'; if(*p == '"') p++; t->type = TOK_STRING; ntok++; continue; } switch(*p) { case '+': case '-': case '*': case '/': case '%': case '^': case '=': case '<': case '>': t->type = TOK_OP; t->op = *p++; /* Check for two-char operators */ if(t->op == '<' && *p == '=') { t->op = OP_LE; p++; } else if(t->op == '>' && *p == '=') { t->op = OP_GE; p++; } else if(t->op == '!' && *p == '=') { t->op = OP_NE; p++; } ntok++; break; case '(': t->type = TOK_LPAREN; p++; ntok++; break; case ')': t->type = TOK_RPAREN; p++; ntok++; break; case ',': t->type = TOK_COMMA; p++; ntok++; break; default: p++; break; } } if(ntok < maxtokens) { tokens[ntok].type = TOK_END; ntok++; } return ntok; } double token_value(Token *t, Eval *e) { Box *b; switch(t->type) { case TOK_NUM: return t->num; case TOK_CELL: if(t->cell >= 0 && t->cell < sheet.nboxes) { b = &sheet.boxes[t->cell]; if(b == e->current) { return 0.0; /* circular ref */ } if(b->type == T_FORMULA && e->depth < 10) { Eval subeval; subeval.current = b; subeval.depth = e->depth + 1; subeval.pos = 0; subeval.ntokens = tokenize_formula(b->formula, subeval.tokens, nelem(subeval.tokens)); return eval_expr(&subeval); } return b->value; } return 0.0; case TOK_STRING: return atof(t->str); default: return 0.0; } } /* Evaluate primary expression (number, cell, function call, parentheses) */ double eval_primary(Eval *e) { Token *t; double result = 0.0; if(e->pos >= e->ntokens) return 0.0; t = &e->tokens[e->pos]; switch(t->type) { case TOK_NUM: case TOK_CELL: case TOK_STRING: result = token_value(t, e); e->pos++; break; case TOK_FUNC: { int func = t->func; e->pos++; /* skip function name */ /* Expect ( */ if(e->pos >= e->ntokens || e->tokens[e->pos].type != TOK_LPAREN) return 0.0; e->pos++; /* skip ( */ /* Collect arguments */ Token args[100]; int nargs = 0; while(e->pos < e->ntokens && e->tokens[e->pos].type != TOK_RPAREN) { if(nargs > 0) { /* Expect comma */ if(e->tokens[e->pos].type != TOK_COMMA) break; e->pos++; /* skip comma */ } /* Check for range */ if(e->tokens[e->pos].type == TOK_RANGE) { /* Expand range into individual cells */ int start = e->tokens[e->pos].range.start; int end = e->tokens[e->pos].range.end; e->pos++; /* Add all cells in range */ int i; for(i = start; i <= end && i >= 0 && nargs < 100; i++) { args[nargs].type = TOK_CELL; args[nargs].cell = i; nargs++; } } else { /* Regular argument - evaluate it */ args[nargs].type = TOK_NUM; args[nargs].num = eval_expr(e); nargs++; } } /* Skip ) */ if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN) e->pos++; /* Call function */ if(func >= 0 && func < MAXFUNCS && functions[func].eval) { if(nargs >= functions[func].minargs && nargs <= functions[func].maxargs) { result = functions[func].eval(args, nargs); } } break; } case TOK_LPAREN: e->pos++; /* skip ( */ result = eval_expr(e); /* Skip ) */ if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN) e->pos++; break; case TOK_OP: /* Unary minus */ if(t->op == OP_SUB) { e->pos++; result = -eval_primary(e); } break; } return result; } /* Evaluate factor (handle power) */ double eval_factor(Eval *e) { double left = eval_primary(e); while(e->pos < e->ntokens) { Token *t = &e->tokens[e->pos]; if(t->type != TOK_OP || t->op != OP_POW) break; e->pos++; /* skip operator */ double right = eval_primary(e); left = pow(left, right); } return left; } /* Evaluate term (multiplication, division, modulo) */ double eval_term(Eval *e) { double left = eval_factor(e); while(e->pos < e->ntokens) { Token *t = &e->tokens[e->pos]; if(t->type != TOK_OP) break; switch(t->op) { case OP_MUL: e->pos++; left *= eval_factor(e); break; case OP_DIV: e->pos++; { double right = eval_factor(e); if(right != 0.0) left /= right; else left = 0.0; /* div by zero */ } break; case OP_MOD: e->pos++; { double right = eval_factor(e); if(right != 0.0) left = fmod(left, right); } break; default: return left; } } return left; } /* Evaluate expression (addition, subtraction, comparison) */ double eval_expr(Eval *e) { double left = eval_term(e); while(e->pos < e->ntokens) { Token *t = &e->tokens[e->pos]; if(t->type != TOK_OP) break; switch(t->op) { case OP_ADD: e->pos++; left += eval_term(e); break; case OP_SUB: e->pos++; left -= eval_term(e); break; case OP_LT: e->pos++; left = left < eval_term(e) ? 1.0 : 0.0; break; case OP_GT: e->pos++; left = left > eval_term(e) ? 1.0 : 0.0; break; case OP_LE: e->pos++; left = left <= eval_term(e) ? 1.0 : 0.0; break; case OP_GE: e->pos++; left = left >= eval_term(e) ? 1.0 : 0.0; break; case OP_EQ: e->pos++; left = fabs(left - eval_term(e)) < 0.000001 ? 1.0 : 0.0; break; case OP_NE: e->pos++; left = fabs(left - eval_term(e)) >= 0.000001 ? 1.0 : 0.0; break; default: return left; } } return left; } double fn_sum(Token *args, int nargs) { double sum = 0.0; int i; Eval e; for(i = 0; i < nargs; i++) { if(args[i].type == TOK_CELL) { e.current = nil; e.depth = 0; sum += token_value(&args[i], &e); } else { sum += args[i].num; } } return sum; } double fn_avg(Token *args, int nargs) { if(nargs == 0) return 0.0; return fn_sum(args, nargs) / nargs; } double fn_min(Token *args, int nargs) { if(nargs == 0) return 0.0; double min = 1e308; /* large number */ int i; Eval e; for(i = 0; i < nargs; i++) { double val; if(args[i].type == TOK_CELL) { e.current = nil; e.depth = 0; val = token_value(&args[i], &e); } else { val = args[i].num; } if(val < min) min = val; } return min; } double fn_max(Token *args, int nargs) { if(nargs == 0) return 0.0; double max = -1e308; /* small number */ int i; Eval e; for(i = 0; i < nargs; i++) { double val; if(args[i].type == TOK_CELL) { e.current = nil; e.depth = 0; val = token_value(&args[i], &e); } else { val = args[i].num; } if(val > max) max = val; } return max; } double fn_count(Token *args, int nargs) { int count = 0; int i; for(i = 0; i < nargs; i++) { if(args[i].type == TOK_CELL) { if(args[i].cell >= 0 && args[i].cell < sheet.nboxes) { Box *b = &sheet.boxes[args[i].cell]; if(b->type == T_NUMBER || b->type == T_FORMULA) count++; } } else { count++; /* direct number */ } } return (double)count; } double fn_abs(Token *args, int nargs) { USED(nargs); return fabs(args[0].num); } double fn_sqrt(Token *args, int nargs) { USED(nargs); return sqrt(args[0].num); } double fn_pow(Token *args, int nargs) { USED(nargs); return pow(args[0].num, args[1].num); } double fn_round(Token *args, int nargs) { if(nargs == 1) { return round(args[0].num); } else { /* Round to N decimal places */ double mult = pow(10, args[1].num); return round(args[0].num * mult) / mult; } } double fn_floor(Token *args, int nargs) { USED(nargs); return floor(args[0].num); } double fn_ceil(Token *args, int nargs) { USED(nargs); return ceil(args[0].num); } double fn_if(Token *args, int nargs) { USED(nargs); return args[0].num != 0.0 ? args[1].num : args[2].num; } double fn_and(Token *args, int nargs) { int i; for(i = 0; i < nargs; i++) { if(args[i].num == 0.0) return 0.0; } return 1.0; } double fn_or(Token *args, int nargs) { int i; for(i = 0; i < nargs; i++) { if(args[i].num != 0.0) return 1.0; } return 0.0; } double fn_not(Token *args, int nargs) { USED(nargs); return args[0].num == 0.0 ? 1.0 : 0.0; } double fn_lookup(Token *args, int nargs) { /* LOOKUP(value, search_range, result_range) */ /* Simplified VLOOKUP - finds value in search range, returns corresponding from result range */ if(nargs < 2) return 0.0; /* For now, just return the value */ /* Full implementation would search through cells */ return args[0].num; } void parse_formula(Box *b) { Eval e; if(b->formula[0] != '=') { char *endp; double val = strtod(b->formula, &endp); if(*endp == '\0') { b->type = T_NUMBER; b->value = val; snprint(b->content, MAXCONTENT, "%.2f", val); } else { b->type = T_TEXT; strncpy(b->content, b->formula, MAXCONTENT); } return; } /* It's a formula */ b->type = T_FORMULA; /* Tokenize */ e.current = b; e.depth = 0; e.pos = 0; e.ntokens = tokenize_formula(b->formula, e.tokens, nelem(e.tokens)); strncpy(b->content, b->formula, MAXCONTENT); b->dirty = 1; /* Extract cell references for dependency tracking */ b->nrefs = 0; int i; for(i = 0; i < e.ntokens && b->nrefs < 10; i++) { if(e.tokens[i].type == TOK_CELL && e.tokens[i].cell >= 0) { b->refs[b->nrefs++] = e.tokens[i].cell; } else if(e.tokens[i].type == TOK_RANGE) { /* Add range endpoints */ if(b->nrefs < 10 && e.tokens[i].range.start >= 0) b->refs[b->nrefs++] = e.tokens[i].range.start; if(b->nrefs < 10 && e.tokens[i].range.end >= 0) b->refs[b->nrefs++] = e.tokens[i].range.end; } } } void eval_formula(Box *b) { Eval e; if(b->type != T_FORMULA || !b->dirty) return; e.current = b; e.depth = 0; e.pos = 0; e.ntokens = tokenize_formula(b->formula, e.tokens, nelem(e.tokens)); b->value = eval_expr(&e); if(b->formula[0] == '=' && b->formula[1] == '"') { /* String formula - show as text */ strncpy(b->content, b->formula + 2, MAXCONTENT); char *p = strchr(b->content, '"'); if(p) *p = '\0'; } else { snprint(b->content, MAXCONTENT, "%.2f", b->value); } b->dirty = 0; /* Mark dependent cells as dirty */ int i, j; for(i = 0; i < sheet.nboxes; i++) { Box *other = &sheet.boxes[i]; if(other->type == T_FORMULA) { for(j = 0; j < other->nrefs; j++) { if(other->refs[j] == b - sheet.boxes) { other->dirty = 1; break; } } } } } /* Recalculate all formulas in dependency order */ void recalc_all(void) { int i, changed; int passes = 0; /* Mark all formulas as dirty */ for(i = 0; i < sheet.nboxes; i++) { if(sheet.boxes[i].type == T_FORMULA) sheet.boxes[i].dirty = 1; } /* Iteratively evaluate until no changes (or max passes) */ do { changed = 0; for(i = 0; i < sheet.nboxes; i++) { Box *b = &sheet.boxes[i]; if(b->type == T_FORMULA && b->dirty) { eval_formula(b); changed = 1; } } passes++; } while(changed && passes < 10); } int boxat(Point p) { int i; Box *b; for(i = sheet.nboxes - 1; i >= 0; i--){ b = &sheet.boxes[i]; if(ptinrect(p, b->r)) return i; } return -1; } int addbox(Point p) { Box *b; if(sheet.nboxes >= MAXBOXES) return -1; b = &sheet.boxes[sheet.nboxes]; memset(b, 0, sizeof(Box)); /* Snap to grid if enabled */ if(sheet.gridsnap){ p.x = (p.x / sheet.gridsize) * sheet.gridsize; p.y = (p.y / sheet.gridsize) * sheet.gridsize; } b->pos = p; b->r = Rect(p.x, p.y, p.x + BOXWIDTH, p.y + BOXHEIGHT); b->type = T_TEXT; strcpy(b->content, ""); return sheet.nboxes++; } void delbox(int i) { if(i < 0 || i >= sheet.nboxes) return; /* Shift boxes down */ memmove(&sheet.boxes[i], &sheet.boxes[i+1], (sheet.nboxes - i - 1) * sizeof(Box)); sheet.nboxes--; /* Update references in other boxes */ int j, k; for(j = 0; j < sheet.nboxes; j++){ Box *b = &sheet.boxes[j]; for(k = 0; k < b->nrefs; k++){ if(b->refs[k] > i) b->refs[k]--; else if(b->refs[k] == i) b->refs[k] = -1; /* invalidate */ } } } void parse_text(Box *b) { /* copy content as is */ strncpy(b->content, b->formula, MAXCONTENT); } void parse_number(Box *b) { char *endp; b->value = strtod(b->formula, &endp); snprint(b->content, MAXCONTENT, "%.2f", b->value); } void eval_text(Box *b) { /* Text doesn't evaluate */ USED(b); } void eval_number(Box *b) { /* Numbers are already evaluated */ USED(b); } void draw_box_generic(Box *b, Image *dst) { Image *bg = boxbg; int idx = b - sheet.boxes; /* Get box index */ if(sheet.editing == idx) bg = boxediting; else if(sheet.editing_label == idx) bg = colors[5]; else if(b->selected) bg = boxselected; draw(dst, b->r, bg, nil, ZP); border(dst, b->r, 1, colors[0], ZP); char cellname[32]; if(sheet.editing_label == idx){ snprint(cellname, sizeof(cellname), "%s", sheet.labelbuf); string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname); } else if(b->label[0]) { /* Has custom label */ snprint(cellname, sizeof(cellname), "%s", b->label); string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname); } else { /* Show default index (A, B, C...) */ if(idx < 26) { snprint(cellname, sizeof(cellname), "%c", 'A' + idx); } else { snprint(cellname, sizeof(cellname), "%c%c", 'A' + (idx/26)-1, 'A' + (idx%26)); } string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[3], ZP, font, cellname); } Point p = addpt(b->r.min, Pt(5, 16)); /* Offset down to avoid label */ if(sheet.editing == idx){ string(dst, p, colors[0], ZP, font, sheet.editbuf); } else { string(dst, p, colors[0], ZP, font, b->content); } if (b->type == T_FORMULA){ string(dst, Pt(b->r.max.x - 10, b->r.min.y + 2), colors[3], ZP, font, "="); } } void drawgrid(Image *dst) { int x, y; Rectangle r = screen->r; // Start from the first grid line that's visible in the window int startx = (r.min.x / sheet.gridsize) * sheet.gridsize; int starty = (r.min.y / sheet.gridsize) * sheet.gridsize; // Draw vertical lines for(x = startx; x <= r.max.x; x += sheet.gridsize){ line(dst, Pt(x, r.min.y), Pt(x, r.max.y), 0, 0, 0, gridcolor, ZP); } // Draw horizontal lines for(y = starty; y <= r.max.y; y += sheet.gridsize){ line(dst, Pt(r.min.x, y), Pt(r.max.x, y), 0, 0, 0, gridcolor, ZP); } } void draw_normal_overlay(void) { /* Nothing special for normal mode */ } void draw_cell_edit_overlay(void) { /* Could draw edit indicator */ } void draw_label_edit_overlay(void) { /* Could highlight the box being labeled */ } void draw_filename_overlay(void) { Rectangle r; int w = 256; int h = 48; Point center = Pt(screen->r.min.x + Dx(screen->r)/2, screen->r.min.y + Dy(screen->r)/2); r = Rect(center.x - w/2, center.y - h/2, center.x + w/2, center.y + h/2); /* Draw dialog */ draw(screen, r, colors[1], nil, ZP); border(screen, r, 2, colors[0], ZP); /* Title */ char *title = sheet.save_mode == 1 ? "Save As:" : "Open File:"; string(screen, Pt(r.min.x + 10, r.min.y + 5), colors[0], ZP, font, title); /* Filename with cursor */ char display[256]; snprint(display, sizeof(display), "%s_", sheet.filenamebuf); string(screen, Pt(r.min.x + 10, r.min.y + 20), colors[0], ZP, font, display); } void draw_background(void) { draw(screen, screen->r, colors[2], nil, ZP); } void draw_grid_lines(void) { if(!sheet.gridsnap) return; drawgrid(screen); } void draw_all_boxes(void) { int i; for(i = 0; i < sheet.nboxes; i++) { Box *b = &sheet.boxes[i]; if (b->type >= 0 && b->type < MAXBOXTYPES) { BoxType *bt = &boxtypes[b->type]; if (bt->draw) { bt->draw(b, screen); } } } } void draw_status_line(void) { char buf[256]; InputMode *mode = &input_modes[sheet.current_mode]; snprint(buf, sizeof(buf), "Selected: %d | Mode: %s | Boxes: %d | %s", sheet.selected, mode->name, sheet.nboxes, mode->status); string(screen, Pt(screen->r.min.x + 10, screen->r.max.y - 20), colors[0], ZP, font, buf); } void redraw(void) { DrawStep *step; /* Execute drawing steps from table */ for(step = draw_steps; step->draw; step++) { if(step->condition == 0 || (step->condition == 1 && sheet.gridsnap)) { step->draw(); } } /* Draw mode-specific overlay */ InputMode *mode = &input_modes[sheet.current_mode]; if(mode->draw) mode->draw(); flushimage(display, 1); } int edit_finish(char *buf, int *pos, int maxlen) { USED(buf); USED(pos); USED(maxlen); return 1; /* Signal to exit edit mode */ } int edit_cancel(char *buf, int *pos, int maxlen) { USED(maxlen); buf[0] = '\0'; *pos = 0; return -1; /* Signal cancellation */ } int edit_backspace(char *buf, int *pos, int maxlen) { USED(maxlen); if(*pos > 0) { (*pos)--; buf[*pos] = '\0'; sheet.needredraw = 1; } return 0; } int edit_add_char(char *buf, int *pos, int maxlen) { /* Note: key is passed through global or parameter */ /* For this example, we'll use a global current_key */ return 0; } void cmd_quit(void) { int i; for(i = 0; i < 6; i++) if(colors[i]) freeimage(colors[i]); if(gridcolor) freeimage(gridcolor); closedisplay(display); exits(nil); } void cmd_save(void) { sheet.current_mode = 3; /* Enter filename mode */ sheet.entering_filename = 1; sheet.save_mode = 1; strcpy(sheet.filenamebuf, "/tmp/sheet.spr"); sheet.filenamepos = strlen(sheet.filenamebuf); sheet.needredraw = 1; } void cmd_save_as(void) { sheet.current_mode = 3; sheet.entering_filename = 1; sheet.save_mode = 1; sheet.filenamebuf[0] = '\0'; sheet.filenamepos = 0; sheet.needredraw = 1; } void cmd_open(void) { sheet.current_mode = 3; sheet.entering_filename = 1; sheet.save_mode = 2; strcpy(sheet.filenamebuf, "/tmp/sheet.spr"); sheet.filenamepos = strlen(sheet.filenamebuf); sheet.needredraw = 1; } void cmd_open_file(void) { sheet.current_mode = 3; sheet.entering_filename = 1; sheet.save_mode = 2; sheet.filenamebuf[0] = '\0'; sheet.filenamepos = 0; sheet.needredraw = 1; } void cmd_start_label(void) { if(sheet.selected >= 0) { sheet.current_mode = 2; /* Label edit mode */ sheet.editing_label = sheet.selected; strncpy(sheet.labelbuf, sheet.boxes[sheet.selected].label, 31); sheet.labelbuf[31] = '\0'; sheet.labelpos = strlen(sheet.labelbuf); sheet.needredraw = 1; } } void cmd_toggle_grid(void) { sheet.gridsnap = !sheet.gridsnap; sheet.needredraw = 1; } void cmd_delete_box(void) { if(sheet.selected >= 0) { delbox(sheet.selected); sheet.selected = -1; sheet.needredraw = 1; } } void handle_normal_mode(int key) { Command *cmd; /* Look up command in table */ for(cmd = commands; cmd->action; cmd++) { if(cmd->key == key) { cmd->action(); return; } } /* No match - could show help or ignore */ } void handle_cell_edit(int key) { Box *b = &sheet.boxes[sheet.editing]; if(key == '\n') { /* Save the edit */ strcpy(b->formula, sheet.editbuf); /* Determine type */ if(sheet.editbuf[0] == '=') { b->type = T_FORMULA; } else { char *endp; strtod(sheet.editbuf, &endp); b->type = (*endp == '\0') ? T_NUMBER : T_TEXT; } /* Parse and evaluate */ if (b->type >= 0 && b->type < MAXBOXTYPES) { BoxType *bt = &boxtypes[b->type]; if (bt->parse) bt->parse(b); if (bt->eval) bt->eval(b); } recalc_all(); /* Return to normal mode */ sheet.editing = -1; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == Kesc) { /* Cancel */ sheet.editing = -1; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == Kbs) { /* Backspace */ if(sheet.editpos > 0) { sheet.editpos--; sheet.editbuf[sheet.editpos] = '\0'; sheet.needredraw = 1; } } else if(key >= 32 && key < 127 && sheet.editpos < MAXCONTENT-1) { /* Add character */ sheet.editbuf[sheet.editpos++] = key; sheet.editbuf[sheet.editpos] = '\0'; sheet.needredraw = 1; } } void handle_label_edit(int key) { Box *b = &sheet.boxes[sheet.editing_label]; if(key == '\n') { /* Save label */ strncpy(b->label, sheet.labelbuf, 31); b->label[31] = '\0'; sheet.editing_label = -1; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == Kesc) { /* Cancel */ sheet.editing_label = -1; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == Kbs) { /* Backspace */ if(sheet.labelpos > 0) { sheet.labelpos--; sheet.labelbuf[sheet.labelpos] = '\0'; sheet.needredraw = 1; } } else if(key >= 32 && key < 127 && sheet.labelpos < 30) { /* Add character */ sheet.labelbuf[sheet.labelpos++] = key; sheet.labelbuf[sheet.labelpos] = '\0'; sheet.needredraw = 1; } } void save(char *file) { int fd; Biobuf *b; int i; Box *box; fd = create(file, OWRITE, 0644); if(fd < 0){ fprint(2, "cannot create %s: %r\n", file); return; } b = Bfdopen(fd, OWRITE); for(i = 0; i < sheet.nboxes; i++){ box = &sheet.boxes[i]; Bprint(b, "box %d\n", i); Bprint(b, " pos %d %d\n", box->pos.x, box->pos.y); Bprint(b, " type %d\n", box->type); Bprint(b, " formula %s\n", box->formula); Bprint(b, " value %g\n", box->value); Bprint(b, " label %s\n", box->label); if(box->nrefs > 0){ Bprint(b, " refs"); int j; for(j = 0; j < box->nrefs; j++) Bprint(b, " %d", box->refs[j]); Bprint(b, "\n"); } } Bterm(b); close(fd); } void load(char *file) { Biobuf *b; char *line; char *fields[10]; int nf; b = Bopen(file, OREAD); if(b == nil){ fprint(2, "cannot open %s: %r\n", file); return; } sheet.nboxes = 0; while((line = Brdline(b, '\n')) != nil){ line[Blinelen(b)-1] = '\0'; if(line[0] == '#' || line[0] == '\0') continue; nf = tokenize(line, fields, nelem(fields)); if(nf >= 2 && strcmp(fields[0], "box") == 0){ if(sheet.nboxes < MAXBOXES) sheet.nboxes++; } else if(nf >= 3 && strcmp(fields[0], "pos") == 0){ Box *box = &sheet.boxes[sheet.nboxes-1]; box->pos.x = atoi(fields[1]); box->pos.y = atoi(fields[2]); box->r = Rect(box->pos.x, box->pos.y, box->pos.x + BOXWIDTH, box->pos.y + BOXHEIGHT); } else if(nf >= 2 && strcmp(fields[0], "type") == 0){ int type = atoi(fields[1]); if (type >= 0 && type < MAXBOXTYPES) { sheet.boxes[sheet.nboxes-1].type = type; } else { /* Default to text type if file is corrupted */ sheet.boxes[sheet.nboxes-1].type = T_TEXT; } } else if(nf >= 2 && strcmp(fields[0], "formula") == 0){ strcpy(sheet.boxes[sheet.nboxes-1].formula, fields[1]); } else if(nf >= 2 && strcmp(fields[0], "value") == 0){ sheet.boxes[sheet.nboxes-1].value = atof(fields[1]); } else if(nf >= 2 && strcmp(fields[0], "label") == 0){ strncpy(sheet.boxes[sheet.nboxes-1].label, fields[1], 31); } } Bterm(b); int i; for(i = 0; i < sheet.nboxes; i++){ Box *box = &sheet.boxes[i]; if (box->type >= 0 && box->type < MAXBOXTYPES) { BoxType *bt = &boxtypes[box->type]; if (bt->parse) bt->parse(box); if (bt->eval) bt->eval(box); } } redraw(); } void handle_filename_input(int key) { if(key == '\n') { /* Execute save/load */ if(sheet.filenamebuf[0] == '\0') { strcpy(sheet.filenamebuf, "/tmp/sheet.spr"); } if(sheet.save_mode == 1) { save(sheet.filenamebuf); } else { load(sheet.filenamebuf); recalc_all(); } sheet.entering_filename = 0; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == Kesc) { /* Cancel */ sheet.entering_filename = 0; sheet.current_mode = 0; sheet.needredraw = 1; } else if(key == '\t') { /* Tab completion */ if(!strstr(sheet.filenamebuf, ".spr")) { strcat(sheet.filenamebuf, ".spr"); sheet.filenamepos = strlen(sheet.filenamebuf); sheet.needredraw = 1; } } else if(key == Kbs) { /* Backspace */ if(sheet.filenamepos > 0) { sheet.filenamepos--; sheet.filenamebuf[sheet.filenamepos] = '\0'; sheet.needredraw = 1; } } else if(key >= 32 && key < 127 && sheet.filenamepos < 250) { /* Add character */ sheet.filenamebuf[sheet.filenamepos++] = key; sheet.filenamebuf[sheet.filenamepos] = '\0'; sheet.needredraw = 1; } } void handle_normal_mouse(Mouse m) { int i; if(m.buttons & 1){ /* Left click */ i = boxat(m.xy); if(i >= 0){ /* Select existing box */ sheet.selected = i; /* Drag box with button held */ while(m.buttons & 1){ sheet.boxes[i].pos = subpt(m.xy, Pt(BOXWIDTH/2, BOXHEIGHT/2)); if(sheet.gridsnap){ sheet.boxes[i].pos.x = (sheet.boxes[i].pos.x / sheet.gridsize) * sheet.gridsize; sheet.boxes[i].pos.y = (sheet.boxes[i].pos.y / sheet.gridsize) * sheet.gridsize; } sheet.boxes[i].r = Rect(sheet.boxes[i].pos.x, sheet.boxes[i].pos.y, sheet.boxes[i].pos.x + BOXWIDTH, sheet.boxes[i].pos.y + BOXHEIGHT); redraw(); m = emouse(); } } else { /* Create new box */ i = addbox(m.xy); if(i >= 0){ sheet.selected = i; sheet.editing = i; sheet.current_mode = 1; sheet.editbuf[0] = '\0'; sheet.editpos = 0; } } sheet.needredraw = 1; } if(m.buttons & 2){ /* Middle click - edit box */ i = boxat(m.xy); if(i >= 0){ sheet.selected = i; sheet.editing = i; sheet.current_mode = 1; strcpy(sheet.editbuf, sheet.boxes[i].formula); sheet.editpos = strlen(sheet.editbuf); sheet.needredraw = 1; } } } void handle_edit_mouse(Mouse m) { if(m.buttons & 4){ Box *b = &sheet.boxes[sheet.editing]; /* Save the edit */ strcpy(b->formula, sheet.editbuf); /* Determine type */ if(sheet.editbuf[0] == '=') { b->type = T_FORMULA; } else { char *endp; strtod(sheet.editbuf, &endp); b->type = (*endp == '\0') ? T_NUMBER : T_TEXT; } /* Parse and evaluate */ if (b->type >= 0 && b->type < MAXBOXTYPES) { BoxType *bt = &boxtypes[b->type]; if (bt->parse) bt->parse(b); if (bt->eval) bt->eval(b); } recalc_all(); /* Return to normal mode */ sheet.editing = -1; sheet.current_mode = 0; sheet.needredraw = 1; } /* Clicking outside the box could cancel the edit */ int i = boxat(m.xy); if(i != sheet.editing && (m.buttons & 1)){ sheet.editing = -1; sheet.current_mode = 0; sheet.needredraw = 1; } } void handle_label_mouse(Mouse m) { /* Clicking outside the box could cancel the label edit */ int i = boxat(m.xy); if(i != sheet.editing_label && (m.buttons & 1)){ sheet.editing_label = -1; sheet.current_mode = 0; sheet.needredraw = 1; } } void handle_filename_mouse(Mouse m) { /* Clicks do nothing in this mode */ USED(m); } void eresized(int new) { if(new && getwindow(display, Refnone) < 0) sysfatal("can't reattach to window"); } void main(int argc, char *argv[]) { Event e; USED(argc); USED(argv); if(initdraw(nil, nil, "freecell") < 0) sysfatal("initdraw: %r"); initcolors(); einit(Emouse | Ekeyboard); eresized(0); memset(&sheet, 0, sizeof(sheet)); sheet.selected = -1; sheet.editing = -1; sheet.editing_label = -1; sheet.entering_filename = 0; sheet.current_mode = 0; sheet.gridsize = 32; sheet.gridsnap = 1; redraw(); for(;;){ switch(event(&e)){ case Emouse: handlemouse(e.mouse); break; case Ekeyboard: handlekey(e.kbdc); break; } if(sheet.needredraw){ redraw(); sheet.needredraw = 0; } } }