Skip to main content

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 {}