diff --git a/base/src/cell.rs b/base/src/cell.rs index 70299f1..76a8639 100644 --- a/base/src/cell.rs +++ b/base/src/cell.rs @@ -89,6 +89,8 @@ impl Cell { Cell::CellFormulaNumber { s, .. } => *s = style, Cell::CellFormulaString { s, .. } => *s = style, Cell::CellFormulaError { s, .. } => *s = style, + // Should we throw an error here? + Cell::Merged { .. } => {} }; } @@ -104,6 +106,8 @@ impl Cell { Cell::CellFormulaNumber { s, .. } => *s, Cell::CellFormulaString { s, .. } => *s, Cell::CellFormulaError { s, .. } => *s, + // A merged cell has no style + Cell::Merged { .. } => 0, } } @@ -119,6 +123,7 @@ impl Cell { Cell::CellFormulaNumber { .. } => CellType::Number, Cell::CellFormulaString { .. } => CellType::Text, Cell::CellFormulaError { .. } => CellType::ErrorValue, + Cell::Merged { .. } => CellType::Number, } } @@ -156,6 +161,7 @@ impl Cell { let v = ei.to_localized_error_string(language); CellValue::String(v) } + Cell::Merged { .. } => CellValue::None, } } diff --git a/base/src/lib.rs b/base/src/lib.rs index 08fc107..496c907 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -59,6 +59,7 @@ pub mod mock_time; pub use model::get_milliseconds_since_epoch; pub use model::Model; +pub use model::CellStructure; pub use user_model::BorderArea; pub use user_model::ClipboardData; pub use user_model::UserModel; diff --git a/base/src/model.rs b/base/src/model.rs index 7ceb536..f035a8f 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -31,6 +31,7 @@ use crate::{ }; use chrono_tz::Tz; +use serde::{Deserialize, Serialize}; #[cfg(test)] pub use crate::mock_time::get_milliseconds_since_epoch; @@ -72,6 +73,27 @@ pub(crate) enum CellState { Evaluating, } +/// Cell structure indicates if the cell is part of a merged cell or not +#[derive(Clone, Serialize, Deserialize)] +pub enum CellStructure { + /// The cell is not part of a merged cell + Simple, + /// The cell is part of a merged cell, and teh root cell is (row, column) + Merged { + /// Row of the root cell + row: i32, + /// Column of the root cell + column: i32, + }, + /// The cell is the root of a merged cell of dimensions (width, height) + MergedRoot { + /// Width of the merged cell + width: i32, + /// Height of the merged cell + height: i32, + }, +} + /// A parsed formula for a defined name #[derive(Clone)] pub(crate) enum ParsedDefinedName { @@ -751,6 +773,7 @@ impl Model { } } } + Merged { .. } => CalcResult::EmptyCell, } } @@ -1438,6 +1461,10 @@ impl Model { value: String, ) -> Result<(), String> { // If value starts with "'" then we force the style to be quote_prefix + let cell = self.workbook.worksheet(sheet)?.cell(row, column); + if matches!(cell, Some(Cell::Merged { .. })) { + return Err("Cannot set value on merged cell".to_string()); + } let style_index = self.get_cell_style_index(sheet, row, column)?; if let Some(new_value) = value.strip_prefix('\'') { // First check if it needs quoting @@ -2258,6 +2285,91 @@ impl Model { pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> { self.workbook.worksheet_mut(sheet)?.delete_row_style(row) } + + /// Returns the geometric structure of a cell + pub fn get_cell_structure( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result { + let worksheet = self.workbook.worksheet(sheet)?; + worksheet.get_cell_structure(row, column) + } + + /// Merges cells + pub fn merge_cells( + &mut self, + sheet: u32, + row: i32, + column: i32, + width: i32, + height: i32, + ) -> Result<(), String> { + let worksheet = self.workbook.worksheet_mut(sheet)?; + let sheet_data = &mut worksheet.sheet_data; + // First check that it is possible to merge the cells + for r in row..(row + height) { + for c in column..(column + width) { + if let Some(Cell::Merged { .. }) = + sheet_data.get(&r).and_then(|row_data| row_data.get(&c)) + { + return Err("Cannot merge cells".to_string()); + } + } + } + worksheet + .merged_cells + .insert((row, column), (width, height)); + for r in row..(row + height) { + for c in column..(column + width) { + // We remove everything except the "root" cell: + if r == row && c == column { + continue; + } + if let Some(row_data) = sheet_data.get_mut(&r) { + row_data.remove(&c); + row_data.insert(c, Cell::Merged { r: row, c: column }); + } else { + let mut row_data = HashMap::new(); + row_data.insert(c, Cell::Merged { r: row, c: column }); + sheet_data.insert(r, row_data); + } + } + } + Ok(()) + } + + /// Unmerges cells + pub fn unmerge_cells(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { + let s = self.get_cell_style_index(sheet, row, column)?; + let worksheet = self.workbook.worksheet_mut(sheet)?; + let sheet_data = &mut worksheet.sheet_data; + let (width, height) = match worksheet.merged_cells.get(&(row, column)) { + Some((w, h)) => (*w, *h), + None => return Ok(()), + }; + worksheet.merged_cells.remove(&(row, column)); + for r in row..(row + width) { + for c in column..(column + height) { + // We remove everything except the "root" cell: + if r == row && c == column { + continue; + } + if let Some(row_data) = sheet_data.get_mut(&r) { + row_data.remove(&c); + if s != 0 { + row_data.insert(c, Cell::EmptyCell { s }); + } + } else if s != 0 { + let mut row_data = HashMap::new(); + row_data.insert(c, Cell::EmptyCell { s }); + sheet_data.insert(r, row_data); + } + } + } + Ok(()) + } } #[cfg(test)] diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index a07b30e..9cfed5a 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -58,10 +58,10 @@ impl Model { rows: vec![], comments: vec![], dimension: "A1".to_string(), - merge_cells: vec![], name: name.to_string(), shared_formulas: vec![], sheet_data: Default::default(), + merged_cells: HashMap::new(), sheet_id, state: SheetState::Visible, color: Default::default(), diff --git a/base/src/types.rs b/base/src/types.rs index 3b516af..ea53f02 100644 --- a/base/src/types.rs +++ b/base/src/types.rs @@ -110,7 +110,7 @@ pub struct Worksheet { pub sheet_id: u32, pub state: SheetState, pub color: Option, - pub merge_cells: Vec, + pub merged_cells: HashMap<(i32, i32), (i32, i32)>, pub comments: Vec, pub frozen_rows: i32, pub frozen_columns: i32, @@ -217,7 +217,10 @@ pub enum Cell { // Error Message: "Not implemented function" m: String, }, - // TODO: Array formulas + Merged { + r: i32, + c: i32, + }, // TODO: Array formulas } impl Default for Cell { diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index 0b4b172..cf14962 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -11,7 +11,7 @@ use crate::{ types::{Area, CellReferenceIndex}, utils::{is_valid_column_number, is_valid_row}, }, - model::Model, + model::{CellStructure, Model}, types::{ Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState, Style, VerticalAlignment, @@ -1869,6 +1869,57 @@ impl UserModel { Ok(()) } + /// Merges cells + pub fn merge_cells( + &mut self, + sheet: u32, + row: i32, + column: i32, + width: i32, + height: i32, + ) -> Result<(), String> { + let old_data = Vec::new(); + let diff_list = vec![Diff::MergeCells { + sheet, + row, + column, + width, + height, + old_data, + }]; + self.model.merge_cells(sheet, row, column, width, height)?; + self.push_diff_list(diff_list); + self.evaluate_if_not_paused(); + Ok(()) + } + + /// Check if cell is part of a merged cell + pub fn get_cell_structure(&self, sheet: u32, row: i32, column: i32) -> Result { + self.model.get_cell_structure(sheet, row, column) + } + + /// Unmerges cells + pub fn unmerge_cells(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { + let (width, height) = self + .model + .workbook + .worksheet(sheet)? + .merged_cells + .get(&(row, column)) + .ok_or("No merged cells found")?; + let diff_list = vec![Diff::UnmergeCells { + sheet, + row, + column, + width: *width, + height: *height, + }]; + self.model.unmerge_cells(sheet, row, column)?; + self.push_diff_list(diff_list); + self.evaluate_if_not_paused(); + Ok(()) + } + // **** Private methods ****** // pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) { @@ -2112,7 +2163,6 @@ impl UserModel { worksheet.frozen_rows = old_data.frozen_rows; worksheet.state = old_data.state.clone(); worksheet.color = old_data.color.clone(); - worksheet.merge_cells = old_data.merge_cells.clone(); worksheet.shared_formulas = old_data.shared_formulas.clone(); self.model.reset_parsed_structures(); @@ -2163,6 +2213,34 @@ impl UserModel { self.model.delete_row_style(*sheet, *row)?; } } + Diff::MergeCells { + sheet, + row, + column, + width, + height, + old_data, + } => { + needs_evaluation = true; + self.model.unmerge_cells(*sheet, *row, *column)?; + // for (r, c, v) in old_data.iter() { + // self.model + // .workbook + // .worksheet_mut(*sheet)? + // .update_cell(*r, *c, v.clone())?; + // } + } + Diff::UnmergeCells { + sheet, + row, + column, + width, + height, + } => { + needs_evaluation = true; + self.model + .merge_cells(*sheet, *row, *column, *width, *height)?; + } } } if needs_evaluation { @@ -2364,6 +2442,34 @@ impl UserModel { } => { self.model.delete_row_style(*sheet, *row)?; } + Diff::MergeCells { + sheet, + row, + column, + width, + height, + old_data: _, + } => { + needs_evaluation = true; + self.model + .merge_cells(*sheet, *row, *column, *width, *height)?; + // for (r, c, v) in old_data.iter() { + // self.model + // .workbook + // .worksheet_mut(*sheet)? + // .update_cell(*r, *c, v.clone())?; + // } + } + Diff::UnmergeCells { + sheet, + row, + column, + width, + height, + } => { + needs_evaluation = true; + self.model.unmerge_cells(*sheet, *row, *column)?; + } } } diff --git a/base/src/user_model/history.rs b/base/src/user_model/history.rs index 53268e1..25c8acf 100644 --- a/base/src/user_model/history.rs +++ b/base/src/user_model/history.rs @@ -161,7 +161,21 @@ pub(crate) enum Diff { new_scope: Option, new_formula: String, }, - // FIXME: we are missing SetViewDiffs + MergeCells { + sheet: u32, + row: i32, + column: i32, + width: i32, + height: i32, + old_data: Vec<(Cell, Style)>, + }, + UnmergeCells { + sheet: u32, + row: i32, + column: i32, + width: i32, + height: i32, + }, // FIXME: we are missing SetViewDiffs } pub(crate) type DiffList = Vec; diff --git a/base/src/user_model/ui.rs b/base/src/user_model/ui.rs index b217b1b..ee0fb25 100644 --- a/base/src/user_model/ui.rs +++ b/base/src/user_model/ui.rs @@ -2,7 +2,10 @@ use serde::{Deserialize, Serialize}; -use crate::expressions::utils::{is_valid_column_number, is_valid_row}; +use crate::{ + expressions::utils::{is_valid_column_number, is_valid_row}, + CellStructure, +}; use super::common::UserModel; @@ -97,26 +100,47 @@ impl UserModel { if !is_valid_row(row) { return Err(format!("Invalid row: '{row}'")); } - if self.model.workbook.worksheet(sheet).is_err() { - return Err(format!("Invalid worksheet index {}", sheet)); - } - if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { - if let Some(view) = worksheet.views.get_mut(&0) { - view.row = row; - view.column = column; - view.range = [row, column, row, column]; + let worksheet = self.model.workbook.worksheet_mut(sheet)?; + let structure = worksheet.get_cell_structure(row, column)?; + // check if the selected cell is a merged cell + let [row_start, columns_start, row_end, columns_end] = match structure { + CellStructure::Simple => [row, column, row, column], + CellStructure::Merged { + row: row_start, + column: column_start, + } => { + let (width, height) = match worksheet.merged_cells.get(&(row_start, column_start)) { + Some(s) => s, + None => return Err(format!("Merged cell not found: ({row_start}, {column_start}) when clicking at ({row}, {column}).")), + }; + let row_end = row_start + height - 1; + let column_end = column_start + width - 1; + [row_start, column_start, row_end, column_end] } + CellStructure::MergedRoot { width, height } => { + let row_start = row; + let columns_start = column; + let row_end = row + height - 1; + let columns_end = column + width - 1; + [row_start, columns_start, row_end, columns_end] + } + }; + if let Some(view) = worksheet.views.get_mut(&0) { + view.row = row_start; + view.column = columns_start; + view.range = [row_start, columns_start, row_end, columns_end]; } + Ok(()) } /// Sets the selected range. Note that the selected cell must be in one of the corners. pub fn set_selected_range( &mut self, - start_row: i32, - start_column: i32, - end_row: i32, - end_column: i32, + row_start: i32, + column_start: i32, + row_end: i32, + column_end: i32, ) -> Result<(), String> { let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) { view.sheet @@ -124,42 +148,72 @@ impl UserModel { 0 }; - if !is_valid_column_number(start_column) { - return Err(format!("Invalid column: '{start_column}'")); + if !is_valid_column_number(column_start) { + return Err(format!("Invalid column: '{column_start}'")); } - if !is_valid_row(start_row) { - return Err(format!("Invalid row: '{start_row}'")); + if !is_valid_row(row_start) { + return Err(format!("Invalid row: '{row_start}'")); } - if !is_valid_column_number(end_column) { - return Err(format!("Invalid column: '{end_column}'")); + if !is_valid_column_number(column_end) { + return Err(format!("Invalid column: '{column_end}'")); } - if !is_valid_row(end_row) { - return Err(format!("Invalid row: '{end_row}'")); + if !is_valid_row(row_end) { + return Err(format!("Invalid row: '{row_end}'")); } - if self.model.workbook.worksheet(sheet).is_err() { - return Err(format!("Invalid worksheet index {}", sheet)); - } - if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) { - if let Some(view) = worksheet.views.get_mut(&0) { - let selected_row = view.row; - let selected_column = view.column; - // The selected cells must be on one of the corners of the selected range: - if selected_row != start_row && selected_row != end_row { - return Err(format!( - "The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'", - selected_row, start_row, end_row - )); + let mut start_row = row_start; + let mut start_column = column_start; + let mut end_row = row_end; + let mut end_column = column_end; + let worksheet = self.model.workbook.worksheet_mut(sheet)?; + let merged_cells = &worksheet.merged_cells; + if !merged_cells.is_empty() { + // We need to check if there are merged cells in the selected range + for row in row_start..=row_end { + for column in column_start..=column_end { + let structure = &worksheet.get_cell_structure(row, column)?; + match structure { + CellStructure::Simple => {} + CellStructure::Merged { row: r, column: c } => { + // The selected range must contain the merged cell + let (width, height) = match merged_cells.get(&(*r, *c)) { + Some(s) => s, + None => return Err(format!("Merged cell not found: ({r}, {c}) when selecting range ({start_row}, {start_column}, {end_row}, {end_column}).")), + }; + start_row = start_row.min(*r); + start_column = start_column.min(*c); + end_row = end_row.max(*r + height - 1); + end_column = end_column.max(*c + width - 1); + + } + CellStructure::MergedRoot { width, height } => { + // The selected range must contain the merged cell + end_row = end_row.max(row + height - 1); + end_column = end_column.max(column + width - 1); + } + } } - if selected_column != start_column && selected_column != end_column { - return Err(format!( - "The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'", - selected_column, start_column, end_column - )); - } - view.range = [start_row, start_column, end_row, end_column]; } } + if let Some(view) = worksheet.views.get_mut(&0) { + // let selected_row = view.row; + // let selected_column = view.column; + // // The selected cells must be on one of the corners of the selected range: + // if selected_row != start_row && selected_row != end_row { + // return Err(format!( + // "The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'", + // selected_row, start_row, end_row + // )); + // } + // if selected_column != start_column && selected_column != end_column { + // return Err(format!( + // "The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'", + // selected_column, start_column, end_column + // )); + // } + view.range = [start_row, start_column, end_row, end_column]; + } + Ok(()) } diff --git a/base/src/worksheet.rs b/base/src/worksheet.rs index bf90899..b8a0020 100644 --- a/base/src/worksheet.rs +++ b/base/src/worksheet.rs @@ -1,6 +1,7 @@ use crate::constants::{self, LAST_COLUMN, LAST_ROW}; use crate::expressions::types::CellReferenceIndex; use crate::expressions::utils::{is_valid_column_number, is_valid_row}; +use crate::CellStructure; use crate::{expressions::token::Error, types::*}; use std::collections::HashMap; @@ -38,6 +39,24 @@ impl Worksheet { self.sheet_data.get(&row)?.get(&column) } + pub fn get_cell_structure(&self, row: i32, column: i32) -> Result { + if let Some((width, height)) = self.merged_cells.get(&(row, column)) { + return Ok(CellStructure::MergedRoot { + width: *width, + height: *height, + }); + } + let cell = self.cell(row, column); + if let Some(Cell::Merged { r, c }) = cell { + return Ok(CellStructure::Merged { + row: *r, + column: *c, + }); + } + + Ok(CellStructure::Simple) + } + pub(crate) fn cell_mut(&mut self, row: i32, column: i32) -> Option<&mut Cell> { self.sheet_data.get_mut(&row)?.get_mut(&column) } diff --git a/bindings/wasm/fix_types.py b/bindings/wasm/fix_types.py index fd466ee..df2f7eb 100644 --- a/bindings/wasm/fix_types.py +++ b/bindings/wasm/fix_types.py @@ -201,6 +201,26 @@ defined_name_list_types = r""" getDefinedNameList(): DefinedName[]; """ +merged_cells = r""" +/** +* @param {number} sheet +* @param {number} row +* @param {number} column +* @returns {any} +*/ + getCellStructure(sheet: number, row: number, column: number): any; +""" + +merged_cells_types = r""" +/** +* @param {number} sheet +* @param {number} row +* @param {number} column +* @returns {CellStructure} +*/ + getCellStructure(sheet: number, row: number, column: number): CellStructure; +""" + def fix_types(text): text = text.replace(get_tokens_str, get_tokens_str_types) text = text.replace(update_style_str, update_style_str_types) @@ -215,6 +235,7 @@ def fix_types(text): text = text.replace(clipboard, clipboard_types) text = text.replace(paste_from_clipboard, paste_from_clipboard_types) text = text.replace(defined_name_list, defined_name_list_types) + text = text.replace(merged_cells, merged_cells_types) with open("types.ts") as f: types_str = f.read() header_types = "{}\n\n{}".format(header, types_str) diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 5767a2e..114b837 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -5,9 +5,7 @@ use wasm_bindgen::{ }; use ironcalc_base::{ - expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, - types::{CellType, Style}, - BorderArea, ClipboardData, UserModel as BaseModel, + expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column}, types::{CellType, Style}, BorderArea, ClipboardData, UserModel as BaseModel }; fn to_js_error(error: String) -> JsError { @@ -672,4 +670,36 @@ impl Model { .delete_defined_name(name, scope) .map_err(|e| to_js_error(e.to_string())) } + + #[wasm_bindgen(js_name = "mergeCells")] + pub fn merge_cells( + &mut self, + sheet: u32, + row: i32, + column: i32, + width: i32, + height: i32, + ) -> Result<(), JsError> { + self.model + .merge_cells(sheet, row, column, width, height) + .map_err(to_js_error) + } + + #[wasm_bindgen(js_name = "unmergeCells")] + pub fn unmerge_cells(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), JsError> { + self.model + .unmerge_cells(sheet, row, column) + .map_err(to_js_error) + } + + #[wasm_bindgen(js_name = "getCellStructure")] + pub fn get_cell_structure( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result { + let data = self.model.get_cell_structure(sheet, row, column).map_err(|e| to_js_error(e.to_string()))?; + serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string())) + } } diff --git a/bindings/wasm/types.ts b/bindings/wasm/types.ts index 7af55b8..799bad4 100644 --- a/bindings/wasm/types.ts +++ b/bindings/wasm/types.ts @@ -216,7 +216,7 @@ export interface SelectedView { // }; // type ClipboardData = Record>; -type ClipboardData = Map>; +type ClipboardData = Map>; export interface ClipboardCell { text: string; @@ -233,4 +233,9 @@ export interface DefinedName { name: string; scope?: number; formula: string; -} \ No newline at end of file +} + +export type CellStructure = + | "Simple" + | { Merged: { row: number; column: number } } + | { MergedRoot: { width: number; height: number } }; diff --git a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx index 8c26e98..8fc4afd 100644 --- a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx +++ b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx @@ -40,6 +40,8 @@ import { ArrowMiddleFromLine, DecimalPlacesDecreaseIcon, DecimalPlacesIncreaseIcon, + MergeCellsIcon, + UnmergeCellsIcon, } from "../../icons"; import { theme } from "../../theme"; import BorderPicker from "../BorderPicker/BorderPicker"; @@ -74,6 +76,8 @@ type ToolbarProperties = { onClearFormatting: () => void; onIncreaseFontSize: (delta: number) => void; onDownloadPNG: () => void; + onMergeCells: () => void; + onUnmergeCells: () => void; fillColor: string; fontColor: string; fontSize: number; @@ -429,6 +433,28 @@ function Toolbar(properties: ToolbarProperties) { > + { + properties.onMergeCells(); + }} + title={t("toolbar.merge_cells")} + > + + + { + properties.onUnmergeCells(); + }} + title={t("toolbar.unmerge_cells")} + > + + { downloadLink.download = "ironcalc.png"; downloadLink.click(); }} + onMergeCells={() => { + const { + sheet, + range: [rowStart, columnStart, rowEnd, columnEnd], + } = model.getSelectedView(); + const row = Math.min(rowStart, rowEnd); + const column = Math.min(columnStart, columnEnd); + + const width = Math.abs(columnEnd - columnStart) + 1; + const height = Math.abs(rowEnd - rowStart) + 1; + model.mergeCells(sheet, row, column, width, height); + setRedrawId((id) => id + 1); + }} + onUnmergeCells={() => { + const { + sheet, + range: [rowStart, columnStart, rowEnd, columnEnd], + } = model.getSelectedView(); + const row = Math.min(rowStart, rowEnd); + const column = Math.min(columnStart, columnEnd); + model.unmergeCells(sheet, row, column); + setRedrawId((id) => id + 1); + }} onBorderChanged={(border: BorderOptions): void => { const { sheet, diff --git a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts index 5a4ea4b..9f7431a 100644 --- a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -386,10 +386,29 @@ export default class WorksheetCanvas { column: number, x: number, y: number, - width: number, - height: number, + width1: number, + height1: number, ): void { const selectedSheet = this.model.getSelectedSheet(); + const structure = this.model.getCellStructure(selectedSheet, row, column); + if (typeof structure === 'object' && 'Merged' in structure) { + // We don't render merged cells + return; + } + let width = width1; + let height = height1; + if (typeof structure === 'object' && 'MergedRoot' in structure) { + const root = structure.MergedRoot; + const columns = root.width; + const rows = root.height; + for (let i = 1; i < columns; i += 1) { + width += this.getColumnWidth(selectedSheet, column + i); + } + for (let i = 1; i < rows; i += 1) { + height += this.getRowHeight(selectedSheet, row + i); + } + }; + const style = this.model.getCellStyle(selectedSheet, row, column); let backgroundColor = "#FFFFFF"; diff --git a/webapp/IronCalc/src/icons/index.ts b/webapp/IronCalc/src/icons/index.ts index 10773a3..9f39fde 100644 --- a/webapp/IronCalc/src/icons/index.ts +++ b/webapp/IronCalc/src/icons/index.ts @@ -23,6 +23,9 @@ import InsertRowBelow from "./insert-row-below.svg?react"; import IronCalcIcon from "./ironcalc_icon.svg?react"; import IronCalcLogo from "./orange+black.svg?react"; +import MergeCellsIcon from "./merge-cells.svg?react"; +import UnmergeCellsIcon from "./unmerge-cells.svg?react"; + import Fx from "./fx.svg?react"; export { @@ -47,5 +50,7 @@ export { InsertRowBelow, IronCalcIcon, IronCalcLogo, + MergeCellsIcon, + UnmergeCellsIcon, Fx, }; diff --git a/webapp/IronCalc/src/icons/merge-cells.svg b/webapp/IronCalc/src/icons/merge-cells.svg new file mode 100644 index 0000000..eab1faf --- /dev/null +++ b/webapp/IronCalc/src/icons/merge-cells.svg @@ -0,0 +1,4 @@ + + + + diff --git a/webapp/IronCalc/src/icons/unmerge-cells.svg b/webapp/IronCalc/src/icons/unmerge-cells.svg new file mode 100644 index 0000000..7e33f75 --- /dev/null +++ b/webapp/IronCalc/src/icons/unmerge-cells.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/IronCalc/src/locale/en_us.json b/webapp/IronCalc/src/locale/en_us.json index db69a3a..5256d2f 100644 --- a/webapp/IronCalc/src/locale/en_us.json +++ b/webapp/IronCalc/src/locale/en_us.json @@ -27,6 +27,8 @@ "vertical_align_top": "Align top", "selected_png": "Export Selected area as PNG", "wrap_text": "Wrap text", + "merge_cells": "Merge cells", + "unmerge_cells": "Unmerge cells", "format_menu": { "auto": "Auto", "number": "Number", diff --git a/xlsx/src/export/worksheets.rs b/xlsx/src/export/worksheets.rs index 7bf6fbe..7c9e88a 100644 --- a/xlsx/src/export/worksheets.rs +++ b/xlsx/src/export/worksheets.rs @@ -220,6 +220,7 @@ pub(crate) fn get_worksheet_xml( "{formula}{ei}" )); } + Cell::Merged { .. } => { /* do nothing */ } } } let row_style_str = match row_style_dict.get(row_index) { @@ -247,7 +248,7 @@ pub(crate) fn get_worksheet_xml( } let sheet_data = sheet_data_str.join(""); - for merge_cell_ref in &worksheet.merge_cells { + for merge_cell_ref in &worksheet.merged_cells { merged_cells_str.push(format!("")) } let merged_cells_count = merged_cells_str.len(); diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index 322c9dc..3925a1d 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -989,7 +989,7 @@ pub(super) fn load_sheet( sheet_data.insert(row_index, data_row); } - let merge_cells = load_merge_cells(ws)?; + let merged_cells = load_merged_cells(ws)?; // Conditional Formatting // @@ -1028,7 +1028,7 @@ pub(super) fn load_sheet( sheet_id, state: state.to_owned(), color, - merge_cells, + merged_cells, comments: settings.comments, frozen_rows: sheet_view.frozen_rows, frozen_columns: sheet_view.frozen_columns,