#![deny(missing_docs)] use std::{collections::HashMap, fmt::Debug}; use serde::{Deserialize, Serialize}; use crate::{ constants, expressions::{ types::Area, utils::{is_valid_column_number, is_valid_row}, }, model::Model, types::{ Alignment, BorderItem, BorderStyle, CellType, Col, HorizontalAlignment, SheetProperties, Style, VerticalAlignment, }, utils::is_valid_hex_color, }; use crate::user_model::history::{ ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData, }; #[derive(Serialize, Deserialize)] 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 { item: BorderItem, 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 border(value: &str) -> Result, String> { if value.is_empty() { return Ok(None); } let parts = value.split(','); let values = parts.collect::>(); match values[..] { [border_style, color_str] => { let style = match border_style { "thin" => BorderStyle::Thin, "medium" => BorderStyle::Medium, "thick" => BorderStyle::Thick, "double" => BorderStyle::Double, "dotted" => BorderStyle::Dotted, "slantDashDot" => BorderStyle::SlantDashDot, "mediumDashed" => BorderStyle::MediumDashed, "mediumDashDotDot" => BorderStyle::MediumDashDotDot, "mediumDashDot" => BorderStyle::MediumDashDot, _ => { return Err(format!("Invalid border style: '{border_style}'.")); } }; Ok(Some(BorderItem { style, color: color(color_str)?, })) } _ => Err(format!("Invalid border value: '{value}'.")), } } 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}'.")), } } /// # 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 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 diff_list = vec![Diff::SetCellValue { sheet, row, column, new_value: value.to_string(), old_value: Box::new(old_value), }]; 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) { let (name, index) = self.model.new_sheet(); self.push_diff_list(vec![Diff::NewSheet { index, name }]); } /// Deletes sheet by index /// /// See also: /// * [Model::delete_sheet] pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), String> { self.push_diff_list(vec![Diff::DeleteSheet { sheet }]); // There is no coming back self.history.clear(); 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(); 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(()) } /// 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; 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); 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(); 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); Ok(()) } /// Inserts a row /// /// See also: /// * [Model::insert_rows] pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> { let diff_list = vec![Diff::InsertRow { sheet, row }]; self.push_diff_list(diff_list); self.model.insert_rows(sheet, row, 1) } /// Deletes a row /// /// See also: /// * [Model::delete_rows] pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> { let mut row_data = None; let worksheet = self.model.workbook.worksheet(sheet)?; for rd in &worksheet.rows { if rd.r == row { row_data = Some(rd.clone()); break; } } let data = worksheet.sheet_data.get(&row).unwrap().clone(); let old_data = Box::new(RowData { row: row_data, data, }); let diff_list = vec![Diff::DeleteRow { sheet, row, old_data, }]; self.push_diff_list(diff_list); self.model.delete_rows(sheet, row, 1) } /// Inserts a column /// /// See also: /// * [Model::insert_columns] pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> { let diff_list = vec![Diff::InsertColumn { sheet, column }]; self.push_diff_list(diff_list); self.model.insert_columns(sheet, column, 1) } /// Deletes a column /// /// See also: /// * [Model::delete_columns] pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> { let worksheet = self.model.workbook.worksheet(sheet)?; if !is_valid_column_number(column) { return Err(format!("Column number '{column}' is not valid.")); } let mut column_data = None; for col in &worksheet.cols { let min = col.min; let max = col.max; if column >= min && column <= max { column_data = Some(Col { min: column, max: column, width: col.width, custom_width: col.custom_width, style: col.style, }); break; } } let mut data = HashMap::new(); for (row, row_data) in &worksheet.sheet_data { if let Some(cell) = row_data.get(&column) { data.insert(*row, cell.clone()); } } let diff_list = vec![Diff::DeleteColumn { sheet, column, old_data: Box::new(ColumnData { column: column_data, data, }), }]; self.push_diff_list(diff_list); self.model.delete_columns(sheet, column, 1) } /// Sets the width of a column /// /// See also: /// * [Model::set_column_width] pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> { let old_value = self.model.get_column_width(sheet, column)?; self.push_diff_list(vec![Diff::SetColumnWidth { sheet, column, new_value: width, old_value, }]); self.model.set_column_width(sheet, column, width) } /// Sets the height of a row /// /// See also: /// * [Model::set_row_height] pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), String> { let old_value = self.model.get_row_height(sheet, row)?; self.push_diff_list(vec![Diff::SetRowHeight { sheet, row, new_value: height, old_value, }]); self.model.set_row_height(sheet, row, height) } /// 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