ref: c5d556434652b8f00fb0ec89fe2d1fc162f5e4c5
author: glenda <glenda@krsna>
date: Sat Aug 16 15:48:47 EDT 2025
ry-again
--- /dev/null
+++ b/fc.c
@@ -1,0 +1,1897 @@
+#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;
+ }
+ }
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,24 @@
+
+</$objtype/mkfile
+
+# Compiler flags
+CFLAGS=-FVTw
+
+# Targets
+BIN=$home/bin/amd64
+TARG=fc
+
+# Source files
+GAME=fc.6
+
+# Build rules
+all:V: $BIN/$TARG
+
+$GAME: $TARG.c
+ 6c $CFLAGS $TARG.c
+
+$BIN/$TARG: $GAME
+ 6l -o $target $GAME
+
+nuke:
+ rm -f *.6 $BIN/$TARG
--
⑨