Skip to main content

spreadsheet/
lib.rs

1//! A household budget in the RavenRS spreadsheet, shared by the native and the web build.
2
3use ravenrs::prelude::*;
4use ravenrs_spreadsheet::{Spreadsheet, SpreadsheetState};
5use serde_json::json;
6
7/// Category, budget and spent, for each line of the sample.
8const LINES: [(&str, f64, f64); 8] = [
9    ("Moradia", 2400.0, 2400.0),
10    ("Mercado", 1300.0, 1452.37),
11    ("Transporte", 600.0, 488.9),
12    ("Saúde", 450.0, 512.0),
13    ("Educação", 900.0, 900.0),
14    ("Lazer", 400.0, 615.5),
15    ("Contas da casa", 520.0, 498.13),
16    ("Reserva", 800.0, 800.0),
17];
18
19/// A budget with formulas, formats and a second sheet that reads the first.
20pub fn sample() -> SpreadsheetState {
21    let sheet = SpreadsheetState::new();
22    sheet.set_file_name("Orçamento");
23    let set = |cell: &str, input: &str| {
24        let _ = sheet.set_cell(cell, input);
25    };
26    let run = |command: &str, params: serde_json::Value| {
27        let _ = sheet.execute(command, params);
28    };
29    run("sheet.rename", json!({ "name": "Orçamento" }));
30    set("A1", "Orçamento de novembro");
31    run("home.bold", json!({ "range": "A1", "on": true }));
32    run("home.fontSize", json!({ "range": "A1", "size": 16 }));
33    for (cell, title) in
34        [("A3", "Categoria"), ("B3", "Previsto"), ("C3", "Gasto"), ("D3", "Diferença"), ("E3", "% usado")]
35    {
36        set(cell, title);
37    }
38    run("home.bold", json!({ "range": "A3:E3", "on": true }));
39    run("home.fillColor", json!({ "range": "A3:E3", "color": "#DCE6F1" }));
40    run("home.borders", json!({ "range": "A3:E3", "border": "bottom" }));
41    for (i, (name, budget, spent)) in LINES.iter().enumerate() {
42        let row = i + 4;
43        set(&format!("A{row}"), name);
44        set(&format!("B{row}"), &budget.to_string());
45        set(&format!("C{row}"), &spent.to_string());
46        set(&format!("D{row}"), &format!("=B{row}-C{row}"));
47        set(&format!("E{row}"), &format!("=C{row}/B{row}"));
48    }
49    let last = LINES.len() + 3;
50    let total = last + 1;
51    set(&format!("A{total}"), "Total");
52    for col in ["B", "C", "D"] {
53        set(&format!("{col}{total}"), &format!("=SUM({col}4:{col}{last})"));
54    }
55    set(&format!("E{total}"), &format!("=C{total}/B{total}"));
56    run("home.bold", json!({ "range": format!("A{total}:E{total}"), "on": true }));
57    run("home.borders", json!({ "range": format!("A{total}:E{total}"), "border": "top" }));
58    run(
59        "home.numberFormat",
60        json!({ "range": format!("B4:D{total}"), "code": "\"R$\" #,##0.00;[Red]-\"R$\" #,##0.00" }),
61    );
62    run("home.numberFormat", json!({ "range": format!("E4:E{total}"), "code": "0%" }));
63    run("home.columnWidth", json!({ "cols": "A:A", "width": 150 }));
64    run("home.columnWidth", json!({ "cols": "B:D", "width": 110 }));
65
66    run("home.insertSheet", json!({ "name": "Resumo", "before": 9 }));
67    run("sheet.activate", json!({ "sheet": 1 }));
68    set("A1", "Maior gasto");
69    set(
70        "B1",
71        &format!("=INDEX(Orçamento!A4:A{last},MATCH(MAX(Orçamento!C4:C{last}),Orçamento!C4:C{last},0))"),
72    );
73    set("A2", "Acima do previsto");
74    set("B2", &format!("=COUNTIF(Orçamento!D4:D{last},\"<0\")"));
75    set("A3", "Sobra do mês");
76    set("B3", &format!("=Orçamento!D{total}"));
77    run("home.numberFormat", json!({ "range": "B3", "code": "\"R$\" #,##0.00" }));
78    run("home.columnWidth", json!({ "cols": "A:B", "width": 140 }));
79    run("sheet.activate", json!({ "sheet": 0 }));
80    let _ = sheet.select("B4");
81    sheet.with_session(|s| {
82        if let Some(doc) = s.active_mut() {
83            doc.undo.clear();
84        }
85    });
86    sheet
87}
88
89/// The app: the sample budget, in a spreadsheet filling the window.
90pub fn app() -> impl View {
91    Spreadsheet::new(&sample())
92}
93
94#[cfg(target_arch = "wasm32")]
95mod web_entry {
96    use std::cell::RefCell;
97
98    use wasm_bindgen::prelude::*;
99
100    thread_local! {
101        static APP: RefCell<Option<ravenrs::web::WebApp>> = const { RefCell::new(None) };
102    }
103
104    /// Runs automatically when the module is instantiated.
105    #[wasm_bindgen(start)]
106    pub fn start() {
107        console_error_panic_hook::set_once();
108        wasm_bindgen_futures::spawn_local(async {
109            match ravenrs::web::start(super::app(), ravenrs::web::WebOptions::new("raven-canvas")).await {
110                Ok(app) => APP.with(|a| *a.borrow_mut() = Some(app)),
111                Err(e) => web_sys::console::error_1(&e.to_string().into()),
112            }
113        });
114    }
115
116    /// Unmounts the app and releases its listeners and GPU resources.
117    #[wasm_bindgen]
118    pub fn stop() {
119        APP.with(|a| a.borrow_mut().take());
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    #![allow(clippy::unwrap_used)]
126
127    use super::*;
128
129    #[test]
130    fn the_sample_adds_up() {
131        let sheet = sample();
132        assert_eq!(sheet.sheet_names(), ["Orçamento", "Resumo"]);
133        assert_eq!(sheet.cell_text("B12"), "R$ 7,370.00");
134        assert_eq!(sheet.cell_text("E5"), "112%");
135        assert!(
136            sheet.cell_text("D5").starts_with('-'),
137            "overspent shows negative: {}",
138            sheet.cell_text("D5")
139        );
140        sheet.execute("sheet.activate", json!({ "sheet": 1 })).unwrap();
141        assert_eq!(sheet.cell_text("B1"), "Moradia");
142        assert_eq!(sheet.cell_text("B2"), "3");
143        assert!(!sheet.can_undo());
144    }
145}