1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub(crate) enum EditPlace {
15 Cell,
17 Bar,
19}
20
21#[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 version: State<u64>,
33 edit: State<Option<Edit>>,
34 error: State<Option<String>>,
36 file_name: RefCell<String>,
38}
39
40#[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 pub fn new() -> Self {
78 let mut session = Session::new();
79 session.new_workbook();
80 Self::from_session(session, "Book1.xlsx")
81 }
82
83 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 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 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 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 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 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 pub fn active_cell(&self) -> String {
154 self.selection_now().active.a1()
155 }
156
157 pub fn selection(&self) -> String {
159 self.selection_now().a1()
160 }
161
162 pub fn select(&self, range: &str) -> Result<(), String> {
164 self.execute("selection.set", json!({ "range": range })).map(|_| ())
165 }
166
167 pub fn sheet_names(&self) -> Vec<String> {
169 self.read(|wb| wb.sheets.iter().map(|s| s.name.clone()).collect())
170 }
171
172 pub fn active_sheet(&self) -> usize {
174 self.read(Workbook::active_sheet)
175 }
176
177 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 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 pub fn file_name(&self) -> String {
189 self.inner.file_name.borrow().clone()
190 }
191
192 pub fn set_file_name(&self, name: &str) {
194 *self.inner.file_name.borrow_mut() = xlsx_name(name);
195 }
196
197 pub fn can_undo(&self) -> bool {
199 self.inner.session.borrow().active().is_some_and(|d| !d.undo.is_empty())
200 }
201
202 pub fn can_redo(&self) -> bool {
204 self.inner.session.borrow().active().is_some_and(|d| !d.redo.is_empty())
205 }
206
207 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 pub fn version(&self) -> u64 {
218 self.inner.version.get()
219 }
220
221 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 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 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 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
280fn 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}