Capture a native OS window into a Flutter Texture and control its
geometry, raise, and engagement lifecycle — the mechanism behind an
infinite-canvas "window stream" tile, extracted as a federated plugin so a
second platform (Windows) can be added without touching consumers.
Status. Both backends implement the full v1 contract. macOS is in production use (extracted from the Campus
work_canvasRunner): ScreenCaptureKit capture, geometry/raise/engage, and the engagement-event push. Windows implements the same surface over Windows.Graphics.Capture (frames reach the Flutter texture as DXGI shared handles, so they never leave the GPU), Win32 window control, andSetWinEventHookengagement events — see its README for the platform-specific conventions. A runnableexample/app exercises the API on both (list windows → capture → engage).
The capture + window-control mechanism is genuinely OS-specific and self-contained — it depends only on OS frameworks, not on any host app's canvas/tile model. Extracting it behind a stable Dart contract means a new platform is "implement this interface in an endorsed package," and the consumer's tile code doesn't change a line.
Unlike a CEF-style plugin, there is no bundled runtime binary: every backend compiles from source against OS frameworks (macOS: ScreenCaptureKit
- Accessibility + CoreGraphics; Windows: Windows.Graphics.Capture + Direct3D
- Win32). So there is nothing to fetch, content-hash-match, or drift —
the failure mode that a fetched prebuilt (like
cef_host) introduces simply does not exist here.
flutter_window_portal/ app-facing API — WindowPortal
packages/
flutter_window_portal_platform_interface/ the contract + method-channel default
flutter_window_portal_macos/ endorsed macOS impl (ScreenCaptureKit + AX)
flutter_window_portal_windows/ endorsed Windows impl (WGC + Win32)
The platform interface (WindowPortalPlatform) is the complete spec any
backend satisfies. v1 scope is the capture + window-control/engage core:
- Capture —
listWindows·captureSingleFrame·startStream→ textureId ·stopStream·reconfigureStream·streamStopped - Geometry / engage —
engageWindow(tileRect, viewId)·conformWindowToSize·releaseEngagement·isEngaged·getSourceFrame·getContentMeasurement·isWindowMinimized·isWindowHidden·detectSeparateWindows· accessibility-permission check/request - Events —
engagementEventsstream:engaged/disengaged(reason)/alignmentBroken(frame)pushed from the backend, so the consumer's engagement state machine can end honestly (external move/close) and a watchdog can reconcile a missed notification.
Wire channels: flutter_window_portal/control and
flutter_window_portal/capture.
Out of scope for v1 (host-side for now; separable, own per-platform stories):
synthetic HID input (SendInput / CGEvent) and the
accessibility-tree automation contract (cua-driver).
Mirror the sibling-checkout convention: add an uncommitted
dependency_overrides in the consumer's pubspec pointing at this checkout,
so local edits are picked up without a pin bump (never commit it — CI has no
sibling):
dependency_overrides:
flutter_window_portal:
path: ../flutter_window_portalBecause the app-facing package pulls its federation members as path deps
within this repo, a consumer only overrides the one flutter_window_portal
entry — there is no second pin to keep in sync.