mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 07:08:16 +00:00
fix(grid): sharpen canvas rendering under ui zoom
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user