fix(grid): sharpen canvas rendering under ui zoom

This commit is contained in:
t8y2
2026-06-08 12:46:56 +08:00
parent afc8d8f3c1
commit 03f273bb4d
3 changed files with 89 additions and 16 deletions
+1
View File
@@ -252,6 +252,7 @@ async function applyUiScale(scale: number) {
try {
const { getCurrentWebview } = await import("@tauri-apps/api/webview");
await getCurrentWebview().setZoom(scale);
window.dispatchEvent(new CustomEvent("dbx:ui-scale-applied", { detail: { scale } }));
} catch (error) {
console.warn("[DBX] Failed to apply UI scale", { scale, error });
}
+66 -2
View File
@@ -3337,14 +3337,22 @@ const canvasViewportWidth = ref(0);
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 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));
const canvasRenderStyleKey = computed(
() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${isDark.value}`,
() =>
`${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}`,
);
const CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER = 1.5;
const CANVAS_TRACKPAD_DELTA_THRESHOLD = 40;
let canvasResizeObserver: ResizeObserver | null = null;
let canvasPixelRatioMediaQuery: MediaQueryList | null = null;
let canvasPixelRatioMediaQueryCleanup: (() => void) | null = null;
let canvasPixelRatioFrame = 0;
let canvasDrawFrame = 0;
let dataGridIsActive = true;
@@ -3376,6 +3384,39 @@ function syncCanvasViewport() {
drawCanvasGridNow();
}
function currentCanvasDevicePixelRatio(): number {
return typeof window === "undefined" ? 1 : Math.max(1, window.devicePixelRatio || 1);
}
function scheduleCanvasPixelRatioRefresh() {
if (!dataGridIsActive || canvasPixelRatioFrame) return;
canvasPixelRatioFrame = requestAnimationFrame(() => {
canvasPixelRatioFrame = 0;
const next = currentCanvasDevicePixelRatio();
if (Math.abs(next - canvasDevicePixelRatio.value) > 0.001) {
canvasDevicePixelRatio.value = next;
attachCanvasPixelRatioWatcher();
}
syncCanvasViewport();
});
}
function attachCanvasPixelRatioWatcher() {
canvasPixelRatioMediaQueryCleanup?.();
canvasPixelRatioMediaQueryCleanup = null;
canvasPixelRatioMediaQuery = null;
if (!dataGridIsActive || !useCanvasGridRows.value || typeof window === "undefined" || !window.matchMedia) return;
const ratio = currentCanvasDevicePixelRatio();
canvasDevicePixelRatio.value = ratio;
canvasPixelRatioMediaQuery = window.matchMedia(`(resolution: ${ratio}dppx)`);
const onChange = () => scheduleCanvasPixelRatioRefresh();
canvasPixelRatioMediaQuery.addEventListener("change", onChange);
canvasPixelRatioMediaQueryCleanup = () => {
canvasPixelRatioMediaQuery?.removeEventListener("change", onChange);
};
}
function attachCanvasResizeObserver() {
canvasResizeObserver?.disconnect();
canvasResizeObserver = null;
@@ -3383,6 +3424,7 @@ function attachCanvasResizeObserver() {
if (!useCanvasGridRows.value) return;
const scroller = canvasScrollerElement();
if (!scroller) return;
attachCanvasPixelRatioWatcher();
syncCanvasViewport();
if (typeof ResizeObserver === "undefined") return;
canvasResizeObserver = new ResizeObserver(syncCanvasViewport);
@@ -3723,6 +3765,7 @@ function drawCanvasGrid() {
scroller,
width: Math.max(1, canvasViewportWidth.value || scroller.clientWidth),
height: Math.max(1, canvasViewportHeight.value || scroller.clientHeight),
pixelRatio: canvasBackingPixelRatio.value,
isDark: isDark.value,
styleKey: canvasRenderStyleKey.value,
rowCount: displayRowCount.value,
@@ -3760,6 +3803,8 @@ watch(
currentSearchMatch,
isDark,
canvasRenderStyleKey,
canvasDevicePixelRatio,
canvasBackingPixelRatio,
isScrolling,
hoveredDetailCell,
detailCell,
@@ -3774,6 +3819,13 @@ function pauseCanvasGridWork() {
dataGridIsActive = false;
canvasResizeObserver?.disconnect();
canvasResizeObserver = null;
canvasPixelRatioMediaQueryCleanup?.();
canvasPixelRatioMediaQueryCleanup = null;
canvasPixelRatioMediaQuery = null;
if (canvasPixelRatioFrame) {
cancelAnimationFrame(canvasPixelRatioFrame);
canvasPixelRatioFrame = 0;
}
if (canvasDrawFrame) {
cancelAnimationFrame(canvasDrawFrame);
canvasDrawFrame = 0;
@@ -3787,8 +3839,20 @@ function resumeCanvasGridWork() {
onMounted(resumeCanvasGridWork);
onActivated(resumeCanvasGridWork);
onMounted(() => {
if (typeof window === "undefined") return;
window.addEventListener("resize", scheduleCanvasPixelRatioRefresh);
window.visualViewport?.addEventListener("resize", scheduleCanvasPixelRatioRefresh);
window.addEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh);
});
onDeactivated(pauseCanvasGridWork);
onUnmounted(pauseCanvasGridWork);
onUnmounted(() => {
pauseCanvasGridWork();
if (typeof window === "undefined") return;
window.removeEventListener("resize", scheduleCanvasPixelRatioRefresh);
window.visualViewport?.removeEventListener("resize", scheduleCanvasPixelRatioRefresh);
window.removeEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh);
});
function setRowStatusFilter(value: string) {
rowStatusFilter.value = value as RowStatusFilter;
+22 -14
View File
@@ -43,6 +43,7 @@ export interface DrawCanvasDataGridOptions {
scroller: HTMLElement;
width: number;
height: number;
pixelRatio?: number;
isDark: boolean;
styleKey?: string;
rowCount: number;
@@ -156,8 +157,12 @@ function firstVisibleColumn(offsets: number[], contentStart: number): number {
return low;
}
function crispCanvasLine(value: number): number {
return Math.round(value) + 0.5;
function alignCanvasPixel(value: number, dpr: number): number {
return Math.round(value * dpr) / dpr;
}
function crispCanvasLine(value: number, dpr: number): number {
return alignCanvasPixel(value, dpr) + 0.5 / dpr;
}
function resolveCanvasRenderState(canvas: HTMLCanvasElement, isDark: boolean, styleKey?: string): CanvasRenderState {
@@ -236,9 +241,9 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
cellIsSelected,
cellCanHover,
} = options;
const dpr = window.devicePixelRatio || 1;
const pixelWidth = Math.floor(width * dpr);
const pixelHeight = Math.floor(height * dpr);
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));
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
@@ -251,6 +256,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, width, height);
const {
@@ -279,8 +285,9 @@ 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;
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);
for (let rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
const item = rowAt(rowIndex);
@@ -340,7 +347,8 @@ 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), rowNumberTextX, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
const textY = alignCanvasPixel(y + rowTextOffsetY, dpr);
ctx.fillText(String(item.displayIndex + 1), rowNumberTextX, textY);
ctx.font = normalFont;
let x = rowNumberWidth + columnOffset - scrollLeft;
@@ -411,20 +419,20 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
ctx.fillStyle = value === null ? theme.mutedForeground : theme.foreground;
ctx.font = value === null ? italicFont : typeof value === "number" ? tabularFont : normalFont;
setCanvasNumericVariant(ctx, typeof value === "number" ? "tabular-nums" : "normal");
const textLeft = x + 12;
const textLeft = alignCanvasPixel(x + 12, dpr);
const paddedMaxWidth = Math.max(0, x + colWidth - textLeft - 12);
const isEditingThisCell = editingCell?.rowId === item.id && editingCell.col === actualColIdx;
const displayText = isEditingThisCell ? "" : formatCell(value, actualColIdx);
const needsTruncation = ctx.measureText(displayText).width > paddedMaxWidth;
const textMaxWidth = needsTruncation ? Math.max(0, x + colWidth - textLeft) : paddedMaxWidth;
const text = isEditingThisCell ? displayText : fitCanvasText(ctx, displayText, textMaxWidth);
ctx.fillText(text, textLeft, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
ctx.fillText(text, textLeft, textY);
if (item.isDeleted && text) {
const textWidth = Math.min(ctx.measureText(text).width, textMaxWidth);
ctx.strokeStyle = theme.foreground;
ctx.beginPath();
ctx.moveTo(textLeft, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
ctx.lineTo(textLeft + textWidth, y + CANVAS_DATA_GRID_ROW_HEIGHT / 2);
ctx.moveTo(textLeft, textY);
ctx.lineTo(alignCanvasPixel(textLeft + textWidth, dpr), textY);
ctx.stroke();
}
ctx.restore();
@@ -433,7 +441,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
ctx.strokeStyle = theme.border;
ctx.beginPath();
const columnBorderX = crispCanvasLine(x + colWidth - 1);
const columnBorderX = crispCanvasLine(x + colWidth - 1, dpr);
ctx.moveTo(columnBorderX, y);
ctx.lineTo(columnBorderX, y + CANVAS_DATA_GRID_ROW_HEIGHT);
ctx.stroke();
@@ -470,7 +478,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
}
ctx.strokeStyle = theme.border;
ctx.beginPath();
const rowBorderY = crispCanvasLine(y + CANVAS_DATA_GRID_ROW_HEIGHT - 1);
const rowBorderY = crispCanvasLine(y + CANVAS_DATA_GRID_ROW_HEIGHT - 1, dpr);
ctx.moveTo(0, rowBorderY);
ctx.lineTo(width, rowBorderY);
ctx.stroke();