/** * Tauri adapter for {@link WindowGateway} (ticket #23). Detaches a View into its * own OS window by delegating to the backend's window-lifecycle commands. Like * the sibling adapters, this is the only layer allowed to touch * `@tauri-apps/api`; components reach it exclusively through the port. * * Command/event names and payloads below mirror the backend contract owned by * DevBackend (build-verified). The backend exposes a single lifecycle channel * `view-window://lifecycle`; this adapter narrows it to the port's close-only * `onViewWindowClosed` by filtering on `kind === "closed"`. */ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import type { Unsubscribe } from "@/domain"; import type { PluginLayoutWindowOpenInput, PluginLayoutWindowOpenResult, ViewWindowClosed, ViewWindowSnapshot, WindowGateway, } from "@/ports"; /** The single Tauri event carrying detached-window lifecycle transitions. */ const VIEW_WINDOW_LIFECYCLE = "view-window://lifecycle"; // The backend keeps the historical `context` window identity. Project context // now lives inside `projectSettings`, so the adapter owns the compatibility // alias while the UI consistently uses the current panel id. function toBackendPanel(panel: string): string { return panel === "projectSettings" ? "context" : panel; } function fromBackendPanel(panel: string): string { return panel === "context" ? "projectSettings" : panel; } /** * Backend lifecycle payload (discriminated on `kind`). Ticket #47 dropped the * `projectId` field — a detached window is panel-only and follows the focused * project, so lifecycle events carry only `panel` + `label`. */ interface ViewWindowLifecycle { kind: "opened" | "focused" | "closed"; panel: string; label: string; } export class TauriWindowGateway implements WindowGateway { async openViewWindow(panel: string): Promise { // Panel-only window (#47): no project id — the window follows the focused // project published on `focused-project://changed`. await invoke("open_view_window", { panel: toBackendPanel(panel) }); } async openPluginLayoutWindow( input: PluginLayoutWindowOpenInput, ): Promise { return invoke("open_plugin_layout_window", { input, }); } async closeViewWindow(panel: string): Promise { await invoke("close_view_window", { panel: toBackendPanel(panel) }); } async listOpenViewWindows(): Promise { // Backend serialises the snapshots in camelCase (panel/label/visible), so the // wire shape matches the port type directly. const snapshots = await invoke("list_open_view_windows"); return snapshots.map((snapshot) => ({ ...snapshot, panel: fromBackendPanel(snapshot.panel), })); } async onViewWindowClosed( handler: (event: ViewWindowClosed) => void, ): Promise { // One backend channel for opened/focused/closed; the port only surfaces the // close, so drop the other transitions and forward `{ panel }`. const unlisten = await listen( VIEW_WINDOW_LIFECYCLE, (e) => { if (e.payload.kind !== "closed") return; handler({ panel: fromBackendPanel(e.payload.panel) }); }, ); return unlisten; } }