Skip to content

Add scrolling capture of the window under the cursor - #2097

Open
x1xhlol wants to merge 11 commits into
CapSoftware:mainfrom
x1xhlol:feat/scrolling-capture
Open

Add scrolling capture of the window under the cursor#2097
x1xhlol wants to merge 11 commits into
CapSoftware:mainfrom
x1xhlol:feat/scrolling-capture

Conversation

@x1xhlol

@x1xhlol x1xhlol commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Adds scrolling capture: a new hotkey action ("Scrolling capture of window under cursor", unbound by default) that captures an entire scrollable window — not just the visible part — by scrolling it and stitching the frames into one tall screenshot, saved as a normal Cap screenshot project.

How it works (scrolling_capture.rs):

capture window frame
loop (≤60 steps, ≤16000px):
    inject 2 wheel notches at the cursor (SendInput on Windows,
    CGEventCreateScrollWheelEvent2 on macOS) → wait 600ms for
    smooth-scroll to settle → capture next frame
    → detect vertical offset: minimal mean-absolute-difference over
      overlapping rows (top 20% skipped for sticky headers, ≥25%
      overlap required, 1px granularity)
    → offset == 0 (bottom reached) or no match → stop
    → append the frame's new bottom rows to the stitched image
  • The hotkey targets the window under the cursor (same pattern as the existing "Screenshot current window" action), so the injected wheel events reach the right window.
  • The stitched image goes through the normal screenshot-project save path: same toast, automations, editor-open behavior, library entry.
  • No new dependencies; the stitching is a few dozen lines over image buffers.
  • Known cosmetic limitation: sticky side columns (e.g. Wikipedia's TOC panel) repeat down the image — only top-anchored sticky content is compensated.

Showcase

A 60-section page captured into one 1280x7404 image:

Stitched 7404px-tall capture

Bottom of the stitch — capture stops at the page footer with no repeated content:

Footer reached, no repeats

Testing (Windows, end-to-end on a live dev build)

Test Result
60-section long page → 1280x7404 stitch, all sections in order, zero seam artifacts
Stops at page bottom (footer present, nothing repeated)
Wikipedia article → 1280x12680 continuous stitch (hit the 60-frame cap on a very long article)
Non-scrolling window → single frame in ~5s, no hang
Toast + normal .cap screenshot project on every run
Regression: normal screenshot flow unaffected

macOS compiles the same capture/stitch path with a CoreGraphics scroll-event injector; runtime-tested on Windows only.

Builds on #2095 (OCR text capture — shares its capture/save helpers); the scrolling-capture change itself is the top two commits.

Greptile Summary

This PR adds window scrolling capture, OCR area capture, associated hotkeys and settings, and refactors screenshot capture/saving into reusable backend helpers.

  • Adds platform-specific synthetic scrolling and frame stitching for whole-window screenshots.
  • Adds OCR selection, clipboard, notification, and optional screenshot-preservation flows.
  • Extends persisted settings, hotkey actions, target-selection UI, window handling, and Tauri IPC bindings.

Confidence Score: 3/5

The PR should not merge until scrolling captures are capped before appending oversized strips and the Linux action is either implemented or disabled.

Near-limit captures can produce images rejected by the screenshot editor, while Linux exposes an action whose scroll injector performs no operation and therefore cannot capture off-screen content.

Files Needing Attention: apps/desktop/src-tauri/src/scrolling_capture.rs, apps/desktop/src/routes/(window-chrome)/settings/hotkeys.tsx, apps/desktop/src/utils/tauri.ts

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/scrolling_capture.rs Implements scrolling and stitching, but can exceed the image-height cap and silently degenerates to a single-frame capture on Linux.
apps/desktop/src-tauri/src/hotkeys.rs Adds OCR and scrolling-capture actions plus default OCR hotkey seeding; the scrolling action reaches the unsupported Linux implementation.
apps/desktop/src-tauri/src/recording.rs Refactors image capture and project saving and adds OCR capture while preserving the existing asynchronous PNG-save behavior.
apps/desktop/src/routes/target-select-overlay.tsx Adds OCR area-selection capture and explicit success/error overlay lifecycle handling.
apps/desktop/src/utils/tauri.ts Updates generated IPC bindings, contrary to the repository rule prohibiting direct changes to generated tauri.ts files.
apps/desktop/src-tauri/src/general_settings.rs Adds backward-compatible defaulted OCR preferences.
apps/desktop/src-tauri/src/lib.rs Correctly registers the new commands and scrolling-capture module.
Prompt To Fix All With AI
### Issue 1
apps/desktop/src-tauri/src/scrolling_capture.rs:74-83
**Height cap checked too late**

When a capture is near 16,000 pixels, the full next strip is appended before the cap is checked, allowing the result to exceed the screenshot editor's 16,384-pixel limit and fail to open.

```suggestion
        let remaining_height = MAX_STITCHED_HEIGHT.saturating_sub(stitched_height);
        let new_rows = offset.min(frame_height).min(remaining_height);
        if new_rows == 0 {
            debug!("Stitched image reached the height cap; stopping");
            break;
        }

        let strip = frame.crop_imm(0, frame_height - new_rows, frame_width, new_rows);
        stitched.push(strip);
        stitched_height += new_rows;
        prev = cur;

        if stitched_height >= MAX_STITCHED_HEIGHT {
            debug!("Stitched image reached the height cap; stopping");
            break;
        }
```

### Issue 2
apps/desktop/src-tauri/src/scrolling_capture.rs:158-187
**Linux scroll injection is absent**

When a Linux user invokes the exposed scrolling-capture hotkey, `inject_scroll` performs no operation, so the loop waits once and saves only the initially visible frame instead of capturing off-screen content.

### Issue 3
apps/desktop/src/utils/tauri.ts:59-70
**Generated bindings changed directly**

This directly modifies the generated `tauri.ts` IPC surface and includes unrelated generated type churn. These changes can be overwritten during regeneration and leave the checked-in bindings out of sync; regenerate the file from the Rust command definitions instead.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Tune scrolling capture stepping and matc..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Context used (3)

Comment on lines +74 to +83
let new_rows = offset.min(frame_height);
let strip = frame.crop_imm(0, frame_height - new_rows, frame_width, new_rows);
stitched.push(strip);
stitched_height += new_rows;
prev = cur;

if stitched_height >= MAX_STITCHED_HEIGHT {
debug!("Stitched image reached the height cap; stopping");
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Height cap checked too late

When a capture is near 16,000 pixels, the full next strip is appended before the cap is checked, allowing the result to exceed the screenshot editor's 16,384-pixel limit and fail to open.

Suggested change
let new_rows = offset.min(frame_height);
let strip = frame.crop_imm(0, frame_height - new_rows, frame_width, new_rows);
stitched.push(strip);
stitched_height += new_rows;
prev = cur;
if stitched_height >= MAX_STITCHED_HEIGHT {
debug!("Stitched image reached the height cap; stopping");
break;
}
let remaining_height = MAX_STITCHED_HEIGHT.saturating_sub(stitched_height);
let new_rows = offset.min(frame_height).min(remaining_height);
if new_rows == 0 {
debug!("Stitched image reached the height cap; stopping");
break;
}
let strip = frame.crop_imm(0, frame_height - new_rows, frame_width, new_rows);
stitched.push(strip);
stitched_height += new_rows;
prev = cur;
if stitched_height >= MAX_STITCHED_HEIGHT {
debug!("Stitched image reached the height cap; stopping");
break;
}

Knowledge Base Used: Desktop Tauri App (Rust Backend)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/scrolling_capture.rs
Line: 74-83

Comment:
**Height cap checked too late**

When a capture is near 16,000 pixels, the full next strip is appended before the cap is checked, allowing the result to exceed the screenshot editor's 16,384-pixel limit and fail to open.

```suggestion
        let remaining_height = MAX_STITCHED_HEIGHT.saturating_sub(stitched_height);
        let new_rows = offset.min(frame_height).min(remaining_height);
        if new_rows == 0 {
            debug!("Stitched image reached the height cap; stopping");
            break;
        }

        let strip = frame.crop_imm(0, frame_height - new_rows, frame_width, new_rows);
        stitched.push(strip);
        stitched_height += new_rows;
        prev = cur;

        if stitched_height >= MAX_STITCHED_HEIGHT {
            debug!("Stitched image reached the height cap; stopping");
            break;
        }
```

**Knowledge Base Used:** [Desktop Tauri App (Rust Backend)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/desktop-tauri-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +158 to +187
#[allow(unused_variables)]
fn inject_scroll(notches: i32) {
#[cfg(windows)]
{
use ::windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_WHEEL, MOUSEINPUT, SendInput,
};

const WHEEL_DELTA: i32 = 120;

let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: (notches * WHEEL_DELTA) as u32,
dwFlags: MOUSEEVENTF_WHEEL,
time: 0,
dwExtraInfo: 0,
},
},
};

unsafe {
SendInput(&[input], std::mem::size_of::<INPUT>() as i32);
}
}

#[cfg(target_os = "macos")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Linux scroll injection is absent

When a Linux user invokes the exposed scrolling-capture hotkey, inject_scroll performs no operation, so the loop waits once and saves only the initially visible frame instead of capturing off-screen content.

Knowledge Base Used: Desktop Tauri App (Rust Backend)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/scrolling_capture.rs
Line: 158-187

Comment:
**Linux scroll injection is absent**

When a Linux user invokes the exposed scrolling-capture hotkey, `inject_scroll` performs no operation, so the loop waits once and saves only the initially visible frame instead of capturing off-screen content.

**Knowledge Base Used:** [Desktop Tauri App (Rust Backend)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/desktop-tauri-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines 59 to 70
async takeScreenshot(target: ScreenCaptureTarget) : Promise<string> {
return await TAURI_INVOKE("take_screenshot", { target });
},
async captureOcrText(target: ScreenCaptureTarget) : Promise<string> {
return await TAURI_INVOKE("capture_ocr_text", { target });
},
async captureScrollingWindow(windowId: WindowId) : Promise<string> {
return await TAURI_INVOKE("capture_scrolling_window", { windowId });
},
async importCurrentDesktopBackground(projectPath: string) : Promise<string> {
return await TAURI_INVOKE("import_current_desktop_background", { projectPath });
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Generated bindings changed directly

This directly modifies the generated tauri.ts IPC surface and includes unrelated generated type churn. These changes can be overwritten during regeneration and leave the checked-in bindings out of sync; regenerate the file from the Rust command definitions instead.

Context Used: AGENTS.md (source)

Knowledge Base Used: Desktop Frontend (apps/desktop/src)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/tauri.ts
Line: 59-70

Comment:
**Generated bindings changed directly**

This directly modifies the generated `tauri.ts` IPC surface and includes unrelated generated type churn. These changes can be overwritten during regeneration and leave the checked-in bindings out of sync; regenerate the file from the Rust command definitions instead.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

**Knowledge Base Used:** [Desktop Frontend (apps/desktop/src)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/desktop-frontend.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These bindings match what specta regenerates from the Rust command definitions - the dev build regenerated tauri.ts after the capture_scrolling_window command was added and the checked-in file is byte-identical to the regenerated output, so it's in sync, not hand-drifted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense — if the file is byte-identical to the specta output after adding capture_scrolling_window, checking it in is the right move. The concern was about hand-drifted bindings, which doesn't apply here. This comment is resolved.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant