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 dedicated winpane-engine thread that owns the Win32 message loop, GPU resources, and all surface state. The caller thread communicates via mpsc::channel<Command> and wakes the engine with PostMessageW(WM_APP) to a control window.
  • renderer - GpuResources holds the shared D3D11 device, DXGI device, D2D1 device, DirectWrite factory, and DirectComposition device. SurfaceRenderer is per-surface: DXGI swap chain (flip-sequential, premultiplied alpha), DComp target/visual chain, and a D2D1 device context. Rendering pipeline: BeginDraw on D2D context, iterate scene graph, EndDraw, Present swap chain, Commit DComp.
  • scene - SceneGraph is an IndexMap<String, Element> preserving insertion order (back-to-front). Elements are Text, Rect, or Image. Tracks a dirty flag; 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. HitTestMap is 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 - WindowMonitor uses SetWinEventHook to track external windows (for PiP source close detection and anchor target position updates).
  • persist - Saves/restores surface positions to %APPDATA%/winpane/positions.json with 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

TypeWindow styleInteractionScene graphUse case
HudClick-through, topmost, no taskbar entryNone (input passes through)YesStats overlays, notifications, timers
PanelTopmost, hit-testableClick, hover, dragYesTool palettes, floating controls, popups
PipClick-through, topmostNoneNo (DWM thumbnail)Live preview of another window
TraySystem tray iconLeft-click toggles popup Panel, right-click context menuN/ABackground 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.