screencapturekit/stream/
content_filter.rs

1//! Content filter for `ScreenCaptureKit` streams
2//!
3//! This module provides a wrapper around `SCContentFilter` that uses the Swift bridge.
4//!
5//! # Examples
6//!
7//! ```no_run
8//! use screencapturekit::shareable_content::SCShareableContent;
9//! use screencapturekit::stream::content_filter::SCContentFilter;
10//!
11//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! let content = SCShareableContent::get()?;
13//! let display = &content.displays()[0];
14//!
15//! // Capture entire display
16//! let filter = SCContentFilter::builder()
17//!     .display(display)
18//!     .exclude_windows(&[])
19//!     .build();
20//! # Ok(())
21//! # }
22//! ```
23
24use std::ffi::c_void;
25use std::fmt;
26
27use crate::{
28    ffi,
29    shareable_content::{SCDisplay, SCRunningApplication, SCWindow},
30};
31
32/// Content filter for `ScreenCaptureKit` streams
33///
34/// Defines what content to capture (displays, windows, or applications).
35///
36/// # Examples
37///
38/// ```no_run
39/// use screencapturekit::shareable_content::SCShareableContent;
40/// use screencapturekit::stream::content_filter::SCContentFilter;
41///
42/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
43/// let content = SCShareableContent::get()?;
44/// let display = &content.displays()[0];
45///
46/// // Capture entire display
47/// let filter = SCContentFilter::builder()
48///     .display(display)
49///     .exclude_windows(&[])
50///     .build();
51///
52/// // Or capture a specific window
53/// let window = &content.windows()[0];
54/// let filter = SCContentFilter::builder()
55///     .window(window)
56///     .build();
57/// # Ok(())
58/// # }
59/// ```
60pub struct SCContentFilter(*const c_void);
61
62impl PartialEq for SCContentFilter {
63    fn eq(&self, other: &Self) -> bool {
64        self.0 == other.0
65    }
66}
67
68impl Eq for SCContentFilter {}
69
70impl std::hash::Hash for SCContentFilter {
71    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
72        self.0.hash(state);
73    }
74}
75
76impl Default for SCContentFilter {
77    fn default() -> Self {
78        Self(std::ptr::null())
79    }
80}
81
82impl SCContentFilter {
83    /// Creates a content filter builder
84    ///
85    /// # Examples
86    ///
87    /// ```no_run
88    /// use screencapturekit::prelude::*;
89    ///
90    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
91    /// let content = SCShareableContent::get()?;
92    /// let display = &content.displays()[0];
93    ///
94    /// let filter = SCContentFilter::builder()
95    ///     .display(display)
96    ///     .exclude_windows(&[])
97    ///     .build();
98    /// # Ok(())
99    /// # }
100    /// ```
101    #[must_use]
102    pub fn builder() -> SCContentFilterBuilder {
103        SCContentFilterBuilder::new()
104    }
105
106    /// Creates a new content filter builder (deprecated alias)
107    #[deprecated(since = "1.0.0", note = "Use `builder()` instead")]
108    #[must_use]
109    pub fn build() -> SCContentFilterBuilder {
110        SCContentFilterBuilder::new()
111    }
112
113    /// Returns the raw pointer to the content filter
114    pub(crate) fn as_ptr(&self) -> *const c_void {
115        self.0
116    }
117
118    /// Sets the content rectangle for this filter (macOS 14.2+)
119    ///
120    /// Specifies the rectangle within the content filter to capture.
121    #[must_use]
122    pub fn set_content_rect(self, rect: crate::stream::configuration::Rect) -> Self {
123        unsafe {
124            ffi::sc_content_filter_set_content_rect(
125                self.0,
126                rect.origin.x,
127                rect.origin.y,
128                rect.size.width,
129                rect.size.height,
130            );
131        }
132        self
133    }
134
135    /// Gets the content rectangle for this filter (macOS 14.2+)
136    pub fn get_content_rect(&self) -> crate::stream::configuration::Rect {
137        unsafe {
138            let mut x = 0.0;
139            let mut y = 0.0;
140            let mut width = 0.0;
141            let mut height = 0.0;
142            ffi::sc_content_filter_get_content_rect(
143                self.0,
144                &mut x,
145                &mut y,
146                &mut width,
147                &mut height,
148            );
149            crate::stream::configuration::Rect::new(
150                crate::stream::configuration::Point::new(x, y),
151                crate::stream::configuration::Size::new(width, height),
152            )
153        }
154    }
155}
156
157impl Drop for SCContentFilter {
158    fn drop(&mut self) {
159        unsafe {
160            ffi::sc_content_filter_release(self.0);
161        }
162    }
163}
164
165// TCFType compatibility for legacy objc-based code
166
167pub type SCContentFilterRef = *const c_void;
168
169extern "C" {}
170
171impl Clone for SCContentFilter {
172    fn clone(&self) -> Self {
173        unsafe { Self(crate::ffi::sc_content_filter_retain(self.0)) }
174    }
175}
176
177impl fmt::Debug for SCContentFilter {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("SCContentFilter")
180            .field("ptr", &self.0)
181            .finish()
182    }
183}
184
185impl fmt::Display for SCContentFilter {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(f, "SCContentFilter")
188    }
189}
190
191// Safety: SCContentFilter wraps an Objective-C object that is thread-safe
192// The underlying SCContentFilter object can be safely sent between threads
193unsafe impl Send for SCContentFilter {}
194unsafe impl Sync for SCContentFilter {}
195
196/// Builder for creating `SCContentFilter` instances
197///
198/// # Examples
199///
200/// ```no_run
201/// use screencapturekit::prelude::*;
202///
203/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
204/// let content = SCShareableContent::get()?;
205/// let display = &content.displays()[0];
206///
207/// // Capture entire display
208/// let filter = SCContentFilter::builder()
209///     .display(display)
210///     .exclude_windows(&[])
211///     .build();
212///
213/// // Capture with specific windows excluded
214/// let window = &content.windows()[0];
215/// let filter = SCContentFilter::builder()
216///     .display(display)
217///     .exclude_windows(&[window])
218///     .build();
219///
220/// // Capture specific window
221/// let filter = SCContentFilter::builder()
222///     .window(window)
223///     .build();
224/// # Ok(())
225/// # }
226/// ```
227pub struct SCContentFilterBuilder {
228    filter_type: FilterType,
229    content_rect: Option<crate::stream::configuration::Rect>,
230}
231
232enum FilterType {
233    None,
234    Window(SCWindow),
235    DisplayExcluding {
236        display: SCDisplay,
237        windows: Vec<SCWindow>,
238    },
239    DisplayIncluding {
240        display: SCDisplay,
241        windows: Vec<SCWindow>,
242    },
243    DisplayApplications {
244        display: SCDisplay,
245        applications: Vec<SCRunningApplication>,
246        excepting_windows: Vec<SCWindow>,
247    },
248}
249
250impl SCContentFilterBuilder {
251    fn new() -> Self {
252        Self {
253            filter_type: FilterType::None,
254            content_rect: None,
255        }
256    }
257
258    /// Set the display to capture
259    #[must_use]
260    pub fn display(mut self, display: &SCDisplay) -> Self {
261        self.filter_type = FilterType::DisplayExcluding {
262            display: display.clone(),
263            windows: Vec::new(),
264        };
265        self
266    }
267
268    /// Set the window to capture
269    #[must_use]
270    pub fn window(mut self, window: &SCWindow) -> Self {
271        self.filter_type = FilterType::Window(window.clone());
272        self
273    }
274
275    /// Exclude specific windows from the display capture
276    #[must_use]
277    pub fn exclude_windows(mut self, windows: &[&SCWindow]) -> Self {
278        if let FilterType::DisplayExcluding {
279            windows: ref mut excluded,
280            ..
281        } = self.filter_type
282        {
283            *excluded = windows.iter().map(|w| (*w).clone()).collect();
284        }
285        self
286    }
287
288    /// Include only specific windows in the display capture
289    #[must_use]
290    pub fn include_windows(mut self, windows: &[&SCWindow]) -> Self {
291        if let FilterType::DisplayExcluding { display, .. } = self.filter_type {
292            self.filter_type = FilterType::DisplayIncluding {
293                display,
294                windows: windows.iter().map(|w| (*w).clone()).collect(),
295            };
296        }
297        self
298    }
299
300    /// Include specific applications and optionally except certain windows
301    #[must_use]
302    pub fn include_applications(
303        mut self,
304        applications: &[&SCRunningApplication],
305        excepting_windows: &[&SCWindow],
306    ) -> Self {
307        if let FilterType::DisplayExcluding { display, .. }
308        | FilterType::DisplayIncluding { display, .. } = self.filter_type
309        {
310            self.filter_type = FilterType::DisplayApplications {
311                display,
312                applications: applications.iter().map(|a| (*a).clone()).collect(),
313                excepting_windows: excepting_windows.iter().map(|w| (*w).clone()).collect(),
314            };
315        }
316        self
317    }
318
319    /// Set the content rectangle (macOS 14.2+)
320    #[must_use]
321    pub fn content_rect(mut self, rect: crate::stream::configuration::Rect) -> Self {
322        self.content_rect = Some(rect);
323        self
324    }
325
326    /// Build the content filter
327    #[must_use]
328    pub fn build(self) -> SCContentFilter {
329        let filter = match self.filter_type {
330            FilterType::Window(window) => unsafe {
331                let ptr =
332                    ffi::sc_content_filter_create_with_desktop_independent_window(window.as_ptr());
333                SCContentFilter(ptr)
334            },
335            FilterType::DisplayExcluding { display, windows } => {
336                let window_refs: Vec<&SCWindow> = windows.iter().collect();
337                unsafe {
338                    let window_ptrs: Vec<*const c_void> =
339                        window_refs.iter().map(|w| w.as_ptr()).collect();
340
341                    let ptr = if window_ptrs.is_empty() {
342                        ffi::sc_content_filter_create_with_display_excluding_windows(
343                            display.as_ptr(),
344                            std::ptr::null(),
345                            0,
346                        )
347                    } else {
348                        #[allow(clippy::cast_possible_wrap)]
349                        ffi::sc_content_filter_create_with_display_excluding_windows(
350                            display.as_ptr(),
351                            window_ptrs.as_ptr(),
352                            window_ptrs.len() as isize,
353                        )
354                    };
355                    SCContentFilter(ptr)
356                }
357            }
358            FilterType::DisplayIncluding { display, windows } => {
359                let window_refs: Vec<&SCWindow> = windows.iter().collect();
360                unsafe {
361                    let window_ptrs: Vec<*const c_void> =
362                        window_refs.iter().map(|w| w.as_ptr()).collect();
363
364                    let ptr = if window_ptrs.is_empty() {
365                        ffi::sc_content_filter_create_with_display_including_windows(
366                            display.as_ptr(),
367                            std::ptr::null(),
368                            0,
369                        )
370                    } else {
371                        #[allow(clippy::cast_possible_wrap)]
372                        ffi::sc_content_filter_create_with_display_including_windows(
373                            display.as_ptr(),
374                            window_ptrs.as_ptr(),
375                            window_ptrs.len() as isize,
376                        )
377                    };
378                    SCContentFilter(ptr)
379                }
380            }
381            FilterType::DisplayApplications {
382                display,
383                applications,
384                excepting_windows,
385            } => {
386                let app_refs: Vec<&SCRunningApplication> = applications.iter().collect();
387                let window_refs: Vec<&SCWindow> = excepting_windows.iter().collect();
388                unsafe {
389                    let app_ptrs: Vec<*const c_void> =
390                        app_refs.iter().map(|a| a.as_ptr()).collect();
391
392                    let window_ptrs: Vec<*const c_void> =
393                        window_refs.iter().map(|w| w.as_ptr()).collect();
394
395                    #[allow(clippy::cast_possible_wrap)]
396                    let ptr = ffi::sc_content_filter_create_with_display_including_applications_excepting_windows(
397                        display.as_ptr(),
398                        if app_ptrs.is_empty() { std::ptr::null() } else { app_ptrs.as_ptr() },
399                        app_ptrs.len() as isize,
400                        if window_ptrs.is_empty() { std::ptr::null() } else { window_ptrs.as_ptr() },
401                        window_ptrs.len() as isize,
402                    );
403                    SCContentFilter(ptr)
404                }
405            }
406            FilterType::None => {
407                // Return a null filter
408                SCContentFilter(std::ptr::null())
409            }
410        };
411
412        // Apply content rect if set
413        if let Some(rect) = self.content_rect {
414            filter.set_content_rect(rect)
415        } else {
416            filter
417        }
418    }
419}