import { RangeSelection, Selection } from "../../selection"; import { CellRef } from "../../types"; import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid"; interface GridStyles { bgFill: string; borderStroke: string; cellStroke: string; selelectionRangeFill: string; selectionRangeStroke: string; activeCellStroke: string; cellValueFont: string; cellValueLineHeight: string; } export function getGridStyles(el: HTMLElement): GridStyles { const style = window.getComputedStyle(el); const prop = (k: string) => style.getPropertyValue(k); return { bgFill: prop("--grid-bg-fill"), borderStroke: prop("--grid-border-stroke"), cellStroke: prop("--grid-cell-stroke"), selelectionRangeFill: prop("--grid-selection-range-fill"), selectionRangeStroke: prop("--grid-selection-range-stroke"), activeCellStroke: prop("--grid-active-cell-stroke"), cellValueFont: prop("font"), cellValueLineHeight: prop("line-height"), }; } function excursion(ctx: CanvasRenderingContext2D, f: () => void) { ctx.save(); f(); ctx.restore(); } function fillBackground( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, ) { ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); ctx.fillStyle = styles.bgFill; ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); } function strokeGrid( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, ) { ctx.strokeStyle = styles.borderStroke; ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1); } function strokeGridLines( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, ) { ctx.strokeStyle = styles.cellStroke; grid.renderedRows.forEach((row, renderedRowIndex) => { const isLastRow = renderedRowIndex === grid.renderedRows.length - 1; row.renderedCells.forEach((cell, cellIndex) => { const { topLeft, width, height } = cell.rect; const isLastCell = cellIndex === row.renderedCells.length - 1; ctx.strokeRect( topLeft.x + 0.5, topLeft.y + 0.5, isLastCell ? width - 1 : width, isLastRow ? height - 1 : height, ); }); }); } function strokeActiveCell( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, cell: RenderedCell, ) { const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; ctx.strokeStyle = styles.activeCellStroke; ctx.lineWidth = 2; ctx.strokeRect( cell.rect.topLeft.x + 1, cell.rect.topLeft.y + 1, isLastCell ? cell.rect.width - 2 : cell.rect.width - 1, isLastRow ? cell.rect.height - 2 : cell.rect.height - 1, ); } function drawCellRange( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, start: CellRef, end: CellRef, { stroke }: { stroke: boolean }, ) { const startCell = getRenderedCell(grid, start); const endCell = getRenderedCell(grid, end); if (!startCell || !endCell) return; const rect = startCell.rect.extend(endCell.rect); const isRightEdge = rect.bottomRight.x === grid.rect.bottomRight.x; const isBottomEdge = rect.bottomRight.y === grid.rect.bottomRight.y; ctx.fillStyle = styles.selelectionRangeFill; ctx.fillRect( rect.topLeft.x + 1, rect.topLeft.y + 1, isRightEdge ? rect.width - 2 : rect.width - 1, isBottomEdge ? rect.height - 2 : rect.height - 1, ); if (!stroke) return; ctx.strokeStyle = styles.selectionRangeStroke; ctx.strokeRect( rect.topLeft.x + 0.5, rect.topLeft.y + 0.5, isRightEdge ? rect.width - 1 : rect.width, isBottomEdge ? rect.height - 1 : rect.height, ); } function drawPendingSelection( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, selection: Selection, ) { if (selection.gridId !== grid.id) return; const activeCell = getRenderedCell(grid, selection.activeCellRef); if (!activeCell) return; if (selection instanceof RangeSelection) { excursion(ctx, () => { drawCellRange(ctx, styles, grid, selection.range[0], selection.range[1], { stroke: false, }); }); } excursion(ctx, () => strokeActiveCell(ctx, styles, grid, activeCell)); } function drawSelection( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, selection: Selection, ) { if (selection.gridId !== grid.id) return; const activeCell = getRenderedCell(grid, selection.activeCellRef); if (!activeCell) return; if (selection instanceof RangeSelection) { excursion(ctx, () => { drawCellRange(ctx, styles, grid, selection.range[0], selection.range[1], { stroke: true, }); }); } excursion(ctx, () => strokeActiveCell(ctx, styles, grid, activeCell)); } function drawCellValues( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, ) { grid.renderedRows.forEach((row) => row.renderedCells.forEach((cell) => { if (!cell.value) return; ctx.fillStyle = "white"; ctx.textAlign = "center"; ctx.font = styles.cellValueFont; ctx.fillText( cell.value, cell.rect.center.x, cell.rect.center.y + parseInt(styles.cellValueLineHeight) / 4, ); }), ); } export default function drawGrid( ctx: CanvasRenderingContext2D, styles: GridStyles, grid: RenderedGrid, selection?: Selection, pendingSelection?: Selection, ) { excursion(ctx, () => fillBackground(ctx, styles, grid)); excursion(ctx, () => strokeGridLines(ctx, styles, grid)); excursion(ctx, () => strokeGrid(ctx, styles, grid)); excursion(ctx, () => drawCellValues(ctx, styles, grid)); if (pendingSelection) { excursion(ctx, () => drawPendingSelection(ctx, styles, grid, pendingSelection), ); } else if (selection) { excursion(ctx, () => drawSelection(ctx, styles, grid, selection)); } }