Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b3a4b48
fix(recording): capture the selected window itself on macOS
MinitJain Oct 11, 2026
c3d41d2
fix(recording): keep excluded windows hidden in macOS window capture
MinitJain Oct 12, 2026
3ac35ea
Merge branch 'main' into fix/macos-window-capture
MinitJain Oct 13, 2026
7971485
fix(recording): capture the window itself on Windows, sized from its …
Stephen-Psaradellis Oct 20, 2026
fd8daaa
fix(scap-direct3d): only follow the item's size for window captures
Stephen-Psaradellis Oct 23, 2026
890182d
Merge branch 'main' into fix/windows-window-capture-item
richiemcilroy Oct 23, 2026
e71c443
style(scap-targets): rustfmt the Windows capture target
richiemcilroy Oct 23, 2026
e3693ac
Merge branch 'main' into fix/windows-window-capture-item
richiemcilroy Oct 24, 2026
25c952b
test(scap-direct3d): follow the item's size in the even-dimension tri…
Stephen-Psaradellis Oct 24, 2026
9e1a11d
fix(scap-direct3d): bound GPU copies by the frame surface, not Conten…
Stephen-Psaradellis Oct 24, 2026
326a6e0
fix(scap-direct3d): copy the content region when the surface is large…
Stephen-Psaradellis Oct 24, 2026
75057f7
Merge remote-tracking branch 'refs/remotes/pr/2301' into HEAD
richiemcilroy Oct 27, 2026
27855ea
Merge remote-tracking branch 'refs/remotes/pr/2469' into HEAD
richiemcilroy Oct 27, 2026
3a936e3
feat(recording): record a window by itself only when asked, on macOS …
richiemcilroy Oct 27, 2026
f55698e
Merge remote-tracking branch 'origin/feat/gpui-area-aspect-picker' in…
richiemcilroy Oct 27, 2026
40423e7
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
2d1d32e
fix(recording): window-only capture never falls back to the display, …
richiemcilroy Oct 27, 2026
a442d79
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
c4569a1
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
2d30eae
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
76c8906
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
c0b11f5
fix(recording): read a focused window inside its rounded corners so t…
richiemcilroy Oct 27, 2026
3455c60
fix(recording): capture a focused window as a display crop without wh…
richiemcilroy Oct 27, 2026
e1d1c1b
fix(recording): record a focused window with the window filter again
richiemcilroy Oct 27, 2026
0f49c38
fix(recording): capture a focused window at full resolution, scaled t…
richiemcilroy Oct 27, 2026
727ccab
test(verify): real-capture Focus case that must keep a window in fron…
richiemcilroy Oct 27, 2026
a983ef2
test(verify): declare per-monitor DPI awareness on Windows, as Cap do…
richiemcilroy Oct 27, 2026
84f4115
fix(recording): read macOS crops at whole even pixels so odd-sized wi…
richiemcilroy Oct 27, 2026
bfffc7b
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
7ddbc0f
Merge feat/gpui-area-aspect-picker into feat/recording-window-only
richiemcilroy Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cursor-capture/src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl std::fmt::Debug for RelativeCursorPosition {
}
}

#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
/// Needs to be logical coordinates on macOS and physical on Windows
/// This type is opqaue on purpose as the logical/physical invariants need to hold
pub struct CursorCropBounds {
Expand Down
1 change: 0 additions & 1 deletion crates/recording/examples/cursor-poll-benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ fn main() -> anyhow::Result<()> {
CursorCaptureTarget {
crop_bounds,
display,
#[cfg(target_os = "linux")]
window: None,
},
dir.path().join("cursors"),
Expand Down
104 changes: 103 additions & 1 deletion crates/recording/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,61 @@ pub struct IncrementalCaptureOutputs {
pub struct CursorCaptureTarget {
pub crop_bounds: CursorCropBounds,
pub display: scap_targets::Display,
#[cfg(target_os = "linux")]
pub window: Option<scap_targets::WindowId>,
}

#[cfg(target_os = "macos")]
fn macos_window_crop(
window: &scap_targets::WindowId,
display: &scap_targets::Display,
) -> Option<CursorCropBounds> {
use scap_targets::bounds::{LogicalBounds, LogicalPosition};

let window_bounds = scap_targets::Window::from_id_direct(window)?
.raw_handle()
.logical_bounds()?;
let display_position = display.raw_handle().logical_position();
// The video reads the window inside its rounded corners; the cursor
// is placed against the same trimmed frame.
Some(CursorCropBounds::new_macos(
crate::sources::screen_capture::trim_window_corners(LogicalBounds::new(
LogicalPosition::new(
window_bounds.position().x() - display_position.x(),
window_bounds.position().y() - display_position.y(),
),
window_bounds.size(),
)),
))
}

#[cfg(windows)]
fn windows_window_crop(
window: &scap_targets::WindowId,
display: &scap_targets::Display,
) -> Option<CursorCropBounds> {
let window_bounds = scap_targets::Window::from_id_direct(window)?
.raw_handle()
.physical_bounds()?;
let display_bounds = display.raw_handle().physical_bounds()?;
Some(window_crop_on_display(window_bounds, display_bounds))
}

#[cfg(windows)]
fn window_crop_on_display(
window_bounds: scap_targets::bounds::PhysicalBounds,
display_bounds: scap_targets::bounds::PhysicalBounds,
) -> CursorCropBounds {
use scap_targets::bounds::{PhysicalBounds, PhysicalPosition};

CursorCropBounds::new_windows(PhysicalBounds::new(
PhysicalPosition::new(
window_bounds.position().x() - display_bounds.position().x(),
window_bounds.position().y() - display_bounds.position().y(),
),
window_bounds.size(),
))
}

#[cfg(target_os = "linux")]
struct X11WindowCursor {
connection: x11rb::rust_connection::RustConnection,
Expand Down Expand Up @@ -108,6 +159,32 @@ fn normalized_window_cursor(x: i16, y: i16, width: u16, height: u16) -> Option<(
})
}

#[cfg(all(test, windows))]
mod windows_window_cursor_tests {
use super::window_crop_on_display;
use scap_targets::bounds::{PhysicalBounds, PhysicalPosition, PhysicalSize};

fn bounds(x: f64, y: f64, width: f64, height: f64) -> PhysicalBounds {
PhysicalBounds::new(
PhysicalPosition::new(x, y),
PhysicalSize::new(width, height),
)
}

#[test]
fn the_cursor_crop_follows_the_window_when_it_moves() {
let display = bounds(-1920.0, 0.0, 1920.0, 1080.0);
let started = window_crop_on_display(bounds(-1720.0, 200.0, 800.0, 600.0), display);
let moved = window_crop_on_display(bounds(-1320.0, 350.0, 800.0, 600.0), display);

assert_ne!(started, moved);
assert_eq!(
moved,
cap_cursor_capture::CursorCropBounds::new_windows(bounds(600.0, 350.0, 800.0, 600.0))
);
}
}

#[cfg(all(test, target_os = "linux"))]
mod window_cursor_tests {
use super::normalized_window_cursor;
Expand Down Expand Up @@ -634,6 +711,8 @@ pub fn spawn_cursor_recorder(
});
#[cfg(target_os = "linux")]
let mut last_window_position = None;
#[cfg(any(windows, target_os = "macos"))]
let mut last_window_crop: Option<CursorCropBounds> = None;
let device_state = DeviceState::new();
// Reading the held keys asks the OS about every key it knows (two
// CoreGraphics queries per key on macOS, 256 GetAsyncKeyState calls
Expand Down Expand Up @@ -714,6 +793,29 @@ pub fn spawn_cursor_recorder(
{
last_window_position = window_position;
}
// A window recording follows the window, so the cursor has to be
// placed against where the window is now, not where it started.
#[cfg(windows)]
let window_crop = target
.window
.as_ref()
.and_then(|window| windows_window_crop(window, &display));
#[cfg(target_os = "macos")]
let window_crop = target
.window
.as_ref()
.and_then(|window| macos_window_crop(window, &display));
#[cfg(any(windows, target_os = "macos"))]
let position_changed =
position_changed || (window_crop.is_some() && window_crop != last_window_crop);
#[cfg(any(windows, target_os = "macos"))]
{
if window_crop.is_some() {
last_window_crop = window_crop;
}
}
#[cfg(any(windows, target_os = "macos"))]
let crop_bounds = last_window_crop.unwrap_or(crop_bounds);

// The cursor image only labels this tick's move and click events,
// and reading it builds a full TIFF of every representation
Expand Down
48 changes: 48 additions & 0 deletions crates/recording/src/instant_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ impl Actor {
pub struct ActorBuilder {
output_path: PathBuf,
capture_target: ScreenCaptureTarget,
window_only: bool,
system_audio: bool,
mic_feed: Option<Arc<MicrophoneFeedLock>>,
camera_feed: Option<Arc<crate::feeds::camera::CameraFeedLock>>,
Expand All @@ -884,6 +885,7 @@ impl ActorBuilder {
Self {
output_path: output,
capture_target,
window_only: false,
system_audio: false,
mic_feed: None,
camera_feed: None,
Expand Down Expand Up @@ -922,6 +924,15 @@ impl ActorBuilder {
self
}

/// Record only the target window, following it, on macOS and Windows.
/// Ignored while a camera is attached there: Instant has no camera
/// compositing on those platforms, so the camera bubble only reaches
/// the video because the screen capture records it.
pub fn with_window_only(mut self, window_only: bool) -> Self {
self.window_only = window_only;
self
}

pub fn with_mic_feed(mut self, mic_feed: Arc<MicrophoneFeedLock>) -> Self {
self.mic_feed = Some(mic_feed);
self
Expand Down Expand Up @@ -986,6 +997,7 @@ impl ActorBuilder {
self.output_path,
RecordingBaseInputs {
capture_target: self.capture_target,
window_only: self.window_only,
capture_system_audio: self.system_audio,
mic_feed: self.mic_feed,
camera_feed: self.camera_feed,
Expand Down Expand Up @@ -1337,6 +1349,13 @@ async fn build_instant_recording_actor(
#[cfg(not(target_os = "macos"))]
let max_capture_size = None;

#[cfg(any(target_os = "macos", windows))]
let capture_window = instant_capture_window(
&inputs.capture_target,
inputs.window_only,
inputs.camera_feed.is_some(),
);

let screen_source = ScreenCaptureConfig::<ScreenCaptureMethod>::init(
display,
crop_bounds,
Expand All @@ -1345,6 +1364,8 @@ async fn build_instant_recording_actor(
max_capture_size,
timestamps.system_time(),
inputs.capture_system_audio,
#[cfg(any(target_os = "macos", windows))]
capture_window,
#[cfg(target_os = "linux")]
crate::sources::screen_capture::LinuxCaptureSource::from_target(
&inputs.capture_target,
Expand Down Expand Up @@ -1567,10 +1588,37 @@ fn clamp_size(input: (u32, u32), max: (u32, u32)) -> (u32, u32) {
}
}

// Instant mode on macOS and Windows has no camera compositing: the camera
// preview only reaches the video because the display capture records it, so a
// window-only capture would drop it.
#[cfg(any(target_os = "macos", windows))]
fn instant_capture_window(
target: &ScreenCaptureTarget,
window_only: bool,
camera_attached: bool,
) -> Option<scap_targets::WindowId> {
crate::window_only_target(target, window_only).filter(|_| !camera_attached)
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(any(target_os = "macos", windows))]
#[test]
fn instant_captures_the_window_only_when_asked_and_without_a_camera() {
let id: scap_targets::WindowId = "247".parse().unwrap();
let window = ScreenCaptureTarget::Window { id: id.clone() };

assert_eq!(instant_capture_window(&window, true, false), Some(id));
assert_eq!(instant_capture_window(&window, true, true), None);
assert_eq!(instant_capture_window(&window, false, false), None);
assert_eq!(
instant_capture_window(&ScreenCaptureTarget::CameraOnly, true, false),
None
);
}

#[cfg(target_os = "linux")]
#[test]
fn linux_presentation_builder_preserves_explicit_config_and_cli_default() {
Expand Down
12 changes: 12 additions & 0 deletions crates/recording/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,21 @@ pub struct RecordingOptions {
pub mode: RecordingMode,
}

/// The window a capture should record by itself: only for a window target
/// with window-only capture on.
pub fn window_only_target(
target: &ScreenCaptureTarget,
window_only: bool,
) -> Option<scap_targets::WindowId> {
target.window().filter(|_| window_only)
}

#[derive(Clone)]
pub struct RecordingBaseInputs {
pub capture_target: ScreenCaptureTarget,
/// For a window target on macOS and Windows: record only that window,
/// following it, instead of the region of the screen it starts in.
pub window_only: bool,
pub capture_system_audio: bool,
pub mic_feed: Option<Arc<MicrophoneFeedLock>>,
pub camera_feed: Option<Arc<CameraFeedLock>>,
Expand Down
Loading
Loading