UPDATE: Text spills now to adjacent cells if needed

This commit is contained in:
Nicolás Hatcher
2025-07-07 23:21:13 +02:00
committed by Nicolás Hatcher Andrés
parent 107fc99409
commit 9852ce2504
8 changed files with 1501 additions and 1106 deletions

View File

@@ -11,6 +11,7 @@ mod test_evaluation;
mod test_general;
mod test_grid_lines;
mod test_keyboard_navigation;
mod test_last_empty_cell;
mod test_on_area_selection;
mod test_on_expand_selected_range;
mod test_on_paste_styles;

View File

@@ -0,0 +1,55 @@
#![allow(clippy::unwrap_used)]
use crate::constants::LAST_ROW;
use crate::expressions::types::Area;
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// This is the first row, column 5
model.set_user_input(0, 3, 5, "todo").unwrap();
// Row 3 before column 5 should be empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 4)
.unwrap(),
None
);
// Row 3 before column 5 should be 5
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 7)
.unwrap(),
Some(5)
);
}
#[test]
fn test_last_empty_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
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", "#333444")
.unwrap();
// Column 7 has a style but it is empty
assert_eq!(
model
.get_last_non_empty_in_row_before_column(0, 3, 14)
.unwrap(),
None
);
}

View File

@@ -13,8 +13,8 @@ use crate::{
},
model::Model,
types::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
Style, VerticalAlignment,
Alignment, BorderItem, Cell, CellType, Col, HorizontalAlignment, SheetProperties,
SheetState, Style, VerticalAlignment,
},
utils::is_valid_hex_color,
};
@@ -1602,6 +1602,64 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
}
/// Returns the largest column in the row less than a column whose cell has a non empty value.
/// If the row is empty, it returns `None`.
/// This is useful when rendering a part of a worksheet to know which cells spill over
pub fn get_last_non_empty_in_row_before_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut last_column = None;
let mut columns: Vec<i32> = row_data.keys().copied().collect();
columns.sort_unstable();
for col in columns {
if col < column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
last_column = Some(col);
}
}
Ok(last_column)
} else {
Ok(None)
}
}
/// Returns the largest column in the row greater than a column whose cell has a non empty value.
pub fn get_first_non_empty_in_row_after_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
let data = worksheet.sheet_data.get(&row);
if let Some(row_data) = data {
let mut columns: Vec<i32> = row_data.keys().copied().collect();
// We sort the keys to ensure we are going from left to right
columns.sort_unstable();
for col in columns {
if col > column {
if let Some(cell) = worksheet.cell(row, col) {
if matches!(cell, Cell::EmptyCell { .. }) {
continue;
}
}
return Ok(Some(col));
}
}
}
Ok(None)
}
/// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view();

View File

@@ -705,4 +705,28 @@ impl Model {
.delete_defined_name(name, scope)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "getLastNonEmptyInRowBeforeColumn")]
pub fn get_last_non_empty_in_row_before_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, JsError> {
self.model
.get_last_non_empty_in_row_before_column(sheet, row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "getFirstNonEmptyInRowAfterColumn")]
pub fn get_first_non_empty_in_row_after_column(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<Option<i32>, JsError> {
self.model
.get_first_non_empty_in_row_after_column(sheet, row, column)
.map_err(to_js_error)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,3 +19,5 @@ export const LAST_ROW = 1_048_576;
export const ROW_HEIGH_SCALE = 1;
export const COLUMN_WIDTH_SCALE = 1;
export const cellPadding = 4;

View File

@@ -1,4 +1,4 @@
import type { Model } from "@ironcalc/wasm";
import type { CellStyle, Model } from "@ironcalc/wasm";
import { columnNameFromNumber } from "@ironcalc/wasm";
import { getColor } from "../Editor/util";
import type { Cell } from "../types";
@@ -8,6 +8,7 @@ import {
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
cellPadding,
defaultTextColor,
gridColor,
gridSeparatorColor,
@@ -54,6 +55,22 @@ export const defaultCellFontFamily = fonts.regular;
export const headerFontFamily = fonts.regular;
export const frozenSeparatorWidth = 3;
type Tuple = [number, number];
interface TextProperties {
row: number;
column: number;
x: number;
y: number;
width: number;
height: number;
fontSize: number;
textColor: string;
font: string;
underlined: boolean;
strike: boolean;
lines: [string, number, number, number][];
}
export default class WorksheetCanvas {
sheetWidth: number;
@@ -93,6 +110,9 @@ export default class WorksheetCanvas {
refresh: () => void;
cells: TextProperties[];
spills: Map<string, number>;
constructor(options: CanvasSettings) {
this.model = options.model;
this.sheetWidth = 0;
@@ -116,6 +136,15 @@ export default class WorksheetCanvas {
this.onRowHeightChanges = options.onRowHeightChanges;
this.resetHeaders();
this.cellOutlineHandle = attachOutlineHandle(this);
// +1 means the cell is filled with a spill from a cell in the left
// -1 means the cell is filled with a spill from a cell in the right
this.spills = new Map<string, number>();
this.cells = [];
}
removeSpills(): void {
this.spills.clear();
this.cells = [];
}
setScrollPosition(scrollPosition: { left: number; top: number }): void {
@@ -318,7 +347,372 @@ export default class WorksheetCanvas {
return left;
}
private renderCell(
private getFontStyle(style: CellStyle): {
font: string;
color: string;
fontSize: number;
} {
const fontSize = style.font?.sz || 13;
let font = `${fontSize}px ${defaultCellFontFamily}`;
let color = defaultTextColor;
if (style.font) {
color = style.font.color;
font = style.font.b ? `bold ${font}` : `400 ${font}`;
if (style.font.i) {
font = `italic ${font}`;
}
}
return { font, color, fontSize };
}
private getAlignment(
style: CellStyle,
cellType: number,
): { horizontal: string; vertical: string } {
let horizontal = style.alignment?.horizontal || "general";
const vertical = style.alignment?.vertical || "bottom";
if (horizontal === "general") {
if (cellType === 1) {
horizontal = "right";
} else if (cellType === 4) {
horizontal = "center";
} else {
horizontal = "left";
}
}
return { horizontal, vertical };
}
// Computes the text of cells that are off the screen. They are important because their text may spill into the viewport.
private computeCellTextLeftRight(
selectedSheet: number,
row: number,
rowHeight: number,
topLeftCell: Cell,
bottomRightCell: Cell,
): void {
// First compute the text to the left of the viewport
const columnToTheLeft = this.model.getLastNonEmptyInRowBeforeColumn(
selectedSheet,
row,
topLeftCell.column + 1,
);
if (columnToTheLeft) {
const columnWidth = this.getColumnWidth(selectedSheet, columnToTheLeft);
const [textX, textY] = this.getCoordinatesByCell(row, columnToTheLeft);
this.computeCellText(
row,
columnToTheLeft,
textX,
textY,
columnWidth,
rowHeight,
);
}
// Compute the text to the right of the viewport
const columnToTheRight = this.model.getFirstNonEmptyInRowAfterColumn(
selectedSheet,
row,
bottomRightCell.column,
);
if (columnToTheRight) {
const columnWidth = this.getColumnWidth(selectedSheet, columnToTheRight);
const [textX, textY] = this.getCoordinatesByCell(row, columnToTheRight);
this.computeCellText(
row,
columnToTheRight,
textX,
textY,
columnWidth,
rowHeight,
);
}
}
// Goes through all the visible cells and computes their text properties
private computeCellsText(): void {
const { topLeftCell, bottomRightCell } = this.getVisibleCells();
const selectedSheet = this.model.getSelectedSheet();
this.cells = [];
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
// Top-left-pane
let x = headerColumnWidth + 0.5;
let y = headerRowHeight + 0.5;
for (let row = 1; row <= frozenRows; row += 1) {
const rowHeight = this.getRowHeight(selectedSheet, row);
this.computeCellTextLeftRight(
selectedSheet,
row,
rowHeight,
topLeftCell,
bottomRightCell,
);
x = headerColumnWidth + 0.5;
for (let column = 1; column <= frozenColumns; column += 1) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
}
if (frozenRows === 0 && frozenColumns !== 0) {
x = headerColumnWidth + 0.5;
for (let column = 1; column <= frozenColumns; column += 1) {
x += this.getColumnWidth(selectedSheet, column);
}
}
// If there are frozen rows draw a separator
if (frozenRows) {
y += frozenSeparatorWidth;
}
// If there are frozen columns draw a separator
if (frozenColumns) {
x += frozenSeparatorWidth;
}
const frozenX = x;
const frozenY = y;
// Top-right pane
y = headerRowHeight + 0.5;
for (let row = 1; row <= frozenRows; row += 1) {
x = frozenX;
const rowHeight = this.getRowHeight(selectedSheet, row);
for (
let { column } = topLeftCell;
column <= bottomRightCell.column;
column += 1
) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
}
// Bottom-left pane
y = frozenY;
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
x = headerColumnWidth;
const rowHeight = this.getRowHeight(selectedSheet, row);
this.computeCellTextLeftRight(
selectedSheet,
row,
rowHeight,
topLeftCell,
bottomRightCell,
);
for (let column = 1; column <= frozenColumns; column += 1) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
}
// Bottom-right pane
y = frozenY;
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
x = frozenX;
const rowHeight = this.getRowHeight(selectedSheet, row);
for (
let { column } = topLeftCell;
column <= bottomRightCell.column;
column += 1
) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.computeCellText(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
}
}
// Compute the text properties for a cell
private computeCellText(
row: number,
column: number,
x: number,
y: number,
width: number,
height: number,
) {
const selectedSheet = this.model.getSelectedSheet();
const style = this.model.getCellStyle(selectedSheet, row, column);
const { font, color: textColor, fontSize } = this.getFontStyle(style);
// Number = 1,
// Text = 2,
// LogicalValue = 4,
// ErrorValue = 16,
// Array = 64,
// CompoundData = 128,
const cellType = this.model.getCellType(selectedSheet, row, column);
const { horizontal: horizontalAlign, vertical: verticalAlign } =
this.getAlignment(style, cellType);
const wrapText = style.alignment?.wrap_text || false;
const context = this.ctx;
context.font = font;
context.fillStyle = textColor;
const fullText = this.model.getFormattedCellValue(
selectedSheet,
row,
column,
);
// Is there any better to determine the line height?
const lineHeight = fontSize * 1.5;
const lines = computeWrappedLines(
fullText,
wrapText,
context,
width - cellPadding,
);
const lineCount = lines.length;
let maxWidth = 0;
let minX = x;
let maxX = x + width;
const textProperties = {
row,
column,
x: minX,
y,
width,
height,
fontSize,
textColor,
font,
underlined: style.font?.u || false,
strike: style.font?.strike || false,
lines: [] as [string, number, number, number][],
};
lines.forEach((text, line) => {
const textWidth = context.measureText(text).width;
let textX: number;
let textY: number;
// The idea is that in the present font-size and default row heigh,
// top/bottom and center horizontalAlign coincide
const verticalPadding = 4;
if (horizontalAlign === "right") {
textX = width - cellPadding + x - textWidth / 2;
} else if (horizontalAlign === "center") {
textX = x + width / 2;
} else {
// left aligned
textX = cellPadding + x + textWidth / 2;
}
if (verticalAlign === "bottom") {
textY =
y +
height -
fontSize / 2 -
verticalPadding +
(line - lineCount + 1) * lineHeight;
} else if (verticalAlign === "center") {
textY = y + height / 2 + (line + (1 - lineCount) / 2) * lineHeight;
} else {
// aligned top
textY = y + fontSize / 2 + verticalPadding + line * lineHeight;
}
textProperties.lines.push([text, textX, textY, textWidth]);
minX = Math.min(minX, textX - textWidth / 2);
maxX = Math.max(maxX, textX + textWidth / 2);
maxWidth = Math.max(maxWidth, textX + textWidth / 2 - x);
});
// we need to see if the text spills to the right of the cell
let leftColumnX = x;
let rightColumnX = x + width;
if (
maxX > rightColumnX &&
column < LAST_COLUMN &&
this.model.getFormattedCellValue(selectedSheet, row, column + 1) === ""
) {
let spillColumn = column + 1;
// Keep expanding the spill to the right until:
// 1. There is a non-empty cell
// 2. Reaches the end of the row
// 3. There is the end of frozen columns
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
while (
rightColumnX < maxX &&
this.model.getFormattedCellValue(selectedSheet, row, spillColumn) ===
"" &&
spillColumn <= LAST_COLUMN &&
((column < frozenColumns && spillColumn <= frozenColumns) ||
column > frozenColumns)
) {
rightColumnX += this.model.getColumnWidth(selectedSheet, spillColumn);
// marks (row, spillColumn) as spilling so we don't draw a border to the left
this.spills.set(`${row}-${spillColumn}`, 1);
spillColumn += 1;
}
}
// Same thing in the other direction, to the left of the cell
const frozenColumnsCount = this.model.getFrozenColumnsCount(selectedSheet);
if (
minX < leftColumnX &&
column > 1 &&
this.model.getFormattedCellValue(selectedSheet, row, column - 1) === ""
) {
let spillColumn = column - 1;
// Keep expanding the spill to the left until:
// 1. There is a non-empty cell
// 2. Reaches the beginning of the row
// 3. There is the end of frozen columns
while (
leftColumnX > minX &&
this.model.getFormattedCellValue(selectedSheet, row, spillColumn) ===
"" &&
spillColumn > 0 &&
((column <= frozenColumnsCount && spillColumn <= frozenColumnsCount) ||
column > frozenColumnsCount)
) {
leftColumnX -= this.getColumnWidth(selectedSheet, spillColumn);
// marks (row, spillColumn) as spilling so we don't draw a border to the left
this.spills.set(`${row}-${spillColumn + 1}`, 1);
spillColumn -= 1;
}
}
if (frozenColumnsCount > 0) {
const frozenColumnsX =
this.getCoordinatesByCell(row, frozenColumnsCount)[0] +
this.getColumnWidth(selectedSheet, frozenColumnsCount);
if (column > frozenColumnsCount) {
leftColumnX = Math.max(leftColumnX, frozenColumnsX);
} else {
rightColumnX = Math.min(rightColumnX, frozenColumnsX);
}
}
textProperties.x = leftColumnX;
textProperties.width = rightColumnX - leftColumnX;
this.cells.push(textProperties);
}
/// Renders the cell style: colors, borders, etc. But not the text.
private renderCellStyle(
row: number,
column: number,
x: number,
@@ -329,6 +723,7 @@ export default class WorksheetCanvas {
const selectedSheet = this.model.getSelectedSheet();
const style = this.model.getCellStyle(selectedSheet, row, column);
// first the background
let backgroundColor = "#FFFFFF";
if (style.fill.fg_color) {
backgroundColor = style.fill.fg_color;
@@ -336,32 +731,9 @@ export default class WorksheetCanvas {
const cellGridColor = this.model.getShowGridLines(selectedSheet)
? gridColor
: backgroundColor;
const fontSize = style.font?.sz || 13;
let font = `${fontSize}px ${defaultCellFontFamily}`;
let textColor = defaultTextColor;
if (style.font) {
textColor = style.font.color;
font = style.font.b ? `bold ${font}` : `400 ${font}`;
if (style.font.i) {
font = `italic ${font}`;
}
}
let horizontalAlign = "general";
if (style.alignment?.horizontal) {
horizontalAlign = style.alignment.horizontal;
}
let verticalAlign = "bottom";
if (style.alignment?.vertical) {
verticalAlign = style.alignment.vertical;
}
const wrapText = style.alignment?.wrap_text || false;
const context = this.ctx;
context.font = font;
context.fillStyle = backgroundColor;
context.fillRect(x, y, width, height);
context.fillStyle = textColor;
// Let's do the border
// Algorithm:
@@ -373,6 +745,8 @@ export default class WorksheetCanvas {
// We only set the left and top borders (right and bottom are set later)
const border = style.border;
// we skip don't draw a left border if it is marked as a "spill cell"
if (this.spills.get(`${row}-${column}`) !== 1) {
let borderLeftColor = cellGridColor;
let borderLeftWidth = 1;
if (border.left) {
@@ -387,7 +761,11 @@ export default class WorksheetCanvas {
borderLeftWidth = 3;
}
} else {
const leftStyle = this.model.getCellStyle(selectedSheet, row, column - 1);
const leftStyle = this.model.getCellStyle(
selectedSheet,
row,
column - 1,
);
if (leftStyle.border.right) {
borderLeftColor = leftStyle.border.right.color;
switch (leftStyle.border.right.style) {
@@ -405,12 +783,14 @@ export default class WorksheetCanvas {
borderLeftColor = leftStyle.fill.fg_color;
}
}
context.beginPath();
context.strokeStyle = borderLeftColor;
context.lineWidth = borderLeftWidth;
context.moveTo(x, y);
context.lineTo(x, y + height);
context.stroke();
}
let borderTopColor = cellGridColor;
let borderTopWidth = 1;
@@ -450,78 +830,37 @@ export default class WorksheetCanvas {
context.moveTo(x, y);
context.lineTo(x + width, y);
context.stroke();
// Number = 1,
// Text = 2,
// LogicalValue = 4,
// ErrorValue = 16,
// Array = 64,
// CompoundData = 128,
const cellType = this.model.getCellType(selectedSheet, row, column);
const fullText = this.model.getFormattedCellValue(
selectedSheet,
row,
column,
);
const padding = 4;
if (horizontalAlign === "general") {
if (cellType === 1) {
horizontalAlign = "right";
} else if (cellType === 4) {
horizontalAlign = "center";
} else {
horizontalAlign = "left";
}
}
/// Renders the text in the cell.
private renderCellText(textProperties: TextProperties) {
const {
x,
y,
width,
height,
font,
underlined,
strike,
fontSize,
textColor,
lines,
} = textProperties;
const context = this.ctx;
context.font = font;
context.fillStyle = textColor;
// Create a rectangular clipping region
context.save();
context.beginPath();
context.rect(x, y, width, height);
context.clip();
// Is there any better to determine the line height?
const lineHeight = fontSize * 1.5;
const lines = computeWrappedLines(
fullText,
wrapText,
context,
width - padding,
);
const lineCount = lines.length;
lines.forEach((line, _) => {
const [textContent, textX, textY, textWidth] = line;
context.fillText(textContent, textX, textY);
lines.forEach((text, line) => {
const textWidth = context.measureText(text).width;
let textX: number;
let textY: number;
// The idea is that in the present font-size and default row heigh,
// top/bottom and center horizontalAlign coincide
const verticalPadding = 4;
if (horizontalAlign === "right") {
textX = width - padding + x - textWidth / 2;
} else if (horizontalAlign === "center") {
textX = x + width / 2;
} else {
// left aligned
textX = padding + x + textWidth / 2;
}
if (verticalAlign === "bottom") {
textY =
y +
height -
fontSize / 2 -
verticalPadding +
(line - lineCount + 1) * lineHeight;
} else if (verticalAlign === "center") {
textY = y + height / 2 + (line + (1 - lineCount) / 2) * lineHeight;
} else {
// aligned top
textY = y + fontSize / 2 + verticalPadding + line * lineHeight;
}
context.fillText(text, textX, textY);
if (style.font) {
if (style.font.u) {
if (underlined) {
// There are no text-decoration in canvas. You have to do the underline yourself.
const offset = Math.floor(fontSize / 2);
context.beginPath();
@@ -531,7 +870,7 @@ export default class WorksheetCanvas {
context.lineTo(textX + textWidth / 2, textY + offset);
context.stroke();
}
if (style.font.strike) {
if (strike) {
// There are no text-decoration in canvas. You have to do the strikethrough yourself.
context.beginPath();
context.strokeStyle = textColor;
@@ -540,10 +879,7 @@ export default class WorksheetCanvas {
context.lineTo(textX + textWidth / 2, textY);
context.stroke();
}
}
});
// remove the clipping region
context.restore();
}
@@ -1399,6 +1735,12 @@ export default class WorksheetCanvas {
}
renderSheet(): void {
console.time("renderSheet");
this._renderSheet();
console.timeEnd("renderSheet");
}
_renderSheet(): void {
const context = this.ctx;
const { canvas } = this;
const selectedSheet = this.model.getSelectedSheet();
@@ -1412,6 +1754,7 @@ export default class WorksheetCanvas {
this.removeHandles();
const { topLeftCell, bottomRightCell } = this.getVisibleCells();
this.computeCellsText();
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
@@ -1424,7 +1767,7 @@ export default class WorksheetCanvas {
x = headerColumnWidth + 0.5;
for (let column = 1; column <= frozenColumns; column += 1) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.renderCell(row, column, x, y, columnWidth, rowHeight);
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
@@ -1474,7 +1817,7 @@ export default class WorksheetCanvas {
column += 1
) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.renderCell(row, column, x, y, columnWidth, rowHeight);
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
@@ -1488,7 +1831,7 @@ export default class WorksheetCanvas {
for (let column = 1; column <= frozenColumns; column += 1) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.renderCell(row, column, x, y, columnWidth, rowHeight);
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
@@ -1507,13 +1850,18 @@ export default class WorksheetCanvas {
column += 1
) {
const columnWidth = this.getColumnWidth(selectedSheet, column);
this.renderCell(row, column, x, y, columnWidth, rowHeight);
this.renderCellStyle(row, column, x, y, columnWidth, rowHeight);
x += columnWidth;
}
y += rowHeight;
}
// Render all cell texts
for (const cell of this.cells) {
this.renderCellText(cell);
}
// Draw column headers
this.renderColumnHeaders(
frozenColumns,

File diff suppressed because it is too large Load Diff