import { RangeSelection, Selection } from "../../selection"; import { CellRef } from "../../types"; import { RenderedCell, RenderedGrid } from "./renderGrid"; interface GridColors { bgFill: string; borderStroke: string; cellStroke: string; selelectionRangeFill: string; selectionRangeStroke: string; activeCellStroke: string; } export function getGridColors(el: HTMLElement): GridColors { const style = window.getComputedStyle(el); const color = (k: string) => style.getPropertyValue(k); return { bgFill: color("--grid-bg-fill"), borderStroke: color("--grid-border-stroke"), cellStroke: color("--grid-cell-stroke"), selelectionRangeFill: color("--grid-selection-range-fill"), selectionRangeStroke: color("--grid-selection-range-stroke"), activeCellStroke: color("--grid-active-cell-stroke"), }; } function excursion(ctx: CanvasRenderingContext2D, f: () => void) { ctx.save(); f(); ctx.restore(); } function fillBackground( ctx: CanvasRenderingContext2D, colors: GridColors, grid: RenderedGrid, ) { ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); ctx.fillStyle = colors.bgFill; ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); } function strokeGrid( ctx: CanvasRenderingContext2D, colors: GridColors, grid: RenderedGrid, ) { ctx.strokeStyle = colors.borderStroke; ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1); } function strokeGridLines( ctx: CanvasRenderingContext2D, colors: GridColors, grid: RenderedGrid, ) { ctx.strokeStyle = colors.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 getRenderedCell( grid: RenderedGrid, cellRef: CellRef, ): RenderedCell | undefined { const rowsPerPart = grid.renderedRows.length / grid.parts.length; const renderedRowIndex = cellRef.partIndex * rowsPerPart + cellRef.rowIndex; return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex]; } function strokeActiveCell( ctx: CanvasRenderingContext2D, colors: GridColors, 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 = colors.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, colors: GridColors, 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 = colors.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 = colors.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, colors: GridColors, 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, colors, grid, selection.range[0], selection.range[1], { stroke: false, }); }); } excursion(ctx, () => strokeActiveCell(ctx, colors, grid, activeCell)); } function drawSelection( ctx: CanvasRenderingContext2D, colors: GridColors, 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, colors, grid, selection.range[0], selection.range[1], { stroke: true, }); }); } excursion(ctx, () => strokeActiveCell(ctx, colors, grid, activeCell)); } export default function drawGrid( ctx: CanvasRenderingContext2D, colors: GridColors, grid: RenderedGrid, selection?: Selection, pendingSelection?: Selection, ) { excursion(ctx, () => fillBackground(ctx, colors, grid)); excursion(ctx, () => strokeGridLines(ctx, colors, grid)); excursion(ctx, () => strokeGrid(ctx, colors, grid)); if (pendingSelection) { excursion(ctx, () => drawPendingSelection(ctx, colors, grid, pendingSelection), ); } else if (selection) { excursion(ctx, () => drawSelection(ctx, colors, grid, selection)); } }