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