Skip to main content

ravenrs_spreadsheet/
state.rs

1//! The workbook a [`Spreadsheet`](crate::Spreadsheet) shows, shared with the application.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use ravenrs_reactive::State;
7use ravenrs_sheet::base::{CellRef, RangeRef};
8use ravenrs_sheet::engine::{DocState, Selection, Session, Trust, display, io};
9use ravenrs_sheet::model::Workbook;
10use serde_json::{Value as Json, json};
11
12/// Where an edit in progress is typed.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub(crate) enum EditPlace {
15    /// Over the cell itself.
16    Cell,
17    /// In the formula bar.
18    Bar,
19}
20
21/// An edit in progress: what the person typed so far, for which cell.
22#[derive(Clone, Debug, PartialEq)]
23pub(crate) struct Edit {
24    pub(crate) cell: CellRef,
25    pub(crate) text: String,
26    pub(crate) place: EditPlace,
27}
28
29struct Inner {
30    session: RefCell<Session>,
31    /// Goes up after every change, so the views showing the workbook rebuild.
32    version: State<u64>,
33    edit: State<Option<Edit>>,
34    /// The latest refusal from the engine, shown until the next command.
35    error: State<Option<String>>,
36    /// The name the workbook is saved under.
37    file_name: RefCell<String>,
38}
39
40/// A workbook with its selection and undo history, shared between a [`Spreadsheet`] and the
41/// application. Clones share the same workbook.
42///
43/// Every change goes through the engine's commands (see [`Self::execute`]), which is how the
44/// toolbar, the keyboard and the application change it alike.
45///
46/// ```
47/// use ravenrs_spreadsheet::SpreadsheetState;
48///
49/// let sheet = SpreadsheetState::new();
50/// sheet.set_cell("A1", "2").unwrap();
51/// sheet.set_cell("A2", "=A1*21").unwrap();
52/// assert_eq!(sheet.cell_text("A2"), "42");
53/// ```
54///
55/// [`Spreadsheet`]: crate::Spreadsheet
56#[derive(Clone)]
57pub struct SpreadsheetState {
58    inner: Rc<Inner>,
59}
60
61impl std::fmt::Debug for SpreadsheetState {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("SpreadsheetState")
64            .field("file_name", &self.inner.file_name.borrow())
65            .finish_non_exhaustive()
66    }
67}
68
69impl Default for SpreadsheetState {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl SpreadsheetState {
76    /// A new, empty workbook with one sheet.
77    pub fn new() -> Self {
78        let mut session = Session::new();
79        session.new_workbook();
80        Self::from_session(session, "Book1.xlsx")
81    }
82
83    /// Opens a workbook from the bytes of a file: `.xlsx`, `.xlsm`, `.xls`, `.ods`, `.csv` or
84    /// `.tsv`, told apart by `name`'s extension and by the content.
85    pub fn open(name: &str, bytes: &[u8]) -> Result<Self, String> {
86        let state = Self::new();
87        state.replace(name, bytes)?;
88        Ok(state)
89    }
90
91    /// Replaces the workbook with the one in a file's bytes, as [`Self::open`] reads them.
92    /// Undo starts over.
93    pub fn replace(&self, name: &str, bytes: &[u8]) -> Result<(), String> {
94        let (workbook, _warnings) = io::open_bytes(name, bytes).map_err(|e| e.to_string())?;
95        let title = name.rsplit_once('.').map_or(name, |(stem, _)| stem).to_owned();
96        {
97            let mut session = self.inner.session.borrow_mut();
98            while !session.documents().is_empty() {
99                session.close_document(0);
100            }
101            let index =
102                session.add_document(DocState::new(workbook, Some(name.to_owned()), title, Trust::FromFile));
103            session.set_active(index);
104        }
105        *self.inner.file_name.borrow_mut() = xlsx_name(name);
106        self.inner.edit.set(None);
107        self.inner.error.set(None);
108        self.changed();
109        Ok(())
110    }
111
112    fn from_session(session: Session, file_name: &str) -> Self {
113        Self {
114            inner: Rc::new(Inner {
115                session: RefCell::new(session),
116                version: State::new(0),
117                edit: State::new(None),
118                error: State::new(None),
119                file_name: RefCell::new(file_name.to_owned()),
120            }),
121        }
122    }
123
124    /// Runs one of the engine's commands, such as `cell.set`, `home.bold` or `edit.undo`,
125    /// with its parameters, and gives what it answered. A refused command changes nothing and
126    /// its reason shows under the formula bar.
127    pub fn execute(&self, command: &str, params: Json) -> Result<Json, String> {
128        let result = self.inner.session.borrow_mut().execute(command, params).map_err(|e| e.to_string());
129        self.inner.error.set(result.as_ref().err().cloned());
130        self.changed();
131        result
132    }
133
134    /// Types `input` into a cell given as `A1`: a number, text, a date or a `=formula`, read as
135    /// a person typing it would be.
136    pub fn set_cell(&self, cell: &str, input: &str) -> Result<(), String> {
137        self.execute("cell.set", json!({ "cell": cell, "input": input })).map(|_| ())
138    }
139
140    /// What a cell given as `A1` shows on the active sheet, with its number format applied.
141    pub fn cell_text(&self, cell: &str) -> String {
142        let Some(at) = CellRef::parse(cell) else { return String::new() };
143        self.read(|wb| wb.active().map(|sheet| display::cell_text(wb, sheet, at)).unwrap_or_default())
144    }
145
146    /// What editing a cell given as `A1` starts from: its formula, or the value as typed.
147    pub fn cell_input(&self, cell: &str) -> String {
148        let Some(at) = CellRef::parse(cell) else { return String::new() };
149        self.read(|wb| wb.active().map(|sheet| display::edit_text(wb, sheet, at)).unwrap_or_default())
150    }
151
152    /// The active cell, as `A1`.
153    pub fn active_cell(&self) -> String {
154        self.selection_now().active.a1()
155    }
156
157    /// The selection, as `A1:B2` (areas joined by commas).
158    pub fn selection(&self) -> String {
159        self.selection_now().a1()
160    }
161
162    /// Selects a cell or a range given as `A1` or `A1:C3`.
163    pub fn select(&self, range: &str) -> Result<(), String> {
164        self.execute("selection.set", json!({ "range": range })).map(|_| ())
165    }
166
167    /// The names of the sheets, in order.
168    pub fn sheet_names(&self) -> Vec<String> {
169        self.read(|wb| wb.sheets.iter().map(|s| s.name.clone()).collect())
170    }
171
172    /// The position of the sheet being shown.
173    pub fn active_sheet(&self) -> usize {
174        self.read(Workbook::active_sheet)
175    }
176
177    /// The workbook as an `.xlsx` file.
178    pub fn to_xlsx(&self) -> Result<Vec<u8>, String> {
179        self.read(|wb| io::save_bytes(wb, "book.xlsx").map_err(|e| e.to_string()))
180    }
181
182    /// The active sheet's values as CSV.
183    pub fn to_csv(&self) -> Result<Vec<u8>, String> {
184        self.read(|wb| io::save_bytes(wb, "book.csv").map_err(|e| e.to_string()))
185    }
186
187    /// The name Save offers, ending in `.xlsx`.
188    pub fn file_name(&self) -> String {
189        self.inner.file_name.borrow().clone()
190    }
191
192    /// Changes the name Save offers; `.xlsx` is added when missing.
193    pub fn set_file_name(&self, name: &str) {
194        *self.inner.file_name.borrow_mut() = xlsx_name(name);
195    }
196
197    /// Whether there is something to undo.
198    pub fn can_undo(&self) -> bool {
199        self.inner.session.borrow().active().is_some_and(|d| !d.undo.is_empty())
200    }
201
202    /// Whether there is something to redo.
203    pub fn can_redo(&self) -> bool {
204        self.inner.session.borrow().active().is_some_and(|d| !d.redo.is_empty())
205    }
206
207    /// Reads the workbook, for anything the methods here do not cover.
208    pub fn read<R>(&self, f: impl FnOnce(&Workbook) -> R) -> R {
209        let session = self.inner.session.borrow();
210        match session.active() {
211            Some(doc) => f(&doc.wb),
212            None => f(&Workbook::new()),
213        }
214    }
215
216    /// Goes up after every change: reading it in a view rebuilds that view on the next one.
217    pub fn version(&self) -> u64 {
218        self.inner.version.get()
219    }
220
221    /// Runs `f` with the engine's session, for commands with replies or state not covered here.
222    /// Views showing the workbook rebuild afterwards.
223    pub fn with_session<R>(&self, f: impl FnOnce(&mut Session) -> R) -> R {
224        let result = f(&mut self.inner.session.borrow_mut());
225        self.changed();
226        result
227    }
228
229    /// The latest refusal from the engine, if the last command was refused.
230    pub fn error(&self) -> Option<String> {
231        self.inner.error.get()
232    }
233
234    pub(crate) fn show_error(&self, error: String) {
235        self.inner.error.set(Some(error));
236    }
237
238    pub(crate) fn selection_now(&self) -> Selection {
239        self.inner.session.borrow().active().map(|d| d.selection.clone()).unwrap_or_default()
240    }
241
242    pub(crate) fn edit(&self) -> &State<Option<Edit>> {
243        &self.inner.edit
244    }
245
246    /// Starts editing the active cell, from its content or from `text`.
247    pub(crate) fn begin_edit(&self, text: Option<String>, place: EditPlace) {
248        let cell = self.selection_now().active;
249        let text = text.unwrap_or_else(|| self.cell_input(&cell.a1()));
250        self.inner.edit.set(Some(Edit { cell, text, place }));
251    }
252
253    /// Types the edit in progress into its cell, then moves the active cell by (`dr`, `dc`).
254    pub(crate) fn commit_edit(&self, dr: i64, dc: i64) {
255        let Some(edit) = self.inner.edit.get() else { return };
256        self.inner.edit.set(None);
257        let done = self.execute("cell.set", json!({ "cell": edit.cell.a1(), "input": edit.text }));
258        if done.is_ok() && (dr != 0 || dc != 0) {
259            self.move_by(dr, dc, false);
260        }
261    }
262
263    pub(crate) fn cancel_edit(&self) {
264        self.inner.edit.set(None);
265    }
266
267    pub(crate) fn move_by(&self, dr: i64, dc: i64, extend: bool) {
268        let _ = self.execute("selection.move", json!({ "dr": dr, "dc": dc, "extend": extend }));
269    }
270
271    pub(crate) fn select_range(&self, range: RangeRef, active: CellRef) {
272        let _ = self.execute("selection.set", json!({ "range": range.a1(), "active": active.a1() }));
273    }
274
275    fn changed(&self) {
276        self.inner.version.update(|v| *v = v.wrapping_add(1));
277    }
278}
279
280/// `name` with its extension changed to `.xlsx`.
281fn xlsx_name(name: &str) -> String {
282    let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem);
283    let stem = if stem.trim().is_empty() { "Book1" } else { stem };
284    format!("{stem}.xlsx")
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn formulas_recalculate_when_their_inputs_change() {
293        let s = SpreadsheetState::new();
294        s.set_cell("A1", "10").unwrap();
295        s.set_cell("A2", "32").unwrap();
296        s.set_cell("A3", "=SUM(A1:A2)").unwrap();
297        assert_eq!(s.cell_text("A3"), "42");
298        s.set_cell("A1", "20").unwrap();
299        assert_eq!(s.cell_text("A3"), "52");
300        assert_eq!(s.cell_input("A3"), "=SUM(A1:A2)");
301    }
302
303    #[test]
304    fn a_workbook_survives_a_trip_through_xlsx() {
305        let s = SpreadsheetState::new();
306        s.set_cell("B2", "=6*7").unwrap();
307        s.set_cell("C3", "olá").unwrap();
308        let bytes = s.to_xlsx().unwrap();
309        let back = SpreadsheetState::open("conta.xlsx", &bytes).unwrap();
310        assert_eq!(back.cell_text("B2"), "42");
311        assert_eq!(back.cell_text("C3"), "olá");
312        assert_eq!(back.file_name(), "conta.xlsx");
313    }
314
315    #[test]
316    fn undo_puts_the_cell_back_and_refusals_are_kept() {
317        let s = SpreadsheetState::new();
318        s.set_cell("A1", "1").unwrap();
319        s.set_cell("A1", "2").unwrap();
320        assert!(s.can_undo());
321        s.execute("edit.undo", json!({})).unwrap();
322        assert_eq!(s.cell_text("A1"), "1");
323        assert!(s.execute("no.such", json!({})).is_err());
324        assert!(s.error().is_some());
325        assert_eq!(xlsx_name("dados.csv"), "dados.xlsx");
326    }
327}