diff options
Diffstat (limited to 'web/src/components')
| -rw-r--r-- | web/src/components/app/index.ts | 1 | ||||
| -rw-r--r-- | web/src/components/grid/drawGrid.ts | 188 | ||||
| -rw-r--r-- | web/src/components/grid/index.css | 11 | ||||
| -rw-r--r-- | web/src/components/grid/index.ts | 69 |
4 files changed, 213 insertions, 56 deletions
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts index 910aa52..ec40754 100644 --- a/web/src/components/app/index.ts +++ b/web/src/components/app/index.ts @@ -1,4 +1,3 @@ -import h from "../../html"; import ntvGrid from "../grid"; import ntvToolbar from "../toolbar"; import "./index.css"; diff --git a/web/src/components/grid/drawGrid.ts b/web/src/components/grid/drawGrid.ts index 8e64479..5ea17b6 100644 --- a/web/src/components/grid/drawGrid.ts +++ b/web/src/components/grid/drawGrid.ts @@ -1,21 +1,61 @@ -import colors from "tailwindcss/colors"; -import { PendingSelection, Selection } from "../../selection"; +import { RangeSelection, Selection } from "../../selection"; import { CellRef } from "../../types"; import { RenderedCell, RenderedGrid } from "./renderGrid"; -function fillBackground(ctx: CanvasRenderingContext2D, grid: RenderedGrid) { +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.neutral[900]; + ctx.fillStyle = colors.bgFill; ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); } -function strokeGrid(ctx: CanvasRenderingContext2D, grid: RenderedGrid) { - ctx.strokeStyle = colors.neutral[700]; +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, grid: RenderedGrid) { - ctx.strokeStyle = colors.neutral[800]; +function strokeGridLines( + ctx: CanvasRenderingContext2D, + colors: GridColors, + grid: RenderedGrid, +) { + ctx.strokeStyle = colors.cellStroke; grid.renderedRows.forEach((row, renderedRowIndex) => { const isLastRow = renderedRowIndex === grid.renderedRows.length - 1; @@ -43,65 +83,127 @@ function getRenderedCell( return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex]; } -function drawPendingSelection( +function strokeActiveCell( ctx: CanvasRenderingContext2D, + colors: GridColors, grid: RenderedGrid, - selection: PendingSelection, -) {} + cell: RenderedCell, +) { + const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; + const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; -function drawSelection( + 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, - selection: Selection, + start: CellRef, + end: CellRef, + { stroke }: { stroke: boolean }, ) { - if (selection.gridId !== grid.id) return; + const startCell = getRenderedCell(grid, start); + const endCell = getRenderedCell(grid, end); - const cell = getRenderedCell(grid, selection.activeCellRef); + if (!startCell || !endCell) return; - if (!cell) return; + const rect = startCell.rect.extend(endCell.rect); - const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; - const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; + const isRightEdge = rect.bottomRight.x === grid.rect.bottomRight.x; + const isBottomEdge = rect.bottomRight.y === grid.rect.bottomRight.y; + + ctx.fillStyle = colors.selelectionRangeFill; - // ctx.fillStyle = colors.green[4] + "30"; + ctx.fillRect( + rect.topLeft.x + 1, + rect.topLeft.y + 1, + isRightEdge ? rect.width - 2 : rect.width - 1, + isBottomEdge ? rect.height - 2 : rect.height - 1, + ); - // ctx.fillRect( - // cell.rect.topLeft.x + 1, - // cell.rect.topLeft.y + 1, - // cell.rect.width - 1, - // cell.rect.height - 1, - // ); + if (!stroke) return; - ctx.strokeStyle = colors.green[400]; - ctx.lineWidth = 2; + ctx.strokeStyle = colors.selectionRangeStroke; 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, + 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?: PendingSelection, + pendingSelection?: Selection, ) { - const excursion = (f: () => void) => { - ctx.save(); - f(); - ctx.restore(); - }; - - excursion(() => fillBackground(ctx, grid)); - excursion(() => strokeGridLines(ctx, grid)); - excursion(() => strokeGrid(ctx, grid)); + excursion(ctx, () => fillBackground(ctx, colors, grid)); + excursion(ctx, () => strokeGridLines(ctx, colors, grid)); + excursion(ctx, () => strokeGrid(ctx, colors, grid)); if (pendingSelection) { - excursion(() => drawPendingSelection(ctx, grid, pendingSelection)); + excursion(ctx, () => + drawPendingSelection(ctx, colors, grid, pendingSelection), + ); } else if (selection) { - excursion(() => drawSelection(ctx, grid, selection)); + excursion(ctx, () => drawSelection(ctx, colors, grid, selection)); } } diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css index a733015..825a74a 100644 --- a/web/src/components/grid/index.css +++ b/web/src/components/grid/index.css @@ -1,5 +1,16 @@ ntv-grid { display: block; + + --grid-bg-fill: var(--color-neutral-900); + --grid-border-stroke: var(--color-neutral-700); + --grid-cell-stroke: var(--color-neutral-800); + --grid-active-cell-stroke: var(--color-green-400); + --grid-selection-range-fill: color-mix( + in oklab, + var(--color-green-400) 10%, + transparent + ); + --grid-selection-range-stroke: var(--color-green-400); } ntv-grid > canvas { diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts index 0acace4..fbb9c60 100644 --- a/web/src/components/grid/index.ts +++ b/web/src/components/grid/index.ts @@ -1,6 +1,7 @@ import h, { type CreateElement } from "../../html"; +import { CellRef } from "../../types"; import cellAtCoord from "./cellAtCoord"; -import drawGrid from "./drawGrid"; +import drawGrid, { getGridColors } from "./drawGrid"; import "./index.css"; class NotiveGridElement extends HTMLElement { @@ -26,18 +27,12 @@ class NotiveGridElement extends HTMLElement { throw new Error("ntv-grid requries gridId attribute"); } - this.canvasEl.addEventListener("mousedown", (event) => { - const clientRect = this.canvasEl.getBoundingClientRect(); - const x = event.x - clientRect.x; - const y = event.y - clientRect.y; - const cellRef = cellAtCoord(this.renderedGrid, x, y); - if (!cellRef) return; - window.notive.selectCell(this.#gridId, cellRef); - }); + window.addEventListener("ntv:selection-changed", () => this.draw()); - window.addEventListener("ntv:selection-changed", () => { - this.draw(); - }); + this.canvasEl.addEventListener( + "mousedown", + this.#canvasMouseDownCallback.bind(this), + ); this.append(this.canvasEl); this.draw(); @@ -55,13 +50,63 @@ class NotiveGridElement extends HTMLElement { this.canvasEl.setAttribute("width", grid.rect.width + "px"); this.canvasEl.setAttribute("height", grid.rect.height + "px"); + const colors = getGridColors(this); + drawGrid( ctx, + colors, grid, window.notive.selection, window.notive.pendingSelection, ); } + + #mouseEventCellRef( + this: NotiveGridElement, + event: MouseEvent, + ): CellRef | undefined { + const clientRect = this.canvasEl.getBoundingClientRect(); + const x = event.x - clientRect.x; + const y = event.y - clientRect.y; + return cellAtCoord(this.renderedGrid, x, y); + } + + #canvasMouseDownCallback(this: NotiveGridElement, event: MouseEvent) { + const cellRef = this.#mouseEventCellRef(event); + + if (!cellRef) return; + + window.notive.startSelecting(this.gridId, cellRef); + + this.#selectionAbortController = new AbortController(); + const { signal } = this.#selectionAbortController; + + window.addEventListener( + "mousemove", + this.#selectionMouseMoveCallback.bind(this), + { signal }, + ); + + window.addEventListener( + "mouseup", + this.#selectionMouseUpCallback.bind(this), + { signal }, + ); + } + + #selectionAbortController?: AbortController; + + #selectionMouseMoveCallback(this: NotiveGridElement, event: MouseEvent) { + const cellRef = this.#mouseEventCellRef(event); + if (!cellRef) return; + window.notive.extendSelection(cellRef); + } + + #selectionMouseUpCallback(this: NotiveGridElement, event: MouseEvent) { + this.#selectionAbortController?.abort(); + this.#selectionAbortController = undefined; + window.notive.finishSelecting(); + } } customElements.define("ntv-grid", NotiveGridElement); |
