From f8b2924f4288bf906d0a0ad09123fa2516d33151 Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 7 Aug 2026 02:40:49 +0800 Subject: [PATCH] fix(diagram): preserve viewport across fullscreen and restore on exit --- .../diagram/SchemaDiagramDialog.vue | 79 +++++++++++++++---- .../app-tests/tauriWindowPermissions.test.ts | 11 +++ src-tauri/capabilities/default.json | 2 + 3 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 packages/app-tests/tauriWindowPermissions.test.ts diff --git a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue index d6e8ba402a..28603de71a 100644 --- a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue +++ b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { VueFlow, useVueFlow, type NodeChange, type EdgeChange, type NodeDragEvent, type EdgeMouseEvent, type NodeMouseEvent, type NodeTypesObject, type EdgeTypesObject } from "@vue-flow/core"; +import { VueFlow, useVueFlow, type NodeChange, type EdgeChange, type NodeDragEvent, type EdgeMouseEvent, type NodeMouseEvent, type NodeTypesObject, type EdgeTypesObject, type ViewportTransform } from "@vue-flow/core"; import { Background } from "@vue-flow/background"; import { MiniMap } from "@vue-flow/minimap"; import "@vue-flow/core/dist/style.css"; @@ -61,7 +61,7 @@ const open = defineModel("open", { default: false }); const store = useConnectionStore(); const graphStore = useGraphStore(); const layerStore = useLayerStore(); -const { nodes, edges, applyNodeChanges, applyEdgeChanges, setNodes, setEdges, zoomIn: vfZoomIn, zoomOut: vfZoomOut, fitView } = useVueFlow(); +const { nodes, edges, applyNodeChanges, applyEdgeChanges, setNodes, setEdges, zoomIn: vfZoomIn, zoomOut: vfZoomOut, fitView, getViewport, setViewport } = useVueFlow(); const diagramPaneRef = ref(null); const edgeWaypoints = ref>({}); @@ -1460,6 +1460,11 @@ const isFullscreen = ref(false); /** Only exit fullscreen on dialog close when this page entered it. */ let diagramOwnedFullscreen = false; let unlistenWindowResize: (() => void) | null = null; +let fullscreenTransitionTarget: boolean | null = null; +let viewportBeforeFullscreen: ViewportTransform | null = null; + +const FULLSCREEN_SYNC_ATTEMPTS = 40; +const FULLSCREEN_SYNC_INTERVAL_MS = 50; const dialogStyle = computed(() => { if (isFullscreen.value) { @@ -1483,22 +1488,61 @@ async function syncFullscreenState() { if (isTauriRuntime()) { try { const { getCurrentWindow } = await import("@tauri-apps/api/window"); - isFullscreen.value = await getCurrentWindow().isFullscreen(); + const actualFullscreen = await getCurrentWindow().isFullscreen(); + isFullscreen.value = fullscreenTransitionTarget ?? actualFullscreen; } catch { - isFullscreen.value = false; + isFullscreen.value = fullscreenTransitionTarget ?? false; } } else { isFullscreen.value = !!document.fullscreenElement; } - if (!isFullscreen.value) { + if (!isFullscreen.value && fullscreenTransitionTarget === null) { diagramOwnedFullscreen = false; } } +async function waitForTauriFullscreenState(appWindow: { isFullscreen: () => Promise }, expected: boolean) { + for (let attempt = 0; attempt < FULLSCREEN_SYNC_ATTEMPTS; attempt += 1) { + if ((await appWindow.isFullscreen()) === expected) return; + await new Promise((resolve) => window.setTimeout(resolve, FULLSCREEN_SYNC_INTERVAL_MS)); + } +} + +function captureDiagramViewport() { + if (diagramMode.value !== "table" || viewportBeforeFullscreen) return; + viewportBeforeFullscreen = { ...getViewport() }; +} + +async function restoreDiagramViewport() { + const viewport = viewportBeforeFullscreen; + viewportBeforeFullscreen = null; + if (!viewport || diagramMode.value !== "table") return; + await setViewport(viewport, { duration: 150 }); +} + +async function syncFullscreenLayout() { + const wasFullscreen = isFullscreen.value; + await syncFullscreenState(); + if (wasFullscreen === isFullscreen.value) return; + await nextTick(); + if (isFullscreen.value) fitDiagramView(); + else await restoreDiagramViewport(); +} + async function enterFullscreen() { + captureDiagramViewport(); if (isTauriRuntime()) { const { getCurrentWindow } = await import("@tauri-apps/api/window"); - await getCurrentWindow().setFullscreen(true); + const appWindow = getCurrentWindow(); + fullscreenTransitionTarget = true; + try { + await appWindow.setFullscreen(true); + isFullscreen.value = true; + await waitForTauriFullscreenState(appWindow, true); + } finally { + fullscreenTransitionTarget = null; + await syncFullscreenState(); + } } else { await document.documentElement.requestFullscreen(); } @@ -1512,15 +1556,24 @@ async function exitFullscreen() { const owned = diagramOwnedFullscreen; if (isTauriRuntime()) { const { getCurrentWindow } = await import("@tauri-apps/api/window"); - const currently = await getCurrentWindow().isFullscreen(); - if (currently) await getCurrentWindow().setFullscreen(false); + const appWindow = getCurrentWindow(); + fullscreenTransitionTarget = false; + try { + const currently = await appWindow.isFullscreen(); + if (currently) await appWindow.setFullscreen(false); + isFullscreen.value = false; + await waitForTauriFullscreenState(appWindow, false); + } finally { + fullscreenTransitionTarget = null; + await syncFullscreenState(); + } } else if (document.fullscreenElement) { await document.exitFullscreen(); } if (owned) diagramOwnedFullscreen = false; await syncFullscreenState(); await nextTick(); - fitDiagramView(); + await restoreDiagramViewport(); } async function toggleFullscreen() { @@ -1529,18 +1582,14 @@ async function toggleFullscreen() { } function handleWebFullscreenChange() { - void syncFullscreenState().then(() => { - void nextTick().then(() => { - fitDiagramView(); - }); - }); + void syncFullscreenLayout(); } async function setupFullscreenListeners() { if (isTauriRuntime()) { const { getCurrentWindow } = await import("@tauri-apps/api/window"); unlistenWindowResize = await getCurrentWindow().onResized(() => { - void syncFullscreenState(); + void syncFullscreenLayout(); }); } else { document.addEventListener("fullscreenchange", handleWebFullscreenChange); diff --git a/packages/app-tests/tauriWindowPermissions.test.ts b/packages/app-tests/tauriWindowPermissions.test.ts new file mode 100644 index 0000000000..9c6e4354da --- /dev/null +++ b/packages/app-tests/tauriWindowPermissions.test.ts @@ -0,0 +1,11 @@ +import { strict as assert } from "node:assert"; +import { readFile } from "node:fs/promises"; +import { test } from "vitest"; + +test("allows the relationship diagram to control window fullscreen state", async () => { + const capabilityUrl = new URL("../../src-tauri/capabilities/default.json", import.meta.url); + const capability = JSON.parse(await readFile(capabilityUrl, "utf8")) as { permissions?: string[] }; + + assert.ok(capability.permissions?.includes("core:window:allow-is-fullscreen")); + assert.ok(capability.permissions?.includes("core:window:allow-set-fullscreen")); +}); diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c3c181cfee..772922e238 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,6 +13,8 @@ "core:window:allow-unmaximize", "core:window:allow-toggle-maximize", "core:window:allow-is-maximized", + "core:window:allow-is-fullscreen", + "core:window:allow-set-fullscreen", "core:window:allow-close", "core:window:allow-start-dragging", "core:webview:allow-set-webview-zoom",