Winpane
Windows overlay SDK for building HUDs, interactive panels, picture-in-picture thumbnails, and system tray indicators. Rust core with bindings for TypeScript, C, Python, Go, Zig, and any language that can speak JSON-RPC over stdin/stdout.
Overview
- Language: Rust (edition 2024, MSRV 1.88)
- Repo: peteretelej/winpane
- Install:
cargo add winpane(Rust),npm install winpane(Node.js) - Status: Pre-1.0, actively developed. API works but expect breaking changes.
Architecture
The workspace is split into four crates that form a strict dependency chain, plus an out-of-workspace Node.js addon:
winpane-core (internal Win32/GPU logic, pub(crate))
^
winpane (public Rust API, thin wrapper over core)
^
winpane-ffi (C ABI via cbindgen, cdylib + staticlib)
winpane-host (JSON-RPC 2.0 CLI binary over stdin/stdout)
winpane-node (Node.js addon via napi-rs, separate from workspace)
winpane-core - All Win32, DirectComposition, Direct2D, and Direct3D11 logic lives here. Modules are pub(crate) except for types (re-exported as the public type surface). Key modules:
engine- Spawns a dedicatedwinpane-enginethread that owns the Win32 message loop, GPU resources, and all surface state. The caller thread communicates viampsc::channel<Command>and wakes the engine withPostMessageW(WM_APP)to a control window.renderer-GpuResourcesholds the shared D3D11 device, DXGI device, D2D1 device, DirectWrite factory, and DirectComposition device.SurfaceRendereris per-surface: DXGI swap chain (flip-sequential, premultiplied alpha), DComp target/visual chain, and a D2D1 device context. Rendering pipeline:BeginDrawon D2D context, iterate scene graph,EndDraw,Presentswap chain,CommitDComp.scene-SceneGraphis anIndexMap<String, Element>preserving insertion order (back-to-front). Elements areText,Rect, orImage. Tracks adirtyflag; the engine only re-renders when dirty.command- Enum of all cross-thread commands: surface lifecycle, scene mutations, property changes (position, size, opacity, backdrop, fade), tray operations, custom draw, anchoring, capture exclusion.window- Win32 window creation (WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW), DPI awareness (Per-Monitor v2), Mica/Acrylic backdrop via DWM, capture exclusion via WDA_EXCLUDEFROMCAPTURE.input- Hit testing for Panel surfaces.HitTestMapis rebuilt from the scene graph when interactive elements change, enabling click/hover/leave events.tray- System tray icon management via Shell_NotifyIconW, including HICON creation from RGBA data.monitor-WindowMonitoruses SetWinEventHook to track external windows (for PiP source close detection and anchor target position updates).persist- Saves/restores surface positions to%APPDATA%/winpane/positions.jsonwith trailing-edge debounce (500ms).display- Monitor enumeration and placement resolution (Position or Monitor anchor with margin).
winpane - The public Rust API. A pure Rust crate that re-exports types from core and wraps EngineHandle behind a clean Context / Hud / Panel / Pip / Tray API. Context::new() spawns the engine thread. Surface structs hold a command sender and the control window handle; every method sends a Command and wakes the engine. poll_event() does a non-blocking try_recv on the event channel.
winpane-ffi - C ABI layer. Generates winpane.dll (cdylib) and winpane.lib (staticlib). Uses cbindgen to produce winpane.h. Wraps every extern “C” function in catch_unwind via an ffi_try! macro (returns 0 on success, -1 on error, -2 on panic). Thread-local error storage via winpane_last_error(). Opaque handle types (WinpaneContext, WinpaneSurface, WinpaneTray). Versioned config structs (WINPANE_CONFIG_VERSION = 2) with size fields for forward compatibility. Canvas API for custom draw (begin_draw/end_draw accumulates DrawOps then flushes).
winpane-host - Standalone CLI binary. Reads JSON-RPC 2.0 requests from stdin, dispatches to winpane::Context, writes responses and event notifications to stdout. Main loop alternates between try_recv on stdin lines and poll_event on the engine, sleeping 5ms when idle. This is the universal entry point for Python, Go, Zig, and any language that can spawn a process.
winpane-node - Node.js native addon built with napi-rs. Lives in bindings/node/, excluded from the Cargo workspace. Wraps the winpane crate directly (not through FFI).
Surface Types
| Type | Window style | Interaction | Scene graph | Use case |
|---|---|---|---|---|
| Hud | Click-through, topmost, no taskbar entry | None (input passes through) | Yes | Stats overlays, notifications, timers |
| Panel | Topmost, hit-testable | Click, hover, drag | Yes | Tool palettes, floating controls, popups |
| Pip | Click-through, topmost | None | No (DWM thumbnail) | Live preview of another window |
| Tray | System tray icon | Left-click toggles popup Panel, right-click context menu | N/A | Background app controls |
Surfaces compose together. A Tray can toggle a Panel. A Panel can anchor to another window. Any surface can fade, exclude itself from screen capture, and use Mica/Acrylic backdrops.
Key Design Decisions
Dedicated engine thread. All Win32 window management and GPU rendering runs on a single winpane-engine thread that owns the message loop. Caller threads send commands via mpsc::channel and wake the engine with PostMessageW. This avoids Win32’s thread-affinity constraints while keeping the public API thread-safe.
Retained-mode scene graph, not immediate-mode. You set_text("label", ...) and set_rect("bg", ...) with string keys. Winpane tracks what changed via a dirty flag and only re-renders when needed. Updates to existing keys preserve insertion order (IndexMap). This is simpler than a full layout engine but more efficient than redrawing everything every frame.
Polled events for FFI safety. Events (clicks, hovers, tray interactions) are queued to a channel and consumed via poll_event() / winpane_poll_event(). No callbacks cross the FFI boundary. This avoids the complexity of function pointer registration in C and callback safety in every target language.
DirectComposition over GDI. Rendering uses D3D11 device -> DXGI swap chain (flip-sequential, premultiplied alpha) -> D2D1 device context -> DirectComposition visual. DComp composites outside the target application’s process, so overlays work without DLL injection. The trade-off is 1-3ms compositing latency, which is fine for dashboards and tools but not competitive game overlays.
Layered multi-language story. Rather than one binding strategy, winpane provides three tiers: direct Rust API (zero overhead), C ABI + napi-rs addon (for C/C++/Node.js with native performance), and JSON-RPC host process (for Python/Go/Zig/anything, trading some latency for universal compatibility). Each tier wraps the one below it.
Versioned FFI config structs. C-side config structs carry version and size fields. The library rejects configs with wrong versions and checks that size >= sizeof(struct), enabling forward-compatible struct extension without breaking existing binaries.
GPU device loss recovery. The engine detects DXGI_ERROR_DEVICE_REMOVED / DEVICE_RESET, recreates all GPU resources (D3D11/D2D/DComp), rebuilds per-surface swap chains and render targets, marks all scenes dirty, and emits a DeviceRecovered event.
Development
# Build all workspace crates
cargo build
# Run an example
cargo run -p winpane --example clock_overlay
# Run tests (scene graph, color conversion, protocol parsing)
cargo test
# Build the C FFI DLL + header
cargo build -p winpane-ffi --release
# Build the JSON-RPC host binary
cargo build -p winpane-host --release
# Build the Node.js addon (from bindings/node/)
cd bindings/node && npm run build
Requires Windows 10 1903+ as a build/run target. The crate compiles on non-Windows (for IDE support and CI) but all Win32 functionality is behind #[cfg(target_os = "windows")] gates.