fix(grid): align canvas cell state rendering
This commit is contained in:
parent
631584bed1
commit
fcf5ad7261
|
|
@ -156,6 +156,10 @@ function firstVisibleColumn(offsets: number[], contentStart: number): number {
|
|||
return low;
|
||||
}
|
||||
|
||||
function crispCanvasLine(value: number): number {
|
||||
return Math.round(value) + 0.5;
|
||||
}
|
||||
|
||||
function resolveCanvasRenderState(canvas: HTMLCanvasElement, isDark: boolean, styleKey?: string): CanvasRenderState {
|
||||
const cacheKey = `${styleKey ?? "default"}:${isDark ? "dark" : "light"}`;
|
||||
const cached = canvasRenderStateCache.get(canvas);
|
||||
|
|
@ -275,6 +279,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
const firstCol = firstVisibleColumn(offsets, contentStart);
|
||||
const columnOffset = offsets[firstCol] ?? 0;
|
||||
const paintSearchMatches = !isScrolling && searchMatchKeys.size > 0;
|
||||
const rowNumberBorderX = crispCanvasLine(rowNumberWidth - 1);
|
||||
const rowNumberTextX = Math.max(0, rowNumberWidth - 1) / 2;
|
||||
|
||||
for (let rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
|
||||
const item = rowAt(rowIndex);
|
||||
|
|
@ -317,8 +323,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
}
|
||||
ctx.strokeStyle = theme.border;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(rowNumberWidth + 0.5, y);
|
||||
ctx.lineTo(rowNumberWidth + 0.5, y + CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
ctx.moveTo(rowNumberBorderX, y);
|
||||
ctx.lineTo(rowNumberBorderX, y + CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
ctx.stroke();
|
||||
|
||||
const rowNumberText =
|
||||
|
|
@ -334,7 +340,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
ctx.fillStyle = rowNumberText;
|
||||
ctx.font = item.status === "new" || item.status === "edited" || isRowSelected(item.id) ? semiboldFont : normalFont;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(String(item.displayIndex + 1), rowNumberWidth / 2, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
|
||||
ctx.fillText(String(item.displayIndex + 1), rowNumberTextX, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
|
||||
ctx.font = normalFont;
|
||||
|
||||
let x = rowNumberWidth + columnOffset - scrollLeft;
|
||||
|
|
@ -352,7 +358,8 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
singleSelectedCell?.rowIndex === item.displayIndex && singleSelectedCell.visibleColIdx === visibleColIdx;
|
||||
const isDirtyCell = item.isDirtyCol[actualColIdx];
|
||||
const selectedFillVisual =
|
||||
rowSelectionVisual || (selectedCell && !isSingleSelectedCell && (!rowIsActive || isDirtyCell));
|
||||
rowSelectionVisual ||
|
||||
(selectedCell && (!isSingleSelectedCell || isDirtyCell) && (!rowIsActive || isDirtyCell));
|
||||
const selectedBorderVisual = rowSelectionVisual || selectedCell;
|
||||
const isSearchMatch = paintSearchMatches && searchMatchKeys.has(`${item.displayIndex}:${actualColIdx}`);
|
||||
const isCurrentSearchMatch =
|
||||
|
|
@ -362,7 +369,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
const clippedX = Math.max(x, rowNumberWidth);
|
||||
const cellPaintWidth = colWidth - Math.max(0, clippedX - x);
|
||||
|
||||
if (isDirtyCell) {
|
||||
if (isDirtyCell && !selectedFillVisual) {
|
||||
ctx.fillStyle = theme.cellDirty;
|
||||
ctx.fillRect(clippedX, y, cellPaintWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
}
|
||||
|
|
@ -372,6 +379,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
!isScrolling &&
|
||||
!isSearchMatch &&
|
||||
!isCurrentSearchMatch &&
|
||||
!isDirtyCell &&
|
||||
cellCanHover(item, actualColIdx)
|
||||
) {
|
||||
ctx.fillStyle = theme.cellHover;
|
||||
|
|
@ -425,8 +433,9 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
|
||||
ctx.strokeStyle = theme.border;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + colWidth - 0.5, y);
|
||||
ctx.lineTo(x + colWidth - 0.5, y + CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
const columnBorderX = crispCanvasLine(x + colWidth - 1);
|
||||
ctx.moveTo(columnBorderX, y);
|
||||
ctx.lineTo(columnBorderX, y + CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
ctx.stroke();
|
||||
|
||||
if (selectedBorderVisual && cellPaintWidth >= 2) {
|
||||
|
|
@ -434,14 +443,17 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
const selectedRightX = clippedX + cellPaintWidth - 1.5;
|
||||
const selectedTopY = y + 0.5;
|
||||
const selectedBottomY = y + CANVAS_DATA_GRID_ROW_HEIGHT - 1.5;
|
||||
const drawSelectedLeftBorder = selectedLeftX > rowNumberWidth + 0.5;
|
||||
ctx.strokeStyle = theme.cellSelectedBorder;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(selectedLeftX, selectedTopY);
|
||||
ctx.lineTo(selectedRightX, selectedTopY);
|
||||
ctx.moveTo(selectedLeftX, selectedBottomY);
|
||||
ctx.lineTo(selectedRightX, selectedBottomY);
|
||||
ctx.moveTo(selectedLeftX, selectedTopY);
|
||||
ctx.lineTo(selectedLeftX, selectedBottomY);
|
||||
if (drawSelectedLeftBorder) {
|
||||
ctx.moveTo(selectedLeftX, selectedTopY);
|
||||
ctx.lineTo(selectedLeftX, selectedBottomY);
|
||||
}
|
||||
ctx.moveTo(selectedRightX, selectedTopY);
|
||||
ctx.lineTo(selectedRightX, selectedBottomY);
|
||||
ctx.stroke();
|
||||
|
|
@ -456,13 +468,12 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
|||
}
|
||||
x += colWidth;
|
||||
}
|
||||
if (!isRowSelected(item.id)) {
|
||||
ctx.strokeStyle = theme.border;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y + CANVAS_DATA_GRID_ROW_HEIGHT - 0.5);
|
||||
ctx.lineTo(width, y + CANVAS_DATA_GRID_ROW_HEIGHT - 0.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.strokeStyle = theme.border;
|
||||
ctx.beginPath();
|
||||
const rowBorderY = crispCanvasLine(y + CANVAS_DATA_GRID_ROW_HEIGHT - 1);
|
||||
ctx.moveTo(0, rowBorderY);
|
||||
ctx.lineTo(width, rowBorderY);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const DATA_GRID_LIGHT_ROW_NUMBER_EDITED_BG = "rgb(253, 241, 219)";
|
|||
const DATA_GRID_LIGHT_ROW_NUMBER_DELETED_BG = "rgb(255, 244, 244)";
|
||||
const CANVAS_SAFE_COLOR_RE = /^(#|rgb\(|rgba\(|hsl\(|hsla\()/i;
|
||||
const ADVANCED_COLOR_RE = /^(oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\()/i;
|
||||
let browserColorProbe: HTMLElement | null = null;
|
||||
|
||||
interface RgbaColor {
|
||||
r: number;
|
||||
|
|
@ -157,15 +158,36 @@ function parseColorMix(value: string): string | null {
|
|||
return formatRgb(mixRgb(firstColor, first.percent, secondColor, secondPercent));
|
||||
}
|
||||
|
||||
function normalizeCssColorWithBrowser(value: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
try {
|
||||
browserColorProbe ??= document.createElement("span");
|
||||
const probe = browserColorProbe;
|
||||
probe.style.color = "";
|
||||
probe.style.color = value;
|
||||
if (!probe.style.color) return null;
|
||||
if (!probe.isConnected) {
|
||||
probe.style.position = "absolute";
|
||||
probe.style.left = "-9999px";
|
||||
probe.style.top = "-9999px";
|
||||
probe.style.visibility = "hidden";
|
||||
document.body?.appendChild(probe);
|
||||
}
|
||||
const computed = getComputedStyle(probe).color.trim();
|
||||
return computed || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toCanvasSafeColor(value: string, fallback: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return fallback;
|
||||
const mixed = parseColorMix(trimmed);
|
||||
if (mixed) return mixed;
|
||||
const rgb = parseRgbColor(trimmed);
|
||||
if (rgb) return formatRgb(rgb);
|
||||
if (CANVAS_SAFE_COLOR_RE.test(trimmed)) return trimmed;
|
||||
if (ADVANCED_COLOR_RE.test(trimmed)) return fallback;
|
||||
if (ADVANCED_COLOR_RE.test(trimmed))
|
||||
return normalizeCssColorWithBrowser(trimmed) ?? parseColorMix(trimmed) ?? fallback;
|
||||
return `hsl(${trimmed})`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue