diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 061b62983..b286e4c1f 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, dataGridTruncationHintKey, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal, type DataGridInexactTotalRowCountMode } from "@/lib/dataGrid/dataGridPagination"; import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions"; import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, isDataGridPrefixAppend, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll"; -import { CANVAS_DATA_GRID_ROW_HEIGHT, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer"; +import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, 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"; @@ -4775,7 +4775,7 @@ 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 canvasBackingPixelRatio = computed(() => Math.min(MAX_CANVAS_DATA_GRID_PIXEL_RATIO, 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)); // Clamp the sticky canvas/overlay to the content width. A viewport-wide sticky surface inflates the diff --git a/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts index 27da09b48..d006b99f8 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/canvasDataGridRendererFrozen.spec.ts @@ -97,6 +97,17 @@ describe("drawCanvasDataGrid with frozen columns", () => { }); }); + it.each([1, 1.125, 1.1875, 1.2345])("keeps the exact observed density when the nominal ratio is %s", (pixelRatio) => { + expect( + resolveCanvasBackingStoreMetrics({ + width: 800, + height: 400, + pixelRatio, + devicePixelSize: { cssWidth: 800, cssHeight: 400, pixelWidth: 1000, pixelHeight: 500 }, + }), + ).toEqual({ pixelWidth: 1000, pixelHeight: 500, scaleX: 1.25, scaleY: 1.25, measured: true }); + }); + it("ignores a stale observed size after the CSS viewport changes", () => { expect( resolveCanvasBackingStoreMetrics({ @@ -108,17 +119,21 @@ describe("drawCanvasDataGrid with frozen columns", () => { ).toEqual({ pixelWidth: 1125, pixelHeight: 500, scaleX: 1.25, scaleY: 1.25, measured: false }); }); - it("caps the observed backing store at the configured pixel ratio", () => { + it("caps the observed backing store only at the absolute safety limit", () => { expect( resolveCanvasBackingStoreMetrics({ width: 801, height: 399, - pixelRatio: 4, + pixelRatio: 1.125, devicePixelSize: { cssWidth: 801, cssHeight: 399, pixelWidth: 4005, pixelHeight: 1995 }, }), ).toEqual({ pixelWidth: 3204, pixelHeight: 1596, scaleX: 4, scaleY: 4, measured: true }); }); + it("caps a fallback ratio at the absolute safety limit", () => { + expect(resolveCanvasBackingStoreMetrics({ width: 801, height: 399, pixelRatio: 5 })).toEqual({ pixelWidth: 3204, pixelHeight: 1596, scaleX: 4, scaleY: 4, measured: false }); + }); + 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 1fc551c78..4b9dd7714 100644 --- a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts +++ b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts @@ -3,6 +3,7 @@ import type { RowStatus } from "@/lib/dataGrid/gridRowStatus"; import { DATA_GRID_DARK_SEARCH_COLORS, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme"; export const CANVAS_DATA_GRID_ROW_HEIGHT = 26; +export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4; export interface CanvasDevicePixelSize { cssWidth: number; @@ -111,13 +112,15 @@ export interface CanvasBackingStoreMetrics { 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 fallbackRatio = Math.min(MAX_CANVAS_DATA_GRID_PIXEL_RATIO, 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; + const maxPixelWidth = Math.max(1, Math.ceil(width * MAX_CANVAS_DATA_GRID_PIXEL_RATIO)); + const maxPixelHeight = Math.max(1, Math.ceil(height * MAX_CANVAS_DATA_GRID_PIXEL_RATIO)); + const pixelWidth = measurementMatches ? Math.min(measured.pixelWidth, maxPixelWidth) : fallbackPixelWidth; + const pixelHeight = measurementMatches ? Math.min(measured.pixelHeight, maxPixelHeight) : fallbackPixelHeight; return { pixelWidth, pixelHeight,