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;
}
}
}