summaryrefslogtreecommitdiff
path: root/web/src/components
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-10-26 18:31:23 +0200
committerJosh Kingsley <josh@joshkingsley.me>2025-10-26 18:31:23 +0200
commita984c69e3cca4bcf822989bb78a8befe2397e487 (patch)
treeb7599295851dcc87a89e696f32cdc211e77eab18 /web/src/components
parent6ae3218e2dd130c085074a0d13d156bdba99716f (diff)
feat(web): draw selection
Diffstat (limited to 'web/src/components')
-rw-r--r--web/src/components/app/index.ts1
-rw-r--r--web/src/components/grid/drawGrid.ts188
-rw-r--r--web/src/components/grid/index.css11
-rw-r--r--web/src/components/grid/index.ts69
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);