screencapturekit/shareable_content/mod.rs
1//! Shareable content types - displays, windows, and applications
2//!
3//! This module provides access to the system's displays, windows, and running
4//! applications that can be captured by `ScreenCaptureKit`.
5//!
6//! ## Main Types
7//!
8//! - [`SCShareableContent`] - Container for all available content (displays, windows, apps)
9//! - [`SCDisplay`] - A physical or virtual display that can be captured
10//! - [`SCWindow`] - A window that can be captured
11//! - [`SCRunningApplication`] - A running application whose windows can be captured
12//!
13//! ## Workflow
14//!
15//! 1. Call [`SCShareableContent::get()`] to retrieve available content
16//! 2. Select displays/windows/apps to capture
17//! 3. Create an [`SCContentFilter`](crate::stream::content_filter::SCContentFilter) from the selection
18//!
19//! # Examples
20//!
21//! ## List All Content
22//!
23//! ```no_run
24//! use screencapturekit::shareable_content::SCShareableContent;
25//!
26//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! // Get all shareable content
28//! let content = SCShareableContent::get()?;
29//!
30//! // List displays
31//! for display in content.displays() {
32//! println!("Display {}: {}x{}",
33//! display.display_id(),
34//! display.width(),
35//! display.height()
36//! );
37//! }
38//!
39//! // List windows
40//! for window in content.windows() {
41//! if let Some(title) = window.title() {
42//! println!("Window: {}", title);
43//! }
44//! }
45//!
46//! // List applications
47//! for app in content.applications() {
48//! println!("App: {} ({})", app.application_name(), app.bundle_identifier());
49//! }
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ## Filter On-Screen Windows Only
55//!
56//! ```no_run
57//! use screencapturekit::shareable_content::SCShareableContent;
58//!
59//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
60//! let content = SCShareableContent::create()
61//! .with_on_screen_windows_only(true)
62//! .with_exclude_desktop_windows(true)
63//! .get()?;
64//!
65//! println!("Found {} on-screen windows", content.windows().len());
66//! # Ok(())
67//! # }
68//! ```
69
70pub mod display;
71pub mod running_application;
72pub mod snapshot;
73pub mod window;
74pub use display::SCDisplay;
75pub use running_application::SCRunningApplication;
76pub use snapshot::{ApplicationSnapshot, ContentSnapshot, DisplaySnapshot, WindowSnapshot};
77pub use window::SCWindow;
78
79use crate::error::SCError;
80use crate::utils::completion::{error_from_cstr, SyncCompletion};
81use core::fmt;
82use std::ffi::c_void;
83
84#[repr(transparent)]
85pub struct SCShareableContent(*const c_void);
86
87// SAFETY: `SCShareableContent` wraps an immutable Objective-C ScreenCaptureKit
88// object. ObjC reference counting is atomic and these accessor-only objects are
89// safe to send between and share across threads.
90unsafe impl Send for SCShareableContent {}
91unsafe impl Sync for SCShareableContent {}
92
93/// Callback for shareable content retrieval
94extern "C" fn shareable_content_callback(
95 content_ptr: *const c_void,
96 error_ptr: *const i8,
97 user_data: *mut c_void,
98) {
99 crate::utils::panic_safe::catch_user_panic("shareable_content_callback", move || {
100 if !error_ptr.is_null() {
101 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
102 let error = unsafe { error_from_cstr(error_ptr) };
103 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
104 unsafe { SyncCompletion::<SCShareableContent>::complete_err(user_data, error) };
105 } else if !content_ptr.is_null() {
106 // SAFETY: `content` is non-null (checked above) and is a valid `SCShareableContent` pointer retained for us by the Swift completion handler.
107 let content = unsafe { SCShareableContent::from_ptr(content_ptr) };
108 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
109 unsafe { SyncCompletion::complete_ok(user_data, content) };
110 } else {
111 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
112 unsafe {
113 SyncCompletion::<SCShareableContent>::complete_err(
114 user_data,
115 "Unknown error".to_string(),
116 );
117 };
118 }
119 });
120}
121
122impl PartialEq for SCShareableContent {
123 fn eq(&self, other: &Self) -> bool {
124 self.0 == other.0
125 }
126}
127
128impl Eq for SCShareableContent {}
129
130impl std::hash::Hash for SCShareableContent {
131 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
132 self.0.hash(state);
133 }
134}
135
136impl SCShareableContent {
137 /// Create from raw pointer (used internally)
138 ///
139 /// # Safety
140 /// The pointer must be a valid retained `SCShareableContent` pointer from Swift FFI.
141 pub(crate) unsafe fn from_ptr(ptr: *const c_void) -> Self {
142 Self(ptr)
143 }
144
145 /// Get shareable content (displays, windows, and applications)
146 ///
147 /// # Examples
148 ///
149 /// ```no_run
150 /// use screencapturekit::shareable_content::SCShareableContent;
151 ///
152 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
153 /// let content = SCShareableContent::get()?;
154 /// println!("Found {} displays", content.displays().len());
155 /// println!("Found {} windows", content.windows().len());
156 /// println!("Found {} apps", content.applications().len());
157 /// # Ok(())
158 /// # }
159 /// ```
160 ///
161 /// # Errors
162 ///
163 /// Returns an error if screen recording permission is not granted.
164 pub fn get() -> Result<Self, SCError> {
165 SCShareableContentOptions::default().get()
166 }
167
168 /// Create options builder for customizing shareable content retrieval
169 ///
170 /// # Examples
171 ///
172 /// ```no_run
173 /// use screencapturekit::shareable_content::SCShareableContent;
174 ///
175 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
176 /// let content = SCShareableContent::create()
177 /// .with_on_screen_windows_only(true)
178 /// .with_exclude_desktop_windows(true)
179 /// .get()?;
180 /// # Ok(())
181 /// # }
182 /// ```
183 #[must_use]
184 pub fn create() -> SCShareableContentOptions {
185 SCShareableContentOptions::default()
186 }
187
188 /// Get all available displays
189 ///
190 /// # Examples
191 ///
192 /// ```no_run
193 /// use screencapturekit::shareable_content::SCShareableContent;
194 ///
195 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
196 /// let content = SCShareableContent::get()?;
197 /// for display in content.displays() {
198 /// println!("Display: {}x{}", display.width(), display.height());
199 /// }
200 /// # Ok(())
201 /// # }
202 /// ```
203 pub fn displays(&self) -> Vec<SCDisplay> {
204 unsafe {
205 let count = crate::ffi::sc_shareable_content_get_displays_count(self.0);
206 // FFI returns isize but count is always positive
207 #[allow(clippy::cast_sign_loss)]
208 let mut displays = Vec::with_capacity(count as usize);
209
210 for i in 0..count {
211 let display_ptr = crate::ffi::sc_shareable_content_get_display_at(self.0, i);
212 displays.extend(SCDisplay::from_retained_ptr(display_ptr));
213 }
214
215 displays
216 }
217 }
218
219 /// Get all available windows
220 ///
221 /// # Examples
222 ///
223 /// ```no_run
224 /// use screencapturekit::shareable_content::SCShareableContent;
225 ///
226 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
227 /// let content = SCShareableContent::get()?;
228 /// for window in content.windows() {
229 /// if let Some(title) = window.title() {
230 /// println!("Window: {}", title);
231 /// }
232 /// }
233 /// # Ok(())
234 /// # }
235 /// ```
236 pub fn windows(&self) -> Vec<SCWindow> {
237 unsafe {
238 let count = crate::ffi::sc_shareable_content_get_windows_count(self.0);
239 // FFI returns isize but count is always positive
240 #[allow(clippy::cast_sign_loss)]
241 let mut windows = Vec::with_capacity(count as usize);
242
243 for i in 0..count {
244 let window_ptr = crate::ffi::sc_shareable_content_get_window_at(self.0, i);
245 windows.extend(SCWindow::from_retained_ptr(window_ptr));
246 }
247
248 windows
249 }
250 }
251
252 /// Get all available running applications
253 ///
254 /// # Examples
255 ///
256 /// ```no_run
257 /// use screencapturekit::shareable_content::SCShareableContent;
258 ///
259 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
260 /// let content = SCShareableContent::get()?;
261 /// for app in content.applications() {
262 /// println!("App: {} (PID: {})", app.application_name(), app.process_id());
263 /// }
264 /// # Ok(())
265 /// # }
266 /// ```
267 pub fn applications(&self) -> Vec<SCRunningApplication> {
268 unsafe {
269 let count = crate::ffi::sc_shareable_content_get_applications_count(self.0);
270 // FFI returns isize but count is always positive
271 #[allow(clippy::cast_sign_loss)]
272 let mut apps = Vec::with_capacity(count as usize);
273
274 for i in 0..count {
275 let app_ptr = crate::ffi::sc_shareable_content_get_application_at(self.0, i);
276 apps.extend(SCRunningApplication::from_retained_ptr(app_ptr));
277 }
278
279 apps
280 }
281 }
282
283 #[allow(dead_code)]
284 pub(crate) fn as_ptr(&self) -> *const c_void {
285 self.0
286 }
287
288 /// Fetch every display, window, and running application in a single batched
289 /// FFI round-trip — see [`ContentSnapshot`] for what's returned.
290 ///
291 /// **Use this in any code path that reads more than one attribute per
292 /// window / display / app.** The per-element accessors ([`displays`],
293 /// [`windows`], [`applications`] + per-attribute methods like
294 /// [`SCWindow::title`] / [`SCWindow::frame`]) issue one FFI call each
295 /// — a typical "list windows with title and frame" walk on a 220-window
296 /// system measured at ~73 µs. `snapshot()` collapses that to ~5 µs (~15×
297 /// faster) by going through the bridge's packed FFI surface and copying
298 /// every attribute into Rust-side plain data structs in a single pass.
299 ///
300 /// Returns `None` if the bridge couldn't fit the data into the static
301 /// scratch buffers (extremely unlikely — limits are 64 displays, 4096
302 /// windows, 1024 apps with a 256 KiB string pool).
303 ///
304 /// [`displays`]: Self::displays
305 /// [`windows`]: Self::windows
306 /// [`applications`]: Self::applications
307 /// [`SCWindow::title`]: crate::shareable_content::SCWindow::title
308 /// [`SCWindow::frame`]: crate::shareable_content::SCWindow::frame
309 #[must_use]
310 pub fn snapshot(&self) -> Option<ContentSnapshot> {
311 ContentSnapshot::collect(self.0)
312 }
313}
314
315crate::utils::retained::sc_retained!(
316 SCShareableContent,
317 retain = crate::ffi::sc_shareable_content_retain,
318 release = crate::ffi::sc_shareable_content_release,
319);
320
321impl fmt::Debug for SCShareableContent {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 // Use the cheap _count FFIs instead of full enumerations. The previous
324 // implementation called `.windows()` / `.displays()` / `.applications()`
325 // which each issue 1 + N FFI calls and allocate a Vec — measured at
326 // ~47 µs total on a system with ~220 windows. The count FFIs are O(1)
327 // each.
328 unsafe {
329 f.debug_struct("SCShareableContent")
330 .field(
331 "displays",
332 &crate::ffi::sc_shareable_content_get_displays_count(self.0),
333 )
334 .field(
335 "windows",
336 &crate::ffi::sc_shareable_content_get_windows_count(self.0),
337 )
338 .field(
339 "applications",
340 &crate::ffi::sc_shareable_content_get_applications_count(self.0),
341 )
342 .finish()
343 }
344 }
345}
346
347impl fmt::Display for SCShareableContent {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 // O(1) count FFIs instead of full enumerations — see the Debug impl.
350 unsafe {
351 write!(
352 f,
353 "SCShareableContent ({} displays, {} windows, {} applications)",
354 crate::ffi::sc_shareable_content_get_displays_count(self.0),
355 crate::ffi::sc_shareable_content_get_windows_count(self.0),
356 crate::ffi::sc_shareable_content_get_applications_count(self.0),
357 )
358 }
359 }
360}
361
362#[derive(Default, Debug, Clone, PartialEq, Eq)]
363pub struct SCShareableContentOptions {
364 exclude_desktop_windows: bool,
365 on_screen_windows_only: bool,
366}
367
368impl SCShareableContentOptions {
369 /// Exclude desktop windows from the shareable content.
370 ///
371 /// When set to `true`, desktop-level windows (like the desktop background)
372 /// are excluded from the returned window list.
373 #[must_use]
374 pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
375 self.exclude_desktop_windows = exclude;
376 self
377 }
378
379 /// Include only on-screen windows in the shareable content.
380 ///
381 /// When set to `true`, only windows that are currently visible on screen
382 /// are included. Minimized or off-screen windows are excluded.
383 #[must_use]
384 pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
385 self.on_screen_windows_only = on_screen_only;
386 self
387 }
388
389 // =========================================================================
390 // Deprecated methods - use with_* versions instead
391 // =========================================================================
392
393 /// Exclude desktop windows from the shareable content.
394 #[must_use]
395 #[deprecated(since = "1.5.0", note = "Use with_exclude_desktop_windows() instead")]
396 pub fn exclude_desktop_windows(self, exclude: bool) -> Self {
397 self.with_exclude_desktop_windows(exclude)
398 }
399
400 /// Include only on-screen windows in the shareable content.
401 #[must_use]
402 #[deprecated(since = "1.5.0", note = "Use with_on_screen_windows_only() instead")]
403 pub fn on_screen_windows_only(self, on_screen_only: bool) -> Self {
404 self.with_on_screen_windows_only(on_screen_only)
405 }
406
407 /// Get shareable content synchronously
408 ///
409 /// This blocks until the content is retrieved.
410 ///
411 /// # Errors
412 ///
413 /// Returns an error if screen recording permission is not granted or retrieval fails.
414 pub fn get(self) -> Result<SCShareableContent, SCError> {
415 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
416
417 unsafe {
418 crate::ffi::sc_shareable_content_get_with_options(
419 self.exclude_desktop_windows,
420 self.on_screen_windows_only,
421 shareable_content_callback,
422 context,
423 );
424 }
425
426 completion.wait().map_err(SCError::NoShareableContent)
427 }
428
429 /// Get shareable content with only windows below a reference window
430 ///
431 /// This returns windows that are stacked below the specified reference window
432 /// in the window layering order.
433 ///
434 /// # Arguments
435 ///
436 /// * `reference_window` - The window to use as the reference point
437 ///
438 /// # Errors
439 ///
440 /// Returns an error if screen recording permission is not granted or retrieval fails.
441 pub fn below_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
442 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
443
444 unsafe {
445 crate::ffi::sc_shareable_content_get_below_window(
446 self.exclude_desktop_windows,
447 reference_window.as_ptr(),
448 shareable_content_callback,
449 context,
450 );
451 }
452
453 completion.wait().map_err(SCError::NoShareableContent)
454 }
455
456 /// Get shareable content with only windows above a reference window
457 ///
458 /// This returns windows that are stacked above the specified reference window
459 /// in the window layering order.
460 ///
461 /// # Arguments
462 ///
463 /// * `reference_window` - The window to use as the reference point
464 ///
465 /// # Errors
466 ///
467 /// Returns an error if screen recording permission is not granted or retrieval fails.
468 pub fn above_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
469 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
470
471 unsafe {
472 crate::ffi::sc_shareable_content_get_above_window(
473 self.exclude_desktop_windows,
474 reference_window.as_ptr(),
475 shareable_content_callback,
476 context,
477 );
478 }
479
480 completion.wait().map_err(SCError::NoShareableContent)
481 }
482}
483
484impl SCShareableContent {
485 /// Get shareable content for the current process only (macOS 14.4+)
486 ///
487 /// This retrieves content that the current process can capture without
488 /// requiring user authorization via TCC (Transparency, Consent, and Control).
489 ///
490 /// # Errors
491 ///
492 /// Returns an error if retrieval fails.
493 #[cfg(feature = "macos_14_4")]
494 pub fn current_process() -> Result<Self, SCError> {
495 let (completion, context) = SyncCompletion::<Self>::new();
496
497 unsafe {
498 crate::ffi::sc_shareable_content_get_current_process_displays(
499 shareable_content_callback,
500 context,
501 );
502 }
503
504 completion.wait().map_err(SCError::NoShareableContent)
505 }
506}
507
508// MARK: - SCShareableContentInfo (macOS 14.0+)
509
510/// Information about shareable content from a filter (macOS 14.0+)
511///
512/// Provides metadata about the content being captured, including dimensions and scale factor.
513#[cfg(feature = "macos_14_0")]
514pub struct SCShareableContentInfo(*const c_void);
515
516#[cfg(feature = "macos_14_0")]
517impl SCShareableContentInfo {
518 /// Get content info for a filter
519 ///
520 /// Returns information about the content described by the given filter.
521 pub fn for_filter(filter: &crate::stream::content_filter::SCContentFilter) -> Option<Self> {
522 let ptr = unsafe { crate::ffi::sc_shareable_content_info_for_filter(filter.as_ptr()) };
523 if ptr.is_null() {
524 None
525 } else {
526 Some(Self(ptr))
527 }
528 }
529
530 /// Get the content style
531 pub fn style(&self) -> crate::stream::content_filter::SCShareableContentStyle {
532 let value = unsafe { crate::ffi::sc_shareable_content_info_get_style(self.0) };
533 crate::stream::content_filter::SCShareableContentStyle::from(value)
534 }
535
536 /// Get the point-to-pixel scale factor
537 ///
538 /// Typically 2.0 for Retina displays.
539 pub fn point_pixel_scale(&self) -> f32 {
540 unsafe { crate::ffi::sc_shareable_content_info_get_point_pixel_scale(self.0) }
541 }
542
543 /// Get the content rectangle in points
544 pub fn content_rect(&self) -> crate::cg::CGRect {
545 let mut x = 0.0;
546 let mut y = 0.0;
547 let mut width = 0.0;
548 let mut height = 0.0;
549 unsafe {
550 crate::ffi::sc_shareable_content_info_get_content_rect(
551 self.0,
552 &mut x,
553 &mut y,
554 &mut width,
555 &mut height,
556 );
557 }
558 crate::cg::CGRect::new(x, y, width, height)
559 }
560
561 /// Get the content size in pixels
562 ///
563 /// Convenience method that multiplies `content_rect` dimensions by `point_pixel_scale`.
564 pub fn pixel_size(&self) -> (u32, u32) {
565 let rect = self.content_rect();
566 let scale = self.point_pixel_scale();
567 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
568 let width = (rect.size.width * f64::from(scale)) as u32;
569 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
570 let height = (rect.size.height * f64::from(scale)) as u32;
571 (width, height)
572 }
573}
574
575#[cfg(feature = "macos_14_0")]
576crate::utils::retained::sc_retained!(
577 SCShareableContentInfo,
578 retain = crate::ffi::sc_shareable_content_info_retain,
579 release = crate::ffi::sc_shareable_content_info_release,
580);
581
582#[cfg(feature = "macos_14_0")]
583impl fmt::Debug for SCShareableContentInfo {
584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585 f.debug_struct("SCShareableContentInfo")
586 .field("style", &self.style())
587 .field("point_pixel_scale", &self.point_pixel_scale())
588 .field("content_rect", &self.content_rect())
589 .finish()
590 }
591}
592
593#[cfg(feature = "macos_14_0")]
594impl fmt::Display for SCShareableContentInfo {
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 let (width, height) = self.pixel_size();
597 write!(
598 f,
599 "ContentInfo({:?}, {}x{} px, scale: {})",
600 self.style(),
601 width,
602 height,
603 self.point_pixel_scale()
604 )
605 }
606}
607
608// SAFETY: `SCShareableContentInfo` wraps an immutable Objective-C
609// ScreenCaptureKit object. ObjC reference counting is atomic and these
610// accessor-only objects are safe to send between and share across threads.
611#[cfg(feature = "macos_14_0")]
612unsafe impl Send for SCShareableContentInfo {}
613#[cfg(feature = "macos_14_0")]
614unsafe impl Sync for SCShareableContentInfo {}