#![deny(missing_docs)] use std::{collections::HashMap, fmt::Debug, io::Cursor}; use csv::{ReaderBuilder, WriterBuilder}; use serde::{Deserialize, Serialize}; use crate::{ constants::{self, LAST_COLUMN, LAST_ROW}, expressions::{ types::{Area, CellReferenceIndex}, utils::{is_valid_column_number, is_valid_row}, }, model::Model, types::{ Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties, SheetState, Style, VerticalAlignment, }, utils::is_valid_hex_color, }; use crate::user_model::history::{ ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData, }; use super::border_utils::is_max_border; /// Data for the clipboard pub type ClipboardData = HashMap>; pub type ClipboardTuple = (i32, i32, i32, i32); #[derive(Serialize, Deserialize)] pub struct ClipboardCell { text: String, style: Style, } #[derive(Serialize, Deserialize)] pub struct Clipboard { pub(crate) csv: String, pub(crate) data: ClipboardData, pub(crate) sheet: u32, pub(crate) range: (i32, i32, i32, i32), } #[derive(Serialize, Deserialize, PartialEq)] pub enum BorderType { All, Inner, Outer, Top, Right, Bottom, Left, CenterH, CenterV, None, } /// This is the struct for a border area #[derive(Serialize, Deserialize)] pub struct BorderArea { pub(crate) item: BorderItem, pub(crate) r#type: BorderType, } fn boolean(value: &str) -> Result { match value { "true" => Ok(true), "false" => Ok(false), _ => Err(format!("Invalid value for boolean: '{value}'.")), } } fn color(value: &str) -> Result, String> { if value.is_empty() { return Ok(None); } if !is_valid_hex_color(value) { return Err(format!("Invalid color: '{value}'.")); } Ok(Some(value.to_owned())) } fn horizontal(value: &str) -> Result { match value { "center" => Ok(HorizontalAlignment::Center), "centerContinuous" => Ok(HorizontalAlignment::CenterContinuous), "distributed" => Ok(HorizontalAlignment::Distributed), "fill" => Ok(HorizontalAlignment::Fill), "general" => Ok(HorizontalAlignment::General), "justify" => Ok(HorizontalAlignment::Justify), "left" => Ok(HorizontalAlignment::Left), "right" => Ok(HorizontalAlignment::Right), _ => Err(format!( "Invalid value for horizontal alignment: '{value}'." )), } } fn vertical(value: &str) -> Result { match value { "bottom" => Ok(VerticalAlignment::Bottom), "center" => Ok(VerticalAlignment::Center), "distributed" => Ok(VerticalAlignment::Distributed), "justify" => Ok(VerticalAlignment::Justify), "top" => Ok(VerticalAlignment::Top), _ => Err(format!("Invalid value for vertical alignment: '{value}'.")), } } fn update_style(old_value: &Style, style_path: &str, value: &str) -> Result { let mut style = old_value.clone(); match style_path { "font.b" => { style.font.b = boolean(value)?; } "font.i" => { style.font.i = boolean(value)?; } "font.u" => { style.font.u = boolean(value)?; } "font.strike" => { style.font.strike = boolean(value)?; } "font.color" => { style.font.color = color(value)?; } "font.size_delta" => { // This is a special case, we need to add the value to the current size let size_delta: i32 = value .parse() .map_err(|_| format!("Invalid value for font size: '{value}'."))?; let new_size = style.font.sz + size_delta; if new_size < 1 { return Err(format!("Invalid value for font size: '{new_size}'.")); } style.font.sz = new_size; } "fill.bg_color" => { style.fill.bg_color = color(value)?; style.fill.pattern_type = "solid".to_string(); } "fill.fg_color" => { style.fill.fg_color = color(value)?; style.fill.pattern_type = "solid".to_string(); } "num_fmt" => { value.clone_into(&mut style.num_fmt); } "alignment" => { if !value.is_empty() { return Err(format!("Alignment must be empty, but found: '{value}'.")); } style.alignment = None; } "alignment.horizontal" => match style.alignment { Some(ref mut s) => s.horizontal = horizontal(value)?, None => { let alignment = Alignment { horizontal: horizontal(value)?, ..Default::default() }; style.alignment = Some(alignment) } }, "alignment.vertical" => match style.alignment { Some(ref mut s) => s.vertical = vertical(value)?, None => { let alignment = Alignment { vertical: vertical(value)?, ..Default::default() }; style.alignment = Some(alignment) } }, "alignment.wrap_text" => match style.alignment { Some(ref mut s) => s.wrap_text = boolean(value)?, None => { let alignment = Alignment { wrap_text: boolean(value)?, ..Default::default() }; style.alignment = Some(alignment) } }, _ => { return Err(format!("Invalid style path: '{style_path}'.")); } } Ok(style) } /// # A wrapper around [`Model`] for a spreadsheet end user. /// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management. /// /// A diff in this context (or more correctly a _user diff_) is a change created by a user. /// /// Automatic evaluation means that actions like setting a value on a cell or deleting a column /// will evaluate the model if needed. /// /// It is meant to be used by UI applications like Web IronCalc or TironCalc. /// /// /// # Examples /// /// ```rust /// # use ironcalc_base::UserModel; /// # fn main() -> Result<(), Box> { /// let mut model = UserModel::new_empty("model", "en", "UTC")?; /// model.set_user_input(0, 1, 1, "=1+1")?; /// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2"); /// model.undo()?; /// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, ""); /// model.redo()?; /// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2"); /// # Ok(()) /// # } /// ``` pub struct UserModel { pub(crate) model: Model, history: History, send_queue: Vec, pause_evaluation: bool, } impl Debug for UserModel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UserModel").finish() } } impl UserModel { /// Creates a user model from an existing model pub fn from_model(model: Model) -> UserModel { UserModel { model, history: History::default(), send_queue: vec![], pause_evaluation: false, } } /// Creates a new UserModel. /// /// See also: /// * [Model::new_empty] pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result { let model = Model::new_empty(name, locale_id, timezone)?; Ok(UserModel { model, history: History::default(), send_queue: vec![], pause_evaluation: false, }) } /// Creates a model from it's internal representation /// /// See also: /// * [Model::from_bytes] pub fn from_bytes(s: &[u8]) -> Result { let model = Model::from_bytes(s)?; Ok(UserModel { model, history: History::default(), send_queue: vec![], pause_evaluation: false, }) } /// Returns the internal representation of a model /// /// See also: /// * [Model::to_json_str] pub fn to_bytes(&self) -> Vec { self.model.to_bytes() } /// Returns the internal model pub fn get_model(&self) -> &Model { &self.model } /// Returns the workbook name pub fn get_name(&self) -> String { self.model.workbook.name.clone() } /// Sets the name of a workbook pub fn set_name(&mut self, name: &str) { self.model.workbook.name = name.to_string(); } /// Undoes last change if any, places the change in the redo list and evaluates the model if needed /// /// See also: /// * [UserModel::redo] pub fn undo(&mut self) -> Result<(), String> { if let Some(diff_list) = self.history.undo() { self.apply_undo_diff_list(&diff_list)?; self.send_queue.push(QueueDiffs { r#type: DiffType::Undo, list: diff_list.clone(), }); }; Ok(()) } /// Redoes the last undone change, places the change in the undo list and evaluates the model if needed /// /// See also: /// * [UserModel::redo] pub fn redo(&mut self) -> Result<(), String> { if let Some(diff_list) = self.history.redo() { self.apply_diff_list(&diff_list)?; self.send_queue.push(QueueDiffs { r#type: DiffType::Redo, list: diff_list.clone(), }); }; Ok(()) } /// Returns true if there are items to be undone pub fn can_undo(&self) -> bool { !self.history.undo_stack.is_empty() } /// Returns true if there are items to be redone pub fn can_redo(&self) -> bool { !self.history.redo_stack.is_empty() } /// Pauses automatic evaluation. /// /// See also: /// * [UserModel::evaluate] /// * [UserModel::resume_evaluation] pub fn pause_evaluation(&mut self) { self.pause_evaluation = true; } /// Resumes automatic evaluation. /// /// See also: /// * [UserModel::evaluate] /// * [UserModel::pause_evaluation] pub fn resume_evaluation(&mut self) { self.pause_evaluation = false; } /// Forces an evaluation of the model /// /// See also: /// * [Model::evaluate] /// * [UserModel::pause_evaluation] pub fn evaluate(&mut self) { self.model.evaluate() } /// Returns the list of pending diffs and removes them from the queue /// /// This is used together with [apply_external_diffs](UserModel::apply_external_diffs) to keep two remote models /// in sync. /// /// See also: /// * [UserModel::apply_external_diffs] pub fn flush_send_queue(&mut self) -> Vec { // This can never fail :O: let q = bitcode::encode(&self.send_queue); self.send_queue = vec![]; q } /// This are external diffs that need to be applied to the model /// /// This is used together with [flush_send_queue](UserModel::flush_send_queue) to keep two remote models in sync /// /// See also: /// * [UserModel::flush_send_queue] pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> { if let Ok(queue_diffs_list) = bitcode::decode::>(diff_list_str) { for queue_diff in queue_diffs_list { if matches!(queue_diff.r#type, DiffType::Redo) { self.apply_diff_list(&queue_diff.list)?; } else { self.apply_undo_diff_list(&queue_diff.list)?; } } } else { return Err("Error parsing diff list".to_string()); } Ok(()) } /// Set the input in a cell /// /// See also: /// * [Model::set_user_input] pub fn set_user_input( &mut self, sheet: u32, row: i32, column: i32, value: &str, ) -> Result<(), String> { if !is_valid_column_number(column) { return Err("Invalid column".to_string()); } if !is_valid_row(row) { return Err("Invalid row".to_string()); } let old_value = self .model .workbook .worksheet(sheet)? .cell(row, column) .cloned(); self.model .set_user_input(sheet, row, column, value.to_string())?; self.evaluate_if_not_paused(); let mut diff_list = vec![Diff::SetCellValue { sheet, row, column, new_value: value.to_string(), old_value: Box::new(old_value), }]; let style = self.model.get_style_for_cell(sheet, row, column)?; let line_count = value.split('\n').count() as f64; let row_height = self.model.get_row_height(sheet, row)?; // This is in sync with the front-end auto fit row let font_size = style.font.sz as f64; let line_height = font_size * 1.5; let cell_height = (line_count - 1.0) * line_height + 8.0 + font_size; if cell_height > row_height { diff_list.push(Diff::SetRowHeight { sheet, row, new_value: cell_height, old_value: row_height, }); self.model.set_row_height(sheet, row, cell_height)?; } self.push_diff_list(diff_list); Ok(()) } /// Returns the content of a cell /// /// See also: /// * [Model::get_cell_content] #[inline] pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result { self.model.get_cell_content(sheet, row, column) } /// Returns the formatted value of a cell /// /// See also: /// * [Model::get_formatted_cell_value] #[inline] pub fn get_formatted_cell_value( &self, sheet: u32, row: i32, column: i32, ) -> Result { self.model.get_formatted_cell_value(sheet, row, column) } /// Returns the type of the cell /// /// See also /// * [Model::get_cell_type] pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result { self.model.get_cell_type(sheet, row, column) } /// Adds new sheet /// /// See also: /// * [Model::new_sheet] pub fn new_sheet(&mut self) -> Result<(), String> { let (name, index) = self.model.new_sheet(); self.set_selected_sheet(index)?; self.push_diff_list(vec![Diff::NewSheet { index, name }]); Ok(()) } /// Deletes sheet by index /// /// See also: /// * [Model::delete_sheet] pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), String> { let worksheet = self.model.workbook.worksheet(sheet)?; self.push_diff_list(vec![Diff::DeleteSheet { sheet, old_data: Box::new(worksheet.clone()), }]); let sheet_count = self.model.workbook.worksheets.len() as u32; // If we are deleting the last sheet we need to change the selected sheet if sheet == sheet_count - 1 && sheet_count > 1 { if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) { view.sheet = sheet_count - 2; }; } self.model.delete_sheet(sheet)?; Ok(()) } /// Renames a sheet by index /// /// See also: /// * [Model::rename_sheet_by_index] pub fn rename_sheet(&mut self, sheet: u32, new_name: &str) -> Result<(), String> { let old_value = self.model.workbook.worksheet(sheet)?.name.clone(); if old_value == new_name { return Ok(()); } self.model.rename_sheet_by_index(sheet, new_name)?; self.push_diff_list(vec![Diff::RenameSheet { index: sheet, old_value, new_value: new_name.to_string(), }]); Ok(()) } /// Hides sheet by index /// /// See also: /// * [Model::set_sheet_state] /// * [UserModel::unhide_sheet] pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> { let sheet_count = self.model.workbook.worksheets.len() as u32; for index in 1..sheet_count { let sheet_index = (sheet + index) % sheet_count; if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible { if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) { view.sheet = sheet_index; }; break; } } let old_value = self.model.workbook.worksheet(sheet)?.state.clone(); self.push_diff_list(vec![Diff::SetSheetState { index: sheet, new_value: SheetState::Hidden, old_value, }]); self.model.set_sheet_state(sheet, SheetState::Hidden)?; Ok(()) } /// Un hides sheet by index /// /// See also: /// * [Model::set_sheet_state] /// * [UserModel::hide_sheet] pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> { let old_value = self.model.workbook.worksheet(sheet)?.state.clone(); self.push_diff_list(vec![Diff::SetSheetState { index: sheet, new_value: SheetState::Visible, old_value, }]); self.model.set_sheet_state(sheet, SheetState::Visible)?; Ok(()) } /// Sets sheet color /// /// Note: an empty string will remove the color /// /// See also /// * [Model::set_sheet_color] /// * [UserModel::get_worksheets_properties] pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), String> { let old_value = match &self.model.workbook.worksheet(sheet)?.color { Some(c) => c.clone(), None => "".to_string(), }; self.model.set_sheet_color(sheet, color)?; self.push_diff_list(vec![Diff::SetSheetColor { index: sheet, old_value, new_value: color.to_string(), }]); Ok(()) } /// Removes cells contents and style /// /// See also: /// * [Model::cell_clear_all] pub fn range_clear_all(&mut self, range: &Area) -> Result<(), String> { let sheet = range.sheet; // TODO: full rows/columns let mut diff_list = Vec::new(); for row in range.row..range.row + range.height { for column in range.column..range.column + range.width { let old_value = self .model .workbook .worksheet(sheet)? .cell(row, column) .cloned(); let old_style = self.model.get_style_for_cell(sheet, row, column)?; self.model.cell_clear_all(sheet, row, column)?; diff_list.push(Diff::CellClearAll { sheet, row, column, old_value: Box::new(old_value), old_style: Box::new(old_style), }); } } self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } /// Deletes the content in cells, but keeps the style /// /// See also: /// * [Model::cell_clear_contents] pub fn range_clear_contents(&mut self, range: &Area) -> Result<(), String> { let sheet = range.sheet; let mut diff_list = Vec::new(); // TODO: full rows/columns for row in range.row..range.row + range.height { for column in range.column..range.column + range.width { let old_value = self .model .workbook .worksheet(sheet)? .cell(row, column) .cloned(); self.model.cell_clear_contents(sheet, row, column)?; diff_list.push(Diff::CellClearContents { sheet, row, column, old_value: Box::new(old_value), }); } } self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } fn clear_column_formatting( &mut self, sheet: u32, column: i32, diff_list: &mut Vec, ) -> Result<(), String> { let old_value = self.model.get_column_style(sheet, column)?; self.model.delete_column_style(sheet, column)?; diff_list.push(Diff::DeleteColumnStyle { sheet, column, old_value: Box::new(old_value), }); let data_rows: Vec = self .model .workbook .worksheet(sheet)? .sheet_data .keys() .copied() .collect(); let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone(); // Delete the formatting in all non empty cells for row in data_rows { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? { // We can always assume that style with style_index 0 exists and it is the default self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(Some(old_style)), }); } else { let old_style = self.model.get_style_for_cell(sheet, row, column)?; if old_style != Style::default() { self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(None), }); } } } // Delete the formatting in all cells with a row style for row in styled_rows { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row.r, column)? { // We can always assume that style with style_index 0 exists and it is the default self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row.r, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row: row.r, column, old_style: Box::new(Some(old_style)), }); } else { let old_style = self.model.get_style_for_cell(sheet, row.r, column)?; if old_style != Style::default() { self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row.r, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row: row.r, column, old_style: Box::new(None), }); } } } Ok(()) } fn clear_row_formatting( &mut self, sheet: u32, row: i32, diff_list: &mut Vec, ) -> Result<(), String> { let old_value = self.model.get_row_style(sheet, row)?; self.model.delete_row_style(sheet, row)?; diff_list.push(Diff::DeleteRowStyle { sheet, row, old_value: Box::new(old_value), }); // Delete the formatting in all non empty cells let columns: Vec = self .model .workbook .worksheet(sheet)? .sheet_data .get(&row) .map(|row_data| row_data.keys().copied().collect()) .unwrap_or_default(); for column in columns { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? { // We can always assume that style with style_index 0 exists and it is the default self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(Some(old_style)), }); } else { let old_style = self.model.get_style_for_cell(sheet, row, column)?; if old_style != Style::default() { self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(None), }); } } } Ok(()) } /// Removes cells styles and formatting, but keeps the content /// /// See also: /// * [UserModel::range_clear_all] /// * [UserModel::range_clear_contents] pub fn range_clear_formatting(&mut self, range: &Area) -> Result<(), String> { let sheet = range.sheet; let mut diff_list = Vec::new(); if range.row == 1 && range.height == LAST_ROW { for column in range.column..range.column + range.width { self.clear_column_formatting(sheet, column, &mut diff_list)?; } self.push_diff_list(diff_list); return Ok(()); } if range.column == 1 && range.width == LAST_COLUMN { for row in range.row..range.row + range.height { self.clear_row_formatting(sheet, row, &mut diff_list)?; } self.push_diff_list(diff_list); return Ok(()); } for row in range.row..range.row + range.height { for column in range.column..range.column + range.width { if let Some(old_style) = self.model.get_cell_style_or_none(sheet, row, column)? { // We can always assume that style with style_index 0 exists and it is the default self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(Some(old_style)), }); } else { let old_style = self.model.get_style_for_cell(sheet, row, column)?; if old_style != Style::default() { self.model .workbook .worksheet_mut(sheet)? .set_cell_style(row, column, 0)?; diff_list.push(Diff::CellClearFormatting { sheet, row, column, old_style: Box::new(None), }); } } } } self.push_diff_list(diff_list); Ok(()) } /// Inserts `row_count` blank rows starting at `row` (both 0-based). /// /// Parameters /// * `sheet` – worksheet index. /// * `row` – first row to insert. /// * `row_count` – number of rows (> 0). /// /// History: the method pushes `row_count` [`crate::user_model::history::Diff::InsertRow`] /// items **all using the same `row` index**. Replaying those diffs (undo / redo) /// is therefore immune to the row-shifts that happen after each individual /// insertion. /// /// See also [`Model::insert_rows`]. pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> { self.model.insert_rows(sheet, row, row_count)?; let diff_list = vec![Diff::InsertRows { sheet, row, count: row_count, }]; self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } /// Inserts `column_count` blank columns starting at `column` (0-based). /// /// Parameters /// * `sheet` – worksheet index. /// * `column` – first column to insert. /// * `column_count` – number of columns (> 0). /// /// History: pushes one [`crate::user_model::history::Diff::InsertColumn`] /// per inserted column, all with the same `column` value, preventing index /// drift when the diffs are reapplied. /// /// See also [`Model::insert_columns`]. pub fn insert_columns( &mut self, sheet: u32, column: i32, column_count: i32, ) -> Result<(), String> { self.model.insert_columns(sheet, column, column_count)?; let diff_list = vec![Diff::InsertColumns { sheet, column, count: column_count, }]; self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } /// Deletes `row_count` rows starting at `row`. /// /// History: a [`crate::user_model::history::Diff::DeleteRow`] is created for /// each row, ordered **bottom → top**. Undo therefore recreates rows from /// top → bottom and redo removes them bottom → top, avoiding index drift. /// /// See also [`Model::delete_rows`]. pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> { let worksheet = self.model.workbook.worksheet(sheet)?; let mut old_data = Vec::new(); // Collect data for all rows to be deleted for r in row..row + row_count { let mut row_data = None; for rd in &worksheet.rows { if rd.r == r { row_data = Some(rd.clone()); break; } } let data = match worksheet.sheet_data.get(&r) { Some(s) => s.clone(), None => HashMap::new(), }; old_data.push(RowData { row: row_data, data, }); } self.model.delete_rows(sheet, row, row_count)?; let diff_list = vec![Diff::DeleteRows { sheet, row, count: row_count, old_data, }]; self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } /// Deletes `column_count` columns starting at `column`. /// /// History: pushes one [`crate::user_model::history::Diff::DeleteColumn`] /// per column, **right → left**, so replaying the list is always safe with /// respect to index shifts. /// /// See also [`Model::delete_columns`]. pub fn delete_columns( &mut self, sheet: u32, column: i32, column_count: i32, ) -> Result<(), String> { let worksheet = self.model.workbook.worksheet(sheet)?; let mut old_data = Vec::new(); // Collect data for all columns to be deleted for c in column..column + column_count { let mut column_data = None; for col in &worksheet.cols { if c >= col.min && c <= col.max { column_data = Some(Col { min: c, max: c, width: col.width, custom_width: col.custom_width, style: col.style, }); break; } } let mut data = HashMap::new(); for (row_idx, row_data) in &worksheet.sheet_data { if let Some(cell) = row_data.get(&c) { data.insert(*row_idx, cell.clone()); } } old_data.push(ColumnData { column: column_data, data, }); } self.model.delete_columns(sheet, column, column_count)?; let diff_list = vec![Diff::DeleteColumns { sheet, column, count: column_count, old_data, }]; self.push_diff_list(diff_list); self.evaluate_if_not_paused(); Ok(()) } /// Moves a column horizontally and adjusts formulas pub fn move_column_action( &mut self, sheet: u32, column: i32, delta: i32, ) -> Result<(), String> { let diff_list = vec![Diff::MoveColumn { sheet, column, delta, }]; self.push_diff_list(diff_list); self.model.move_column_action(sheet, column, delta)?; self.evaluate_if_not_paused(); Ok(()) } /// Moves a row vertically and adjusts formulas pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> { let diff_list = vec![Diff::MoveRow { sheet, row, delta }]; self.push_diff_list(diff_list); self.model.move_row_action(sheet, row, delta)?; self.evaluate_if_not_paused(); Ok(()) } /// Sets the width of a group of columns in a single diff list /// /// See also: /// * [Model::set_column_width] pub fn set_columns_width( &mut self, sheet: u32, column_start: i32, column_end: i32, width: f64, ) -> Result<(), String> { let mut diff_list = Vec::new(); for column in column_start..=column_end { let old_value = self.model.get_column_width(sheet, column)?; diff_list.push(Diff::SetColumnWidth { sheet, column, new_value: width, old_value, }); self.model.set_column_width(sheet, column, width)?; } self.push_diff_list(diff_list); Ok(()) } /// Sets the height of a range of rows in a single diff list /// /// See also: /// * [Model::set_row_height] pub fn set_rows_height( &mut self, sheet: u32, row_start: i32, row_end: i32, height: f64, ) -> Result<(), String> { let mut diff_list = Vec::new(); for row in row_start..=row_end { let old_value = self.model.get_row_height(sheet, row)?; diff_list.push(Diff::SetRowHeight { sheet, row, new_value: height, old_value, }); self.model.set_row_height(sheet, row, height)?; } self.push_diff_list(diff_list); Ok(()) } /// Gets the height of a row /// /// See also: /// * [Model::get_row_height] #[inline] pub fn get_row_height(&self, sheet: u32, row: i32) -> Result { self.model.get_row_height(sheet, row) } /// Gets the width of a column /// /// See also: /// * [Model::get_column_width] #[inline] pub fn get_column_width(&self, sheet: u32, column: i32) -> Result { self.model.get_column_width(sheet, column) } /// Returns the number of frozen rows in the sheet /// /// See also: /// * [Model::get_frozen_rows_count()] #[inline] pub fn get_frozen_rows_count(&self, sheet: u32) -> Result { self.model.get_frozen_rows_count(sheet) } /// Returns the number of frozen columns in the sheet /// /// See also: /// * [Model::get_frozen_columns_count()] #[inline] pub fn get_frozen_columns_count(&self, sheet: u32) -> Result { self.model.get_frozen_columns_count(sheet) } /// Sets the number of frozen rows in sheet /// /// See also: /// * [Model::set_frozen_rows()] pub fn set_frozen_rows_count(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> { let old_value = self.model.get_frozen_rows_count(sheet)?; self.push_diff_list(vec![Diff::SetFrozenRowsCount { sheet, new_value: frozen_rows, old_value, }]); self.model.set_frozen_rows(sheet, frozen_rows) } /// Sets the number of frozen columns in sheet /// /// See also: /// * [Model::set_frozen_columns()] pub fn set_frozen_columns_count( &mut self, sheet: u32, frozen_columns: i32, ) -> Result<(), String> { let old_value = self.model.get_frozen_columns_count(sheet)?; self.push_diff_list(vec![Diff::SetFrozenColumnsCount { sheet, new_value: frozen_columns, old_value, }]); self.model.set_frozen_columns(sheet, frozen_columns) } /// Paste `styles` in the selected area pub fn on_paste_styles(&mut self, styles: &[Vec