Skip to main content

screencapturekit/shareable_content/
snapshot.rs

1//! Batched data snapshot of `SCShareableContent`.
2//!
3//! Returned by [`SCShareableContent::snapshot`]. Every field on every
4//! display / window / running application is fetched in **one** Swift FFI
5//! call per category (instead of `1 + N + 6N` for the per-element accessor
6//! pattern), saving ~70 µs on a system with ~220 windows.
7//!
8//! [`SCShareableContent::snapshot`]: super::SCShareableContent::snapshot
9
10#![allow(
11    clippy::cast_possible_wrap,
12    clippy::cast_sign_loss,
13    clippy::cast_possible_truncation
14)]
15
16use crate::cg::CGRect;
17use crate::ffi::{FFIApplicationData, FFIDisplayData, FFIWindowData};
18use std::ffi::c_void;
19
20// Static caps for the bridge's batch FFI scratch buffers. These are
21// intentionally generous — the bridge silently truncates above the cap, so
22// erring high is the safe default. A 256 KiB string pool covers ~256 windows
23// each with a 1 KiB title (real-world titles are <128 bytes typically).
24const MAX_DISPLAYS: usize = 64;
25const MAX_WINDOWS: usize = 4096;
26const MAX_APPS: usize = 1024;
27const STRING_POOL_BYTES: usize = 256 * 1024;
28
29/// Plain data describing one display.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct DisplaySnapshot {
32    pub display_id: u32,
33    pub width: i32,
34    pub height: i32,
35    pub frame: CGRect,
36}
37
38/// Plain data describing one running application.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ApplicationSnapshot {
41    pub process_id: i32,
42    pub bundle_identifier: String,
43    pub application_name: String,
44}
45
46/// Plain data describing one window.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct WindowSnapshot {
49    pub window_id: u32,
50    pub window_layer: i32,
51    pub is_on_screen: bool,
52    pub is_active: bool,
53    pub frame: CGRect,
54    pub title: Option<String>,
55    /// Index into [`ContentSnapshot::applications`], or `None` if the
56    /// window has no owning application or the owner wasn't returned in
57    /// the same snapshot batch.
58    pub owning_app_index: Option<usize>,
59}
60
61/// All shareable content collected in one batched FFI round-trip.
62#[derive(Debug, Default, Clone, PartialEq, Eq)]
63pub struct ContentSnapshot {
64    pub displays: Vec<DisplaySnapshot>,
65    pub applications: Vec<ApplicationSnapshot>,
66    pub windows: Vec<WindowSnapshot>,
67}
68
69impl ContentSnapshot {
70    /// Drive the three `_batch` Swift FFI functions and unpack their packed
71    /// `repr(C)` payloads into Rust-side data structures.
72    pub(crate) fn collect(content: *const c_void) -> Option<Self> {
73        if content.is_null() {
74            return None;
75        }
76
77        // SAFETY: each batch FFI function writes at most `max_*` packed
78        // entries into the supplied buffer. We size the buffers to the caps
79        // declared above and trust the bridge to honour them (the bridge
80        // takes the cap as a parameter and uses `min(actual, cap)`).
81        let displays = unsafe { collect_displays(content) };
82        let applications = unsafe { collect_applications(content) };
83        let windows = unsafe { collect_windows(content, applications.len()) };
84
85        Some(Self {
86            displays,
87            applications,
88            windows,
89        })
90    }
91}
92
93unsafe fn collect_displays(content: *const c_void) -> Vec<DisplaySnapshot> {
94    let mut buffer: Vec<FFIDisplayData> = Vec::with_capacity(MAX_DISPLAYS);
95    // We pass uninitialised memory; Swift writes exactly `count` valid
96    // entries and we set_len to that.
97    let written = crate::ffi::sc_shareable_content_get_displays_batch(
98        content,
99        buffer.as_mut_ptr().cast::<c_void>(),
100        MAX_DISPLAYS as isize,
101    );
102    if written <= 0 {
103        return Vec::new();
104    }
105    let count = (written as usize).min(MAX_DISPLAYS);
106    buffer.set_len(count);
107
108    buffer
109        .into_iter()
110        .map(|d| DisplaySnapshot {
111            display_id: d.display_id,
112            width: d.width,
113            height: d.height,
114            frame: CGRect::new(d.frame.x, d.frame.y, d.frame.width, d.frame.height),
115        })
116        .collect()
117}
118
119unsafe fn collect_applications(content: *const c_void) -> Vec<ApplicationSnapshot> {
120    let mut packed: Vec<FFIApplicationData> = Vec::with_capacity(MAX_APPS);
121    let mut strings: Vec<i8> = vec![0; STRING_POOL_BYTES];
122    let mut strings_used: isize = 0;
123
124    let written = crate::ffi::sc_shareable_content_get_applications_batch(
125        content,
126        packed.as_mut_ptr().cast::<c_void>(),
127        MAX_APPS as isize,
128        strings.as_mut_ptr(),
129        STRING_POOL_BYTES as isize,
130        &mut strings_used,
131    );
132    if written <= 0 {
133        return Vec::new();
134    }
135    let count = (written as usize).min(MAX_APPS);
136    packed.set_len(count);
137
138    let pool: &[u8] = std::slice::from_raw_parts(
139        strings.as_ptr().cast::<u8>(),
140        (strings_used as usize).min(STRING_POOL_BYTES),
141    );
142
143    packed
144        .into_iter()
145        .map(|app| ApplicationSnapshot {
146            process_id: app.process_id,
147            bundle_identifier: read_string(pool, app.bundle_id_offset, app.bundle_id_length),
148            application_name: read_string(pool, app.app_name_offset, app.app_name_length),
149        })
150        .collect()
151}
152
153unsafe fn collect_windows(content: *const c_void, app_count_hint: usize) -> Vec<WindowSnapshot> {
154    let mut packed: Vec<FFIWindowData> = Vec::with_capacity(MAX_WINDOWS);
155    let mut strings: Vec<i8> = vec![0; STRING_POOL_BYTES];
156    let mut strings_used: isize = 0;
157    // The Swift bridge also retains app pointers into this buffer for the
158    // owning-app index lookup. We don't need to keep the pointers (we already
159    // have `applications` from the previous batch call), but we still have to
160    // accept and release them so the bridge balances its retain.
161    let app_cap = MAX_APPS.max(app_count_hint);
162    let mut app_pointers: Vec<*const c_void> = vec![std::ptr::null(); app_cap];
163    let mut app_count: isize = 0;
164
165    let written = crate::ffi::sc_shareable_content_get_windows_batch(
166        content,
167        packed.as_mut_ptr().cast::<c_void>(),
168        MAX_WINDOWS as isize,
169        strings.as_mut_ptr(),
170        STRING_POOL_BYTES as isize,
171        &mut strings_used,
172        app_pointers.as_mut_ptr(),
173        app_cap as isize,
174        &mut app_count,
175    );
176
177    // Release the SCRunningApplication pointers the bridge retained for us;
178    // we don't need them in the snapshot path (the snapshot is plain data).
179    let returned_apps = (app_count as usize).min(app_cap);
180    for &ptr in &app_pointers[..returned_apps] {
181        if !ptr.is_null() {
182            crate::ffi::sc_running_application_release(ptr);
183        }
184    }
185
186    if written <= 0 {
187        return Vec::new();
188    }
189    let count = (written as usize).min(MAX_WINDOWS);
190    packed.set_len(count);
191
192    let pool: &[u8] = std::slice::from_raw_parts(
193        strings.as_ptr().cast::<u8>(),
194        (strings_used as usize).min(STRING_POOL_BYTES),
195    );
196
197    packed
198        .into_iter()
199        .map(|w| {
200            let title = if w.title_length == 0 {
201                None
202            } else {
203                let s = read_string(pool, w.title_offset, w.title_length);
204                if s.is_empty() {
205                    None
206                } else {
207                    Some(s)
208                }
209            };
210            WindowSnapshot {
211                window_id: w.window_id,
212                window_layer: w.window_layer,
213                is_on_screen: w.is_on_screen,
214                is_active: w.is_active,
215                frame: CGRect::new(w.frame.x, w.frame.y, w.frame.width, w.frame.height),
216                title,
217                owning_app_index: if w.owning_app_index < 0 {
218                    None
219                } else {
220                    Some(w.owning_app_index as usize)
221                },
222            }
223        })
224        .collect()
225}
226
227fn read_string(pool: &[u8], offset: u32, length: u32) -> String {
228    let start = offset as usize;
229    let end = start.saturating_add(length as usize);
230    if end > pool.len() {
231        return String::new();
232    }
233    String::from_utf8_lossy(&pool[start..end]).into_owned()
234}