Skip to main content

ironcalc_base/user_model/
common.rs

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