Description
On Windows, taking a screenshot of a specific window never produces a screenshot: no file is written and the screenshot editor does not open. There is no error, toast, or log entry. Capturing a display works and creates the expected screenshot project.
Note: earlier revisions of this issue attributed the failure to WebView2-hosted apps and to Windows window-target selection. Follow-up investigation disproved both. The root cause is in the desktop frontend and affects window screenshots generally, regardless of which app is being captured.
Expected behavior: selecting a window in the screenshot picker captures that window.
Actual behavior: nothing happens, silently.
Reproduction
- On Windows, open Cap's screenshot target picker and choose Window.
- Hover a window and click it.
- Observe that no screenshot is saved and the editor does not open, with no error shown.
- Repeat with a display target and observe that it succeeds.
Root cause
In apps/desktop/src/routes/target-select-overlay.tsx, the window variant's onRecordingStart called commands.closeTargetSelectOverlays() while options.mode === "screenshot".
On Windows, that command closes the target-select overlay windows rather than hiding them (they are closed to release the DirectComposition surface). But takeScreenshot is invoked from that same webview immediately afterwards, so the webview is destroyed before the IPC call is dispatched. The screenshot request never reaches the Rust side, and because the caller is gone there is nothing left to surface an error.
This is the exact hazard already documented in dismissPickerForRecordingStart in the same file:
Hide rather than close: startRecording is invoked from THIS webview right after dismissal, and closing destroys the webview before the invoke is dispatched - the recording then silently never starts.
The window + screenshot branch was the one path that did not follow that rule.
Why display and area targets were unaffected: they either use dismissPickerForRecordingStart (hide-only) or do not dismiss the picker at all. Only the window branch closed the overlay outright.
Supporting evidence:
- Across multiple Cap log files,
take_screenshot appears only with target=Display. There is no logged window-target invocation, confirming the failure happens in the frontend before Rust is reached.
- Running Cap's own
scap-targets code against a live window shows selection is healthy: the window is enumerated, is_valid=true, and it reports correct display-relative bounds.
Not the cause (recorded to avoid re-investigation)
Executable-name filtering in crates/scap-targets/src/platform/win.rs was suspected but ruled out:
- A WebView2-hosted app's visible top-level window is owned by the host process, so the ignored-executable list (matched by exact file name against the top-level window's own PID) never applied to it.
get_topmost_at_cursor does reject the msedgewebview2.exe-owned child HWND, but falls through to its top-level-window fallback and returns the correct host window.
- An A/B test at a cursor point where
WindowFromPoint returns the WebView2 child HWND produced identical, correct results with and without a patch resolving the HWND to its root window.
That filtering is still arguably too broad - it substring-matches "cap", which would reject unrelated executables whose names contain it - but it is not the cause of this bug and is better addressed separately.
Fix
Remove the commands.closeTargetSelectOverlays() call from the screenshot branch of the window variant's onRecordingStart. The screenshot handler in RecordingControls already hides the overlay windows, performs the capture, and closes them afterwards, and its error path shows a toast, so genuine failures become visible instead of silent.
The main window still reveals correctly because that is driven by targetModeDismissal: "screenshot", which the fix continues to set.
Additional Context
- Cap version:
main at d0491b1e59d6ffe4386524ba67047d039e46d58e
- Operating system: Windows 11
Verification
Verified in a local pnpm dev:desktop build on Windows: a window screenshot now captures successfully and opens in the screenshot editor, writing an original.png at the exact window bounds with real image content.
Acceptance criteria
- Selecting a window in the screenshot picker captures that window and opens the editor.
- The Screenshot current window hotkey works.
- Cap's own windows and overlays remain excluded from capture targets.
- Display and area screenshot capture remain unchanged.
- Screenshot failures surface an error to the user instead of silently producing nothing.
Description
On Windows, taking a screenshot of a specific window never produces a screenshot: no file is written and the screenshot editor does not open. There is no error, toast, or log entry. Capturing a display works and creates the expected screenshot project.
Note: earlier revisions of this issue attributed the failure to WebView2-hosted apps and to Windows window-target selection. Follow-up investigation disproved both. The root cause is in the desktop frontend and affects window screenshots generally, regardless of which app is being captured.
Expected behavior: selecting a window in the screenshot picker captures that window.
Actual behavior: nothing happens, silently.
Reproduction
Root cause
In
apps/desktop/src/routes/target-select-overlay.tsx, the window variant'sonRecordingStartcalledcommands.closeTargetSelectOverlays()whileoptions.mode === "screenshot".On Windows, that command closes the target-select overlay windows rather than hiding them (they are closed to release the DirectComposition surface). But
takeScreenshotis invoked from that same webview immediately afterwards, so the webview is destroyed before the IPC call is dispatched. The screenshot request never reaches the Rust side, and because the caller is gone there is nothing left to surface an error.This is the exact hazard already documented in
dismissPickerForRecordingStartin the same file:The window + screenshot branch was the one path that did not follow that rule.
Why display and area targets were unaffected: they either use
dismissPickerForRecordingStart(hide-only) or do not dismiss the picker at all. Only the window branch closed the overlay outright.Supporting evidence:
take_screenshotappears only withtarget=Display. There is no logged window-target invocation, confirming the failure happens in the frontend before Rust is reached.scap-targetscode against a live window shows selection is healthy: the window is enumerated,is_valid=true, and it reports correct display-relative bounds.Not the cause (recorded to avoid re-investigation)
Executable-name filtering in
crates/scap-targets/src/platform/win.rswas suspected but ruled out:get_topmost_at_cursordoes reject themsedgewebview2.exe-owned child HWND, but falls through to its top-level-window fallback and returns the correct host window.WindowFromPointreturns the WebView2 child HWND produced identical, correct results with and without a patch resolving the HWND to its root window.That filtering is still arguably too broad - it substring-matches
"cap", which would reject unrelated executables whose names contain it - but it is not the cause of this bug and is better addressed separately.Fix
Remove the
commands.closeTargetSelectOverlays()call from the screenshot branch of the window variant'sonRecordingStart. The screenshot handler inRecordingControlsalready hides the overlay windows, performs the capture, and closes them afterwards, and its error path shows a toast, so genuine failures become visible instead of silent.The main window still reveals correctly because that is driven by
targetModeDismissal: "screenshot", which the fix continues to set.Additional Context
mainatd0491b1e59d6ffe4386524ba67047d039e46d58eVerification
Verified in a local
pnpm dev:desktopbuild on Windows: a window screenshot now captures successfully and opens in the screenshot editor, writing anoriginal.pngat the exact window bounds with real image content.Acceptance criteria