Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.
Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.
Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.
Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
5.2 KiB
TASK 38 — Capture Window… backdrop (in-app window picker submenu)
Status: ✅ Done — shipped 2026-09-21 (Good Dog: ONE integration test)
Idea (creator ruling 2026-09-21)
Add an in-app "Capture Window…" submenu (PreviewPane backdrop context menu + the Live layer-row
menu) listing every visible top-level window owned by another process. Picking one pins that window
as the full-bleed Live backdrop at layer 0 — the same render path as desktop/game capture, captured
via the pre-existing window:<hwnd> key in ScreenCaptureSourceFactory.Resolve (no new capture
class, no compositor change).
Rulings locked in the plan:
- In-app submenu, not the OS GraphicsCapturePicker — the picker dialog is a different flow ("Change Capture…"); this is a catalog of open windows.
- Captured window is the Live backdrop at layer 0; the app's own window never goes OS-fullscreen.
- Session-scoped, never persisted — like
picker:keys. "A new session is a new session": a persistedwindow:<hwnd>key must NOT be resurrected on reload; an HWND can be recycled by the OS, so resurrecting it could capture an unrelated window (the exact hazard that makes persistence wrong). - Pin wins while alive: while the pinned window still enumerates, the auto fullscreen-game / desktop detector must not steal the backdrop.
- On window death → auto-fallback through the existing chain (fullscreen game → desktop → static art). No dead ends.
- Manual "Refresh Window List" menu item included (creator picked the manual refresh option).
What shipped
Services/Win32WindowEnumerator.cs— realEnumWindowsimplementation of theIWindowEnumeratorseam. Filters: visible, non-cloaked (DWMWA_CLOAKED), non-empty rect, other-process (never offers the app's own UI), non-empty title, non-empty process name; ordered by process name then title. MirrorsWin32FullScreenDetector's window predicates so picker and capture-detector agree.ViewModels/MainViewModel.WindowBackdrop.cs(new partial) —OpenWindows(ObservableCollection<WindowInfo>),RefreshWindowsCommand,CaptureWindowCommand(mirrorsRelayCommandpattern; routes toRedesignateBackgroundAsync(info.CaptureKey)),IsTransientCaptureKey(window:+picker:),IsAliveWindowPin, andOnScreenCaptureFailed(the transient-key-session-failure → auto-reacquire heal).MainViewModel.Background.cs—_windowEnumeratorfield +WindowEnumeratorOverridetest seam (mirrorsCameraEnumeratorOverride);ReacquireScreenCapturesnow strips transientwindow:/picker:keys before auto-detection;OnFullscreenMonitorChangedskips redesignation while a live window pin is active ("pin wins while alive").MainViewModel.cs— ctor wires_windowEnumerator,RefreshWindowsCommand,CaptureWindowCommand, initialRefreshOpenWindows(), and routesCaptureFailed→OnScreenCaptureFailed.- UI: "Capture Window…" + "Refresh Window List" added to both the PreviewPane backdrop
context menu (mirrors the "Capture Desktop" submenu binding pattern) and the Live layer-row menu
(LeftPanel.xaml,
Tag→IsBackgroundMultiBinding visibility like its siblings). - ONE integration test:
ytLive.Tests/WindowBackdropIntegrationTests.cs(RealApp + temp DB + fakeIWindowEnumeratorvia the static seam) — asserts (a) the seam feeds the submenu, (b) a persistedwindow:0x…key heals away on reload (never resurrected), (c) pinning an un-capturable window falls back to auto (no deadwindow:pin).
Pre-existing corruption fixed (out-of-scope prerequisite)
ytLive.Tests/PillRadioTests.cs:105 carried a committed stray token
(Assert.Equal("Start", vm.PrimaryStartButtonLabel soil);) that broke the entire test-project
compile — the file was unwritable and NO tests could run. Removed the stray soil token (only
change to the file). This is the fix for the "committed syntax error" landmine.
Model / naming facts
Source.CaptureKeyis persisted (window:<hwnd>,monitor:<n>,picker:<displayname>);Source.IsBackgroundmarks the one Background per scene. See ai.md §Screen backdrop capture.WindowInfo(Models) —Title,ProcessName,Hwnd(long),ProcessId;Label => "{Title} — {ProcessName}";CaptureKey => $"window:0x{Hwnd:X}".IWindowEnumerator(Services) —IReadOnlyList<WindowInfo> Enumerate(); seam likeIFullScreenDetector.- The catastrophic-accident guard lives ONLY at reload/reacquire. Mid-session, when the pinned
window's capture session dies,
ScreenCaptureManager.CaptureFailed→OnScreenCaptureFailed(transient key) →ReacquireScreenCaptures()→ strips + auto-fallback.
Acceptance
- Right-click live backdrop → "Capture Window…" lists open non-app windows with "Title — Process" labels.
- Picking one makes it the full-bleed Live backdrop.
- While the window lives, the fullscreen-game detector does not steal the backdrop.
- Window dies → auto-fallback (game → desktop → static), no dead-end
window:pin. - Closing/reopening the app never resurrects a prior session's
window:key. - "Refresh Window List" re-enumerates.