Skip to main content

ironcalc_base/user_model/
common.rs

1#![deny(missing_docs)]
2
3use std::{collections::HashMap, fmt::Debug};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    cf_types::ExtendedStyle,
9    constants::{LAST_COLUMN, LAST_ROW},
10    expressions::{
11        parser::CompletionContext,
12        types::Area,
13        utils::{is_valid_column_number, is_valid_row},
14    },
15    model::{FmtSettings, Model},
16    types::{
17        Alignment, ArrayKind, BorderItem, Cell, CellType, Col, Color, HorizontalAlignment,
18        SheetProperties, SheetState, Style, Theme, VerticalAlignment,
19    },
20};
21
22use crate::user_model::history::{
23    ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
24};
25
26use super::border_utils::is_max_border;
27
28#[derive(Serialize, Deserialize)]
29pub enum CellArrayStructure {
30    // It's just a single cell
31    SingleCell,
32    // It is part of a dynamic array
33    // (anchor_row, anchor_column, width, height)
34    DynamicChild(i32, i32, i32, i32),
35    // Anchor of a dynamic array (width, height)
36    DynamicAnchor(i32, i32),
37    // It is part of an array formula
38    ArrayChild(i32, i32, i32, i32),
39    // Anchor of an array formula
40    ArrayAnchor(i32, i32),
41}
42
43#[derive(Serialize, Deserialize, PartialEq)]
44pub enum BorderType {
45    All,
46    Inner,
47    Outer,
48    Top,
49    Right,
50    Bottom,
51    Left,
52    CenterH,
53    CenterV,
54    None,
55}
56
57/// This is the struct for a border area
58#[derive(Serialize, Deserialize)]
59pub struct BorderArea {
60    pub(crate) item: BorderItem,
61    pub(crate) r#type: BorderType,
62}
63
64fn boolean(value: &str) -> Result<bool, String> {
65    match value {
66        "true" => Ok(true),
67        "false" => Ok(false),
68        _ => Err(format!("Invalid value for boolean: '{value}'.")),
69    }
70}
71
72fn horizontal(value: &str) -> Result<HorizontalAlignment, String> {
73    match value {
74        "center" => Ok(HorizontalAlignment::Center),
75        "centerContinuous" => Ok(HorizontalAlignment::CenterContinuous),
76        "distributed" => Ok(HorizontalAlignment::Distributed),
77        "fill" => Ok(HorizontalAlignment::Fill),
78        "general" => Ok(HorizontalAlignment::General),
79        "justify" => Ok(HorizontalAlignment::Justify),
80        "left" => Ok(HorizontalAlignment::Left),
81        "right" => Ok(HorizontalAlignment::Right),
82        _ => Err(format!(
83            "Invalid value for horizontal alignment: '{value}'."
84        )),
85    }
86}
87
88fn vertical(value: &str) -> Result<VerticalAlignment, String> {
89    match value {
90        "bottom" => Ok(VerticalAlignment::Bottom),
91        "center" => Ok(VerticalAlignment::Center),
92        "distributed" => Ok(VerticalAlignment::Distributed),
93        "justify" => Ok(VerticalAlignment::Justify),
94        "top" => Ok(VerticalAlignment::Top),
95        _ => Err(format!("Invalid value for vertical alignment: '{value}'.")),
96    }
97}
98
99fn update_style(old_value: &Style, style_path: &str, value: &str) -> Result<Style, String> {
100    let mut style = old_value.clone();
101    match style_path {
102        "font.b" => {
103            style.font.b = boolean(value)?;
104        }
105        "font.i" => {
106            style.font.i = boolean(value)?;
107        }
108        "font.u" => {
109            style.font.u = boolean(value)?;
110        }
111        "font.strike" => {
112            style.font.strike = boolean(value)?;
113        }
114        "font.color" => {
115            style.font.color = Color::from_param(value)?;
116        }
117        "font.size" => {
118            let new_size: i32 = value
119                .parse()
120                .map_err(|_| format!("Invalid value for font size: '{value}'."))?;
121            if new_size < 1 {
122                return Err(format!("Invalid value for font size: '{new_size}'."));
123            }
124            style.font.sz = new_size;
125        }
126        "font.size_delta" => {
127            // This is a special case, we need to add the value to the current size
128            let size_delta: i32 = value
129                .parse()
130                .map_err(|_| format!("Invalid value for font size: '{value}'."))?;
131            let new_size = style.font.sz + size_delta;
132            if new_size < 1 {
133                return Err(format!("Invalid value for font size: '{new_size}'."));
134            }
135            style.font.sz = new_size;
136        }
137        "fill.color" | "fill.bg_color" | "fill.fg_color" => {
138            style.fill.color = Color::from_param(value)?;
139        }
140        "num_fmt" => {
141            value.clone_into(&mut style.num_fmt);
142        }
143        "alignment" => {
144            if !value.is_empty() {
145                return Err(format!("Alignment must be empty, but found: '{value}'."));
146            }
147            style.alignment = None;
148        }
149        "alignment.horizontal" => match style.alignment {
150            Some(ref mut s) => s.horizontal = horizontal(value)?,
151            None => {
152                let alignment = Alignment {
153                    horizontal: horizontal(value)?,
154                    ..Default::default()
155                };
156                style.alignment = Some(alignment)
157            }
158        },
159        "alignment.vertical" => match style.alignment {
160            Some(ref mut s) => s.vertical = vertical(value)?,
161            None => {
162                let alignment = Alignment {
163                    vertical: vertical(value)?,
164                    ..Default::default()
165                };
166                style.alignment = Some(alignment)
167            }
168        },
169        "alignment.wrap_text" => match style.alignment {
170            Some(ref mut s) => s.wrap_text = boolean(value)?,
171            None => {
172                let alignment = Alignment {
173                    wrap_text: boolean(value)?,
174                    ..Default::default()
175                };
176                style.alignment = Some(alignment)
177            }
178        },
179        _ => {
180            return Err(format!("Invalid style path: '{style_path}'."));
181        }
182    }
183    Ok(style)
184}
185
186/// # A wrapper around [`Model`] for a spreadsheet end user.
187/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
188///
189/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
190///
191/// Automatic evaluation means that actions like setting a value on a cell or deleting a column
192/// will evaluate the model if needed.
193///
194/// It is meant to be used by UI applications like Web IronCalc or TironCalc.
195///
196///
197/// # Examples
198///
199/// ```rust
200/// # use ironcalc_base::UserModel;
201/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
202/// let mut model = UserModel::new_empty("model", "en", "UTC", "en")?;
203/// model.set_user_input(0, 1, 1, "=1+1")?;
204/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
205/// model.undo()?;
206/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "");
207/// model.redo()?;
208/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
209/// # Ok(())
210/// # }
211/// ```
212pub struct UserModel<'a> {
213    pub(crate) model: Model<'a>,
214    history: History,
215    send_queue: Vec<QueueDiffs>,
216    pause_evaluation: bool,
217}
218
219/// Given the index of the currently selected sheet, returns the index that same
220/// sheet occupies after the worksheet at `from` is moved to `to`. This lets the
221/// selection follow a sheet by identity across a reorder instead of pointing at
222/// whichever sheet lands in the old slot.
223pub(crate) fn selected_sheet_after_move(selected: u32, from: u32, to: u32) -> u32 {
224    if selected == from {
225        return to;
226    }
227    // Mirror `Model::move_sheet`: remove at `from`, then insert at `to`.
228    let after_remove = if selected > from {
229        selected - 1
230    } else {
231        selected
232    };
233    if after_remove >= to {
234        after_remove + 1
235    } else {
236        after_remove
237    }
238}
239
240impl<'a> Debug for UserModel<'a> {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("UserModel").finish()
243    }
244}
245
246impl<'a> UserModel<'a> {
247    /// Creates a user model from an existing model
248    pub fn from_model(model: Model) -> UserModel {
249        UserModel {
250            model,
251            history: History::default(),
252            send_queue: vec![],
253            pause_evaluation: false,
254        }
255    }
256
257    /// Creates a new UserModel.
258    ///
259    /// See also:
260    /// * [Model::new_empty]
261    pub fn new_empty(
262        name: &'a str,
263        locale_id: &'a str,
264        timezone: &'a str,
265        language_id: &'a str,
266    ) -> Result<UserModel<'a>, String> {
267        let model = Model::new_empty(name, locale_id, timezone, language_id)?;
268        Ok(UserModel {
269            model,
270            history: History::default(),
271            send_queue: vec![],
272            pause_evaluation: false,
273        })
274    }
275
276    /// Creates a model from it's internal representation
277    ///
278    /// See also:
279    /// * [Model::from_bytes]
280    pub fn from_bytes(s: &[u8], language_id: &'a str) -> Result<UserModel<'a>, String> {
281        let model = Model::from_bytes(s, language_id)?;
282        Ok(UserModel {
283            model,
284            history: History::default(),
285            send_queue: vec![],
286            pause_evaluation: false,
287        })
288    }
289
290    /// Returns the internal representation of a model
291    ///
292    /// See also:
293    ///  * [Model::to_bytes]
294    pub fn to_bytes(&self) -> Vec<u8> {
295        self.model.to_bytes()
296    }
297
298    /// Returns the internal model
299    pub fn get_model(&self) -> &Model<'_> {
300        &self.model
301    }
302
303    /// Returns the workbook name
304    pub fn get_name(&self) -> String {
305        self.model.workbook.name.clone()
306    }
307
308    /// Sets the name of a workbook
309    pub fn set_name(&mut self, name: &str) {
310        let old_value = self.model.workbook.name.clone();
311        if old_value == name {
312            return;
313        }
314        self.push_diff_list(vec![Diff::SetWorkbookName {
315            old_value,
316            new_value: name.to_string(),
317        }]);
318        self.model.workbook.name = name.to_string();
319    }
320
321    /// Undoes last change if any, places the change in the redo list and evaluates the model if needed
322    ///
323    /// See also:
324    /// * [UserModel::redo]
325    pub fn undo(&mut self) -> Result<(), String> {
326        if let Some(diff_list) = self.history.undo() {
327            self.apply_undo_diff_list(&diff_list)?;
328            self.send_queue.push(QueueDiffs {
329                r#type: DiffType::Undo,
330                list: diff_list.clone(),
331            });
332        };
333        Ok(())
334    }
335
336    /// Redoes the last undone change, places the change in the undo list and evaluates the model if needed
337    ///
338    /// See also:
339    /// * [UserModel::redo]
340    pub fn redo(&mut self) -> Result<(), String> {
341        if let Some(diff_list) = self.history.redo() {
342            self.apply_diff_list(&diff_list)?;
343            self.send_queue.push(QueueDiffs {
344                r#type: DiffType::Redo,
345                list: diff_list.clone(),
346            });
347        };
348        Ok(())
349    }
350
351    /// Returns true if there are items to be undone
352    pub fn can_undo(&self) -> bool {
353        !self.history.undo_stack.is_empty()
354    }
355
356    /// Returns true if there are items to be redone
357    pub fn can_redo(&self) -> bool {
358        !self.history.redo_stack.is_empty()
359    }
360
361    /// Pauses automatic evaluation.
362    ///
363    /// See also:
364    /// * [UserModel::evaluate]
365    /// * [UserModel::resume_evaluation]
366    pub fn pause_evaluation(&mut self) {
367        self.pause_evaluation = true;
368    }
369
370    /// Resumes automatic evaluation.
371    ///
372    /// See also:
373    /// * [UserModel::evaluate]
374    /// * [UserModel::pause_evaluation]
375    pub fn resume_evaluation(&mut self) {
376        self.pause_evaluation = false;
377    }
378
379    /// Forces an evaluation of the model
380    ///
381    /// See also:
382    /// * [Model::evaluate]
383    /// * [UserModel::pause_evaluation]
384    pub fn evaluate(&mut self) {
385        self.model.evaluate()
386    }
387
388    /// Returns the list of pending diffs and removes them from the queue
389    ///
390    /// This is used together with [apply_external_diffs](UserModel::apply_external_diffs) to keep two remote models
391    /// in sync.
392    ///
393    /// See also:
394    /// * [UserModel::apply_external_diffs]
395    pub fn flush_send_queue(&mut self) -> Vec<u8> {
396        // This can never fail :O:
397        let q = bitcode::encode(&self.send_queue);
398        self.send_queue = vec![];
399        q
400    }
401
402    /// This are external diffs that need to be applied to the model
403    ///
404    /// This is used together with [flush_send_queue](UserModel::flush_send_queue) to keep two remote models in sync
405    ///
406    /// See also:
407    /// * [UserModel::flush_send_queue]
408    pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> {
409        if let Ok(queue_diffs_list) = bitcode::decode::<Vec<QueueDiffs>>(diff_list_str) {
410            for queue_diff in queue_diffs_list {
411                if matches!(queue_diff.r#type, DiffType::Redo) {
412                    self.apply_diff_list(&queue_diff.list)?;
413                } else {
414                    self.apply_undo_diff_list(&queue_diff.list)?;
415                }
416            }
417        } else {
418            return Err("Error parsing diff list".to_string());
419        }
420        Ok(())
421    }
422
423    /// Set the input in a cell
424    ///
425    /// See also:
426    /// * [Model::set_user_input]
427    pub fn set_user_input(
428        &mut self,
429        sheet: u32,
430        row: i32,
431        column: i32,
432        value: &str,
433    ) -> Result<(), String> {
434        if !is_valid_column_number(column) {
435            return Err("Invalid column".to_string());
436        }
437        if !is_valid_row(row) {
438            return Err("Invalid row".to_string());
439        }
440        let old_value = self
441            .model
442            .workbook
443            .worksheet(sheet)?
444            .cell(row, column)
445            .cloned();
446        // If it is a spill cell we want to save the old value as None, because the value of a spill cell is determined by the anchor cell
447        let old_value = if matches!(old_value, Some(Cell::SpillCell { .. })) {
448            None
449        } else {
450            old_value
451        };
452        let mut diff_list = vec![Diff::SetCellValue {
453            sheet,
454            row,
455            column,
456            new_value: value.to_string(),
457            old_value: Box::new(old_value),
458        }];
459        self.set_user_input_with_link_diffs(sheet, row, column, value.to_string(), &mut diff_list)?;
460
461        self.evaluate_if_not_paused();
462
463        let style = self.model.get_style_for_cell(sheet, row, column)?;
464
465        let line_count = value.split('\n').count() as f64;
466        let row_height = self.model.get_row_height(sheet, row)?;
467        // This is in sync with the front-end auto fit row
468        let font_size = style.font.sz as f64;
469        let line_height = font_size * 1.5;
470        let cell_height = (line_count - 1.0) * line_height + 8.0 + font_size;
471        if cell_height > row_height {
472            diff_list.push(Diff::SetRowHeight {
473                sheet,
474                row,
475                new_value: cell_height,
476                old_value: row_height,
477            });
478            self.model.set_row_height(sheet, row, cell_height)?;
479        }
480
481        self.push_diff_list(diff_list);
482        Ok(())
483    }
484
485    /// Calls [`Model::set_user_input`] and appends to `diff_list` the diffs for the
486    /// side effects it has on the cell link: URL-like values are auto-linked (which
487    /// also applies the link style when the cell was not linked before) and an empty
488    /// input removes the link. The `SetCellValue` diff for the input itself is not
489    /// added here.
490    pub(super) fn set_user_input_with_link_diffs(
491        &mut self,
492        sheet: u32,
493        row: i32,
494        column: i32,
495        value: String,
496        diff_list: &mut Vec<Diff>,
497    ) -> Result<(), String> {
498        let old_link = self.model.get_cell_link(sheet, row, column)?;
499        let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
500        self.model.set_user_input(sheet, row, column, value)?;
501        let new_link = self.model.get_cell_link(sheet, row, column)?;
502        if new_link == old_link {
503            return Ok(());
504        }
505        if old_link.is_none() {
506            // a newly auto-created link also applies the link style to the cell
507            let new_style = self.model.get_style_for_cell(sheet, row, column)?;
508            diff_list.push(Diff::SetCellStyle {
509                sheet,
510                row,
511                column,
512                old_value: Box::new(old_style),
513                new_value: Box::new(new_style),
514            });
515        }
516        diff_list.push(Diff::SetCellLink {
517            sheet,
518            row,
519            column,
520            old_value: Box::new(old_link),
521            new_value: Box::new(new_link),
522        });
523        Ok(())
524    }
525
526    /// Returns the content of a cell
527    ///
528    /// See also:
529    /// * [Model::get_localized_cell_content]
530    #[inline]
531    pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, String> {
532        self.model.get_localized_cell_content(sheet, row, column)
533    }
534
535    /// Returns completion information for a formula being edited in a cell.
536    ///
537    /// `formula` is the raw cell input (it may start with `=`) and `cursor` is a
538    /// char offset into it.
539    ///
540    /// See also:
541    /// * [Model::formula_completion]
542    #[inline]
543    pub fn formula_completion(
544        &mut self,
545        sheet: u32,
546        row: i32,
547        column: i32,
548        formula: &str,
549        cursor: usize,
550    ) -> Result<CompletionContext, String> {
551        self.model
552            .formula_completion(sheet, row, column, formula, cursor)
553    }
554
555    /// Cycles the references touched by the cursor through the four
556    /// absolute/relative states, Excel F4 style: A1 -> $A$1 -> A$1 -> $A1 -> A1.
557    /// Returns the new text together with the new cursor start and end.
558    ///
559    /// See also:
560    /// * [Model::cycle_reference]
561    #[inline]
562    pub fn cycle_reference(
563        &self,
564        value: &str,
565        start: usize,
566        end: usize,
567    ) -> Result<(String, i32, i32), String> {
568        self.model.cycle_reference(value, start, end)
569    }
570
571    /// Returns the formatted value of a cell
572    ///
573    /// See also:
574    /// * [Model::get_formatted_cell_value]
575    #[inline]
576    pub fn get_formatted_cell_value(
577        &self,
578        sheet: u32,
579        row: i32,
580        column: i32,
581    ) -> Result<String, String> {
582        self.model.get_formatted_cell_value(sheet, row, column)
583    }
584
585    /// Returns the type of the cell
586    ///
587    /// See also
588    /// * [Model::get_cell_type]
589    pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
590        self.model.get_cell_type(sheet, row, column)
591    }
592
593    /// Adds new sheet
594    ///
595    /// See also:
596    /// * [Model::new_sheet]
597    pub fn new_sheet(&mut self) -> Result<(), String> {
598        let (name, index) = self.model.new_sheet();
599        self.set_selected_sheet(index)?;
600        self.push_diff_list(vec![Diff::NewSheet { index, name }]);
601        Ok(())
602    }
603
604    /// Duplicates a sheet by index, placing the copy right after it and
605    /// selecting it.
606    ///
607    /// See also:
608    /// * [Model::duplicate_sheet]
609    pub fn duplicate_sheet(&mut self, sheet: u32) -> Result<(), String> {
610        let (_name, new_index) = self.model.duplicate_sheet(sheet)?;
611        self.set_selected_sheet(new_index)?;
612        self.push_diff_list(vec![Diff::DuplicateSheet {
613            source_index: sheet,
614            new_index,
615        }]);
616        Ok(())
617    }
618
619    /// Deletes sheet by index
620    ///
621    /// See also:
622    /// * [Model::delete_sheet]
623    pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), String> {
624        let worksheet = self.model.workbook.worksheet(sheet)?;
625
626        self.push_diff_list(vec![Diff::DeleteSheet {
627            sheet,
628            old_data: Box::new(worksheet.clone()),
629        }]);
630
631        let sheet_count = self.model.workbook.worksheets.len() as u32;
632        // If we are deleting the last sheet we need to change the selected sheet
633        if sheet == sheet_count - 1 && sheet_count > 1 {
634            if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
635                view.sheet = sheet_count - 2;
636            };
637        }
638
639        self.model.delete_sheet(sheet)?;
640        Ok(())
641    }
642
643    /// Renames a sheet by index
644    ///
645    /// See also:
646    /// * [Model::rename_sheet_by_index]
647    pub fn rename_sheet(&mut self, sheet: u32, new_name: &str) -> Result<(), String> {
648        let old_value = self.model.workbook.worksheet(sheet)?.name.clone();
649        if old_value == new_name {
650            return Ok(());
651        }
652        self.model.rename_sheet_by_index(sheet, new_name)?;
653        self.push_diff_list(vec![Diff::RenameSheet {
654            index: sheet,
655            old_value,
656            new_value: new_name.to_string(),
657        }]);
658        Ok(())
659    }
660
661    /// Moves the worksheet at `sheet_index` to `new_index` within the workbook,
662    /// shifting the other sheets to accommodate. The moved worksheet ends up at
663    /// exactly `new_index`.
664    ///
665    /// The reorder is undoable/redoable, and the new order is preserved when the
666    /// workbook is saved. Cross-sheet formula references stay valid across the
667    /// move (sheet order is a position, not an identity — references key off the
668    /// sheet name/id). The selection follows the same sheet across the move.
669    ///
670    /// Moving a sheet to its current position is a no-op (no history entry). Fails
671    /// if either index is out of range.
672    ///
673    /// See also:
674    /// * [Model::move_sheet]
675    pub fn move_sheet(&mut self, sheet_index: u32, new_index: u32) -> Result<(), String> {
676        let sheet_count = self.model.workbook.worksheets.len() as u32;
677        if sheet_index >= sheet_count {
678            return Err(format!("Invalid sheet index {sheet_index}"));
679        }
680        if new_index >= sheet_count {
681            return Err(format!("Invalid target index {new_index}"));
682        }
683        if sheet_index == new_index {
684            return Ok(());
685        }
686        let selected = self.get_selected_sheet();
687        self.model.move_sheet(sheet_index, new_index)?;
688        self.set_selected_sheet(selected_sheet_after_move(selected, sheet_index, new_index))?;
689        self.push_diff_list(vec![Diff::MoveSheet {
690            sheet_index,
691            new_index,
692        }]);
693        Ok(())
694    }
695
696    /// Hides sheet by index
697    ///
698    /// See also:
699    /// * [Model::set_sheet_state]
700    /// * [UserModel::unhide_sheet]
701    pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> {
702        let sheet_count = self.model.workbook.worksheets.len() as u32;
703        for index in 1..sheet_count {
704            let sheet_index = (sheet + index) % sheet_count;
705            if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible {
706                if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
707                    view.sheet = sheet_index;
708                };
709                break;
710            }
711        }
712        let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
713        self.push_diff_list(vec![Diff::SetSheetState {
714            index: sheet,
715            new_value: SheetState::Hidden,
716            old_value,
717        }]);
718        self.model.set_sheet_state(sheet, SheetState::Hidden)?;
719        Ok(())
720    }
721
722    /// Un hides sheet by index
723    ///
724    /// See also:
725    /// * [Model::set_sheet_state]
726    /// * [UserModel::hide_sheet]
727    pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> {
728        let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
729        self.push_diff_list(vec![Diff::SetSheetState {
730            index: sheet,
731            new_value: SheetState::Visible,
732            old_value,
733        }]);
734        self.model.set_sheet_state(sheet, SheetState::Visible)?;
735        Ok(())
736    }
737
738    /// Sets sheet color
739    ///
740    /// Note: an empty string will remove the color
741    ///
742    /// See also
743    /// * [Model::set_sheet_color]
744    /// * [UserModel::get_worksheets_properties]
745    pub fn set_sheet_color(&mut self, sheet: u32, color: &Color) -> Result<(), String> {
746        let old_value = self.model.workbook.worksheet(sheet)?.color.clone();
747        self.model.set_sheet_color(sheet, color)?;
748        self.push_diff_list(vec![Diff::SetSheetColor {
749            index: sheet,
750            old_value,
751            new_value: color.clone(),
752        }]);
753        Ok(())
754    }
755
756    /// Removes cells contents and style
757    ///
758    /// See also:
759    /// * [Model::range_clear_all]
760    pub fn range_clear_all(&mut self, range: &Area) -> Result<(), String> {
761        let sheet = range.sheet;
762        // TODO: full rows/columns
763        let mut old_value = Vec::new();
764        let mut old_style = Vec::new();
765
766        for row in range.row..range.row + range.height {
767            let mut data_row = Vec::new();
768            let mut style_row = Vec::new();
769            for column in range.column..range.column + range.width {
770                let old_value = self
771                    .model
772                    .workbook
773                    .worksheet(sheet)?
774                    .cell(row, column)
775                    .cloned();
776                data_row.push(old_value);
777                let old_style = self.model.get_style_for_cell(sheet, row, column)?;
778                style_row.push(old_style);
779            }
780            old_value.push(data_row);
781            old_style.push(style_row);
782        }
783        // Clearing the cells also removes their links: capture them for undo
784        let link_diffs = self.range_link_diffs(range)?;
785        self.model.range_clear_all(range)?;
786        let mut diff_list = vec![Diff::RangeClearAll {
787            sheet,
788            row: range.row,
789            column: range.column,
790            width: range.width,
791            height: range.height,
792            old_value,
793            old_style,
794        }];
795        diff_list.extend(link_diffs);
796
797        self.push_diff_list(diff_list);
798        self.evaluate_if_not_paused();
799        Ok(())
800    }
801
802    /// Deletes the content in cells, but keeps the style
803    ///
804    /// See also:
805    /// * [Model::range_clear_contents]
806    pub fn range_clear_contents(&mut self, range: &Area) -> Result<(), String> {
807        let sheet = range.sheet;
808        // TODO: full rows/columns
809        let mut old_value = Vec::new();
810        for row in range.row..range.row + range.height {
811            let mut data_row = Vec::new();
812            for column in range.column..range.column + range.width {
813                let old_value = self
814                    .model
815                    .workbook
816                    .worksheet(sheet)?
817                    .cell(row, column)
818                    .cloned();
819                data_row.push(old_value);
820            }
821            old_value.push(data_row);
822        }
823        // Clearing the cells also removes their links: capture them for undo
824        let link_diffs = self.range_link_diffs(range)?;
825        self.model.range_clear_contents(range)?;
826        let mut diff_list = vec![Diff::RangeClearContents {
827            sheet,
828            row: range.row,
829            column: range.column,
830            width: range.width,
831            height: range.height,
832            old_value,
833        }];
834        diff_list.extend(link_diffs);
835        self.push_diff_list(diff_list);
836        self.evaluate_if_not_paused();
837        Ok(())
838    }
839
840    /// Returns the diffs that remove the links of the cells in `range`, so that
841    /// undoing a clear operation restores them.
842    pub(super) fn range_link_diffs(&self, range: &Area) -> Result<Vec<Diff>, String> {
843        let mut diffs = Vec::new();
844        for (&(row, column), link) in &self.model.workbook.worksheet(range.sheet)?.links {
845            if row >= range.row
846                && row < range.row + range.height
847                && column >= range.column
848                && column < range.column + range.width
849            {
850                diffs.push(Diff::SetCellLink {
851                    sheet: range.sheet,
852                    row,
853                    column,
854                    old_value: Box::new(Some(link.clone())),
855                    new_value: Box::new(None),
856                });
857            }
858        }
859        Ok(diffs)
860    }
861
862    fn clear_column_formatting(
863        &mut self,
864        sheet: u32,
865        column: i32,
866        diff_list: &mut Vec<Diff>,
867    ) -> Result<(), String> {
868        let old_value = self.model.get_column_style(sheet, column)?;
869        self.model.delete_column_style(sheet, column)?;
870        diff_list.push(Diff::DeleteColumnStyle {
871            sheet,
872            column,
873            old_value: Box::new(old_value),
874        });
875
876        let data_rows: Vec<i32> = self
877            .model
878            .workbook
879            .worksheet(sheet)?
880            .sheet_data
881            .keys()
882            .copied()
883            .collect();
884        let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
885
886        // Delete the formatting in all non empty cells
887        for row in data_rows {
888            if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
889                // We can always assume that style with style_index 0 exists and it is the default
890                self.model
891                    .workbook
892                    .worksheet_mut(sheet)?
893                    .set_cell_style(row, column, 0)?;
894                diff_list.push(Diff::CellClearFormatting {
895                    sheet,
896                    row,
897                    column,
898                    old_style: Box::new(Some(old_style)),
899                });
900            } else {
901                let old_style = self.model.get_style_for_cell(sheet, row, column)?;
902                if old_style != Style::default() {
903                    self.model
904                        .workbook
905                        .worksheet_mut(sheet)?
906                        .set_cell_style(row, column, 0)?;
907                    diff_list.push(Diff::CellClearFormatting {
908                        sheet,
909                        row,
910                        column,
911                        old_style: Box::new(None),
912                    });
913                }
914            }
915        }
916        // Delete the formatting in all cells with a row style
917        for row in styled_rows {
918            if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row.r, column)? {
919                // We can always assume that style with style_index 0 exists and it is the default
920                self.model
921                    .workbook
922                    .worksheet_mut(sheet)?
923                    .set_cell_style(row.r, column, 0)?;
924                diff_list.push(Diff::CellClearFormatting {
925                    sheet,
926                    row: row.r,
927                    column,
928                    old_style: Box::new(Some(old_style)),
929                });
930            } else {
931                let old_style = self.model.get_style_for_cell(sheet, row.r, column)?;
932                if old_style != Style::default() {
933                    self.model
934                        .workbook
935                        .worksheet_mut(sheet)?
936                        .set_cell_style(row.r, column, 0)?;
937                    diff_list.push(Diff::CellClearFormatting {
938                        sheet,
939                        row: row.r,
940                        column,
941                        old_style: Box::new(None),
942                    });
943                }
944            }
945        }
946        Ok(())
947    }
948
949    fn clear_row_formatting(
950        &mut self,
951        sheet: u32,
952        row: i32,
953        diff_list: &mut Vec<Diff>,
954    ) -> Result<(), String> {
955        let old_value = self.model.get_row_style(sheet, row)?;
956        self.model.delete_row_style(sheet, row)?;
957        diff_list.push(Diff::DeleteRowStyle {
958            sheet,
959            row,
960            old_value: Box::new(old_value),
961        });
962
963        // Delete the formatting in all non empty cells
964        let columns: Vec<i32> = self
965            .model
966            .workbook
967            .worksheet(sheet)?
968            .sheet_data
969            .get(&row)
970            .map(|row_data| row_data.keys().copied().collect())
971            .unwrap_or_default();
972        for column in columns {
973            if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
974                // We can always assume that style with style_index 0 exists and it is the default
975                self.model
976                    .workbook
977                    .worksheet_mut(sheet)?
978                    .set_cell_style(row, column, 0)?;
979                diff_list.push(Diff::CellClearFormatting {
980                    sheet,
981                    row,
982                    column,
983                    old_style: Box::new(Some(old_style)),
984                });
985            } else {
986                let old_style = self.model.get_style_for_cell(sheet, row, column)?;
987                if old_style != Style::default() {
988                    self.model
989                        .workbook
990                        .worksheet_mut(sheet)?
991                        .set_cell_style(row, column, 0)?;
992                    diff_list.push(Diff::CellClearFormatting {
993                        sheet,
994                        row,
995                        column,
996                        old_style: Box::new(None),
997                    });
998                }
999            }
1000        }
1001        Ok(())
1002    }
1003
1004    /// Removes cells styles and formatting, but keeps the content
1005    ///
1006    /// See also:
1007    /// * [UserModel::range_clear_all]
1008    /// * [UserModel::range_clear_contents]
1009    pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> {
1010        let sheet = range.sheet;
1011        let mut diff_list = Vec::new();
1012        if range.row == 1 && range.height == LAST_ROW {
1013            for column in range.column..range.column + range.width {
1014                self.clear_column_formatting(sheet, column, &mut diff_list)?;
1015            }
1016            self.push_diff_list(diff_list);
1017            return Ok(());
1018        }
1019        if range.column == 1 && range.width == LAST_COLUMN {
1020            for row in range.row..range.row + range.height {
1021                self.clear_row_formatting(sheet, row, &mut diff_list)?;
1022            }
1023            self.push_diff_list(diff_list);
1024            return Ok(());
1025        }
1026        for row in range.row..range.row + range.height {
1027            for column in range.column..range.column + range.width {
1028                if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? {
1029                    // We can always assume that style with style_index 0 exists and it is the default
1030                    self.model
1031                        .workbook
1032                        .worksheet_mut(sheet)?
1033                        .set_cell_style(row, column, 0)?;
1034                    diff_list.push(Diff::CellClearFormatting {
1035                        sheet,
1036                        row,
1037                        column,
1038                        old_style: Box::new(Some(old_style)),
1039                    });
1040                } else {
1041                    let old_style = self.model.get_style_for_cell(sheet, row, column)?;
1042                    if old_style != Style::default() {
1043                        self.model
1044                            .workbook
1045                            .worksheet_mut(sheet)?
1046                            .set_cell_style(row, column, 0)?;
1047                        diff_list.push(Diff::CellClearFormatting {
1048                            sheet,
1049                            row,
1050                            column,
1051                            old_style: Box::new(None),
1052                        });
1053                    }
1054                }
1055            }
1056        }
1057        self.push_diff_list(diff_list);
1058        Ok(())
1059    }
1060
1061    /// Inserts `row_count` blank rows starting at `row` (both 0-based).
1062    ///
1063    /// Parameters
1064    /// * `sheet` – worksheet index.
1065    /// * `row` – first row to insert.
1066    /// * `row_count` – number of rows (> 0).
1067    ///
1068    /// History: the method pushes `row_count` `Diff::InsertRow`
1069    /// items **all using the same `row` index**.  Replaying those diffs (undo / redo)
1070    /// is therefore immune to the row-shifts that happen after each individual
1071    /// insertion.
1072    ///
1073    /// See also [`Model::insert_rows`].
1074    pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
1075        self.model.insert_rows(sheet, row, row_count)?;
1076
1077        let diff_list = vec![Diff::InsertRows {
1078            sheet,
1079            row,
1080            count: row_count,
1081        }];
1082        self.push_diff_list(diff_list);
1083        self.evaluate_if_not_paused();
1084        Ok(())
1085    }
1086
1087    /// Inserts `column_count` blank columns starting at `column` (0-based).
1088    ///
1089    /// Parameters
1090    /// * `sheet` – worksheet index.
1091    /// * `column` – first column to insert.
1092    /// * `column_count` – number of columns (> 0).
1093    ///
1094    /// History: pushes one `Diff::InsertColumn`
1095    /// per inserted column, all with the same `column` value, preventing index
1096    /// drift when the diffs are reapplied.
1097    ///
1098    /// See also [`Model::insert_columns`].
1099    pub fn insert_columns(
1100        &mut self,
1101        sheet: u32,
1102        column: i32,
1103        column_count: i32,
1104    ) -> Result<(), String> {
1105        self.model.insert_columns(sheet, column, column_count)?;
1106
1107        let diff_list = vec![Diff::InsertColumns {
1108            sheet,
1109            column,
1110            count: column_count,
1111        }];
1112        self.push_diff_list(diff_list);
1113        self.evaluate_if_not_paused();
1114        Ok(())
1115    }
1116
1117    /// Deletes `row_count` rows starting at `row`.
1118    ///
1119    /// History: a `Diff::DeleteRow` is created for
1120    /// each row, ordered **bottom → top**.  Undo therefore recreates rows from
1121    /// top → bottom and redo removes them bottom → top, avoiding index drift.
1122    ///
1123    /// See also [`Model::delete_rows`].
1124    pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
1125        let worksheet = self.model.workbook.worksheet(sheet)?;
1126        let mut old_data = Vec::new();
1127        // Collect data for all rows to be deleted
1128        for r in row..row + row_count {
1129            let mut row_data = None;
1130            for rd in &worksheet.rows {
1131                if rd.r == r {
1132                    row_data = Some(rd.clone());
1133                    break;
1134                }
1135            }
1136            // SpillCells are transient; save their style as EmptyCell so undo can
1137            // restore the style index, letting evaluate() recreate the SpillCell correctly.
1138            let data = match worksheet.sheet_data.get(&r) {
1139                Some(s) => s
1140                    .iter()
1141                    .map(|(k, v)| {
1142                        let cell = if let Cell::SpillCell { s, .. } = v {
1143                            Cell::EmptyCell { s: *s }
1144                        } else {
1145                            v.clone()
1146                        };
1147                        (*k, cell)
1148                    })
1149                    .collect(),
1150                None => HashMap::new(),
1151            };
1152            old_data.push(RowData {
1153                row: row_data,
1154                data,
1155            });
1156        }
1157
1158        // The links of the deleted rows cannot be restored by re-inserting the
1159        // rows: capture them for undo. Links below the deleted rows just shift
1160        // with their cells, [`Model::delete_rows`] takes care of them.
1161        let mut diff_list = self.range_link_diffs(&Area {
1162            sheet,
1163            row,
1164            column: 1,
1165            width: LAST_COLUMN,
1166            height: row_count,
1167        })?;
1168
1169        self.model.delete_rows(sheet, row, row_count)?;
1170
1171        diff_list.push(Diff::DeleteRows {
1172            sheet,
1173            row,
1174            count: row_count,
1175            old_data,
1176        });
1177        self.push_diff_list(diff_list);
1178        self.evaluate_if_not_paused();
1179        Ok(())
1180    }
1181
1182    /// Deletes `column_count` columns starting at `column`.
1183    ///
1184    /// History: pushes one `Diff::DeleteColumn`
1185    /// per column, **right → left**, so replaying the list is always safe with
1186    /// respect to index shifts.
1187    ///
1188    /// See also [`Model::delete_columns`].
1189    pub fn delete_columns(
1190        &mut self,
1191        sheet: u32,
1192        column: i32,
1193        column_count: i32,
1194    ) -> Result<(), String> {
1195        let worksheet = self.model.workbook.worksheet(sheet)?;
1196        let mut old_data = Vec::new();
1197        // Collect data for all columns to be deleted
1198        for c in column..column + column_count {
1199            let mut column_data = None;
1200            for col in &worksheet.cols {
1201                if c >= col.min && c <= col.max {
1202                    column_data = Some(Col {
1203                        min: c,
1204                        max: c,
1205                        width: col.width,
1206                        custom_width: col.custom_width,
1207                        style: col.style,
1208                        hidden: col.hidden,
1209                    });
1210                    break;
1211                }
1212            }
1213
1214            // SpillCells are transient; save their style as EmptyCell so undo can
1215            // restore the style index, letting evaluate() recreate the SpillCell correctly.
1216            let mut data = HashMap::new();
1217            for (row_idx, row_data) in &worksheet.sheet_data {
1218                if let Some(cell) = row_data.get(&c) {
1219                    let saved = if let Cell::SpillCell { s, .. } = cell {
1220                        Cell::EmptyCell { s: *s }
1221                    } else {
1222                        cell.clone()
1223                    };
1224                    data.insert(*row_idx, saved);
1225                }
1226            }
1227
1228            old_data.push(ColumnData {
1229                column: column_data,
1230                data,
1231            });
1232        }
1233
1234        // The links of the deleted columns cannot be restored by re-inserting
1235        // the columns: capture them for undo. Links to the right of the deleted
1236        // columns just shift with their cells, [`Model::delete_columns`] takes
1237        // care of them.
1238        let mut diff_list = self.range_link_diffs(&Area {
1239            sheet,
1240            row: 1,
1241            column,
1242            width: column_count,
1243            height: LAST_ROW,
1244        })?;
1245
1246        self.model.delete_columns(sheet, column, column_count)?;
1247
1248        diff_list.push(Diff::DeleteColumns {
1249            sheet,
1250            column,
1251            count: column_count,
1252            old_data,
1253        });
1254        self.push_diff_list(diff_list);
1255        self.evaluate_if_not_paused();
1256        Ok(())
1257    }
1258
1259    /// Moves a column horizontally and adjusts formulas
1260    pub fn move_columns_action(
1261        &mut self,
1262        sheet: u32,
1263        column: i32,
1264        column_count: i32,
1265        delta: i32,
1266    ) -> Result<(), String> {
1267        if delta == 0 || column_count <= 0 {
1268            return Ok(());
1269        }
1270        // Adjust delta to skip hidden columns in the landing zone
1271        let mut new_delta = delta;
1272        let worksheet = self.model.workbook.worksheet(sheet)?;
1273        if delta > 0 {
1274            for col in column + column_count..=column + column_count + delta {
1275                if worksheet.is_column_hidden(col)? {
1276                    new_delta += 1;
1277                }
1278            }
1279        } else {
1280            for col in column + delta..column {
1281                if worksheet.is_column_hidden(col)? {
1282                    new_delta -= 1;
1283                }
1284            }
1285        }
1286
1287        self.model
1288            .move_columns_action(sheet, column, column_count, new_delta)?;
1289
1290        self.push_diff_list(vec![Diff::MoveColumns {
1291            sheet,
1292            column,
1293            column_count,
1294            delta: new_delta,
1295        }]);
1296        self.evaluate_if_not_paused();
1297        Ok(())
1298    }
1299
1300    /// Moves a group of rows vertically and adjusts formulas
1301    pub fn move_rows_action(
1302        &mut self,
1303        sheet: u32,
1304        row: i32,
1305        row_count: i32,
1306        delta: i32,
1307    ) -> Result<(), String> {
1308        if delta == 0 || row_count <= 0 {
1309            return Ok(());
1310        }
1311        let mut new_delta = delta;
1312        let worksheet = self.model.workbook.worksheet(sheet)?;
1313        if delta > 0 {
1314            for r in row + row_count..=row + row_count + delta {
1315                if worksheet.is_row_hidden(r)? {
1316                    new_delta += 1;
1317                }
1318            }
1319        } else {
1320            for r in row + delta..row {
1321                if worksheet.is_row_hidden(r)? {
1322                    new_delta -= 1;
1323                }
1324            }
1325        }
1326
1327        self.model
1328            .move_rows_action(sheet, row, row_count, new_delta)?;
1329
1330        self.push_diff_list(vec![Diff::MoveRows {
1331            sheet,
1332            row,
1333            row_count,
1334            delta: new_delta,
1335        }]);
1336        self.evaluate_if_not_paused();
1337        Ok(())
1338    }
1339
1340    /// Sets the width of a group of columns in a single diff list
1341    ///
1342    /// See also:
1343    /// * [Model::set_column_width]
1344    pub fn set_columns_width(
1345        &mut self,
1346        sheet: u32,
1347        column_start: i32,
1348        column_end: i32,
1349        width: f64,
1350    ) -> Result<(), String> {
1351        let mut diff_list = Vec::new();
1352        for column in column_start..=column_end {
1353            let old_value = self.model.get_column_width(sheet, column)?;
1354            diff_list.push(Diff::SetColumnWidth {
1355                sheet,
1356                column,
1357                new_value: width,
1358                old_value,
1359            });
1360            self.model.set_column_width(sheet, column, width)?;
1361        }
1362        self.push_diff_list(diff_list);
1363        Ok(())
1364    }
1365
1366    /// Sets the hidden state of a range of columns in a single diff list
1367    ////
1368    /// See also:
1369    /// * [Model::set_column_hidden]
1370    pub fn set_columns_hidden(
1371        &mut self,
1372        sheet: u32,
1373        column_start: i32,
1374        column_end: i32,
1375        hidden: bool,
1376    ) -> Result<(), String> {
1377        let mut diff_list = Vec::new();
1378        for column in column_start..=column_end {
1379            let old_value = self
1380                .model
1381                .workbook
1382                .worksheet(sheet)?
1383                .is_column_hidden(column)?;
1384            diff_list.push(Diff::SetColumnHidden {
1385                sheet,
1386                column,
1387                new_value: hidden,
1388                old_value,
1389            });
1390            self.model.set_column_hidden(sheet, column, hidden)?;
1391        }
1392        // If we are hiding columns we might need to adjust the selected column
1393        if hidden {
1394            if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
1395                if view.sheet == sheet {
1396                    // We select the next visible column
1397                    let mut column = column_end + 1;
1398                    while self
1399                        .model
1400                        .workbook
1401                        .worksheet(sheet)?
1402                        .is_column_hidden(column)?
1403                    {
1404                        column += 1;
1405                        if column > LAST_COLUMN {
1406                            break;
1407                        }
1408                    }
1409                    if column > LAST_COLUMN {
1410                        // We select the previous visible column
1411                        column = column_start - 1;
1412                        while self
1413                            .model
1414                            .workbook
1415                            .worksheet(sheet)?
1416                            .is_column_hidden(column)?
1417                        {
1418                            column -= 1;
1419                            if column <= 0 {
1420                                // We can't find a visible column
1421                                column = 1;
1422                                break;
1423                            }
1424                        }
1425                    }
1426                    self.set_selected_cell(1, column)?;
1427                    self.set_selected_range(1, column, LAST_ROW, column)?;
1428                }
1429            };
1430        }
1431        self.push_diff_list(diff_list);
1432        Ok(())
1433    }
1434
1435    /// Sets the hidden state of a range of rows in a single diff list
1436    ///// See also:
1437    /// * [Model::set_row_hidden]
1438    pub fn set_rows_hidden(
1439        &mut self,
1440        sheet: u32,
1441        row_start: i32,
1442        row_end: i32,
1443        hidden: bool,
1444    ) -> Result<(), String> {
1445        let mut diff_list = Vec::new();
1446        for row in row_start..=row_end {
1447            let old_value = self.model.workbook.worksheet(sheet)?.is_row_hidden(row)?;
1448            diff_list.push(Diff::SetRowHidden {
1449                sheet,
1450                row,
1451                new_value: hidden,
1452                old_value,
1453            });
1454            self.model.set_row_hidden(sheet, row, hidden)?;
1455        }
1456        // Select the next visible row if needed
1457        if hidden {
1458            if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
1459                if view.sheet == sheet {
1460                    // We select the next visible row
1461                    let mut row = row_end + 1;
1462                    while self.model.workbook.worksheet(sheet)?.is_row_hidden(row)? {
1463                        row += 1;
1464                        if row > LAST_ROW {
1465                            break;
1466                        }
1467                    }
1468                    if row > LAST_ROW {
1469                        // We select the previous visible row
1470                        row = row_start - 1;
1471                        while self.model.workbook.worksheet(sheet)?.is_row_hidden(row)? {
1472                            row -= 1;
1473                            if row <= 0 {
1474                                // We can't find a visible row
1475                                row = 1;
1476                                break;
1477                            }
1478                        }
1479                    }
1480                    self.set_selected_cell(row, 1)?;
1481                    self.set_selected_range(row, 1, row, LAST_COLUMN)?;
1482                }
1483            };
1484        }
1485        self.push_diff_list(diff_list);
1486        Ok(())
1487    }
1488
1489    /// Sets the height of a range of rows in a single diff list
1490    ///
1491    /// See also:
1492    /// * [Model::set_row_height]
1493    pub fn set_rows_height(
1494        &mut self,
1495        sheet: u32,
1496        row_start: i32,
1497        row_end: i32,
1498        height: f64,
1499    ) -> Result<(), String> {
1500        let mut diff_list = Vec::new();
1501        for row in row_start..=row_end {
1502            let old_value = self.model.get_row_height(sheet, row)?;
1503            diff_list.push(Diff::SetRowHeight {
1504                sheet,
1505                row,
1506                new_value: height,
1507                old_value,
1508            });
1509            self.model.set_row_height(sheet, row, height)?;
1510        }
1511        self.push_diff_list(diff_list);
1512        Ok(())
1513    }
1514
1515    /// Gets the height of a row
1516    ///
1517    /// See also:
1518    /// * [Model::get_row_height]
1519    #[inline]
1520    pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
1521        self.model.get_row_height(sheet, row)
1522    }
1523
1524    /// Gets the width of a column
1525    ///
1526    /// See also:
1527    /// * [Model::get_column_width]
1528    #[inline]
1529    pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
1530        self.model.get_column_width(sheet, column)
1531    }
1532
1533    /// Returns the number of frozen rows in the sheet
1534    ///
1535    /// See also:
1536    /// * [Model::get_frozen_rows_count()]
1537    #[inline]
1538    pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
1539        self.model.get_frozen_rows_count(sheet)
1540    }
1541
1542    /// Returns the number of frozen columns in the sheet
1543    ///
1544    /// See also:
1545    /// * [Model::get_frozen_columns_count()]
1546    #[inline]
1547    pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
1548        self.model.get_frozen_columns_count(sheet)
1549    }
1550
1551    /// Sets the number of frozen rows in sheet
1552    ///
1553    /// See also:
1554    /// * [Model::set_frozen_rows()]
1555    pub fn set_frozen_rows_count(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
1556        let old_value = self.model.get_frozen_rows_count(sheet)?;
1557        self.push_diff_list(vec![Diff::SetFrozenRowsCount {
1558            sheet,
1559            new_value: frozen_rows,
1560            old_value,
1561        }]);
1562        self.model.set_frozen_rows(sheet, frozen_rows)
1563    }
1564
1565    /// Sets the number of frozen columns in sheet
1566    ///
1567    /// See also:
1568    /// * [Model::set_frozen_columns()]
1569    pub fn set_frozen_columns_count(
1570        &mut self,
1571        sheet: u32,
1572        frozen_columns: i32,
1573    ) -> Result<(), String> {
1574        let old_value = self.model.get_frozen_columns_count(sheet)?;
1575        self.push_diff_list(vec![Diff::SetFrozenColumnsCount {
1576            sheet,
1577            new_value: frozen_columns,
1578            old_value,
1579        }]);
1580        self.model.set_frozen_columns(sheet, frozen_columns)
1581    }
1582
1583    /// Paste `styles` in the selected area
1584    pub fn on_paste_styles(&mut self, styles: &[Vec<Style>]) -> Result<(), String> {
1585        let styles_height = styles.len() as i32;
1586        let styles_width = styles[0].len() as i32;
1587        let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
1588            view.sheet
1589        } else {
1590            return Ok(());
1591        };
1592        let range = if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
1593            if let Some(view) = worksheet.views.get(&self.model.view_id) {
1594                view.range
1595            } else {
1596                return Ok(());
1597            }
1598        } else {
1599            return Ok(());
1600        };
1601
1602        // If the pasted area is smaller than the selected area we increase it
1603        let [row_start, column_start, row_end, column_end] = range;
1604        let last_row = row_end.max(row_start + styles_height - 1);
1605        let last_column = column_end.max(column_start + styles_width - 1);
1606
1607        let mut diff_list = Vec::new();
1608        for row in row_start..=last_row {
1609            for column in column_start..=last_column {
1610                let row_index = ((row - row_start) % styles_height) as usize;
1611                let column_index = ((column - column_start) % styles_width) as usize;
1612                let style = &styles[row_index][column_index];
1613                let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
1614                self.model.set_cell_style(sheet, row, column, style)?;
1615                diff_list.push(Diff::SetCellStyle {
1616                    sheet,
1617                    row,
1618                    column,
1619                    old_value: Box::new(old_value),
1620                    new_value: Box::new(style.clone()),
1621                });
1622            }
1623        }
1624        self.push_diff_list(diff_list);
1625
1626        // select the pasted range
1627        if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
1628            if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
1629                view.range = [row_start, column_start, last_row, last_column];
1630            }
1631        }
1632        Ok(())
1633    }
1634
1635    // Updates the style of a cell, adding the new style to the diff list
1636    fn update_single_cell_style(
1637        &mut self,
1638        sheet: u32,
1639        row: i32,
1640        column: i32,
1641        style_path: &str,
1642        value: &str,
1643        diff_list: &mut Vec<Diff>,
1644    ) -> Result<(), String> {
1645        // This is the value in the cell itself
1646        let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
1647
1648        // This takes into account row or column styles. We use the base style (no CF overlay)
1649        // because we are writing back a persistent style, not a transient CF result.
1650        let old_style = self.model.get_style_for_cell(sheet, row, column)?;
1651        let new_style = update_style(&old_style, style_path, value)?;
1652        self.model.set_cell_style(sheet, row, column, &new_style)?;
1653        diff_list.push(Diff::SetCellStyle {
1654            sheet,
1655            row,
1656            column,
1657            old_value: Box::new(old_value),
1658            new_value: Box::new(new_style),
1659        });
1660        Ok(())
1661    }
1662
1663    /// Updates the range with a cell style.
1664    /// See also:
1665    /// * [Model::set_cell_style]
1666    pub fn update_range_style(
1667        &mut self,
1668        range: &Area,
1669        style_path: &str,
1670        value: &str,
1671    ) -> Result<(), String> {
1672        let sheet = range.sheet;
1673        let mut diff_list = Vec::new();
1674        if range.row == 1 && range.height == LAST_ROW {
1675            // Full columns
1676            let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
1677            // We need all the rows in the column to update the style
1678            // NB: This is too much, this is all the rows that have values
1679            let data_rows: Vec<i32> = self
1680                .model
1681                .workbook
1682                .worksheet(sheet)?
1683                .sheet_data
1684                .keys()
1685                .copied()
1686                .collect();
1687            for column in range.column..range.column + range.width {
1688                // we set the style of the full column
1689                let old_style = self.model.get_column_style(sheet, column)?;
1690                let style = match old_style.as_ref() {
1691                    Some(s) => s,
1692                    None => &Style::default(),
1693                };
1694                let style = update_style(style, style_path, value)?;
1695                self.model.set_column_style(sheet, column, &style)?;
1696                diff_list.push(Diff::SetColumnStyle {
1697                    sheet,
1698                    column,
1699                    old_value: Box::new(old_style),
1700                    new_value: Box::new(style),
1701                });
1702
1703                // We need to update the styles in all cells that have a row style
1704                for row_s in styled_rows.iter() {
1705                    let row = row_s.r;
1706                    self.update_single_cell_style(
1707                        sheet,
1708                        row,
1709                        column,
1710                        style_path,
1711                        value,
1712                        &mut diff_list,
1713                    )?;
1714                }
1715
1716                // Update style in all cells that have different styles
1717                // FIXME: We need a better way to transverse of cells in a column
1718                for &row in &data_rows {
1719                    if let Some(data_row) =
1720                        self.model.workbook.worksheet(sheet)?.sheet_data.get(&row)
1721                    {
1722                        if data_row.get(&column).is_some() {
1723                            // If the cell has non empty content it will always have some style
1724                            self.update_single_cell_style(
1725                                sheet,
1726                                row,
1727                                column,
1728                                style_path,
1729                                value,
1730                                &mut diff_list,
1731                            )?;
1732                        }
1733                    }
1734                }
1735            }
1736        } else if range.column == 1 && range.width == LAST_COLUMN {
1737            // Full rows
1738            let styled_columns = &self.model.workbook.worksheet(sheet)?.cols.clone();
1739            for row in range.row..range.row + range.height {
1740                // Now update style in all cells that are not empty
1741                let columns: Vec<i32> = self
1742                    .model
1743                    .workbook
1744                    .worksheet(sheet)?
1745                    .sheet_data
1746                    .get(&row)
1747                    .map(|row_data| row_data.keys().copied().collect())
1748                    .unwrap_or_default();
1749                for column in columns {
1750                    self.update_single_cell_style(
1751                        sheet,
1752                        row,
1753                        column,
1754                        style_path,
1755                        value,
1756                        &mut diff_list,
1757                    )?;
1758                }
1759
1760                // We need to go through all the cells that have a column style and merge the styles
1761                for col in styled_columns.iter() {
1762                    for column in col.min..col.max + 1 {
1763                        self.update_single_cell_style(
1764                            sheet,
1765                            row,
1766                            column,
1767                            style_path,
1768                            value,
1769                            &mut diff_list,
1770                        )?;
1771                    }
1772                }
1773
1774                // Finally update the style of the row
1775                let old_style = self.model.get_row_style(sheet, row)?;
1776                let style = match old_style.as_ref() {
1777                    Some(s) => s,
1778                    None => &Style::default(),
1779                };
1780                let style = update_style(style, style_path, value)?;
1781                self.model.set_row_style(sheet, row, &style)?;
1782                diff_list.push(Diff::SetRowStyle {
1783                    sheet,
1784                    row,
1785                    old_value: Box::new(old_style),
1786                    new_value: Box::new(style),
1787                });
1788            }
1789        } else {
1790            for row in range.row..range.row + range.height {
1791                for column in range.column..range.column + range.width {
1792                    self.update_single_cell_style(
1793                        sheet,
1794                        row,
1795                        column,
1796                        style_path,
1797                        value,
1798                        &mut diff_list,
1799                    )?;
1800                }
1801            }
1802        }
1803        self.push_diff_list(diff_list);
1804        Ok(())
1805    }
1806
1807    /// Returns the style for a cell
1808    ///
1809    /// Cells share a border, so the left border of B1 is the right border of A1
1810    /// In the object structure the borders of the cells might be difference,
1811    /// We always pick the "heaviest" border.
1812    ///
1813    /// See also:
1814    /// * [Model::get_style_for_cell]
1815    pub fn get_cell_style(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
1816        let mut style = self.model.get_style_for_cell(sheet, row, column)?;
1817
1818        // We need to check if the adjacent cells have a "heavier" border
1819        let border_top = if row > 1 {
1820            self.model
1821                .get_style_for_cell(sheet, row - 1, column)?
1822                .border
1823                .bottom
1824        } else {
1825            None
1826        };
1827
1828        let border_right = if column < LAST_COLUMN {
1829            self.model
1830                .get_style_for_cell(sheet, row, column + 1)?
1831                .border
1832                .left
1833        } else {
1834            None
1835        };
1836
1837        let border_bottom = if row < LAST_ROW {
1838            self.model
1839                .get_style_for_cell(sheet, row + 1, column)?
1840                .border
1841                .top
1842        } else {
1843            None
1844        };
1845
1846        let border_left = if column > 1 {
1847            self.model
1848                .get_style_for_cell(sheet, row, column - 1)?
1849                .border
1850                .right
1851        } else {
1852            None
1853        };
1854
1855        if is_max_border(style.border.top.as_ref(), border_top.as_ref()) {
1856            style.border.top = border_top;
1857        }
1858
1859        if is_max_border(style.border.right.as_ref(), border_right.as_ref()) {
1860            style.border.right = border_right;
1861        }
1862
1863        if is_max_border(style.border.bottom.as_ref(), border_bottom.as_ref()) {
1864            style.border.bottom = border_bottom;
1865        }
1866
1867        if is_max_border(style.border.left.as_ref(), border_left.as_ref()) {
1868            style.border.left = border_left;
1869        }
1870
1871        Ok(style)
1872    }
1873
1874    /// Returns the full extended style for a cell, including any conditional formatting overlay.
1875    ///
1876    /// Identical border-adjacency logic as [`Self::get_cell_style`] but applied to the CF-overlaid style.
1877    /// Use this when you need icon-set or data-bar decorations in addition to the base style.
1878    pub fn get_extended_cell_style(
1879        &self,
1880        sheet: u32,
1881        row: i32,
1882        column: i32,
1883    ) -> Result<ExtendedStyle, String> {
1884        let mut extended = self.model.get_extended_style_for_cell(sheet, row, column)?;
1885
1886        let border_top = if row > 1 {
1887            self.model
1888                .get_style_for_cell(sheet, row - 1, column)?
1889                .border
1890                .bottom
1891        } else {
1892            None
1893        };
1894
1895        let border_right = if column < LAST_COLUMN {
1896            self.model
1897                .get_style_for_cell(sheet, row, column + 1)?
1898                .border
1899                .left
1900        } else {
1901            None
1902        };
1903
1904        let border_bottom = if row < LAST_ROW {
1905            self.model
1906                .get_style_for_cell(sheet, row + 1, column)?
1907                .border
1908                .top
1909        } else {
1910            None
1911        };
1912
1913        let border_left = if column > 1 {
1914            self.model
1915                .get_style_for_cell(sheet, row, column - 1)?
1916                .border
1917                .right
1918        } else {
1919            None
1920        };
1921
1922        if is_max_border(extended.style.border.top.as_ref(), border_top.as_ref()) {
1923            extended.style.border.top = border_top;
1924        }
1925
1926        if is_max_border(extended.style.border.right.as_ref(), border_right.as_ref()) {
1927            extended.style.border.right = border_right;
1928        }
1929
1930        if is_max_border(
1931            extended.style.border.bottom.as_ref(),
1932            border_bottom.as_ref(),
1933        ) {
1934            extended.style.border.bottom = border_bottom;
1935        }
1936
1937        if is_max_border(extended.style.border.left.as_ref(), border_left.as_ref()) {
1938            extended.style.border.left = border_left;
1939        }
1940
1941        Ok(extended)
1942    }
1943
1944    /// Returns information about the sheets
1945    ///
1946    /// See also:
1947    /// * [Model::get_worksheets_properties]
1948    #[inline]
1949    pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
1950        self.model.get_worksheets_properties()
1951    }
1952
1953    /// Sets the workbook theme.
1954    pub fn set_theme(&mut self, theme: Theme) {
1955        let old_value = self.model.workbook.theme.clone();
1956        let new_value = theme.clone();
1957        self.model.set_theme(theme);
1958        self.push_diff_list(vec![Diff::SetTheme {
1959            old_value: Box::new(old_value),
1960            new_value: Box::new(new_value),
1961        }]);
1962    }
1963
1964    /// Returns the current workbook theme.
1965    pub fn get_theme(&self) -> Theme {
1966        self.model.get_theme()
1967    }
1968
1969    /// Resolves a `Color` value to a CSS hex string using the current workbook theme.
1970    /// Returns an empty string for `Color::None`.
1971    pub fn resolve_color(&self, color: &Color) -> String {
1972        color.to_rgb(&self.model.workbook.theme)
1973    }
1974
1975    /// Set the gid lines in the worksheet to visible (`true`) or hidden (`false`)
1976    pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
1977        let old_value = self.model.workbook.worksheet(sheet)?.show_grid_lines;
1978        self.model.set_show_grid_lines(sheet, show_grid_lines)?;
1979
1980        self.push_diff_list(vec![Diff::SetShowGridLines {
1981            sheet,
1982            new_value: show_grid_lines,
1983            old_value,
1984        }]);
1985        Ok(())
1986    }
1987
1988    /// Returns true in the grid lines for
1989    pub fn get_show_grid_lines(&self, sheet: u32) -> Result<bool, String> {
1990        Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
1991    }
1992
1993    /// Returns the largest column in the row less than a column whose cell has a non empty value.
1994    /// If there are none it returns `None`.
1995    /// This is useful when rendering a part of a worksheet to know which cells spill over
1996    pub fn get_last_non_empty_in_row_before_column(
1997        &self,
1998        sheet: u32,
1999        row: i32,
2000        column: i32,
2001    ) -> Result<Option<i32>, String> {
2002        let worksheet = self.model.workbook.worksheet(sheet)?;
2003        let data = worksheet.sheet_data.get(&row);
2004        if let Some(row_data) = data {
2005            let mut last_column = None;
2006            let mut columns: Vec<i32> = row_data.keys().copied().collect();
2007            columns.sort_unstable();
2008            for col in columns {
2009                if col < column {
2010                    if let Some(cell) = worksheet.cell(row, col) {
2011                        if matches!(cell, Cell::EmptyCell { .. }) {
2012                            continue;
2013                        }
2014                    }
2015                    last_column = Some(col);
2016                }
2017            }
2018            Ok(last_column)
2019        } else {
2020            Ok(None)
2021        }
2022    }
2023
2024    /// Returns the smallest column in the row larger than "column" whose cell has a non empty value.
2025    /// If there are none it returns `None`.
2026    /// This is useful when rendering a part of a worksheet to know which cells spill over
2027    pub fn get_first_non_empty_in_row_after_column(
2028        &self,
2029        sheet: u32,
2030        row: i32,
2031        column: i32,
2032    ) -> Result<Option<i32>, String> {
2033        let worksheet = self.model.workbook.worksheet(sheet)?;
2034        let data = worksheet.sheet_data.get(&row);
2035        if let Some(row_data) = data {
2036            let mut columns: Vec<i32> = row_data.keys().copied().collect();
2037            // We sort the keys to ensure we are going from left to right
2038            columns.sort_unstable();
2039            for col in columns {
2040                if col > column {
2041                    if let Some(cell) = worksheet.cell(row, col) {
2042                        if matches!(cell, Cell::EmptyCell { .. }) {
2043                            continue;
2044                        }
2045                    }
2046                    return Ok(Some(col));
2047                }
2048            }
2049        }
2050        Ok(None)
2051    }
2052
2053    /// Returns the geometric structure of a cell
2054    pub fn get_cell_array_structure(
2055        &self,
2056        sheet: u32,
2057        row: i32,
2058        column: i32,
2059    ) -> Result<CellArrayStructure, String> {
2060        let cell = self
2061            .model
2062            .workbook
2063            .worksheet(sheet)?
2064            .cell(row, column)
2065            .cloned()
2066            .unwrap_or_default();
2067        match cell {
2068            Cell::EmptyCell { .. }
2069            | Cell::BooleanCell { .. }
2070            | Cell::NumberCell { .. }
2071            | Cell::ErrorCell { .. }
2072            | Cell::SharedString { .. }
2073            | Cell::CellFormula { .. } => Ok(CellArrayStructure::SingleCell),
2074            Cell::SpillCell { a, .. } => {
2075                let (m_row, m_column) = a;
2076                let m_cell = self
2077                    .model
2078                    .workbook
2079                    .worksheet(sheet)?
2080                    .cell(m_row, m_column)
2081                    .cloned()
2082                    .unwrap_or_default();
2083                let (width, height, is_dynamic) = match m_cell {
2084                    Cell::ArrayFormula {
2085                        r,
2086                        kind: ArrayKind::Dynamic,
2087                        ..
2088                    } => (r.0, r.1, true),
2089                    Cell::ArrayFormula {
2090                        r,
2091                        kind: ArrayKind::Cse,
2092                        ..
2093                    } => (r.0, r.1, false),
2094                    _ => return Err("Invalid structure".to_string()),
2095                };
2096                if is_dynamic {
2097                    Ok(CellArrayStructure::DynamicChild(
2098                        m_row, m_column, width, height,
2099                    ))
2100                } else {
2101                    Ok(CellArrayStructure::ArrayChild(
2102                        m_row, m_column, width, height,
2103                    ))
2104                }
2105            }
2106            Cell::ArrayFormula {
2107                r,
2108                kind: ArrayKind::Dynamic,
2109                ..
2110            } => Ok(CellArrayStructure::DynamicAnchor(r.0, r.1)),
2111            Cell::ArrayFormula {
2112                r,
2113                kind: ArrayKind::Cse,
2114                ..
2115            } => Ok(CellArrayStructure::ArrayAnchor(r.0, r.1)),
2116        }
2117    }
2118
2119    /// Sets an array formula in the given range.
2120    pub fn set_user_array_formula(
2121        &mut self,
2122        sheet: u32,
2123        row: i32,
2124        column: i32,
2125        width: i32,
2126        height: i32,
2127        formula: &str,
2128    ) -> Result<(), String> {
2129        let ws = self.model.workbook.worksheet(sheet)?;
2130        let mut old_values = Vec::new();
2131        for r in row..row + height {
2132            let mut row_vals = Vec::new();
2133            for c in column..column + width {
2134                let cell = ws.cell(r, c).cloned();
2135                // SpillCells are transient — restored by re-evaluation, so store as None.
2136                let cell = if matches!(cell, Some(Cell::SpillCell { .. })) {
2137                    None
2138                } else {
2139                    cell
2140                };
2141                row_vals.push(cell);
2142            }
2143            old_values.push(row_vals);
2144        }
2145        self.model
2146            .set_user_array_formula(sheet, row, column, width, height, formula)?;
2147        self.push_diff_list(vec![Diff::SetArrayValue {
2148            sheet,
2149            row,
2150            column,
2151            width,
2152            height,
2153            new_value: formula.to_string(),
2154            old_values,
2155        }]);
2156        self.evaluate_if_not_paused();
2157        Ok(())
2158    }
2159
2160    /// Returns the list of defined names
2161    pub fn get_defined_name_list(&self) -> Vec<(String, Option<u32>, String)> {
2162        self.model.get_defined_name_list()
2163    }
2164
2165    /// Delete an existing defined name
2166    pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
2167        let old_value = self.model.get_defined_name_formula(name, scope)?;
2168        let diff_list = vec![Diff::DeleteDefinedName {
2169            name: name.to_string(),
2170            scope,
2171            old_value,
2172        }];
2173        self.push_diff_list(diff_list);
2174        self.model.delete_defined_name(name, scope)?;
2175        self.evaluate_if_not_paused();
2176        Ok(())
2177    }
2178
2179    /// Create a new defined name
2180    pub fn new_defined_name(
2181        &mut self,
2182        name: &str,
2183        scope: Option<u32>,
2184        formula: &str,
2185    ) -> Result<(), String> {
2186        self.model.new_defined_name(name, scope, formula)?;
2187        // Diffs store the internal (English) formula so undo/redo replays
2188        // correctly regardless of the active language at replay time. A
2189        // just-created name is guaranteed to be retrievable, so propagate any
2190        // (unexpected) error rather than masking it with a localized formula.
2191        let value = self.model.get_defined_name_formula(name, scope)?;
2192        let diff_list = vec![Diff::CreateDefinedName {
2193            name: name.to_string(),
2194            scope,
2195            value,
2196        }];
2197        self.push_diff_list(diff_list);
2198        self.evaluate_if_not_paused();
2199        Ok(())
2200    }
2201
2202    /// Updates a defined name
2203    pub fn update_defined_name(
2204        &mut self,
2205        name: &str,
2206        scope: Option<u32>,
2207        new_name: &str,
2208        new_scope: Option<u32>,
2209        new_formula: &str,
2210    ) -> Result<(), String> {
2211        // Both formulas in the diff are stored internally (in English) so
2212        // undo/redo replays correctly regardless of the active language.
2213        let old_formula = self
2214            .model
2215            .get_defined_name_formula(name, scope)
2216            .map_err(|_| "General: Failed to get old name")?;
2217        self.model
2218            .update_defined_name(name, scope, new_name, new_scope, new_formula)?;
2219        // Read back the canonical (English) formula that was just stored so the
2220        // diff stays canonical; a successful update guarantees it is retrievable.
2221        let new_formula_internal = self.model.get_defined_name_formula(new_name, new_scope)?;
2222        let diff_list = vec![Diff::UpdateDefinedName {
2223            name: name.to_string(),
2224            scope,
2225            old_formula,
2226            new_name: new_name.to_string(),
2227            new_scope,
2228            new_formula: new_formula_internal,
2229        }];
2230        self.push_diff_list(diff_list);
2231        self.evaluate_if_not_paused();
2232        Ok(())
2233    }
2234
2235    /// validates a new defined name
2236    pub fn is_valid_defined_name(
2237        &mut self,
2238        name: &str,
2239        scope: Option<u32>,
2240        formula: &str,
2241    ) -> Result<Option<u32>, String> {
2242        self.model.is_valid_defined_name(name, scope, formula)
2243    }
2244
2245    /// Sets the timezone for the model
2246    pub fn set_timezone(&mut self, timezone: &str) -> Result<(), String> {
2247        let diff_list = vec![Diff::SetTimezone {
2248            old_value: self.get_timezone(),
2249            new_value: timezone.to_string(),
2250        }];
2251        self.push_diff_list(diff_list);
2252        self.model.set_timezone(timezone)
2253    }
2254
2255    /// Sets the locale for the model
2256    pub fn set_locale(&mut self, locale: &str) -> Result<(), String> {
2257        let diff_list = vec![Diff::SetLocale {
2258            old_value: self.get_locale(),
2259            new_value: locale.to_string(),
2260        }];
2261        self.push_diff_list(diff_list);
2262        self.model.set_locale(locale)
2263    }
2264
2265    /// Gets the timezone of the model
2266    pub fn get_timezone(&self) -> String {
2267        self.model.get_timezone()
2268    }
2269
2270    /// Gets the locale of the model
2271    pub fn get_locale(&self) -> String {
2272        self.model.get_locale()
2273    }
2274
2275    /// Get the language for the model
2276    pub fn get_language(&self) -> String {
2277        self.model.get_language()
2278    }
2279
2280    /// Sets the language for the model
2281    pub fn set_language(&mut self, language: &str) -> Result<(), String> {
2282        self.model.set_language(language)
2283    }
2284
2285    /// Gets the formatting settings for the model
2286    pub fn get_fmt_settings(&self) -> FmtSettings {
2287        self.model.get_fmt_settings()
2288    }
2289
2290    // **** Private methods ****** //
2291
2292    pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
2293        self.send_queue.push(QueueDiffs {
2294            r#type: DiffType::Redo,
2295            list: diff_list.clone(),
2296        });
2297        self.history.push(diff_list);
2298    }
2299
2300    pub(super) fn evaluate_if_not_paused(&mut self) {
2301        if !self.pause_evaluation {
2302            self.model.evaluate();
2303        }
2304    }
2305}
2306
2307#[cfg(test)]
2308mod tests {
2309    use crate::{
2310        types::{HorizontalAlignment, VerticalAlignment},
2311        user_model::common::{horizontal, selected_sheet_after_move, vertical},
2312    };
2313
2314    #[test]
2315    fn test_selected_sheet_after_move() {
2316        // The moved sheet is followed to its destination.
2317        assert_eq!(selected_sheet_after_move(0, 0, 2), 2);
2318        assert_eq!(selected_sheet_after_move(3, 3, 0), 0);
2319
2320        // A sheet between the source and destination shifts by one.
2321        // [A,B,C,D], select C (2), move A (0) -> 2 => [B,C,A,D], C is at 1.
2322        assert_eq!(selected_sheet_after_move(2, 0, 2), 1);
2323        // [A,B,C,D], select A (0), move C (2) -> 0 => [C,A,B,D], A is at 1.
2324        assert_eq!(selected_sheet_after_move(0, 2, 0), 1);
2325
2326        // A sheet outside the moved span keeps its index.
2327        // [A,B,C,D], select D (3), move B (1) -> 2 => [A,C,B,D], D still at 3.
2328        assert_eq!(selected_sheet_after_move(3, 1, 2), 3);
2329    }
2330
2331    #[test]
2332    fn test_vertical() {
2333        let all = vec![
2334            VerticalAlignment::Bottom,
2335            VerticalAlignment::Center,
2336            VerticalAlignment::Distributed,
2337            VerticalAlignment::Justify,
2338            VerticalAlignment::Top,
2339        ];
2340        for a in all {
2341            assert_eq!(vertical(&format!("{a}")), Ok(a));
2342        }
2343    }
2344
2345    #[test]
2346    fn test_horizontal() {
2347        let all = vec![
2348            HorizontalAlignment::Center,
2349            HorizontalAlignment::CenterContinuous,
2350            HorizontalAlignment::Distributed,
2351            HorizontalAlignment::Fill,
2352            HorizontalAlignment::General,
2353            HorizontalAlignment::Justify,
2354            HorizontalAlignment::Left,
2355            HorizontalAlignment::Right,
2356        ];
2357        for a in all {
2358            assert_eq!(horizontal(&format!("{a}")), Ok(a));
2359        }
2360    }
2361}