feat(desktop): always-on-top for main and detached windows

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-09-22 16:30:19 +08:00
1 parent 933ed6a670
commit 78d8076c53
18 files changed
+262 -11

No files matched your search

@@ -3,12 +3,21 @@ import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it, vi } from "vitest";
import { acquireUpdateBarrier, assertUpdateSafe } from "../updatePreparation";
import { openDetachedTabWindow } from "../detachedTabWindow";
const mocks = vi.hoisted(() => ({ getByLabel: vi.fn(), created: vi.fn(), callbacks: new Map<string, (event?: unknown) => unknown>(), show: vi.fn(), focus: vi.fn(), destroy: vi.fn() }));
const mocks = vi.hoisted(() => ({
getByLabel: vi.fn(),
created: vi.fn(),
callbacks: new Map<string, (event?: unknown) => unknown>(),
show: vi.fn(),
focus: vi.fn(),
destroy: vi.fn(),
setAlwaysOnTop: vi.fn(),
}));
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
vi.mock("@/lib/app/windowContext", () => ({ detachedWindowLabel: (id: string) => `detached-${id}`, detachedWindowUrl: () => "http://localhost/" }));
vi.mock("@tauri-apps/api/webviewWindow", () => ({
WebviewWindow: class {
static getByLabel = mocks.getByLabel;
label = "detached-test";
constructor() {
mocks.created();
}
@@ -19,6 +28,7 @@ vi.mock("@tauri-apps/api/webviewWindow", () => ({
show = mocks.show;
setFocus = mocks.focus;
destroy = mocks.destroy;
setAlwaysOnTop = mocks.setAlwaysOnTop;
},
}));
afterEach(() => {
@@ -45,7 +55,7 @@ describe("detached window update exclusion", () => {
const operation = openDetachedTabWindow("one", "SQL");
await vi.waitFor(() => expect(mocks.getByLabel).toHaveBeenCalled());
expect(assertUpdateSafe).toThrow("window operation");
resolve({ show: async () => {}, setFocus: async () => {} });
resolve({ show: async () => {}, setFocus: async () => {}, setAlwaysOnTop: async () => {} });
expect(await operation).toEqual({ opened: true });
expect(assertUpdateSafe).not.toThrow();
});
@@ -0,0 +1,86 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { emitWindowAlwaysOnTopChanged, pinDetachedWindowIfMainPinned, raiseDetachedWindowsAboveMain, WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT } from "../windowAlwaysOnTop";
const mocks = vi.hoisted(() => ({
getByLabel: vi.fn(),
getAllWebviewWindows: vi.fn(),
isTauriRuntime: vi.fn(() => true),
emit: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => mocks.isTauriRuntime() }));
vi.mock("@tauri-apps/api/webviewWindow", () => ({
WebviewWindow: { getByLabel: mocks.getByLabel },
getAllWebviewWindows: mocks.getAllWebviewWindows,
}));
vi.mock("@tauri-apps/api/event", () => ({
emit: (...args: unknown[]) => mocks.emit(...args),
}));
afterEach(() => {
vi.clearAllMocks();
mocks.isTauriRuntime.mockReturnValue(true);
mocks.emit.mockResolvedValue(undefined);
});
describe("windowAlwaysOnTop", () => {
it("pins a detached window when the main window is already on top", async () => {
const setAlwaysOnTop = vi.fn().mockResolvedValue(undefined);
mocks.getByLabel.mockResolvedValue({ isAlwaysOnTop: async () => true });
await pinDetachedWindowIfMainPinned({ label: "detached-tab-one", setAlwaysOnTop });
expect(setAlwaysOnTop).toHaveBeenCalledWith(true);
expect(mocks.emit).toHaveBeenCalledWith(WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT, {
windowLabel: "detached-tab-one",
alwaysOnTop: true,
});
});
it("does not pin a detached window when the main window is not on top", async () => {
const setAlwaysOnTop = vi.fn().mockResolvedValue(undefined);
mocks.getByLabel.mockResolvedValue({ isAlwaysOnTop: async () => false });
await pinDetachedWindowIfMainPinned({ label: "detached-tab-one", setAlwaysOnTop });
expect(setAlwaysOnTop).not.toHaveBeenCalled();
expect(mocks.emit).not.toHaveBeenCalled();
});
it("does not emit when the pinned detached window has no label", async () => {
const setAlwaysOnTop = vi.fn().mockResolvedValue(undefined);
mocks.getByLabel.mockResolvedValue({ isAlwaysOnTop: async () => true });
await pinDetachedWindowIfMainPinned({ setAlwaysOnTop });
expect(setAlwaysOnTop).toHaveBeenCalledWith(true);
expect(mocks.emit).not.toHaveBeenCalled();
});
it("raises only detached-tab windows when pinning the main host", async () => {
const detached = {
label: "detached-tab-one",
setAlwaysOnTop: vi.fn().mockResolvedValue(undefined),
show: vi.fn().mockResolvedValue(undefined),
setFocus: vi.fn().mockResolvedValue(undefined),
};
const other = {
label: "main",
setAlwaysOnTop: vi.fn(),
show: vi.fn(),
setFocus: vi.fn(),
};
mocks.getAllWebviewWindows.mockResolvedValue([other, detached]);
await raiseDetachedWindowsAboveMain();
expect(detached.setAlwaysOnTop).toHaveBeenCalledWith(true);
expect(detached.show).toHaveBeenCalledOnce();
expect(detached.setFocus).toHaveBeenCalledOnce();
expect(other.setAlwaysOnTop).not.toHaveBeenCalled();
expect(mocks.emit).toHaveBeenCalledWith(WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT, {
windowLabel: "detached-tab-one",
alwaysOnTop: true,
});
});
it("emits always-on-top changes for a window label", async () => {
await emitWindowAlwaysOnTopChanged("detached-tab-two", true);
expect(mocks.emit).toHaveBeenCalledWith(WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT, {
windowLabel: "detached-tab-two",
alwaysOnTop: true,
});
});
});
@@ -1,5 +1,6 @@
import { assertUpdateAllowsInteraction, beginUpdateSensitiveOperation } from "@/lib/app/updatePreparation";
import { detachedWindowLabel, detachedWindowUrl } from "@/lib/app/windowContext";
import { pinDetachedWindowIfMainPinned } from "@/lib/app/windowAlwaysOnTop";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
export interface DetachedWindowOpenPosition {
@@ -27,6 +28,7 @@ export async function openDetachedTabWindow(tabId: string, title: string, positi
const label = detachedWindowLabel(tabId);
const existing = await WebviewWindow.getByLabel(label);
if (existing) {
await pinDetachedWindowIfMainPinned(existing);
await existing.show();
await existing.setFocus();
return { opened: true };
@@ -77,6 +79,7 @@ export async function openDetachedTabWindow(tabId: string, title: string, positi
const { PhysicalPosition } = await import("@tauri-apps/api/dpi");
await child.setPosition(new PhysicalPosition(Math.round(position.x - 120), Math.round(position.y - 20)));
}
await pinDetachedWindowIfMainPinned(child);
await child.show();
await child.setFocus();
finish({ opened: true });
@@ -0,0 +1,62 @@
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
const DETACHED_WINDOW_LABEL_PREFIX = "detached-tab-";
export const WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT = "dbx:window-always-on-top-changed";
export type WindowAlwaysOnTopChangedPayload = {
windowLabel: string;
alwaysOnTop: boolean;
};
export async function emitWindowAlwaysOnTopChanged(windowLabel: string, alwaysOnTop: boolean): Promise<void> {
if (!isTauriRuntime() || !windowLabel) return;
try {
const { emit } = await import("@tauri-apps/api/event");
const payload: WindowAlwaysOnTopChangedPayload = { windowLabel, alwaysOnTop };
await emit(WINDOW_ALWAYS_ON_TOP_CHANGED_EVENT, payload);
} catch (error) {
console.error("[DBX][window:always-on-top-event]", windowLabel, error);
}
}
/** Keep detached windows in the same always-on-top band and above the main host. */
export async function raiseDetachedWindowsAboveMain(): Promise<void> {
if (!isTauriRuntime()) return;
const { getAllWebviewWindows } = await import("@tauri-apps/api/webviewWindow");
const windows = await getAllWebviewWindows();
for (const win of windows) {
if (!win.label.startsWith(DETACHED_WINDOW_LABEL_PREFIX)) continue;
try {
await win.setAlwaysOnTop(true);
await emitWindowAlwaysOnTopChanged(win.label, true);
await win.show();
await win.setFocus();
} catch (error) {
console.error("[DBX][window:raise-detached]", win.label, error);
}
}
}
type DetachedAlwaysOnTopTarget = {
label?: string;
setAlwaysOnTop: (value: boolean) => Promise<void>;
};
/** If the main window is pinned, pin this detached window so it stays visible above the host. */
export async function pinDetachedWindowIfMainPinned(detached: DetachedAlwaysOnTopTarget): Promise<void> {
if (!isTauriRuntime()) return;
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
const main = await WebviewWindow.getByLabel("main");
if (!main) return;
try {
if (await main.isAlwaysOnTop()) {
await detached.setAlwaysOnTop(true);
if (detached.label) {
await emitWindowAlwaysOnTopChanged(detached.label, true);
}
}
} catch (error) {
console.error("[DBX][window:pin-detached-from-main]", error);
}
}