From 23ab5dfef2c6b76979170bbb2af2d981658fc294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Wed, 18 Dec 2024 17:22:49 +0100 Subject: [PATCH] UPDATE: Add rows/column style APIs --- base/src/model.rs | 84 ++++ base/src/new_empty.rs | 2 +- base/src/styles.rs | 2 - base/src/test/mod.rs | 1 + base/src/test/test_row_column_styles.rs | 32 ++ base/src/test/user_model/mod.rs | 1 + base/src/test/user_model/test_column_style.rs | 332 ++++++++++++++++ base/src/types.rs | 2 +- base/src/user_model/common.rs | 362 +++++++++++++----- base/src/user_model/history.rs | 14 +- base/src/worksheet.rs | 109 +++++- webapp/IronCalc/.storybook/main.ts | 1 - webapp/IronCalc/package-lock.json | 15 +- webapp/IronCalc/package.json | 3 +- .../WorksheetCanvas/worksheetCanvas.ts | 22 +- webapp/IronCalc/src/components/formulabar.tsx | 2 - webapp/IronCalc/src/components/usePointer.ts | 26 +- webapp/IronCalc/src/components/util.ts | 23 +- webapp/IronCalc/src/components/worksheet.tsx | 16 + .../frontend/package-lock.json | 1 - 20 files changed, 909 insertions(+), 141 deletions(-) create mode 100644 base/src/test/test_row_column_styles.rs create mode 100644 base/src/test/user_model/test_column_style.rs diff --git a/base/src/model.rs b/base/src/model.rs index 58aa59b..f3ea71c 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -1872,12 +1872,29 @@ impl Model { } /// Returns the style for cell (`sheet`, `row`, `column`) + /// If the cell does not have a style defined we check the row, otherwise the column and finally a default pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result { let style_index = self.get_cell_style_index(sheet, row, column)?; let style = self.workbook.styles.get_style(style_index)?; Ok(style) } + /// Returns the style defined in a cell if any. + pub fn get_cell_style_or_none( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result, String> { + let style = self + .workbook + .worksheet(sheet)? + .cell(row, column) + .map(|c| self.workbook.styles.get_style(c.get_style())) + .transpose(); + style + } + /// Returns an internal binary representation of the workbook /// /// See also: @@ -2161,6 +2178,73 @@ impl Model { Err("Defined name not found".to_string()) } } + /// Returns the style object of a column, if any + pub fn get_column_style(&self, sheet: u32, column: i32) -> Result, String> { + if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { + let cols = &worksheet.cols; + for col in cols { + if column >= col.min && column <= col.max { + if let Some(style_index) = col.style { + let style = self.workbook.styles.get_style(style_index)?; + return Ok(Some(style)); + } + return Ok(None); + } + } + Ok(None) + } else { + Err("Invalid sheet".to_string()) + } + } + + /// Returns the style object of a row, if any + pub fn get_row_style(&self, sheet: u32, row: i32) -> Result, String> { + if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { + let rows = &worksheet.rows; + for r in rows { + if row == r.r { + let style = self.workbook.styles.get_style(r.s)?; + return Ok(Some(style)); + } + } + Ok(None) + } else { + Err("Invalid sheet".to_string()) + } + } + + /// Sets a column with style + pub fn set_column_style( + &mut self, + sheet: u32, + column: i32, + style: &Style, + ) -> Result<(), String> { + let style_index = self.workbook.styles.get_style_index_or_create(style); + self.workbook + .worksheet_mut(sheet)? + .set_column_style(column, style_index) + } + + /// Sets a row with style + pub fn set_row_style(&mut self, sheet: u32, row: i32, style: &Style) -> Result<(), String> { + let style_index = self.workbook.styles.get_style_index_or_create(style); + self.workbook + .worksheet_mut(sheet)? + .set_row_style(row, style_index) + } + + /// Deletes the style of a column if the is any + pub fn delete_column_style(&mut self, sheet: u32, column: i32) -> Result<(), String> { + self.workbook + .worksheet_mut(sheet)? + .delete_column_style(column) + } + + /// Deletes the style of a row if there is any + pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> { + self.workbook.worksheet_mut(sheet)?.delete_row_style(row) + } } #[cfg(test)] diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index 5399e23..a313fe0 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -301,7 +301,7 @@ impl Model { }; if sheet_index >= sheet_count { return Err("Sheet index too large".to_string()); - } + }; self.workbook.worksheets.remove(sheet_index as usize); self.reset_parsed_structures(); Ok(()) diff --git a/base/src/styles.rs b/base/src/styles.rs index 4d41cc2..dd95aeb 100644 --- a/base/src/styles.rs +++ b/base/src/styles.rs @@ -4,8 +4,6 @@ use crate::{ types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles}, }; -// TODO: Move Styles and all related types from crate::types here -// Not doing it right now to not have conflicts with exporter branch impl Styles { fn get_font_index(&self, font: &Font) -> Option { for (font_index, item) in self.fonts.iter().enumerate() { diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 80d1c2b..d5e8186 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -37,6 +37,7 @@ mod test_model_cell_clear_all; mod test_model_is_empty_cell; mod test_move_formula; mod test_quote_prefix; +mod test_row_column_styles; mod test_set_user_input; mod test_sheet_markup; mod test_sheets; diff --git a/base/src/test/test_row_column_styles.rs b/base/src/test/test_row_column_styles.rs new file mode 100644 index 0000000..4cb32f8 --- /dev/null +++ b/base/src/test/test_row_column_styles.rs @@ -0,0 +1,32 @@ +#![allow(clippy::unwrap_used)] + +use crate::{constants::DEFAULT_COLUMN_WIDTH, test::util::new_empty_model}; + +#[test] +fn test_model_set_cells_with_values_styles() { + let mut model = new_empty_model(); + + let style_base = model.get_style_for_cell(0, 1, 1).unwrap(); + let mut style = style_base.clone(); + style.font.b = true; + + model.set_column_style(0, 10, &style).unwrap(); + + assert!(model.get_style_for_cell(0, 21, 10).unwrap().font.b); + + model.delete_column_style(0, 10).unwrap(); + + // There are no styles in the column + assert!(model.workbook.worksheets[0].cols.is_empty()); + + // lets change the column width and check it does not affect the style + model + .set_column_width(0, 10, DEFAULT_COLUMN_WIDTH * 2.0) + .unwrap(); + model.set_column_style(0, 10, &style).unwrap(); + + model.delete_column_style(0, 10).unwrap(); + + // There are no styles in the column + assert!(model.workbook.worksheets[0].cols.len() == 1); +} diff --git a/base/src/test/user_model/mod.rs b/base/src/test/user_model/mod.rs index e9912bb..cbb7642 100644 --- a/base/src/test/user_model/mod.rs +++ b/base/src/test/user_model/mod.rs @@ -3,6 +3,7 @@ mod test_autofill_columns; mod test_autofill_rows; mod test_border; mod test_clear_cells; +mod test_column_style; mod test_defined_names; mod test_diff_queue; mod test_evaluation; diff --git a/base/src/test/user_model/test_column_style.rs b/base/src/test/user_model/test_column_style.rs new file mode 100644 index 0000000..b832e8a --- /dev/null +++ b/base/src/test/user_model/test_column_style.rs @@ -0,0 +1,332 @@ +#![allow(clippy::unwrap_used)] + +use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW}; +use crate::expressions::types::Area; +use crate::UserModel; + +#[test] +fn column_width() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 7, + width: 1, + height: LAST_ROW, + }; + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(!style.font.i); + assert!(!style.font.b); + assert!(!style.font.u); + assert!(!style.font.strike); + assert_eq!(style.font.color, Some("#000000".to_owned())); + + // Set the whole column style and check it works + model.update_range_style(&range, "font.b", "true").unwrap(); + let style = model.get_cell_style(0, 109, 7).unwrap(); + assert!(style.font.b); + + // undo and check it works + model.undo().unwrap(); + let style = model.get_cell_style(0, 109, 7).unwrap(); + assert!(!style.font.b); + + // redo and check it works + model.redo().unwrap(); + let style = model.get_cell_style(0, 109, 7).unwrap(); + assert!(style.font.b); + + // change the column width and check it does not affect the style + model + .set_column_width(0, 7, DEFAULT_COLUMN_WIDTH * 2.0) + .unwrap(); + let style = model.get_cell_style(0, 109, 7).unwrap(); + assert!(style.font.b); +} + +#[test] +fn existing_style() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + + let cell_g123 = Area { + sheet: 0, + row: 123, + column: 7, + width: 1, + height: 1, + }; + + let column_g_range = Area { + sheet: 0, + row: 1, + column: 7, + width: 1, + height: LAST_ROW, + }; + + // Set G123 background to red + model + .update_range_style(&cell_g123, "fill.bg_color", "#333444") + .unwrap(); + + // Now set the style of the whole column + model + .update_range_style(&column_g_range, "fill.bg_color", "#555666") + .unwrap(); + + // Get the style of G123 + let style = model.get_cell_style(0, 123, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#555666".to_owned())); + + model.undo().unwrap(); + + // Check the style of G123 is now what it was before + let style = model.get_cell_style(0, 123, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#333444".to_owned())); + + model.redo().unwrap(); + + // Check G123 has the column style now + let style = model.get_cell_style(0, 123, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#555666".to_owned())); +} + +#[test] +fn row_column() { + // We set the row style, then a column style + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + + let column_g_range = Area { + sheet: 0, + row: 1, + column: 7, + width: 1, + height: LAST_ROW, + }; + + let row_3_range = Area { + sheet: 0, + row: 3, + column: 1, + width: LAST_COLUMN, + height: 1, + }; + + // update the row style + model + .update_range_style(&row_3_range, "fill.bg_color", "#333444") + .unwrap(); + + // update the column style + model + .update_range_style(&column_g_range, "fill.bg_color", "#555666") + .unwrap(); + + // Check G3 has the column style + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#555666".to_owned())); + + // undo twice. Color must be default + model.undo().unwrap(); + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#333444".to_owned())); + model.undo().unwrap(); + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, None); +} + +#[test] +fn column_row() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + + let default_style = model.get_cell_style(0, 3, 7).unwrap(); + + let column_g_range = Area { + sheet: 0, + row: 1, + column: 7, + width: 1, + height: LAST_ROW, + }; + + let row_3_range = Area { + sheet: 0, + row: 3, + column: 1, + width: LAST_COLUMN, + height: 1, + }; + + // update the column style + model + .update_range_style(&column_g_range, "fill.bg_color", "#555666") + .unwrap(); + + // update the row style + model + .update_range_style(&row_3_range, "fill.bg_color", "#333444") + .unwrap(); + + // Check G3 has the row style + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#333444".to_owned())); + + model.undo().unwrap(); + + // Check G3 has the column style + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#555666".to_owned())); + + model.undo().unwrap(); + + // Check G3 has the default_style + let style = model.get_cell_style(0, 3, 7).unwrap(); + assert_eq!(style.fill.bg_color, default_style.fill.bg_color); +} + +#[test] +fn row_column_column() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + + let column_c_range = Area { + sheet: 0, + row: 1, + column: 3, + width: 1, + height: LAST_ROW, + }; + + let column_e_range = Area { + sheet: 0, + row: 1, + column: 5, + width: 1, + height: LAST_ROW, + }; + + let row_5_range = Area { + sheet: 0, + row: 5, + column: 1, + width: LAST_COLUMN, + height: 1, + }; + + // update the row style + model + .update_range_style(&row_5_range, "fill.bg_color", "#333444") + .unwrap(); + + // update the column style + model + .update_range_style(&column_c_range, "fill.bg_color", "#555666") + .unwrap(); + + model + .update_range_style(&column_e_range, "fill.bg_color", "#CCC111") + .unwrap(); + + model.undo().unwrap(); + model.undo().unwrap(); + model.undo().unwrap(); + + // Test E5 has the default style + let style = model.get_cell_style(0, 5, 5).unwrap(); + assert_eq!(style.fill.bg_color, None); +} + +#[test] +fn width_column_undo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + + model + .set_column_width(0, 7, DEFAULT_COLUMN_WIDTH * 2.0) + .unwrap(); + + let column_g_range = Area { + sheet: 0, + row: 1, + column: 7, + width: 1, + height: LAST_ROW, + }; + model + .update_range_style(&column_g_range, "fill.bg_color", "#CCC111") + .unwrap(); + + model.undo().unwrap(); + + assert_eq!( + model.get_column_width(0, 7).unwrap(), + DEFAULT_COLUMN_WIDTH * 2.0 + ); +} + +#[test] +fn height_row_undo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model + .set_row_height(0, 10, DEFAULT_ROW_HEIGHT * 2.0) + .unwrap(); + + let row_10_range = Area { + sheet: 0, + row: 10, + column: 1, + width: LAST_COLUMN, + height: 1, + }; + + model + .update_range_style(&row_10_range, "fill.bg_color", "#CCC111") + .unwrap(); + + assert_eq!( + model.get_row_height(0, 10).unwrap(), + 2.0 * DEFAULT_ROW_HEIGHT + ); + model.undo().unwrap(); + assert_eq!( + model.get_row_height(0, 10).unwrap(), + 2.0 * DEFAULT_ROW_HEIGHT + ); + model.undo().unwrap(); + assert_eq!(model.get_row_height(0, 10).unwrap(), DEFAULT_ROW_HEIGHT); +} + +#[test] +fn cell_row_undo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let cell_g12 = Area { + sheet: 0, + row: 12, + column: 7, + width: 1, + height: 1, + }; + + let row_12_range = Area { + sheet: 0, + row: 12, + column: 1, + width: LAST_COLUMN, + height: 1, + }; + + // Set G12 background to red + model + .update_range_style(&cell_g12, "fill.bg_color", "#333444") + .unwrap(); + + model + .update_range_style(&row_12_range, "fill.bg_color", "#CCC111") + .unwrap(); + + let style = model.get_cell_style(0, 12, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#CCC111".to_string())); + model.undo().unwrap(); + + let style = model.get_cell_style(0, 12, 7).unwrap(); + assert_eq!(style.fill.bg_color, Some("#333444".to_string())); +} diff --git a/base/src/types.rs b/base/src/types.rs index fa98aba..726d9e3 100644 --- a/base/src/types.rs +++ b/base/src/types.rs @@ -312,7 +312,7 @@ impl Default for Styles { } } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)] pub struct Style { #[serde(skip_serializing_if = "Option::is_none")] pub alignment: Option, diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index e3b339e..d2fa236 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -109,6 +109,78 @@ fn vertical(value: &str) -> Result { } } +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)?; + } + "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. /// @@ -821,7 +893,7 @@ impl UserModel { let row_index = ((row - row_start) % styles_height) as usize; let column_index = ((column - column_start) % styles_width) as usize; let style = &styles[row_index][column_index]; - let old_value = self.model.get_style_for_cell(sheet, row, column)?; + let old_value = self.model.get_cell_style_or_none(sheet, row, column)?; self.model.set_cell_style(sheet, row, column, style)?; diff_list.push(Diff::SetCellStyle { sheet, @@ -922,7 +994,7 @@ impl UserModel { sheet, row: row - 1, column, - old_value: Box::new(old_value_cell_above), + old_value: Box::new(Some(old_value_cell_above)), new_value: Box::new(new_value_cell_above), }); } @@ -955,7 +1027,7 @@ impl UserModel { sheet, row, column: column + 1, - old_value: Box::new(old_value_cell_right), + old_value: Box::new(Some(old_value_cell_right)), new_value: Box::new(new_value_cell_right), }); } @@ -988,7 +1060,7 @@ impl UserModel { sheet, row: row + 1, column, - old_value: Box::new(old_value_cell_below), + old_value: Box::new(Some(old_value_cell_below)), new_value: Box::new(new_value_cell_below), }); } @@ -1021,7 +1093,7 @@ impl UserModel { sheet, row, column: column - 1, - old_value: Box::new(old_value_cell_left), + old_value: Box::new(Some(old_value_cell_left)), new_value: Box::new(new_value_cell_left), }); } @@ -1056,7 +1128,7 @@ impl UserModel { sheet, row, column, - old_value: Box::new(old_value), + old_value: Box::new(Some(old_value)), new_value: Box::new(new_value), }); } @@ -1081,7 +1153,7 @@ impl UserModel { sheet, row, column, - old_value: Box::new(old_value), + old_value: Box::new(Some(old_value)), new_value: Box::new(style), }); } @@ -1107,7 +1179,7 @@ impl UserModel { sheet, row, column, - old_value: Box::new(old_value), + old_value: Box::new(Some(old_value)), new_value: Box::new(style), }); } @@ -1132,7 +1204,7 @@ impl UserModel { sheet, row, column, - old_value: Box::new(old_value), + old_value: Box::new(Some(old_value)), new_value: Box::new(style), }); } @@ -1157,7 +1229,7 @@ impl UserModel { sheet, row, column, - old_value: Box::new(old_value), + old_value: Box::new(Some(old_value)), new_value: Box::new(style), }); } @@ -1168,6 +1240,32 @@ impl UserModel { Ok(()) } + fn update_single_cell_style( + &mut self, + sheet: u32, + row: i32, + column: i32, + style_path: &str, + value: &str, + diff_list: &mut Vec, + ) -> Result<(), String> { + let old_value = self.model.get_cell_style_or_none(sheet, row, column)?; + let style = match old_value.as_ref() { + Some(s) => s, + None => &Style::default(), + }; + let new_style = update_style(style, style_path, value)?; + self.model.set_cell_style(sheet, row, column, &new_style)?; + diff_list.push(Diff::SetCellStyle { + sheet, + row, + column, + old_value: Box::new(old_value), + new_value: Box::new(new_style), + }); + Ok(()) + } + /// Updates the range with a cell style. /// See also: /// * [Model::set_cell_style] @@ -1179,85 +1277,117 @@ impl UserModel { ) -> Result<(), String> { let sheet = range.sheet; let mut diff_list = Vec::new(); - for row in range.row..range.row + range.height { + // We need all the rows in the column to update the style + // NB: This is too much, this is all the rows that have values + let data_rows: Vec = self + .model + .workbook + .worksheet(sheet)? + .sheet_data + .keys() + .copied() + .collect(); + let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone(); + if range.row == 1 && range.height == LAST_ROW { + // Full columns for column in range.column..range.column + range.width { - let old_value = self.model.get_style_for_cell(sheet, row, column)?; - 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)?; - } - "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}'.")); - } - } - self.model.set_cell_style(sheet, row, column, &style)?; - diff_list.push(Diff::SetCellStyle { + // we set the style of the full column + let old_style = self.model.get_column_style(sheet, column)?; + let style = match old_style.as_ref() { + Some(s) => s, + None => &Style::default(), + }; + let style = update_style(style, style_path, value)?; + self.model.set_column_style(sheet, column, &style)?; + diff_list.push(Diff::SetColumnStyle { sheet, - row, column, - old_value: Box::new(old_value), + old_value: Box::new(old_style), new_value: Box::new(style), }); + + // Update style in all cells that have different styles + // FIXME: We need a better way to transverse of cells in a column + for &row in &data_rows { + if let Some(data_row) = + self.model.workbook.worksheet(sheet)?.sheet_data.get(&row) + { + if data_row.get(&column).is_some() { + self.update_single_cell_style( + sheet, + row, + column, + style_path, + value, + &mut diff_list, + )?; + } + } + } + // We need to update the styles in all cells that have a row style + for row_s in styled_rows.iter() { + self.update_single_cell_style( + sheet, + row_s.r, + column, + style_path, + value, + &mut diff_list, + )?; + } + } + } else if range.column == 1 && range.width == LAST_COLUMN { + // Full rows + for row in range.row..range.row + range.height { + // First update the style of the row + let old_style = self.model.get_row_style(sheet, row)?; + let style = match old_style.as_ref() { + Some(s) => s, + None => &Style::default(), + }; + let style = update_style(style, style_path, value)?; + self.model.set_row_style(sheet, row, &style)?; + diff_list.push(Diff::SetRowStyle { + sheet, + row, + old_value: Box::new(old_style), + new_value: Box::new(style), + }); + + // Now update style in all cells that are not empty + 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 { + self.update_single_cell_style( + sheet, + row, + column, + style_path, + value, + &mut diff_list, + )?; + } + // since rows take precedence over columns we do not need + // to update the styles in all cells that have a column style + } + } else { + for row in range.row..range.row + range.height { + for column in range.column..range.column + range.width { + self.update_single_cell_style( + sheet, + row, + column, + style_path, + value, + &mut diff_list, + )?; + } } } self.push_diff_list(diff_list); @@ -1394,7 +1524,7 @@ impl UserModel { .worksheet(sheet)? .cell(row, column) .cloned(); - let old_style = self.model.get_style_for_cell(sheet, row, column)?; + let old_style = self.model.get_cell_style_or_none(sheet, row, column)?; // compute the new value and set it let source_row = anchor_row + index; @@ -1495,7 +1625,7 @@ impl UserModel { .worksheet(sheet)? .cell(row, column) .cloned(); - let old_style = self.model.get_style_for_cell(sheet, row, column)?; + let old_style = self.model.get_cell_style_or_none(sheet, row, column)?; // compute the new value and set it let source_column = anchor_column + index; @@ -1570,6 +1700,9 @@ impl UserModel { let mut data = HashMap::new(); let [row_start, column_start, row_end, column_end] = selected_area.range; + let dimension = self.model.workbook.worksheet(sheet)?.dimension(); + let row_end = row_end.min(dimension.max_row); + let column_end = column_end.min(dimension.max_column); for row in row_start..=row_end { let mut data_row = HashMap::new(); let mut text_row = Vec::new(); @@ -1667,9 +1800,9 @@ impl UserModel { .cell(target_row, target_column) .cloned(); - let old_style = self - .model - .get_style_for_cell(sheet, target_row, target_column)?; + let old_style = + self.model + .get_cell_style_or_none(sheet, target_row, target_column)?; self.model .set_user_input(sheet, target_row, target_column, new_value.clone())?; @@ -1922,9 +2055,15 @@ impl UserModel { column, old_value, new_value: _, - } => self - .model - .set_cell_style(*sheet, *row, *column, old_value)?, + } => { + if let Some(old_style) = old_value.as_ref() { + self.model + .set_cell_style(*sheet, *row, *column, old_style)?; + } else { + // If the cell did not have a style there was nothing on it + self.model.cell_clear_all(*sheet, *row, *column)?; + } + } Diff::InsertRow { sheet, row } => { self.model.delete_rows(*sheet, *row, 1)?; needs_evaluation = true; @@ -2073,6 +2212,29 @@ impl UserModel { self.set_selected_sheet(sheet_index)?; } + Diff::SetColumnStyle { + sheet, + column, + old_value, + new_value: _, + } => match old_value.as_ref() { + Some(s) => self.model.set_column_style(*sheet, *column, s)?, + None => { + self.model.delete_column_style(*sheet, *column)?; + } + }, + Diff::SetRowStyle { + sheet, + row, + old_value, + new_value: _, + } => { + if let Some(s) = old_value.as_ref() { + self.model.set_row_style(*sheet, *row, s)?; + } else { + self.model.delete_row_style(*sheet, *row)?; + } + } } } if needs_evaluation { @@ -2244,6 +2406,22 @@ impl UserModel { .worksheet_mut(*sheet)? .set_cell_style(*row, *column, 0)?; } + Diff::SetColumnStyle { + sheet, + column, + old_value: _, + new_value, + } => { + self.model.set_column_style(*sheet, *column, new_value)?; + } + Diff::SetRowStyle { + sheet, + row, + old_value: _, + new_value, + } => { + self.model.set_row_style(*sheet, *row, new_value)?; + } } } diff --git a/base/src/user_model/history.rs b/base/src/user_model/history.rs index c6f7c6b..e15d41a 100644 --- a/base/src/user_model/history.rs +++ b/base/src/user_model/history.rs @@ -49,7 +49,7 @@ pub(crate) enum Diff { sheet: u32, row: i32, column: i32, - old_value: Box