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    /// Get shareable content synchronously
390    ///
391    /// This blocks until the content is retrieved.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if screen recording permission is not granted or retrieval fails.
396    pub fn get(self) -> Result<SCShareableContent, SCError> {
397        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
398
399        unsafe {
400            crate::ffi::sc_shareable_content_get_with_options(
401                self.exclude_desktop_windows,
402                self.on_screen_windows_only,
403                shareable_content_callback,
404                context,
405            );
406        }
407
408        completion.wait().map_err(SCError::NoShareableContent)
409    }
410
411    /// Get shareable content with only windows below a reference window
412    ///
413    /// This returns windows that are stacked below the specified reference window
414    /// in the window layering order.
415    ///
416    /// # Arguments
417    ///
418    /// * `reference_window` - The window to use as the reference point
419    ///
420    /// # Errors
421    ///
422    /// Returns an error if screen recording permission is not granted or retrieval fails.
423    pub fn below_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
424        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
425
426        unsafe {
427            crate::ffi::sc_shareable_content_get_below_window(
428                self.exclude_desktop_windows,
429                reference_window.as_ptr(),
430                shareable_content_callback,
431                context,
432            );
433        }
434
435        completion.wait().map_err(SCError::NoShareableContent)
436    }
437
438    /// Get shareable content with only windows above a reference window
439    ///
440    /// This returns windows that are stacked above the specified reference window
441    /// in the window layering order.
442    ///
443    /// # Arguments
444    ///
445    /// * `reference_window` - The window to use as the reference point
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if screen recording permission is not granted or retrieval fails.
450    pub fn above_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
451        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
452
453        unsafe {
454            crate::ffi::sc_shareable_content_get_above_window(
455                self.exclude_desktop_windows,
456                reference_window.as_ptr(),
457                shareable_content_callback,
458                context,
459            );
460        }
461
462        completion.wait().map_err(SCError::NoShareableContent)
463    }
464}
465
466impl SCShareableContent {
467    /// Whether [`SCShareableContent::current_process`] is usable on this
468    /// machine: the crate must have been compiled against a macOS 14.4+ SDK
469    /// **and** be running on macOS 14.4 or later.
470    ///
471    /// Use this to gate UI or pick a fallback strategy before paying for the
472    /// call.
473    #[cfg(feature = "macos_14_4")]
474    #[must_use]
475    pub fn current_process_is_available() -> bool {
476        unsafe { crate::ffi::sc_shareable_content_current_process_is_available() }
477    }
478
479    /// Get shareable content for the current process only (macOS 14.4+)
480    ///
481    /// This retrieves content that the current process can capture without
482    /// requiring user authorization via TCC (Transparency, Consent, and Control).
483    ///
484    /// # Errors
485    ///
486    /// Returns [`SCError::FeatureNotAvailable`] when running on macOS older
487    /// than 14.4, or when the crate was built against an SDK that predates the
488    /// API. There is deliberately no fallback to the system-wide content query:
489    /// that requires screen-recording consent and returns every window on the
490    /// machine, which callers of this method are specifically trying to avoid.
491    ///
492    /// Otherwise returns an error if retrieval fails.
493    #[cfg(feature = "macos_14_4")]
494    pub fn current_process() -> Result<Self, SCError> {
495        if !Self::current_process_is_available() {
496            return Err(SCError::feature_not_available(
497                "SCShareableContent::current_process",
498                "14.4",
499            ));
500        }
501
502        let (completion, context) = SyncCompletion::<Self>::new();
503
504        unsafe {
505            crate::ffi::sc_shareable_content_get_current_process_displays(
506                shareable_content_callback,
507                context,
508            );
509        }
510
511        completion.wait().map_err(SCError::NoShareableContent)
512    }
513}
514
515// MARK: - SCShareableContentInfo (macOS 14.0+)
516
517/// Information about shareable content from a filter (macOS 14.0+)
518///
519/// Provides metadata about the content being captured, including dimensions and scale factor.
520#[cfg(feature = "macos_14_0")]
521pub struct SCShareableContentInfo(*const c_void);
522
523#[cfg(feature = "macos_14_0")]
524impl SCShareableContentInfo {
525    /// Get content info for a filter
526    ///
527    /// Returns information about the content described by the given filter.
528    pub fn for_filter(filter: &crate::stream::content_filter::SCContentFilter) -> Option<Self> {
529        let ptr = unsafe { crate::ffi::sc_shareable_content_info_for_filter(filter.as_ptr()) };
530        if ptr.is_null() {
531            None
532        } else {
533            Some(Self(ptr))
534        }
535    }
536
537    /// Get the content style
538    #[allow(clippy::missing_errors_doc)]
539    pub fn style(&self) -> Result<crate::stream::content_filter::SCShareableContentStyle, SCError> {
540        let mut raw = 0_i32;
541        if !unsafe { crate::ffi::sc_shareable_content_info_get_style(self.0, &raw mut raw) } {
542            return Err(SCError::feature_not_available(
543                "SCShareableContentInfo.style",
544                "14.0",
545            ));
546        }
547        crate::stream::content_filter::SCShareableContentStyle::from_raw(raw).ok_or_else(|| {
548            SCError::UnknownValue {
549                type_name: "SCShareableContentStyle",
550                raw: i64::from(raw),
551            }
552        })
553    }
554
555    /// Get the point-to-pixel scale factor
556    ///
557    /// Typically 2.0 for Retina displays.
558    pub fn point_pixel_scale(&self) -> f32 {
559        unsafe { crate::ffi::sc_shareable_content_info_get_point_pixel_scale(self.0) }
560    }
561
562    /// Get the content rectangle in points
563    pub fn content_rect(&self) -> crate::cg::CGRect {
564        let mut x = 0.0;
565        let mut y = 0.0;
566        let mut width = 0.0;
567        let mut height = 0.0;
568        unsafe {
569            crate::ffi::sc_shareable_content_info_get_content_rect(
570                self.0,
571                &raw mut x,
572                &raw mut y,
573                &raw mut width,
574                &raw mut height,
575            );
576        }
577        crate::cg::CGRect::new(x, y, width, height)
578    }
579
580    /// Get the content size in pixels
581    ///
582    /// Convenience method that multiplies `content_rect` dimensions by `point_pixel_scale`.
583    pub fn pixel_size(&self) -> (u32, u32) {
584        let rect = self.content_rect();
585        let scale = self.point_pixel_scale();
586        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
587        let width = (rect.size.width * f64::from(scale)) as u32;
588        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
589        let height = (rect.size.height * f64::from(scale)) as u32;
590        (width, height)
591    }
592}
593
594#[cfg(feature = "macos_14_0")]
595crate::utils::retained::sc_retained!(
596    SCShareableContentInfo,
597    retain = crate::ffi::sc_shareable_content_info_retain,
598    release = crate::ffi::sc_shareable_content_info_release,
599);
600
601#[cfg(feature = "macos_14_0")]
602impl fmt::Debug for SCShareableContentInfo {
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        f.debug_struct("SCShareableContentInfo")
605            .field("style", &self.style())
606            .field("point_pixel_scale", &self.point_pixel_scale())
607            .field("content_rect", &self.content_rect())
608            .finish()
609    }
610}
611
612#[cfg(feature = "macos_14_0")]
613impl fmt::Display for SCShareableContentInfo {
614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615        let (width, height) = self.pixel_size();
616        write!(
617            f,
618            "ContentInfo({:?}, {}x{} px, scale: {})",
619            self.style(),
620            width,
621            height,
622            self.point_pixel_scale()
623        )
624    }
625}
626
627// SAFETY: `SCShareableContentInfo` wraps an immutable Objective-C
628// ScreenCaptureKit object. ObjC reference counting is atomic and these
629// accessor-only objects are safe to send between and share across threads.
630#[cfg(feature = "macos_14_0")]
631unsafe impl Send for SCShareableContentInfo {}
632#[cfg(feature = "macos_14_0")]
633unsafe impl Sync for SCShareableContentInfo {}