import * as Effect from "effect/Effect"; import type * as Fiber from "effect/Fiber"; import * as Electron from "electron"; const MIN_DURATION_MS = 280; const MAX_DURATION_MS = 680; const DURATION_DISTANCE_SCALE = 2_000; const EASING = "cubic-bezier(.2,.8,.2,1)"; const TIMEOUT_MS = 6_000; type SnapShotAnimationDetails = { readonly appName: string; readonly windowTitle: string; readonly appIconDataUrl?: string | undefined; }; export type SnapShotAnimationDestination = { readonly frame: Electron.Rectangle; /** Unit coordinates in T3's content area; GNOME supplies the real compositor origin. */ readonly relativeFrame?: Electron.Rectangle | undefined; readonly backgroundColor: string; readonly borderColor: string; readonly borderWidth: number; readonly cornerRadius: number; readonly scaleFactor: number; readonly details?: SnapShotAnimationDetails | undefined; }; export function snapShotAnimationDurationMs( source: Electron.Rectangle, target: Electron.Rectangle, ): number { const distance = Math.hypot( source.x + source.width / 2 - target.x - target.width / 2, source.y + source.height / 2 - target.y - target.height / 2, ); return ( MAX_DURATION_MS - (MAX_DURATION_MS - MIN_DURATION_MS) * Math.exp(-distance / DURATION_DISTANCE_SCALE) ); } type ActiveTransition = { readonly id: string; readonly source: Electron.Rectangle; readonly snapshotDataUrl: string; readonly overlays: Array<{ readonly window: Electron.BrowserWindow; readonly requestedBounds: Electron.Rectangle; bounds: Electron.Rectangle; }>; details?: SnapShotAnimationDetails | undefined; timer?: Fiber.Fiber | undefined; flight?: Promise | undefined; }; type SnapShotTransitionOptions = { readonly showWindow?: ((window: Electron.BaseWindow) => void) | undefined; readonly boundOverlayToCaptureDisplays?: boolean | undefined; readonly waitForCompositorFrame?: boolean | undefined; readonly alwaysOnTopLevel?: | NonNullable[1]> | undefined; }; export function snapShotAnimationOverlayBounds( displays: ReadonlyArray>, ): Electron.Rectangle { const firstDisplay = displays[0]; const first = firstDisplay?.bounds ?? { x: 0, y: 0, width: 1, height: 1 }; let left = first.x; let top = first.y; let right = first.x + first.width; let bottom = first.y + first.height; for (const display of displays.slice(1)) { const bounds = display.bounds; left = Math.min(left, bounds.x); top = Math.min(top, bounds.y); right = Math.max(right, bounds.x + bounds.width); bottom = Math.max(bottom, bounds.y + bounds.height); } return { x: left, y: top, width: right - left, height: bottom - top }; } export function snapShotAnimationDisplayBounds( displays: ReadonlyArray>, source: Electron.Rectangle, destination: Electron.Rectangle, ): Array { const flight = snapShotAnimationOverlayBounds([{ bounds: source }, { bounds: destination }]); return displays .map((display) => display.bounds) .filter( (bounds) => bounds.x < flight.x + flight.width && bounds.x + bounds.width > flight.x && bounds.y < flight.y + flight.height && bounds.y + bounds.height > flight.y, ); } function createWindow( bounds: Electron.Rectangle, alwaysOnTopLevel: SnapShotTransitionOptions["alwaysOnTopLevel"], ): Electron.BrowserWindow { const window = new Electron.BrowserWindow({ ...bounds, alwaysOnTop: true, backgroundColor: "#00000000", focusable: false, frame: false, hasShadow: false, resizable: false, show: false, skipTaskbar: true, title: "T3 Code Snapshot Animation", transparent: true, webPreferences: { backgroundThrottling: false, contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); if (alwaysOnTopLevel) window.setAlwaysOnTop(true, alwaysOnTopLevel); window.setIgnoreMouseEvents(true); return window; } function transitionHtml( sourceBounds: Electron.Rectangle, overlayBounds: Electron.Rectangle, flash: boolean, ): string { const source = { x: sourceBounds.x - overlayBounds.x, y: sourceBounds.y - overlayBounds.y, width: sourceBounds.width, height: sourceBounds.height, }; return `
`; } export class SnapShotTransition { private active: ActiveTransition | undefined; private readonly boundOverlayToCaptureDisplays: boolean; private readonly waitForCompositorFrame: boolean; private readonly alwaysOnTopLevel: SnapShotTransitionOptions["alwaysOnTopLevel"]; private readonly showWindow: (window: Electron.BaseWindow) => void; constructor(options: SnapShotTransitionOptions = {}) { this.boundOverlayToCaptureDisplays = options.boundOverlayToCaptureDisplays ?? false; this.waitForCompositorFrame = options.waitForCompositorFrame ?? false; this.alwaysOnTopLevel = options.alwaysOnTopLevel; this.showWindow = options.showWindow ?? ((window) => window.showInactive()); } async begin( id: string, source: Electron.Rectangle, snapshotDataUrl: string, flash: boolean, destinationWindowBounds?: Electron.Rectangle, ): Promise { this.dispose(); const sourceDisplay = Electron.screen.getDisplayMatching(source); const requestedBounds = this.boundOverlayToCaptureDisplays ? snapShotAnimationDisplayBounds( [ sourceDisplay, ...Electron.screen.getAllDisplays().filter((d) => d.id !== sourceDisplay.id), ], source, destinationWindowBounds ?? source, ) : [snapShotAnimationOverlayBounds(Electron.screen.getAllDisplays())]; const active: ActiveTransition = { id, source, snapshotDataUrl, overlays: [], }; active.timer = Effect.runFork( Effect.sleep(TIMEOUT_MS).pipe( Effect.andThen( Effect.sync(() => { if (this.active === active) this.dispose(); }), ), ), ); this.active = active; try { await this.showOverlays(active, requestedBounds, flash); } catch (error) { if (this.active === active) this.dispose(); throw error; } } private async showOverlays( active: ActiveTransition, requestedBounds: ReadonlyArray, flash: boolean, ): Promise { const overlays: ActiveTransition["overlays"] = []; try { for (const bounds of requestedBounds) { const window = createWindow(bounds, this.alwaysOnTopLevel); const overlay = { window, requestedBounds: bounds, bounds: window.getBounds() }; active.overlays.push(overlay); overlays.push(overlay); } await Promise.all( overlays.map(async (overlay) => { await overlay.window.loadURL( "data:text/html;charset=utf-8," + encodeURIComponent(transitionHtml(active.source, overlay.bounds, flash)), ); if (this.active === active && !overlay.window.isDestroyed()) { await overlay.window.webContents.executeJavaScript( `window.setCaptureSnapshot(${JSON.stringify(active.snapshotDataUrl)})`, ); } }), ); for (const overlay of overlays) { if (this.active !== active) return; if (overlay.window.isDestroyed()) continue; this.showWindow(overlay.window); const bounds = overlay.window.getBounds(); if ( bounds.x !== overlay.bounds.x || bounds.y !== overlay.bounds.y || bounds.width !== overlay.bounds.width || bounds.height !== overlay.bounds.height ) { overlay.bounds = bounds; await overlay.window.webContents.executeJavaScript( `window.rebaseCaptureSource(${JSON.stringify({ x: active.source.x - bounds.x, y: active.source.y - bounds.y, width: active.source.width, height: active.source.height, })})`, ); } } if (this.waitForCompositorFrame) { // Cover the source with a composited snapshot before the caller reveals T3. // Decoding and renderer animation frames alone can leave a transparent gap. await Promise.all( overlays.map(async (overlay) => { if (this.active !== active || overlay.window.isDestroyed()) return; await overlay.window.webContents.capturePage({ x: 0, y: 0, width: 1, height: 1 }); }), ); } if (this.active !== active) return; if (flash) { await Promise.all( overlays.map(async (overlay) => { if (overlay.window.isDestroyed()) return; await overlay.window.webContents .executeJavaScript("window.startCaptureFlash()") .catch(() => undefined); }), ); } } catch (error) { for (const overlay of overlays) { if (!overlay.window.isDestroyed()) overlay.window.destroy(); } throw error; } } animateTo(id: string, destination: SnapShotAnimationDestination): void { const active = this.active; if (!active || active.id !== id) return; if (destination.details) active.details = destination.details; if (active.flight) { if (destination.details) { for (const overlay of active.overlays) { if (overlay.window.isDestroyed()) continue; void overlay.window.webContents .executeJavaScript( "window.updateCaptureDetails(" + JSON.stringify(destination.details) + ")", ) .catch(() => undefined); } } return; } active.flight = this.runFlight(active, destination); } private async runFlight( active: ActiveTransition, destination: SnapShotAnimationDestination, ): Promise { if (this.active !== active) return; if (this.boundOverlayToCaptureDisplays) { const missingBounds = snapShotAnimationDisplayBounds( Electron.screen.getAllDisplays(), active.source, destination.frame, ).filter( (bounds) => !active.overlays.some( (overlay) => !overlay.window.isDestroyed() && overlay.requestedBounds.x === bounds.x && overlay.requestedBounds.y === bounds.y && overlay.requestedBounds.width === bounds.width && overlay.requestedBounds.height === bounds.height, ), ); if (missingBounds.length > 0) { await this.showOverlays(active, missingBounds, false).catch(() => undefined); } } if (this.active !== active) return; const durationMs = snapShotAnimationDurationMs(active.source, destination.frame); const prepared = await Promise.allSettled( active.overlays.map(async (overlay) => { if (overlay.window.isDestroyed()) return; await overlay.window.webContents.executeJavaScript( `window.prepareCaptureTransition(${JSON.stringify({ ...destination, frame: { x: destination.frame.x - overlay.bounds.x, y: destination.frame.y - overlay.bounds.y, width: destination.frame.width, height: destination.frame.height, }, borderWidth: Math.max(0, destination.borderWidth), cornerRadius: Math.max(0, destination.cornerRadius), scaleFactor: Math.max(0.1, destination.scaleFactor), details: active.details, durationMs, })})`, ); if (this.active !== active || overlay.window.isDestroyed()) return; if (this.waitForCompositorFrame) { // Read back one pixel after preparation to flush queued compositor work // before the animation clock starts on any of the display surfaces. await overlay.window.webContents.capturePage({ x: 0, y: 0, width: 1, height: 1 }); } return overlay; }), ); if (this.active !== active) return; // One display failing must not cut the flight short on the others. await Promise.allSettled( prepared.map((result) => { if (result.status !== "fulfilled" || !result.value || result.value.window.isDestroyed()) { return; } return result.value.window.webContents.executeJavaScript("window.playCaptureTransition()"); }), ); } async waitForLanding(id: string): Promise { const active = this.active; if (!active || active.id !== id) return; await active.flight?.catch(() => undefined); } async complete(id: string): Promise { const active = this.active; if (!active || active.id !== id) return; await this.waitForLanding(id); if (this.active === active) this.dispose(); } dismiss(id: string): void { if (this.active?.id === id) this.dispose(); } dispose(): void { const active = this.active; this.active = undefined; if (!active) return; active.timer?.interruptUnsafe(); for (const overlay of active.overlays) { if (!overlay.window.isDestroyed()) overlay.window.destroy(); } } }