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 window;
73pub use display::SCDisplay;
74pub use running_application::SCRunningApplication;
75pub use window::SCWindow;
76
77use crate::error::SCError;
78use crate::utils::completion::{error_from_cstr, SyncCompletion};
79use core::fmt;
80use std::ffi::c_void;
81
82#[repr(transparent)]
83pub struct SCShareableContent(*const c_void);
84
85unsafe impl Send for SCShareableContent {}
86unsafe impl Sync for SCShareableContent {}
87
88/// Callback for shareable content retrieval
89extern "C" fn shareable_content_callback(
90    content_ptr: *const c_void,
91    error_ptr: *const i8,
92    user_data: *mut c_void,
93) {
94    if !error_ptr.is_null() {
95        let error = unsafe { error_from_cstr(error_ptr) };
96        unsafe { SyncCompletion::<SCShareableContent>::complete_err(user_data, error) };
97    } else if !content_ptr.is_null() {
98        let content = unsafe { SCShareableContent::from_ptr(content_ptr) };
99        unsafe { SyncCompletion::complete_ok(user_data, content) };
100    } else {
101        unsafe {
102            SyncCompletion::<SCShareableContent>::complete_err(
103                user_data,
104                "Unknown error".to_string(),
105            );
106        };
107    }
108}
109
110impl PartialEq for SCShareableContent {
111    fn eq(&self, other: &Self) -> bool {
112        self.0 == other.0
113    }
114}
115
116impl Eq for SCShareableContent {}
117
118impl std::hash::Hash for SCShareableContent {
119    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
120        self.0.hash(state);
121    }
122}
123
124impl Clone for SCShareableContent {
125    fn clone(&self) -> Self {
126        unsafe { Self(crate::ffi::sc_shareable_content_retain(self.0)) }
127    }
128}
129
130impl SCShareableContent {
131    /// Create from raw pointer (used internally)
132    ///
133    /// # Safety
134    /// The pointer must be a valid retained `SCShareableContent` pointer from Swift FFI.
135    pub(crate) unsafe fn from_ptr(ptr: *const c_void) -> Self {
136        Self(ptr)
137    }
138
139    /// Get shareable content (displays, windows, and applications)
140    ///
141    /// # Examples
142    ///
143    /// ```no_run
144    /// use screencapturekit::shareable_content::SCShareableContent;
145    ///
146    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
147    /// let content = SCShareableContent::get()?;
148    /// println!("Found {} displays", content.displays().len());
149    /// println!("Found {} windows", content.windows().len());
150    /// println!("Found {} apps", content.applications().len());
151    /// # Ok(())
152    /// # }
153    /// ```
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if screen recording permission is not granted.
158    pub fn get() -> Result<Self, SCError> {
159        SCShareableContentOptions::default().get()
160    }
161
162    /// Create options builder for customizing shareable content retrieval
163    ///
164    /// # Examples
165    ///
166    /// ```no_run
167    /// use screencapturekit::shareable_content::SCShareableContent;
168    ///
169    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
170    /// let content = SCShareableContent::create()
171    ///     .with_on_screen_windows_only(true)
172    ///     .with_exclude_desktop_windows(true)
173    ///     .get()?;
174    /// # Ok(())
175    /// # }
176    /// ```
177    #[must_use]
178    pub fn create() -> SCShareableContentOptions {
179        SCShareableContentOptions::default()
180    }
181
182    /// Get all available displays
183    ///
184    /// # Examples
185    ///
186    /// ```no_run
187    /// use screencapturekit::shareable_content::SCShareableContent;
188    ///
189    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
190    /// let content = SCShareableContent::get()?;
191    /// for display in content.displays() {
192    ///     println!("Display: {}x{}", display.width(), display.height());
193    /// }
194    /// # Ok(())
195    /// # }
196    /// ```
197    pub fn displays(&self) -> Vec<SCDisplay> {
198        unsafe {
199            let count = crate::ffi::sc_shareable_content_get_displays_count(self.0);
200            // FFI returns isize but count is always positive
201            #[allow(clippy::cast_sign_loss)]
202            let mut displays = Vec::with_capacity(count as usize);
203
204            for i in 0..count {
205                let display_ptr = crate::ffi::sc_shareable_content_get_display_at(self.0, i);
206                if !display_ptr.is_null() {
207                    displays.push(SCDisplay::from_ptr(display_ptr));
208                }
209            }
210
211            displays
212        }
213    }
214
215    /// Get all available windows
216    ///
217    /// # Examples
218    ///
219    /// ```no_run
220    /// use screencapturekit::shareable_content::SCShareableContent;
221    ///
222    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
223    /// let content = SCShareableContent::get()?;
224    /// for window in content.windows() {
225    ///     if let Some(title) = window.title() {
226    ///         println!("Window: {}", title);
227    ///     }
228    /// }
229    /// # Ok(())
230    /// # }
231    /// ```
232    pub fn windows(&self) -> Vec<SCWindow> {
233        unsafe {
234            let count = crate::ffi::sc_shareable_content_get_windows_count(self.0);
235            // FFI returns isize but count is always positive
236            #[allow(clippy::cast_sign_loss)]
237            let mut windows = Vec::with_capacity(count as usize);
238
239            for i in 0..count {
240                let window_ptr = crate::ffi::sc_shareable_content_get_window_at(self.0, i);
241                if !window_ptr.is_null() {
242                    windows.push(SCWindow::from_ptr(window_ptr));
243                }
244            }
245
246            windows
247        }
248    }
249
250    /// Get all available running applications
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// use screencapturekit::shareable_content::SCShareableContent;
256    ///
257    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
258    /// let content = SCShareableContent::get()?;
259    /// for app in content.applications() {
260    ///     println!("App: {} (PID: {})", app.application_name(), app.process_id());
261    /// }
262    /// # Ok(())
263    /// # }
264    /// ```
265    pub fn applications(&self) -> Vec<SCRunningApplication> {
266        unsafe {
267            let count = crate::ffi::sc_shareable_content_get_applications_count(self.0);
268            // FFI returns isize but count is always positive
269            #[allow(clippy::cast_sign_loss)]
270            let mut apps = Vec::with_capacity(count as usize);
271
272            for i in 0..count {
273                let app_ptr = crate::ffi::sc_shareable_content_get_application_at(self.0, i);
274                if !app_ptr.is_null() {
275                    apps.push(SCRunningApplication::from_ptr(app_ptr));
276                }
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
289impl Drop for SCShareableContent {
290    fn drop(&mut self) {
291        if !self.0.is_null() {
292            unsafe {
293                crate::ffi::sc_shareable_content_release(self.0);
294            }
295        }
296    }
297}
298
299impl fmt::Debug for SCShareableContent {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        f.debug_struct("SCShareableContent")
302            .field("displays", &self.displays().len())
303            .field("windows", &self.windows().len())
304            .field("applications", &self.applications().len())
305            .finish()
306    }
307}
308
309impl fmt::Display for SCShareableContent {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        write!(
312            f,
313            "SCShareableContent ({} displays, {} windows, {} applications)",
314            self.displays().len(),
315            self.windows().len(),
316            self.applications().len()
317        )
318    }
319}
320
321#[derive(Default, Debug, Clone, PartialEq, Eq)]
322pub struct SCShareableContentOptions {
323    exclude_desktop_windows: bool,
324    on_screen_windows_only: bool,
325}
326
327impl SCShareableContentOptions {
328    /// Exclude desktop windows from the shareable content.
329    ///
330    /// When set to `true`, desktop-level windows (like the desktop background)
331    /// are excluded from the returned window list.
332    #[must_use]
333    pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
334        self.exclude_desktop_windows = exclude;
335        self
336    }
337
338    /// Include only on-screen windows in the shareable content.
339    ///
340    /// When set to `true`, only windows that are currently visible on screen
341    /// are included. Minimized or off-screen windows are excluded.
342    #[must_use]
343    pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
344        self.on_screen_windows_only = on_screen_only;
345        self
346    }
347
348    // =========================================================================
349    // Deprecated methods - use with_* versions instead
350    // =========================================================================
351
352    /// Exclude desktop windows from the shareable content.
353    #[must_use]
354    #[deprecated(since = "1.5.0", note = "Use with_exclude_desktop_windows() instead")]
355    pub fn exclude_desktop_windows(self, exclude: bool) -> Self {
356        self.with_exclude_desktop_windows(exclude)
357    }
358
359    /// Include only on-screen windows in the shareable content.
360    #[must_use]
361    #[deprecated(since = "1.5.0", note = "Use with_on_screen_windows_only() instead")]
362    pub fn on_screen_windows_only(self, on_screen_only: bool) -> Self {
363        self.with_on_screen_windows_only(on_screen_only)
364    }
365
366    /// Get shareable content synchronously
367    ///
368    /// This blocks until the content is retrieved.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error if screen recording permission is not granted or retrieval fails.
373    pub fn get(self) -> Result<SCShareableContent, SCError> {
374        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
375
376        unsafe {
377            crate::ffi::sc_shareable_content_get_with_options(
378                self.exclude_desktop_windows,
379                self.on_screen_windows_only,
380                shareable_content_callback,
381                context,
382            );
383        }
384
385        completion.wait().map_err(SCError::NoShareableContent)
386    }
387
388    /// Get shareable content with only windows below a reference window
389    ///
390    /// This returns windows that are stacked below the specified reference window
391    /// in the window layering order.
392    ///
393    /// # Arguments
394    ///
395    /// * `reference_window` - The window to use as the reference point
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if screen recording permission is not granted or retrieval fails.
400    pub fn below_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
401        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
402
403        unsafe {
404            crate::ffi::sc_shareable_content_get_below_window(
405                self.exclude_desktop_windows,
406                reference_window.as_ptr(),
407                shareable_content_callback,
408                context,
409            );
410        }
411
412        completion.wait().map_err(SCError::NoShareableContent)
413    }
414
415    /// Get shareable content with only windows above a reference window
416    ///
417    /// This returns windows that are stacked above the specified reference window
418    /// in the window layering order.
419    ///
420    /// # Arguments
421    ///
422    /// * `reference_window` - The window to use as the reference point
423    ///
424    /// # Errors
425    ///
426    /// Returns an error if screen recording permission is not granted or retrieval fails.
427    pub fn above_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
428        let (completion, context) = SyncCompletion::<SCShareableContent>::new();
429
430        unsafe {
431            crate::ffi::sc_shareable_content_get_above_window(
432                self.exclude_desktop_windows,
433                reference_window.as_ptr(),
434                shareable_content_callback,
435                context,
436            );
437        }
438
439        completion.wait().map_err(SCError::NoShareableContent)
440    }
441}
442
443impl SCShareableContent {
444    /// Get shareable content for the current process only (macOS 14.4+)
445    ///
446    /// This retrieves content that the current process can capture without
447    /// requiring user authorization via TCC (Transparency, Consent, and Control).
448    ///
449    /// # Errors
450    ///
451    /// Returns an error if retrieval fails.
452    #[cfg(feature = "macos_14_4")]
453    pub fn current_process() -> Result<Self, SCError> {
454        let (completion, context) = SyncCompletion::<Self>::new();
455
456        unsafe {
457            crate::ffi::sc_shareable_content_get_current_process_displays(
458                shareable_content_callback,
459                context,
460            );
461        }
462
463        completion.wait().map_err(SCError::NoShareableContent)
464    }
465}
466
467// MARK: - SCShareableContentInfo (macOS 14.0+)
468
469/// Information about shareable content from a filter (macOS 14.0+)
470///
471/// Provides metadata about the content being captured, including dimensions and scale factor.
472#[cfg(feature = "macos_14_0")]
473pub struct SCShareableContentInfo(*const c_void);
474
475#[cfg(feature = "macos_14_0")]
476impl SCShareableContentInfo {
477    /// Get content info for a filter
478    ///
479    /// Returns information about the content described by the given filter.
480    pub fn for_filter(filter: &crate::stream::content_filter::SCContentFilter) -> Option<Self> {
481        let ptr = unsafe { crate::ffi::sc_shareable_content_info_for_filter(filter.as_ptr()) };
482        if ptr.is_null() {
483            None
484        } else {
485            Some(Self(ptr))
486        }
487    }
488
489    /// Get the content style
490    pub fn style(&self) -> crate::stream::content_filter::SCShareableContentStyle {
491        let value = unsafe { crate::ffi::sc_shareable_content_info_get_style(self.0) };
492        crate::stream::content_filter::SCShareableContentStyle::from(value)
493    }
494
495    /// Get the point-to-pixel scale factor
496    ///
497    /// Typically 2.0 for Retina displays.
498    pub fn point_pixel_scale(&self) -> f32 {
499        unsafe { crate::ffi::sc_shareable_content_info_get_point_pixel_scale(self.0) }
500    }
501
502    /// Get the content rectangle in points
503    pub fn content_rect(&self) -> crate::cg::CGRect {
504        let mut x = 0.0;
505        let mut y = 0.0;
506        let mut width = 0.0;
507        let mut height = 0.0;
508        unsafe {
509            crate::ffi::sc_shareable_content_info_get_content_rect(
510                self.0,
511                &mut x,
512                &mut y,
513                &mut width,
514                &mut height,
515            );
516        }
517        crate::cg::CGRect::new(x, y, width, height)
518    }
519
520    /// Get the content size in pixels
521    ///
522    /// Convenience method that multiplies `content_rect` dimensions by `point_pixel_scale`.
523    pub fn pixel_size(&self) -> (u32, u32) {
524        let rect = self.content_rect();
525        let scale = self.point_pixel_scale();
526        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
527        let width = (rect.width * f64::from(scale)) as u32;
528        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
529        let height = (rect.height * f64::from(scale)) as u32;
530        (width, height)
531    }
532}
533
534#[cfg(feature = "macos_14_0")]
535impl Drop for SCShareableContentInfo {
536    fn drop(&mut self) {
537        if !self.0.is_null() {
538            unsafe {
539                crate::ffi::sc_shareable_content_info_release(self.0);
540            }
541        }
542    }
543}
544
545#[cfg(feature = "macos_14_0")]
546impl Clone for SCShareableContentInfo {
547    fn clone(&self) -> Self {
548        unsafe { Self(crate::ffi::sc_shareable_content_info_retain(self.0)) }
549    }
550}
551
552#[cfg(feature = "macos_14_0")]
553impl fmt::Debug for SCShareableContentInfo {
554    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555        f.debug_struct("SCShareableContentInfo")
556            .field("style", &self.style())
557            .field("point_pixel_scale", &self.point_pixel_scale())
558            .field("content_rect", &self.content_rect())
559            .finish()
560    }
561}
562
563#[cfg(feature = "macos_14_0")]
564impl fmt::Display for SCShareableContentInfo {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        let (width, height) = self.pixel_size();
567        write!(
568            f,
569            "ContentInfo({:?}, {}x{} px, scale: {})",
570            self.style(),
571            width,
572            height,
573            self.point_pixel_scale()
574        )
575    }
576}
577
578#[cfg(feature = "macos_14_0")]
579unsafe impl Send for SCShareableContentInfo {}
580#[cfg(feature = "macos_14_0")]
581unsafe impl Sync for SCShareableContentInfo {}