use std::collections::HashMap;
use std::time::{Duration, Instant};
use saudade::{
App, Color, Container, Event, EventCtx, Key, MouseButton, NamedKey, Painter, Point, Rect,
SCROLLBAR_THICKNESS, ScrollBar, TextInput, Theme, Widget, WindowConfig,
};
const W: i32 = 760;
const H: i32 = 460;
const GRID_RECT: Rect = Rect::new(8, 8, W - 16, H - 16);
const COLS: i32 = 26;
const ROWS: i32 = 100;
const COL_W: i32 = 64;
const ROW_H: i32 = 18;
const HEAD_W: i32 = 34;
const HEAD_H: i32 = 18;
const DOUBLE_CLICK: Duration = Duration::from_millis(400);
const GRID_LINE: Color = Color::rgb(0xD0, 0xD0, 0xD0);
fn main() {
let root = Container::new(W, H).add(Grid::new(seed_sheet()));
App::new(WindowConfig::new("Cells", W, H), root)
.with_theme(Theme::windows_31())
.run();
}
fn seed_sheet() -> Sheet {
let mut sheet = Sheet::new();
let rows = [
("A0", "Item"),
("B0", "Qty"),
("C0", "Price"),
("D0", "Total"),
("A1", "Apples"),
("B1", "3"),
("C1", "2"),
("D1", "=B1*C1"),
("A2", "Pears"),
("B2", "5"),
("C2", "1.5"),
("D2", "=B2*C2"),
("A3", "Sum"),
("D3", "=SUM(D1:D2)"),
("A4", "Avg price"),
("D4", "=AVG(C1:C2)"),
];
for (cell, content) in rows {
sheet.set(parse_ref(cell).unwrap(), content.to_string());
}
sheet
}
type CellRef = (u8, u8);
#[derive(Clone, Copy, PartialEq, Eq)]
enum EvalError {
Cycle,
DivZero,
Bad,
}
impl EvalError {
fn marker(self) -> &'static str {
match self {
EvalError::Cycle => "#CYCLE!",
EvalError::DivZero => "#DIV/0!",
EvalError::Bad => "#ERROR!",
}
}
}
struct Sheet {
content: HashMap<CellRef, String>,
}
impl Sheet {
fn new() -> Self {
Self {
content: HashMap::new(),
}
}
fn raw(&self, cell: CellRef) -> &str {
self.content.get(&cell).map(String::as_str).unwrap_or("")
}
fn set(&mut self, cell: CellRef, text: String) {
if text.trim().is_empty() {
self.content.remove(&cell);
} else {
self.content.insert(cell, text);
}
}
fn display(&self, cell: CellRef) -> String {
let raw = self.raw(cell).trim();
if raw.is_empty() {
String::new()
} else if raw.starts_with('=') {
match self.value_of(cell) {
Ok(n) => format_number(n),
Err(e) => e.marker().to_string(),
}
} else {
raw.to_string()
}
}
fn value_of(&self, cell: CellRef) -> Result<f64, EvalError> {
let mut path = Vec::new();
let mut memo = HashMap::new();
self.value(cell, &mut path, &mut memo)
}
fn value(
&self,
cell: CellRef,
path: &mut Vec<CellRef>,
memo: &mut HashMap<CellRef, Result<f64, EvalError>>,
) -> Result<f64, EvalError> {
if let Some(cached) = memo.get(&cell) {
return *cached;
}
let raw = self.raw(cell).trim();
let result = if raw.is_empty() {
Ok(0.0)
} else if let Some(formula) = raw.strip_prefix('=') {
if path.contains(&cell) {
return Err(EvalError::Cycle);
}
path.push(cell);
let r = tokenize(formula)
.and_then(|toks| Parser::new(toks).parse())
.and_then(|node| self.eval_node(&node, path, memo));
path.pop();
r
} else {
Ok(raw.parse::<f64>().unwrap_or(0.0))
};
memo.insert(cell, result);
result
}
fn eval_node(
&self,
node: &Node,
path: &mut Vec<CellRef>,
memo: &mut HashMap<CellRef, Result<f64, EvalError>>,
) -> Result<f64, EvalError> {
match node {
Node::Num(n) => Ok(*n),
Node::Ref(c) => self.value(*c, path, memo),
Node::Range(..) => Err(EvalError::Bad),
Node::Neg(inner) => Ok(-self.eval_node(inner, path, memo)?),
Node::Bin(op, l, r) => {
let a = self.eval_node(l, path, memo)?;
let b = self.eval_node(r, path, memo)?;
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' if b == 0.0 => Err(EvalError::DivZero),
'/' => Ok(a / b),
_ => Err(EvalError::Bad),
}
}
Node::Func(name, args) => {
let values = self.eval_args(args, path, memo)?;
apply_func(name, &values)
}
}
}
fn eval_args(
&self,
args: &[Node],
path: &mut Vec<CellRef>,
memo: &mut HashMap<CellRef, Result<f64, EvalError>>,
) -> Result<Vec<f64>, EvalError> {
let mut out = Vec::new();
for arg in args {
match arg {
Node::Range(a, b) => {
for cell in range_cells(*a, *b) {
out.push(self.value(cell, path, memo)?);
}
}
other => out.push(self.eval_node(other, path, memo)?),
}
}
Ok(out)
}
}
fn range_cells(a: CellRef, b: CellRef) -> Vec<CellRef> {
let (c0, c1) = (a.0.min(b.0), a.0.max(b.0));
let (r0, r1) = (a.1.min(b.1), a.1.max(b.1));
let mut cells = Vec::new();
for c in c0..=c1 {
for r in r0..=r1 {
cells.push((c, r));
}
}
cells
}
fn apply_func(name: &str, values: &[f64]) -> Result<f64, EvalError> {
match name {
"SUM" => Ok(values.iter().sum()),
"PRODUCT" | "PROD" => Ok(values.iter().product()),
"AVG" | "AVERAGE" | "MEAN" => {
if values.is_empty() {
Ok(0.0)
} else {
Ok(values.iter().sum::<f64>() / values.len() as f64)
}
}
"MIN" => Ok(values.iter().copied().fold(f64::INFINITY, f64::min))
.map(|v| if v.is_finite() { v } else { 0.0 }),
"MAX" => Ok(values.iter().copied().fold(f64::NEG_INFINITY, f64::max))
.map(|v| if v.is_finite() { v } else { 0.0 }),
"COUNT" => Ok(values.len() as f64),
_ => Err(EvalError::Bad),
}
}
fn format_number(n: f64) -> String {
if !n.is_finite() {
return "#NUM!".to_string();
}
let rounded = (n * 10_000.0).round() / 10_000.0;
if (rounded - rounded.round()).abs() < 1e-9 {
format!("{}", rounded.round() as i64)
} else {
let mut s = format!("{rounded:.4}");
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
s
}
}
#[derive(Clone, PartialEq)]
enum Token {
Num(f64),
Cell(CellRef),
Func(String),
LParen,
RParen,
Comma,
Colon,
Op(char),
}
enum Node {
Num(f64),
Ref(CellRef),
Range(CellRef, CellRef),
Neg(Box<Node>),
Bin(char, Box<Node>, Box<Node>),
Func(String, Vec<Node>),
}
fn parse_ref(label: &str) -> Option<CellRef> {
let toks = tokenize(label).ok()?;
match toks.as_slice() {
[Token::Cell(c)] => Some(*c),
_ => None,
}
}
fn tokenize(s: &str) -> Result<Vec<Token>, EvalError> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
let mut toks = Vec::new();
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
} else if c.is_ascii_digit() || c == '.' {
let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let num: String = chars[start..i].iter().collect();
toks.push(Token::Num(num.parse().map_err(|_| EvalError::Bad)?));
} else if c.is_ascii_alphabetic() {
let start = i;
while i < chars.len() && chars[i].is_ascii_alphabetic() {
i += 1;
}
let word: String = chars[start..i].iter().collect();
if i < chars.len() && chars[i] == '(' {
toks.push(Token::Func(word.to_ascii_uppercase()));
} else if word.len() == 1 && i < chars.len() && chars[i].is_ascii_digit() {
let col = word.chars().next().unwrap().to_ascii_uppercase() as i32 - 'A' as i32;
let dstart = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let row: i32 = chars[dstart..i]
.iter()
.collect::<String>()
.parse()
.map_err(|_| EvalError::Bad)?;
if !(0..COLS).contains(&col) || !(0..ROWS).contains(&row) {
return Err(EvalError::Bad);
}
toks.push(Token::Cell((col as u8, row as u8)));
} else {
return Err(EvalError::Bad);
}
} else {
let tok = match c {
'+' | '-' | '*' | '/' => Token::Op(c),
'(' => Token::LParen,
')' => Token::RParen,
',' => Token::Comma,
':' => Token::Colon,
_ => return Err(EvalError::Bad),
};
toks.push(tok);
i += 1;
}
}
Ok(toks)
}
struct Parser {
toks: Vec<Token>,
pos: usize,
}
impl Parser {
fn new(toks: Vec<Token>) -> Self {
Self { toks, pos: 0 }
}
fn peek(&self) -> Option<&Token> {
self.toks.get(self.pos)
}
fn bump(&mut self) -> Option<Token> {
let tok = self.toks.get(self.pos).cloned();
if tok.is_some() {
self.pos += 1;
}
tok
}
fn eat(&mut self, expected: &Token) -> Result<(), EvalError> {
if self.peek() == Some(expected) {
self.pos += 1;
Ok(())
} else {
Err(EvalError::Bad)
}
}
fn parse(mut self) -> Result<Node, EvalError> {
let node = self.parse_expr()?;
if self.pos == self.toks.len() {
Ok(node)
} else {
Err(EvalError::Bad)
}
}
fn parse_expr(&mut self) -> Result<Node, EvalError> {
let mut left = self.parse_term()?;
while let Some(Token::Op(op @ ('+' | '-'))) = self.peek().cloned() {
self.pos += 1;
let right = self.parse_term()?;
left = Node::Bin(op, Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_term(&mut self) -> Result<Node, EvalError> {
let mut left = self.parse_factor()?;
while let Some(Token::Op(op @ ('*' | '/'))) = self.peek().cloned() {
self.pos += 1;
let right = self.parse_factor()?;
left = Node::Bin(op, Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_factor(&mut self) -> Result<Node, EvalError> {
match self.bump() {
Some(Token::Num(n)) => Ok(Node::Num(n)),
Some(Token::Op('-')) => Ok(Node::Neg(Box::new(self.parse_factor()?))),
Some(Token::Op('+')) => self.parse_factor(),
Some(Token::LParen) => {
let inner = self.parse_expr()?;
self.eat(&Token::RParen)?;
Ok(inner)
}
Some(Token::Func(name)) => {
self.eat(&Token::LParen)?;
let args = self.parse_args()?;
self.eat(&Token::RParen)?;
Ok(Node::Func(name, args))
}
Some(Token::Cell(c)) => {
if self.peek() == Some(&Token::Colon) {
self.pos += 1;
match self.bump() {
Some(Token::Cell(c2)) => Ok(Node::Range(c, c2)),
_ => Err(EvalError::Bad),
}
} else {
Ok(Node::Ref(c))
}
}
_ => Err(EvalError::Bad),
}
}
fn parse_args(&mut self) -> Result<Vec<Node>, EvalError> {
let mut args = Vec::new();
if self.peek() == Some(&Token::RParen) {
return Ok(args);
}
loop {
args.push(self.parse_expr()?);
if self.peek() == Some(&Token::Comma) {
self.pos += 1;
} else {
break;
}
}
Ok(args)
}
}
fn corner_rect() -> Rect {
Rect::new(GRID_RECT.x, GRID_RECT.y, HEAD_W, HEAD_H)
}
fn vbar_rect() -> Rect {
Rect::new(
GRID_RECT.right() - SCROLLBAR_THICKNESS,
GRID_RECT.y,
SCROLLBAR_THICKNESS,
GRID_RECT.h - SCROLLBAR_THICKNESS,
)
}
fn hbar_rect() -> Rect {
Rect::new(
GRID_RECT.x,
GRID_RECT.bottom() - SCROLLBAR_THICKNESS,
GRID_RECT.w - SCROLLBAR_THICKNESS,
SCROLLBAR_THICKNESS,
)
}
fn viewport_rect() -> Rect {
Rect::new(
GRID_RECT.x + HEAD_W,
GRID_RECT.y + HEAD_H,
GRID_RECT.w - HEAD_W - SCROLLBAR_THICKNESS,
GRID_RECT.h - HEAD_H - SCROLLBAR_THICKNESS,
)
}
fn visible_cols() -> i32 {
(viewport_rect().w / COL_W).max(1)
}
fn visible_rows() -> i32 {
(viewport_rect().h / ROW_H).max(1)
}
fn col_label(col: u8) -> String {
((b'A' + col) as char).to_string()
}
struct Grid {
sheet: Sheet,
sel: CellRef,
editing: Option<CellRef>,
input: TextInput,
vbar: ScrollBar, hbar: ScrollBar, focused: bool,
last_click: Option<(CellRef, Instant)>,
}
impl Grid {
fn new(sheet: Sheet) -> Self {
let mut vbar = ScrollBar::vertical(vbar_rect());
let mut hbar = ScrollBar::horizontal(hbar_rect());
vbar.set_range(visible_rows(), (ROWS - visible_rows()).max(0));
hbar.set_range(visible_cols(), (COLS - visible_cols()).max(0));
Self {
sheet,
sel: (0, 0),
editing: None,
input: TextInput::new(Rect::new(0, 0, COL_W, ROW_H)),
vbar,
hbar,
focused: false,
last_click: None,
}
}
fn sync_scroll_ranges(&mut self) {
self.hbar
.set_range(visible_cols(), (COLS - visible_cols()).max(0));
self.vbar
.set_range(visible_rows(), (ROWS - visible_rows()).max(0));
}
fn cell_rect(&self, cell: CellRef) -> Option<Rect> {
let vp = viewport_rect();
let dc = cell.0 as i32 - self.hbar.value();
let dr = cell.1 as i32 - self.vbar.value();
if dc < 0 || dc >= visible_cols() || dr < 0 || dr >= visible_rows() {
return None;
}
Some(Rect::new(
vp.x + dc * COL_W,
vp.y + dr * ROW_H,
COL_W,
ROW_H,
))
}
fn cell_at(&self, pos: Point) -> Option<CellRef> {
let vp = viewport_rect();
if !vp.contains(pos) {
return None;
}
let col = self.hbar.value() + (pos.x - vp.x) / COL_W;
let row = self.vbar.value() + (pos.y - vp.y) / ROW_H;
if (0..COLS).contains(&col) && (0..ROWS).contains(&row) {
Some((col as u8, row as u8))
} else {
None
}
}
fn move_sel(&mut self, dc: i32, dr: i32) {
self.sel = (
(self.sel.0 as i32 + dc).clamp(0, COLS - 1) as u8,
(self.sel.1 as i32 + dr).clamp(0, ROWS - 1) as u8,
);
self.ensure_visible();
}
fn ensure_visible(&mut self) {
self.sync_scroll_ranges();
let (c, r) = (self.sel.0 as i32, self.sel.1 as i32);
let (left, top) = (self.hbar.value(), self.vbar.value());
if c < left {
self.hbar.set_value(c);
} else if c >= left + visible_cols() {
self.hbar.set_value(c - visible_cols() + 1);
}
if r < top {
self.vbar.set_value(r);
} else if r >= top + visible_rows() {
self.vbar.set_value(r - visible_rows() + 1);
}
}
fn start_edit(&mut self, cell: CellRef, initial: Option<String>) {
self.sel = cell;
self.ensure_visible();
let text = initial.unwrap_or_else(|| self.sheet.raw(cell).to_string());
self.input.set_text(&text);
self.input.set_focused(true);
if let Some(rect) = self.cell_rect(cell) {
self.input.layout(rect);
}
self.editing = Some(cell);
}
fn commit_edit(&mut self) {
if let Some(cell) = self.editing.take() {
self.sheet.set(cell, self.input.text());
self.input.set_focused(false);
}
}
fn event_editing(&mut self, event: &Event, ctx: &mut EventCtx) {
if let Some(cell) = self.editing
&& let Some(rect) = self.cell_rect(cell)
{
self.input.layout(rect);
}
match event {
Event::KeyDown {
key: Key::Named(NamedKey::Enter),
..
} => {
self.commit_edit();
self.move_sel(0, 1);
ctx.request_paint();
}
Event::KeyDown {
key: Key::Named(NamedKey::Escape),
..
} => {
self.editing = None;
self.input.set_focused(false);
ctx.request_paint();
}
Event::KeyDown {
key: Key::Named(NamedKey::Tab),
..
} => {
self.commit_edit();
self.move_sel(1, 0);
ctx.consume_event();
ctx.request_paint();
}
Event::PointerDown {
pos,
button: MouseButton::Left,
..
} => {
if self.input.bounds().contains(*pos) {
self.input.event(event, ctx);
} else {
self.commit_edit();
if let Some(cell) = self.cell_at(*pos) {
self.sel = cell;
}
ctx.request_paint();
}
}
_ => self.input.event(event, ctx),
}
}
fn event_grid(&mut self, event: &Event, ctx: &mut EventCtx) {
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
..
} => {
ctx.request_focus();
if let Some(cell) = self.cell_at(*pos) {
let now = Instant::now();
let double = self
.last_click
.is_some_and(|(c, t)| c == cell && now.duration_since(t) <= DOUBLE_CLICK);
self.sel = cell;
if double {
self.last_click = None;
self.start_edit(cell, None);
} else {
self.last_click = Some((cell, now));
}
}
ctx.request_paint();
}
Event::KeyDown {
key: Key::Named(named),
modifiers,
} if self.focused && !modifiers.has_command() => {
match named {
NamedKey::Up => self.move_sel(0, -1),
NamedKey::Down => self.move_sel(0, 1),
NamedKey::Left => self.move_sel(-1, 0),
NamedKey::Right => self.move_sel(1, 0),
NamedKey::Enter => self.start_edit(self.sel, None),
NamedKey::Backspace | NamedKey::Delete => {
self.sheet.set(self.sel, String::new());
}
_ => {}
}
ctx.request_paint();
}
Event::Char { ch, modifiers }
if self.focused && !modifiers.has_command() && *ch >= ' ' =>
{
self.start_edit(self.sel, Some(ch.to_string()));
ctx.request_paint();
}
_ => {}
}
}
}
impl Widget for Grid {
fn bounds(&self) -> Rect {
GRID_RECT
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.sync_scroll_ranges();
let (left, top) = (self.hbar.value(), self.vbar.value());
let vp = viewport_rect();
painter.fill_rect(GRID_RECT, theme.face);
painter.button(corner_rect(), theme, false, false);
for vc in 0..visible_cols() {
let col = left + vc;
if col >= COLS {
break;
}
let rect = Rect::new(vp.x + vc * COL_W, GRID_RECT.y, COL_W, HEAD_H);
draw_header(
painter,
theme,
rect,
&col_label(col as u8),
col == self.sel.0 as i32,
);
}
for vr in 0..visible_rows() {
let row = top + vr;
if row >= ROWS {
break;
}
let rect = Rect::new(GRID_RECT.x, vp.y + vr * ROW_H, HEAD_W, ROW_H);
draw_header(
painter,
theme,
rect,
&row.to_string(),
row == self.sel.1 as i32,
);
}
let saved = painter.push_clip(vp);
for vr in 0..visible_rows() {
let row = top + vr;
if row >= ROWS {
break;
}
for vc in 0..visible_cols() {
let col = left + vc;
if col >= COLS {
break;
}
let cell = (col as u8, row as u8);
let rect = Rect::new(vp.x + vc * COL_W, vp.y + vr * ROW_H, COL_W, ROW_H);
painter.fill_rect(rect, Color::WHITE);
painter.h_line(rect.x, rect.bottom() - 1, rect.w, GRID_LINE);
painter.v_line(rect.right() - 1, rect.y, rect.h, GRID_LINE);
if self.editing != Some(cell) {
let text = self.sheet.display(cell);
if !text.is_empty() {
let ty = rect.y + (rect.h - theme.font_size as i32) / 2;
let clip = painter.push_clip(rect.inset(1));
painter.text(rect.x + 3, ty, &text, theme.font_size, theme.text);
painter.restore_clip(clip);
}
}
if cell == self.sel && self.editing.is_none() {
painter.stroke_rect(rect, theme.highlight_bg);
painter.stroke_rect(rect.inset(1), theme.highlight_bg);
}
}
}
painter.restore_clip(saved);
if let Some(cell) = self.editing
&& let Some(rect) = self.cell_rect(cell)
{
self.input.layout(rect);
self.input.paint(painter, theme);
}
self.vbar.paint(painter, theme);
self.hbar.paint(painter, theme);
painter.stroke_rect(GRID_RECT, theme.border);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if self.vbar.captures_pointer() {
self.vbar.event(event, ctx);
ctx.request_paint();
return;
}
if self.hbar.captures_pointer() {
self.hbar.event(event, ctx);
ctx.request_paint();
return;
}
if let Event::Scroll { pos, .. } = event {
if viewport_rect().contains(*pos) {
self.vbar.event(event, ctx);
self.hbar.event(event, ctx);
ctx.request_paint();
}
return;
}
if let Some(pos) = event.position() {
if vbar_rect().contains(pos) {
self.vbar.event(event, ctx);
ctx.request_paint();
return;
}
if hbar_rect().contains(pos) {
self.hbar.event(event, ctx);
ctx.request_paint();
return;
}
}
if self.editing.is_some() {
self.event_editing(event, ctx);
} else {
self.event_grid(event, ctx);
}
}
fn captures_pointer(&self) -> bool {
self.vbar.captures_pointer() || self.hbar.captures_pointer()
}
fn focusable(&self) -> bool {
true
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if !focused {
self.commit_edit();
}
}
fn wants_ticks(&self) -> bool {
self.editing.is_some()
}
}
fn draw_header(painter: &mut Painter, theme: &Theme, rect: Rect, label: &str, selected: bool) {
painter.button(rect, theme, false, false);
let fg = if selected {
painter.fill_rect(rect.inset(1), theme.highlight_bg);
theme.highlight_text
} else {
theme.text
};
painter.text_centered(rect, label, theme.font_size, fg);
}
#[cfg(test)]
mod tests {
use super::*;
fn sheet(cells: &[(&str, &str)]) -> Sheet {
let mut s = Sheet::new();
for (r, c) in cells {
s.set(parse_ref(r).unwrap(), c.to_string());
}
s
}
fn display(s: &Sheet, cell: &str) -> String {
s.display(parse_ref(cell).unwrap())
}
#[test]
fn parses_cell_references() {
assert_eq!(parse_ref("A0"), Some((0, 0)));
assert_eq!(parse_ref("Z99"), Some((25, 99)));
assert_eq!(parse_ref("b12"), Some((1, 12))); assert_eq!(parse_ref("A100"), None); assert_eq!(parse_ref("AA1"), None); }
#[test]
fn literals_and_text_display_verbatim() {
let s = sheet(&[("A0", "Hello"), ("B0", "42"), ("C0", "3.5")]);
assert_eq!(display(&s, "A0"), "Hello");
assert_eq!(display(&s, "B0"), "42");
assert_eq!(display(&s, "C0"), "3.5");
assert_eq!(display(&s, "Z9"), ""); }
#[test]
fn arithmetic_respects_precedence_and_parentheses() {
let s = sheet(&[
("A0", "=1+2*3"),
("A1", "=(1+2)*3"),
("A2", "=10/4"),
("A3", "=-2 + 5"),
]);
assert_eq!(display(&s, "A0"), "7");
assert_eq!(display(&s, "A1"), "9");
assert_eq!(display(&s, "A2"), "2.5");
assert_eq!(display(&s, "A3"), "3");
}
#[test]
fn references_and_change_propagation() {
let mut s = sheet(&[("A0", "2"), ("A1", "3"), ("B0", "=A0+A1"), ("C0", "=B0*2")]);
assert_eq!(display(&s, "B0"), "5");
assert_eq!(display(&s, "C0"), "10");
s.set((0, 0), "10".to_string());
assert_eq!(display(&s, "B0"), "13");
assert_eq!(display(&s, "C0"), "26");
}
#[test]
fn functions_over_ranges() {
let s = sheet(&[
("A0", "1"),
("A1", "2"),
("A2", "3"),
("A3", "4"),
("B0", "=SUM(A0:A3)"),
("B1", "=AVG(A0:A3)"),
("B2", "=MIN(A0:A3)"),
("B3", "=MAX(A0:A3)"),
("B4", "=COUNT(A0:A3)"),
("B5", "=SUM(A0:A3) + 10"),
]);
assert_eq!(display(&s, "B0"), "10");
assert_eq!(display(&s, "B1"), "2.5");
assert_eq!(display(&s, "B2"), "1");
assert_eq!(display(&s, "B3"), "4");
assert_eq!(display(&s, "B4"), "4");
assert_eq!(display(&s, "B5"), "20");
}
#[test]
fn errors_are_reported_not_fatal() {
let s = sheet(&[
("A0", "=1/0"),
("A1", "=1+"),
("A2", "=NOPE(1)"),
("A3", "=A0"), ]);
assert_eq!(display(&s, "A0"), "#DIV/0!");
assert_eq!(display(&s, "A1"), "#ERROR!");
assert_eq!(display(&s, "A2"), "#ERROR!");
assert_eq!(display(&s, "A3"), "#DIV/0!");
}
#[test]
fn reference_cycles_are_detected() {
let direct = sheet(&[("A0", "=A0")]);
assert_eq!(display(&direct, "A0"), "#CYCLE!");
let indirect = sheet(&[("A0", "=A1"), ("A1", "=A2"), ("A2", "=A0")]);
assert_eq!(display(&indirect, "A0"), "#CYCLE!");
assert_eq!(display(&indirect, "A1"), "#CYCLE!");
}
#[test]
fn diamond_dependencies_are_not_cycles() {
let s = sheet(&[
("A0", "5"),
("B0", "=A0+1"),
("B1", "=A0+2"),
("C0", "=B0+B1"),
]);
assert_eq!(display(&s, "C0"), "13");
}
#[test]
fn grid_edits_and_scrolls_without_panicking() {
use saudade::Modifiers;
use saudade::mock::MockBackend;
let mut grid = Grid::new(seed_sheet());
grid.set_focused(true);
let backend = MockBackend::new(W, H);
backend.render(&mut grid);
let click = Event::PointerDown {
pos: Point::new(366, 120),
button: MouseButton::Left,
modifiers: Modifiers::default(),
};
backend.dispatch(&mut grid, &click);
backend.dispatch(&mut grid, &click);
backend.render(&mut grid);
for ch in "=6*7".chars() {
backend.dispatch(
&mut grid,
&Event::Char {
ch,
modifiers: Modifiers::default(),
},
);
}
backend.dispatch(
&mut grid,
&Event::KeyDown {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
},
);
assert_eq!(grid.sheet.display((5, 5)), "42");
backend.render(&mut grid);
backend.dispatch(
&mut grid,
&Event::PointerDown {
pos: Point::new(744, 30),
button: MouseButton::Left,
modifiers: Modifiers::default(),
},
);
backend.dispatch(
&mut grid,
&Event::PointerMove {
pos: Point::new(744, 430),
},
);
backend.dispatch(
&mut grid,
&Event::PointerUp {
pos: Point::new(744, 430),
button: MouseButton::Left,
modifiers: Modifiers::default(),
},
);
backend.render(&mut grid);
assert_eq!(grid.sheet.display((5, 5)), "42");
}
}