From 16369acbda24d9072a04b48718b178b986532ff1 Mon Sep 17 00:00:00 2001 From: TangT <31877158+TangTangH@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:59:34 +0800 Subject: [PATCH] fix(grid): sharpen canvas text at fractional scaling --- apps/desktop/src/components/grid/DataGrid.vue | 12 ++- .../useDataGridCanvasRuntime.spec.ts | 30 ++++++- .../composables/useDataGridCanvasRuntime.ts | 28 ++++++- .../canvasDataGridRendererFrozen.spec.ts | 41 +++++++++- .../lib/dataGrid/canvasDataGridRenderer.ts | 81 ++++++++++++++----- 5 files changed, 163 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 0dc0d228e..65cb11f12 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -142,7 +142,7 @@ import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid import { canFetchNextDataGridSegment, canGoNextDataGridPage, dataGridTotalRowCountLabelKey, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal, type DataGridInexactTotalRowCountMode } from "@/lib/dataGrid/dataGridPagination"; import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions"; import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll"; -import { CANVAS_DATA_GRID_ROW_HEIGHT, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid } from "@/lib/dataGrid/canvasDataGridRenderer"; +import { CANVAS_DATA_GRID_ROW_HEIGHT, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer"; import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme"; import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText"; import { dataGridPreviewLabelKey, dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid/dataGridSaveUi"; @@ -194,7 +194,7 @@ import { columnNamesForCopy } from "@/lib/dataGrid/dataGridColumnNameCopy"; import { DATA_GRID_ROW_NUM_WIDTH, dataGridRowNumberColumnWidth, resolveDataGridMaxRowNumber, useDataGridColumnResize } from "@/composables/useDataGridColumnResize"; import { createDataGridColumnStructureSignature } from "@/lib/dataGrid/dataGridColumnWidthState"; import { useDataGridColumnLayout, useDataGridColumnLayoutState } from "@/composables/useDataGridColumnLayout"; -import { useDataGridCanvasRuntime, type DataGridCanvasRuntime } from "@/composables/useDataGridCanvasRuntime"; +import { dataGridCanvasDevicePixelSize, useDataGridCanvasRuntime, type DataGridCanvasRuntime } from "@/composables/useDataGridCanvasRuntime"; import { useDataGridScrollbars, type DataGridScrollbarsRuntime } from "@/composables/useDataGridScrollbars"; import { useDataGridSelection } from "@/composables/useDataGridSelection"; import { moveDataGridCell } from "@/lib/dataGrid/dataGridNavigation"; @@ -4761,6 +4761,7 @@ const canvasViewportHeight = ref(0); const canvasScrollTop = ref(0); const canvasHoverCell = ref<{ rowIndex: number; visibleColIdx: number } | null>(null); const canvasDevicePixelRatio = ref(typeof window === "undefined" ? 1 : window.devicePixelRatio || 1); +const canvasMeasuredDevicePixelSize = ref(null); const canvasBackingPixelRatio = computed(() => Math.min(4, Math.max(1, canvasDevicePixelRatio.value * settingsStore.editorSettings.uiScale))); const useCanvasGridRows = computed(() => dataGridRenderMode.value === "canvas"); const canvasContentHeight = computed(() => Math.max(1, displayRowCount.value * CANVAS_DATA_GRID_ROW_HEIGHT)); @@ -4850,10 +4851,13 @@ function dataGridRowFromClientPoint(_clientX: number, clientY: number): number | return Number.isInteger(rowIndex) ? rowIndex : null; } -function syncCanvasViewport() { +function syncCanvasViewport(entries?: readonly ResizeObserverEntry[]) { if (!dataGridIsActive) return; const scroller = canvasScrollerElement(); if (!scroller) return; + const canvas = canvasRef.value; + const canvasEntry = canvas ? entries?.find((entry) => entry.target === canvas) : undefined; + if (canvasEntry) canvasMeasuredDevicePixelSize.value = dataGridCanvasDevicePixelSize(canvasEntry); canvasViewportWidth.value = scroller.clientWidth; canvasViewportHeight.value = scroller.clientHeight; canvasScrollTop.value = scroller.scrollTop; @@ -4914,6 +4918,7 @@ canvasRuntime = useDataGridCanvasRuntime({ draw: drawCanvasGrid, syncViewport: syncCanvasViewport, getViewport: canvasScrollerElement, + getSurface: () => canvasRef.value ?? null, refreshPixelRatio: () => { const next = currentCanvasDevicePixelRatio(); if (Math.abs(next - canvasDevicePixelRatio.value) > 0.001) { @@ -5343,6 +5348,7 @@ function drawCanvasGrid() { width: Math.max(1, canvasSurfaceWidth.value || scroller.clientWidth), height: Math.max(1, canvasViewportHeight.value || scroller.clientHeight), pixelRatio: canvasBackingPixelRatio.value, + devicePixelSize: canvasMeasuredDevicePixelSize.value, isDark: isDark.value, styleKey: canvasRenderStyleKey.value, rowCount: displayRowCount.value, diff --git a/apps/desktop/src/composables/__tests__/useDataGridCanvasRuntime.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridCanvasRuntime.spec.ts index bc74615fc..1cdefaedb 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridCanvasRuntime.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridCanvasRuntime.spec.ts @@ -1,6 +1,6 @@ import { effectScope } from "vue"; import { describe, expect, it, vi } from "vitest"; -import { useDataGridCanvasRuntime, type DataGridAnimationFrameDriver } from "@/composables/useDataGridCanvasRuntime"; +import { dataGridCanvasDevicePixelSize, useDataGridCanvasRuntime, type DataGridAnimationFrameDriver } from "@/composables/useDataGridCanvasRuntime"; import { useDataGridScrollbars } from "@/composables/useDataGridScrollbars"; function createFrameDriver() { @@ -26,6 +26,34 @@ function createObserver() { } describe("data grid visual runtimes", () => { + it("reads the exact device-pixel content box reported for a canvas", () => { + const size = dataGridCanvasDevicePixelSize({ + contentRect: { width: 801, height: 399 }, + devicePixelContentBoxSize: [{ inlineSize: 1001, blockSize: 499 }], + } as unknown as ResizeObserverEntry); + + expect(size).toEqual({ cssWidth: 801, cssHeight: 399, pixelWidth: 1001, pixelHeight: 499 }); + }); + + it("observes the canvas device-pixel box together with its viewport", () => { + const resize = createObserver(); + const viewport = { children: [] } as unknown as Element; + const surface = { children: [] } as unknown as Element; + const runtime = useDataGridCanvasRuntime({ + draw: vi.fn(), + syncViewport: vi.fn(), + getViewport: () => viewport, + getSurface: () => surface, + createResizeObserver: () => resize.observer, + }); + + runtime.observeViewport(); + + expect(resize.observe).toHaveBeenNthCalledWith(1, viewport); + expect(resize.observe).toHaveBeenNthCalledWith(2, surface, { box: "device-pixel-content-box" }); + runtime.dispose(); + }); + it("coalesces canvas frames and cancels all resources on dispose", () => { const frames = createFrameDriver(); const resize = createObserver(); diff --git a/apps/desktop/src/composables/useDataGridCanvasRuntime.ts b/apps/desktop/src/composables/useDataGridCanvasRuntime.ts index 56619a311..7cf4391ae 100644 --- a/apps/desktop/src/composables/useDataGridCanvasRuntime.ts +++ b/apps/desktop/src/composables/useDataGridCanvasRuntime.ts @@ -1,4 +1,5 @@ import { getCurrentScope, onScopeDispose } from "vue"; +import type { CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer"; export interface DataGridAnimationFrameDriver { request(callback: FrameRequestCallback): number; @@ -7,8 +8,9 @@ export interface DataGridAnimationFrameDriver { export interface DataGridCanvasRuntimeOptions { draw(): void; - syncViewport(): void; + syncViewport(entries?: readonly ResizeObserverEntry[]): void; getViewport(): Element | null; + getSurface?: () => Element | null; refreshPixelRatio?: () => void; frameDriver?: DataGridAnimationFrameDriver; createResizeObserver?: (callback: ResizeObserverCallback) => ResizeObserver | null; @@ -27,6 +29,18 @@ export interface DataGridCanvasRuntime { dispose(): void; } +export function dataGridCanvasDevicePixelSize(entry: ResizeObserverEntry): CanvasDevicePixelSize | null { + const boxes = entry.devicePixelContentBoxSize; + const box = Array.isArray(boxes) ? boxes[0] : (boxes as unknown as ResizeObserverSize | undefined); + if (!box || !Number.isFinite(box.inlineSize) || !Number.isFinite(box.blockSize) || box.inlineSize <= 0 || box.blockSize <= 0) return null; + return { + cssWidth: entry.contentRect.width, + cssHeight: entry.contentRect.height, + pixelWidth: Math.round(box.inlineSize), + pixelHeight: Math.round(box.blockSize), + }; +} + function defaultFrameDriver(): DataGridAnimationFrameDriver { return { request: (callback) => requestAnimationFrame(callback), @@ -88,10 +102,18 @@ export function useDataGridCanvasRuntime(options: DataGridCanvasRuntimeOptions): options.syncViewport(); const viewport = options.getViewport(); if (!viewport) return; - resizeObserver = createResizeObserver(() => { - if (active && !disposed) options.syncViewport(); + resizeObserver = createResizeObserver((entries) => { + if (active && !disposed) options.syncViewport(entries); }); resizeObserver?.observe(viewport); + const surface = options.getSurface?.(); + if (resizeObserver && surface && surface !== viewport) { + try { + resizeObserver.observe(surface, { box: "device-pixel-content-box" }); + } catch { + resizeObserver.observe(surface); + } + } }, pause() { active = false; diff --git a/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts index 57416b60b..27da09b48 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from "vitest"; -import { CANVAS_DATA_GRID_ROW_HEIGHT, drawCanvasDataGrid, type DrawCanvasDataGridOptions } from "@/lib/dataGrid/canvasDataGridRenderer"; +import { drawCanvasDataGrid, resolveCanvasBackingStoreMetrics, type DrawCanvasDataGridOptions } from "@/lib/dataGrid/canvasDataGridRenderer"; function createMockCanvas(width = 800, height = 400) { const canvas = document.createElement("canvas"); @@ -80,6 +80,45 @@ describe("drawCanvasDataGrid with frozen columns", () => { } as CSSStyleDeclaration); }); + it("uses the exact observed device-pixel size instead of a nominal DPR", () => { + expect( + resolveCanvasBackingStoreMetrics({ + width: 801, + height: 399, + pixelRatio: 1.25, + devicePixelSize: { cssWidth: 801, cssHeight: 399, pixelWidth: 1001, pixelHeight: 499 }, + }), + ).toEqual({ + pixelWidth: 1001, + pixelHeight: 499, + scaleX: 1001 / 801, + scaleY: 499 / 399, + measured: true, + }); + }); + + it("ignores a stale observed size after the CSS viewport changes", () => { + expect( + resolveCanvasBackingStoreMetrics({ + width: 900, + height: 400, + pixelRatio: 1.25, + devicePixelSize: { cssWidth: 801, cssHeight: 399, pixelWidth: 1001, pixelHeight: 499 }, + }), + ).toEqual({ pixelWidth: 1125, pixelHeight: 500, scaleX: 1.25, scaleY: 1.25, measured: false }); + }); + + it("caps the observed backing store at the configured pixel ratio", () => { + expect( + resolveCanvasBackingStoreMetrics({ + width: 801, + height: 399, + pixelRatio: 4, + devicePixelSize: { cssWidth: 801, cssHeight: 399, pixelWidth: 4005, pixelHeight: 1995 }, + }), + ).toEqual({ pixelWidth: 3204, pixelHeight: 1596, scaleX: 4, scaleY: 4, measured: true }); + }); + it("draws without errors when frozenColumnCount is 0", () => { const canvas = createMockCanvas(); const options = createBaseOptions({ canvas, frozenColumnCount: 0 }); diff --git a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts index 74cccd54f..7a3f16f66 100644 --- a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts +++ b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts @@ -5,6 +5,13 @@ import { BOOLEAN_CHECKBOX_SIZE, isBooleanCheckboxValue, normalizeBooleanCellValu export const CANVAS_DATA_GRID_ROW_HEIGHT = 26; +export interface CanvasDevicePixelSize { + cssWidth: number; + cssHeight: number; + pixelWidth: number; + pixelHeight: number; +} + export interface CanvasDataGridRow { id: number; displayIndex: number; @@ -49,6 +56,7 @@ export interface DrawCanvasDataGridOptions { width: number; height: number; pixelRatio?: number; + devicePixelSize?: CanvasDevicePixelSize | null; isDark: boolean; styleKey?: string; rowCount: number; @@ -94,6 +102,33 @@ interface CanvasRenderState { currentSearchBorder: string; } +export interface CanvasBackingStoreMetrics { + pixelWidth: number; + pixelHeight: number; + scaleX: number; + scaleY: number; + measured: boolean; +} + +export function resolveCanvasBackingStoreMetrics(options: { width: number; height: number; pixelRatio: number; devicePixelSize?: CanvasDevicePixelSize | null }): CanvasBackingStoreMetrics { + const width = Math.max(1, options.width); + const height = Math.max(1, options.height); + const fallbackRatio = Math.max(1, options.pixelRatio); + const measured = options.devicePixelSize; + const measurementMatches = !!measured && Math.abs(measured.cssWidth - width) <= 0.5 && Math.abs(measured.cssHeight - height) <= 0.5 && measured.pixelWidth > 0 && measured.pixelHeight > 0; + const fallbackPixelWidth = Math.max(1, Math.ceil(width * fallbackRatio)); + const fallbackPixelHeight = Math.max(1, Math.ceil(height * fallbackRatio)); + const pixelWidth = measurementMatches ? Math.min(measured.pixelWidth, fallbackPixelWidth) : fallbackPixelWidth; + const pixelHeight = measurementMatches ? Math.min(measured.pixelHeight, fallbackPixelHeight) : fallbackPixelHeight; + return { + pixelWidth, + pixelHeight, + scaleX: pixelWidth / width, + scaleY: pixelHeight / height, + measured: measurementMatches, + }; +} + export function resolveCanvasDataGridRowFill(theme: Pick, rowBase: string, options: { isActive: boolean; isDeleted: boolean; isSelected: boolean }): string { if (options.isSelected) return theme.cellSelected; if (options.isActive && !options.isDeleted) return theme.cellActive; @@ -193,11 +228,11 @@ function alignCanvasPixel(value: number, dpr: number): number { return Math.round(value * dpr) / dpr; } -function drawBooleanCheckbox(ctx: CanvasRenderingContext2D, options: { drawX: number; y: number; colWidth: number; dpr: number; theme: DataGridPaintTheme; checked: boolean }): void { - const { drawX, y, colWidth, dpr, theme, checked } = options; +function drawBooleanCheckbox(ctx: CanvasRenderingContext2D, options: { drawX: number; y: number; colWidth: number; scaleX: number; scaleY: number; theme: DataGridPaintTheme; checked: boolean }): void { + const { drawX, y, colWidth, scaleX, scaleY, theme, checked } = options; const size = BOOLEAN_CHECKBOX_SIZE; - const boxX = alignCanvasPixel(drawX + (colWidth - size) / 2, dpr); - const boxY = alignCanvasPixel(y + (CANVAS_DATA_GRID_ROW_HEIGHT - size) / 2, dpr); + const boxX = alignCanvasPixel(drawX + (colWidth - size) / 2, scaleX); + const boxY = alignCanvasPixel(y + (CANVAS_DATA_GRID_ROW_HEIGHT - size) / 2, scaleY); ctx.lineWidth = 1; if (checked) { ctx.fillStyle = theme.primary; @@ -302,9 +337,13 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { rightAlignedActionCell, columnIsBoolean, } = options; - const dpr = Math.max(1, options.pixelRatio ?? window.devicePixelRatio ?? 1); - const pixelWidth = Math.max(1, Math.ceil(width * dpr)); - const pixelHeight = Math.max(1, Math.ceil(height * dpr)); + const fallbackRatio = Math.max(1, options.pixelRatio ?? window.devicePixelRatio ?? 1); + const { pixelWidth, pixelHeight, scaleX, scaleY } = resolveCanvasBackingStoreMetrics({ + width, + height, + pixelRatio: fallbackRatio, + devicePixelSize: options.devicePixelSize, + }); if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { canvas.width = pixelWidth; canvas.height = pixelHeight; @@ -316,7 +355,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { const ctx = canvas.getContext("2d"); if (!ctx) return; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0); ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, width, height); @@ -343,9 +382,9 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { const firstCol = firstVisibleColumn(offsets, Math.max(0, contentStart - maxPreviewRightShift)); const columnOffset = offsets[firstCol] ?? 0; const paintSearchMatches = !isScrolling && searchMatchKeys.size > 0; - const rowNumberBorderX = crispCanvasLine(rowNumberWidth - 1, dpr); - const rowNumberTextX = alignCanvasPixel(Math.max(0, rowNumberWidth - 1) / 2, dpr); - const rowTextOffsetY = alignCanvasPixel(CANVAS_DATA_GRID_ROW_HEIGHT / 2, dpr); + const rowNumberBorderX = crispCanvasLine(rowNumberWidth - 1, scaleX); + const rowNumberTextX = alignCanvasPixel(Math.max(0, rowNumberWidth - 1) / 2, scaleX); + const rowTextOffsetY = alignCanvasPixel(CANVAS_DATA_GRID_ROW_HEIGHT / 2, scaleY); for (let rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) { const item = rowAt(rowIndex); @@ -360,7 +399,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { isDeleted: item.isDeleted, isSelected: rowSelectionVisual, }); - const rowBorderY = crispCanvasLine(y + CANVAS_DATA_GRID_ROW_HEIGHT - 1, dpr); + const rowBorderY = crispCanvasLine(y + CANVAS_DATA_GRID_ROW_HEIGHT - 1, scaleY); ctx.globalAlpha = item.isDeleted ? 0.7 : 1; ctx.fillStyle = rowFill; ctx.fillRect(0, y, width, CANVAS_DATA_GRID_ROW_HEIGHT); @@ -390,7 +429,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { ctx.fillStyle = rowNumberText; ctx.font = item.status === "new" || item.status === "edited" || item.status === "draft" ? semiboldFont : normalFont; ctx.textAlign = "center"; - const textY = alignCanvasPixel(y + rowTextOffsetY, dpr); + const textY = alignCanvasPixel(y + rowTextOffsetY, scaleY); ctx.save(); ctx.beginPath(); ctx.rect(0, y, rowNumberWidth, CANVAS_DATA_GRID_ROW_HEIGHT); @@ -472,24 +511,24 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { ctx.font = value === null ? italicFont : tabularFont; setCanvasNumericVariant(ctx, value === null ? "normal" : "tabular-nums"); const reservedWidth = rightAlignedActionCell?.rowIndex === item.displayIndex && rightAlignedActionCell.visibleColIdx === visibleColIdx ? rightAlignedActionCell.reservedWidth : 0; - const { textAnchorX, maxWidth: cellMaxWidth } = resolveCanvasCellTextLayout({ drawX, colWidth, dpr, isRightAlign, reservedWidth }); + const { textAnchorX, maxWidth: cellMaxWidth } = resolveCanvasCellTextLayout({ drawX, colWidth, dpr: scaleX, isRightAlign, reservedWidth }); if (isBooleanCell && value !== null && !isEditingThisCell) { - drawBooleanCheckbox(ctx, { drawX, y, colWidth, dpr, theme, checked: normalizeBooleanCellValue(value) === true }); + drawBooleanCheckbox(ctx, { drawX, y, colWidth, scaleX, scaleY, theme, checked: normalizeBooleanCellValue(value) === true }); if (item.isDeleted) { - const boxX = alignCanvasPixel(drawX + (colWidth - BOOLEAN_CHECKBOX_SIZE) / 2, dpr); - const strikeY = alignCanvasPixel(y + CANVAS_DATA_GRID_ROW_HEIGHT / 2, dpr); + const boxX = alignCanvasPixel(drawX + (colWidth - BOOLEAN_CHECKBOX_SIZE) / 2, scaleX); + const strikeY = alignCanvasPixel(y + CANVAS_DATA_GRID_ROW_HEIGHT / 2, scaleY); ctx.strokeStyle = theme.foreground; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(boxX - 1, strikeY); - ctx.lineTo(alignCanvasPixel(boxX + BOOLEAN_CHECKBOX_SIZE + 1, dpr), strikeY); + ctx.lineTo(alignCanvasPixel(boxX + BOOLEAN_CHECKBOX_SIZE + 1, scaleX), strikeY); ctx.stroke(); } } else { const rawDisplayText = item.isDraft && value === null ? (draftCellPlaceholder ?? "") : formatCell(value, actualColIdx); const displayText = isEditingThisCell ? "" : firstLineCellDisplayValue(rawDisplayText); const text = isEditingThisCell ? displayText : fitCanvasText(ctx, displayText, cellMaxWidth, isBooleanNullCell ? "left" : isRightAlign ? "right" : "left"); - const anchorX = isBooleanNullCell ? alignCanvasPixel(drawX + colWidth / 2, dpr) : textAnchorX; + const anchorX = isBooleanNullCell ? alignCanvasPixel(drawX + colWidth / 2, scaleX) : textAnchorX; ctx.fillText(text, anchorX, textY); if (item.isDeleted && text) { const textWidth = ctx.measureText(text).width; @@ -497,7 +536,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { ctx.strokeStyle = theme.foreground; ctx.beginPath(); ctx.moveTo(lineStartX, textY); - ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, dpr), textY); + ctx.lineTo(alignCanvasPixel(lineStartX + textWidth, scaleX), textY); ctx.stroke(); } } @@ -507,7 +546,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) { ctx.strokeStyle = theme.border; ctx.beginPath(); - const columnBorderX = crispCanvasLine(drawX + colWidth - 1, dpr); + const columnBorderX = crispCanvasLine(drawX + colWidth - 1, scaleX); ctx.moveTo(columnBorderX, y); ctx.lineTo(columnBorderX, y + CANVAS_DATA_GRID_ROW_HEIGHT); ctx.stroke();