/** * `ViewHandle` — the per-view orchestrator (view-handle.md「placeIn 与挂载」/「dispose(viewScope LIFO 序)」/「moveTo 跨窗迁移」). One handle * owns ONE native view and threads it through a window's z-planner. It composes * three INJECTED primitives and nothing else (no deck-app, no Electron): * - a {@link NativeView} (its `ref` + a `setBounds` sink) — the native surface; * - a {@link Scope} (`deps.scope`) — the view's home/native-view lifetime; * - a {@link PlaceTarget} = `{ compositor, windowScope }` — a window's * z-planner + lifetime, handed to {@link ViewHandle.placeIn}. * * Lifetime. `placeIn` adopts a `viewScope` that is a CHILD of the target * window's `windowScope`, so closing the windowScope cascades into the handle * (scope.ts cross-layer LIFO): the native view is detached and the placement * sink goes inert. * * handle 直接驱动 bounds. The handle drives `setBounds` DIRECTLY on its native view; the * Compositor stays a pure z-order planner (mount/unmount/commit only) and never * sees geometry. * * per-window teardown 顺序 (view-handle.md「dispose(viewScope LIFO 序)」). The viewScope owns the native detach * FIRST and the sink-disable LAST, so LIFO teardown runs the sink-disable * (STEP0) BEFORE the detach (STEP1): a late `place` frame can never drive a * native effect on a half-torn-down view. * * Cross-window move (view-handle.md「moveTo 跨窗迁移」/ compositor-and-teardown.md「moveTo 事务状态机」). {@link ViewHandle.moveTo} * migrates the view to another `{ compositor, windowScope }` as TWO independent * Compositor commits, guarded by a per-view async mutex (THE migrationLock — * each handle is one view). The current placement is a MUTABLE token so the * detach `own()` and `applyPlacement` always follow the CURRENT window after a * move. `rehome:true` re-parents the viewScope via {@link Scope.adopt} so * lifetime follows display; without it, lifetime stays under the src window. */ import type { Scope } from './scope.js'; import type { Compositor, NativeViewRef } from './compositor.js'; /** A screen-space rectangle, in CSS px. Mirrors `@dimina-kit/view-anchor`'s * `Bounds` (electron-deck does not depend on view-anchor in this increment). */ export interface Bounds { x: number; y: number; width: number; height: number; } /** * Explicit visibility + geometry for a native view. Structurally identical to * the `@dimina-kit/view-anchor` `Placement` export; mirrored locally so this * increment adds no new package dependency. */ export type Placement = { visible: true; bounds: Bounds; } | { visible: false; }; /** The native surface a handle drives: its z-order identity (`ref`) plus the * `setBounds` sink the handle calls directly (handle 直接驱动 bounds). `destroy` (optional) * destroys the backing native view (its WebContents) — owned by the viewScope so * it runs on teardown AFTER the detach (keepAlive「保活寿命归 Scope、淘汰策略归 host」lifetime/leak fix). * Optional so fakes that don't model a native WebContents stay valid. * * `webContents` / `capturePage` (optional) expose the backing native view's * WebContents and a screenshot pass-through, so a handle accessor can recover * them without re-deriving the WebContentsView from the window's content view. * Optional so geometry-only fakes stay valid. */ export interface NativeView { readonly ref: NativeViewRef; setBounds(b: Bounds): void; destroy?(): void; readonly webContents?: unknown; capturePage?(): Promise; } /** A window's z-planner + lifetime, handed to {@link ViewHandle.placeIn}. The * handle's per-placement teardown scope is a CHILD of `windowScope`. */ export interface PlaceTarget { compositor: Compositor; windowScope: Scope; } export interface ViewHandle { /** Mount the native view into the target window (mount + commit) and adopt a * per-placement viewScope under the target's `windowScope`. Chainable. */ placeIn(target: PlaceTarget, opts: { zone?: number; }): ViewHandle; /** The placement sink. Drops frames once disposed (idempotent late IPC). * `visible:true` ensures mounted + drives `setBounds` directly; `visible:false` * detaches (unmount + commit) but keeps the native view alive. */ applyPlacement(p: Placement): void; /** * Cross-window move (view-handle.md「moveTo 跨窗迁移」/ compositor-and-teardown.md「moveTo 事务状态机」). Migrate the view from its * current placement (`src`) to `dest` as TWO independent Compositor commits, * serialized by a per-view async mutex (migrationLock): * * AT_SRC → DETACHED → AT_DEST (happy path) * └→ (src.commit throws) → AT_SRC (rethrow, no side effect) * DETACHED → (dest.commit throws) → ROLLBACK → AT_SRC (rethrow dest error) * └→ CLOSED (src re-mount ALSO throws) * * On success the compositor token moves to `dest` (later `applyPlacement` * drives the dest host). With `rehome:true`, the viewScope is re-parented under * dest's `windowScope` (lifetime follows display). moveTo 迁移显示而非寿命: moveTo moves DISPLAY * (and, with rehome, LIFETIME) — it does NOT carry capability grants; the dest * window's own control layer issues its own grant. Terminal (Promise, not * chainable). */ moveTo(dest: PlaceTarget, opts: { zone?: number; rehome?: boolean; }): Promise; /** Tear down this placement: run the viewScope's A4 owns (sink-disable then * native detach, via the LIFO completion fence). Idempotent. */ dispose(): Promise; /** The backing native view's WebContents (pass-through from {@link NativeView}). * Available immediately — the handle owns its view before any placeIn. */ readonly webContents: unknown; /** The view's LIVE screen-space rect when it is currently placed AND visible; * `null` before the first placement, after `applyPlacement({visible:false})`, * and after `dispose()`. Tracks the last applied `visible:true` bounds. */ bounds(): Bounds | null; /** Screenshot pass-through to the native view's `capturePage()`. */ capturePage(): Promise; } export interface ViewHandleDeps { nativeView: NativeView; scope: Scope; /** Optional bookkeeping hook, fired whenever the viewScope tears down (window- * close cascade OR explicit dispose). The deck-app uses it to drop the view * from its keepAlive group — a window-close cascades the viewScope directly * (NOT via the host wrapper's dispose), so group cleanup must hang off the * scope to fire on that path too (KA-2). Any order — it is pure bookkeeping. */ onDispose?(): void; } export declare function createViewHandle(deps: ViewHandleDeps): ViewHandle; //# sourceMappingURL=view-handle.d.ts.map