Skip to main content

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::create()
17//!     .with_display(display)
18//!     .with_excluding_windows(&[])
19//!     .build()?;
20//! # Ok(())
21//! # }
22//! ```
23
24use std::ffi::c_void;
25use std::fmt;
26
27#[cfg(feature = "macos_14_0")]
28use crate::cg::CGRect;
29use crate::{
30    error::{SCError, SCResult},
31    ffi,
32    shareable_content::{SCDisplay, SCRunningApplication, SCWindow},
33};
34
35/// Content filter for `ScreenCaptureKit` streams
36///
37/// Defines what content to capture (displays, windows, or applications).
38///
39/// # Immutability, `Clone`, `Send` and `Sync`
40///
41/// An `SCContentFilter` is **immutable once built**. Every method on it is a
42/// read; the one property Apple declares as writable, `includeMenuBar`, is set
43/// by [`SCContentFilterBuilder::with_include_menu_bar`] while the underlying
44/// object is still uniquely owned by the builder and has not yet escaped.
45///
46/// That invariant is what makes the three otherwise-conflicting properties of
47/// this type sound together:
48///
49/// - **`Clone` aliases.** `SCContentFilter` is a plain `NSObject`: Apple
50///   provides no copy initialiser and does not conform it to `NSCopying`, so a
51///   deep copy is impossible. Cloning therefore performs an Objective-C
52///   `retain` and hands back a second handle to the *same* object.
53/// - **`Send + Sync` are `unsafe impl`s.** They promise that sharing a handle
54///   across threads is safe.
55/// - **`includeMenuBar` is `@property(nonatomic, assign)`** — an unsynchronised
56///   `BOOL` ivar.
57///
58/// Exposing a setter alongside an aliasing `Clone` and `Sync` would let two
59/// threads write and read that ivar concurrently through safe Rust, which is a
60/// data race. Removing the setter (rather than `Clone` or `Send`/`Sync`) keeps
61/// the ergonomic handle semantics while leaving nothing to race on. Filters
62/// obtained from the content sharing picker keep whatever `includeMenuBar`
63/// value the system chose; build your own filter if you need to override it.
64///
65/// # Examples
66///
67/// ```no_run
68/// use screencapturekit::shareable_content::SCShareableContent;
69/// use screencapturekit::stream::content_filter::SCContentFilter;
70///
71/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
72/// let content = SCShareableContent::get()?;
73/// let display = &content.displays()[0];
74///
75/// // Capture entire display
76/// let filter = SCContentFilter::create()
77///     .with_display(display)
78///     .with_excluding_windows(&[])
79///     .build()?;
80///
81/// // Or capture a specific window
82/// let window = &content.windows()[0];
83/// let filter = SCContentFilter::create()
84///     .with_window(window)
85///     .build()?;
86/// # Ok(())
87/// # }
88/// ```
89pub struct SCContentFilter(*const c_void);
90
91impl PartialEq for SCContentFilter {
92    fn eq(&self, other: &Self) -> bool {
93        self.0 == other.0
94    }
95}
96
97impl Eq for SCContentFilter {}
98
99impl std::hash::Hash for SCContentFilter {
100    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
101        self.0.hash(state);
102    }
103}
104
105// Note: We intentionally do NOT implement Default for SCContentFilter.
106// A null filter would cause panics/crashes when used with SCStream.
107// Users should always use SCContentFilter::create() to create valid filters.
108
109impl SCContentFilter {
110    /// Creates a content filter builder
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use screencapturekit::prelude::*;
116    ///
117    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
118    /// let content = SCShareableContent::get()?;
119    /// let display = &content.displays()[0];
120    ///
121    /// let filter = SCContentFilter::create()
122    ///     .with_display(display)
123    ///     .with_excluding_windows(&[])
124    ///     .build()?;
125    /// # Ok(())
126    /// # }
127    /// ```
128    #[must_use]
129    pub fn create() -> SCContentFilterBuilder {
130        SCContentFilterBuilder::new()
131    }
132
133    /// Creates a content filter from a picker-returned pointer
134    ///
135    /// This is used internally when the content sharing picker returns a filter.
136    #[cfg(feature = "macos_14_0")]
137    pub(crate) fn from_picker_ptr(ptr: *const c_void) -> Self {
138        Self(ptr)
139    }
140
141    /// Returns the raw pointer to the content filter
142    pub(crate) fn as_ptr(&self) -> *const c_void {
143        self.0
144    }
145
146    /// Gets the content rectangle for this filter (macOS 14.0+)
147    ///
148    /// This mirrors Apple's read-only `SCContentFilter.contentRect`: the rect,
149    /// in points, that the filter's content occupies. There is no setter —
150    /// `SCContentFilter` derives the rect from the display/window/application
151    /// it was built from. Returns a zero rect on macOS < 14.0.
152    #[cfg(feature = "macos_14_0")]
153    pub fn content_rect(&self) -> CGRect {
154        unsafe {
155            let mut x = 0.0;
156            let mut y = 0.0;
157            let mut width = 0.0;
158            let mut height = 0.0;
159            ffi::sc_content_filter_get_content_rect(
160                self.0,
161                &raw mut x,
162                &raw mut y,
163                &raw mut width,
164                &raw mut height,
165            );
166            CGRect::new(x, y, width, height)
167        }
168    }
169
170    /// Get the content style (macOS 14.0+)
171    ///
172    /// Returns the type of content being captured (window, display, application, or none).
173    #[cfg(feature = "macos_14_0")]
174    #[allow(clippy::missing_errors_doc)]
175    pub fn style(&self) -> SCResult<SCShareableContentStyle> {
176        let mut raw = 0_i32;
177        if !unsafe { ffi::sc_content_filter_get_style(self.0, &raw mut raw) } {
178            return Err(SCError::feature_not_available(
179                "SCContentFilter.style",
180                "14.0",
181            ));
182        }
183        SCShareableContentStyle::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
184            type_name: "SCShareableContentStyle",
185            raw: i64::from(raw),
186        })
187    }
188
189    /// Get the stream type (macOS 14.0+)
190    ///
191    /// Returns whether this filter captures a window or a display.
192    #[cfg(feature = "macos_14_0")]
193    #[deprecated(
194        since = "8.0.0",
195        note = "Apple deprecated SCContentFilter.streamType in macOS 14.2 (and SCStreamType \
196                itself in 15.0). Use `style()`, which also distinguishes application filters."
197    )]
198    #[allow(deprecated)]
199    #[allow(clippy::missing_errors_doc)]
200    pub fn stream_type(&self) -> SCResult<SCStreamType> {
201        let mut raw = 0_i32;
202        if !unsafe { ffi::sc_content_filter_get_stream_type(self.0, &raw mut raw) } {
203            return Err(SCError::feature_not_available(
204                "SCContentFilter.streamType",
205                "14.0",
206            ));
207        }
208        SCStreamType::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
209            type_name: "SCStreamType",
210            raw: i64::from(raw),
211        })
212    }
213
214    /// Get the point-to-pixel scale factor (macOS 14.0+)
215    ///
216    /// Returns the scaling factor used to convert points to pixels.
217    /// Typically 2.0 for Retina displays.
218    #[cfg(feature = "macos_14_0")]
219    pub fn point_pixel_scale(&self) -> f32 {
220        unsafe { ffi::sc_content_filter_get_point_pixel_scale(self.0) }
221    }
222
223    /// Whether the menu bar is included in capture (macOS 14.2+)
224    ///
225    /// Fixed when the filter is built. Apple's default depends on the
226    /// constructor — `true` for display-excluding filters, `false` for
227    /// display-including ones — and is overridden by
228    /// [`SCContentFilterBuilder::with_include_menu_bar`].
229    ///
230    /// There is deliberately no setter: `SCContentFilter` is immutable once it
231    /// escapes the builder, which is what makes [`Clone`], [`Send`] and
232    /// [`Sync`] sound for this handle. See the type-level docs.
233    #[cfg(feature = "macos_14_2")]
234    pub fn include_menu_bar(&self) -> bool {
235        unsafe { ffi::sc_content_filter_get_include_menu_bar(self.0) }
236    }
237
238    /// Get included displays (macOS 15.2+)
239    ///
240    /// Returns the displays currently included in this filter.
241    #[cfg(feature = "macos_15_2")]
242    pub fn included_displays(&self) -> Vec<SCDisplay> {
243        let count = unsafe { ffi::sc_content_filter_get_included_displays_count(self.0) };
244        if count <= 0 {
245            return Vec::new();
246        }
247        #[allow(clippy::cast_sign_loss)]
248        (0..count as usize)
249            .filter_map(|i| {
250                #[allow(clippy::cast_possible_wrap)]
251                let ptr =
252                    unsafe { ffi::sc_content_filter_get_included_display_at(self.0, i as isize) };
253                unsafe { SCDisplay::from_retained_ptr(ptr) }
254            })
255            .collect()
256    }
257
258    /// Get included windows (macOS 15.2+)
259    ///
260    /// Returns the windows currently included in this filter.
261    #[cfg(feature = "macos_15_2")]
262    pub fn included_windows(&self) -> Vec<SCWindow> {
263        let count = unsafe { ffi::sc_content_filter_get_included_windows_count(self.0) };
264        if count <= 0 {
265            return Vec::new();
266        }
267        #[allow(clippy::cast_sign_loss)]
268        (0..count as usize)
269            .filter_map(|i| {
270                #[allow(clippy::cast_possible_wrap)]
271                let ptr =
272                    unsafe { ffi::sc_content_filter_get_included_window_at(self.0, i as isize) };
273                unsafe { SCWindow::from_retained_ptr(ptr) }
274            })
275            .collect()
276    }
277
278    /// Get included applications (macOS 15.2+)
279    ///
280    /// Returns the applications currently included in this filter.
281    #[cfg(feature = "macos_15_2")]
282    pub fn included_applications(&self) -> Vec<SCRunningApplication> {
283        let count = unsafe { ffi::sc_content_filter_get_included_applications_count(self.0) };
284        if count <= 0 {
285            return Vec::new();
286        }
287        #[allow(clippy::cast_sign_loss)]
288        (0..count as usize)
289            .filter_map(|i| {
290                #[allow(clippy::cast_possible_wrap)]
291                let ptr = unsafe {
292                    ffi::sc_content_filter_get_included_application_at(self.0, i as isize)
293                };
294                unsafe { SCRunningApplication::from_retained_ptr(ptr) }
295            })
296            .collect()
297    }
298}
299
300/// Content style for filters (macOS 14.0+)
301#[repr(i32)]
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
303#[cfg(feature = "macos_14_0")]
304pub enum SCShareableContentStyle {
305    /// No specific content type
306    #[default]
307    None = 0,
308    /// Window-based content
309    Window = 1,
310    /// Display-based content
311    Display = 2,
312    /// Application-based content
313    Application = 3,
314}
315
316#[cfg(feature = "macos_14_0")]
317impl SCShareableContentStyle {
318    pub const fn from_raw(raw: i32) -> Option<Self> {
319        match raw {
320            0 => Some(Self::None),
321            1 => Some(Self::Window),
322            2 => Some(Self::Display),
323            3 => Some(Self::Application),
324            _ => None,
325        }
326    }
327}
328
329#[cfg(feature = "macos_14_0")]
330impl std::fmt::Display for SCShareableContentStyle {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        match self {
333            Self::None => write!(f, "None"),
334            Self::Window => write!(f, "Window"),
335            Self::Display => write!(f, "Display"),
336            Self::Application => write!(f, "Application"),
337        }
338    }
339}
340
341/// Stream type for filters (macOS 14.0+)
342#[repr(i32)]
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
344#[cfg(feature = "macos_14_0")]
345#[deprecated(
346    since = "8.0.0",
347    note = "Apple deprecated SCStreamType in macOS 15.0. Use SCShareableContentStyle instead."
348)]
349#[allow(deprecated)]
350pub enum SCStreamType {
351    /// Window-based stream
352    #[default]
353    Window = 0,
354    /// Display-based stream
355    Display = 1,
356}
357
358#[cfg(feature = "macos_14_0")]
359#[allow(deprecated)]
360impl SCStreamType {
361    pub const fn from_raw(raw: i32) -> Option<Self> {
362        match raw {
363            0 => Some(Self::Window),
364            1 => Some(Self::Display),
365            _ => None,
366        }
367    }
368}
369
370#[cfg(feature = "macos_14_0")]
371#[allow(deprecated)]
372impl std::fmt::Display for SCStreamType {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        match self {
375            Self::Window => write!(f, "Window"),
376            Self::Display => write!(f, "Display"),
377        }
378    }
379}
380
381// `Clone::clone` is not a `memcpy` and not a deep copy: `SCContentFilter` is a
382// plain `NSObject` with no copy initialiser and no `NSCopying` conformance, so
383// the clone crosses the Swift FFI boundary, calls `sc_content_filter_retain`
384// (an Objective-C `retain`) and returns a second handle to the same object.
385// That aliasing is only sound because the type exposes no mutation — see the
386// `SCContentFilter` docs. For hot-path code that needs many references to the
387// same filter, prefer `Arc<SCContentFilter>` over per-call `.clone()`.
388crate::utils::retained::sc_retained!(
389    SCContentFilter,
390    retain = crate::ffi::sc_content_filter_retain,
391    release = crate::ffi::sc_content_filter_release,
392);
393
394impl fmt::Debug for SCContentFilter {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        f.debug_struct("SCContentFilter")
397            .field("ptr", &self.0)
398            .finish()
399    }
400}
401
402impl fmt::Display for SCContentFilter {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        write!(f, "SCContentFilter")
405    }
406}
407
408// SAFETY: every `SCContentFilter` method is a read of a property that is fixed
409// when the object is built, so no two threads can ever write — or read while
410// another writes — the same Objective-C ivar through safe Rust. `includeMenuBar`
411// is Apple's only writable property and it is `nonatomic`; it is assigned once
412// inside `SCContentFilterBuilder::build`, before the pointer is wrapped and
413// therefore before any handle (or clone) exists that another thread could
414// observe. Adding any post-construction setter would invalidate both impls.
415unsafe impl Send for SCContentFilter {}
416unsafe impl Sync for SCContentFilter {}
417
418/// Builder for creating `SCContentFilter` instances
419///
420/// # Examples
421///
422/// ```no_run
423/// use screencapturekit::prelude::*;
424///
425/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
426/// let content = SCShareableContent::get()?;
427/// let display = &content.displays()[0];
428///
429/// // Capture entire display
430/// let filter = SCContentFilter::create()
431///     .with_display(display)
432///     .with_excluding_windows(&[])
433///     .build()?;
434///
435/// // Capture with specific windows excluded
436/// let window = &content.windows()[0];
437/// let filter = SCContentFilter::create()
438///     .with_display(display)
439///     .with_excluding_windows(&[window])
440///     .build()?;
441///
442/// // Capture specific window
443/// let filter = SCContentFilter::create()
444///     .with_window(window)
445///     .build()?;
446/// # Ok(())
447/// # }
448/// ```
449pub struct SCContentFilterBuilder {
450    filter_type: FilterType,
451    #[cfg(feature = "macos_14_2")]
452    include_menu_bar: Option<bool>,
453}
454
455enum FilterType {
456    None,
457    Window(SCWindow),
458    DisplayExcluding {
459        display: SCDisplay,
460        windows: Vec<SCWindow>,
461    },
462    DisplayIncluding {
463        display: SCDisplay,
464        windows: Vec<SCWindow>,
465    },
466    DisplayIncludingApplications {
467        display: SCDisplay,
468        applications: Vec<SCRunningApplication>,
469        excepting_windows: Vec<SCWindow>,
470    },
471    DisplayExcludingApplications {
472        display: SCDisplay,
473        applications: Vec<SCRunningApplication>,
474        excepting_windows: Vec<SCWindow>,
475    },
476}
477
478impl SCContentFilterBuilder {
479    fn new() -> Self {
480        Self {
481            filter_type: FilterType::None,
482            #[cfg(feature = "macos_14_2")]
483            include_menu_bar: None,
484        }
485    }
486
487    /// Set the display to capture
488    #[must_use]
489    pub fn with_display(mut self, display: &SCDisplay) -> Self {
490        self.filter_type = FilterType::DisplayExcluding {
491            display: display.clone(),
492            windows: Vec::new(),
493        };
494        self
495    }
496
497    /// Set the window to capture
498    #[must_use]
499    pub fn with_window(mut self, window: &SCWindow) -> Self {
500        self.filter_type = FilterType::Window(window.clone());
501        self
502    }
503
504    /// Exclude specific windows from the display capture
505    #[must_use]
506    pub fn with_excluding_windows(mut self, windows: &[&SCWindow]) -> Self {
507        if let FilterType::DisplayExcluding {
508            windows: ref mut excluded,
509            ..
510        } = self.filter_type
511        {
512            // `clone()` on SCWindow is a Swift retain (FFI). Pre-size the Vec
513            // so we don't reallocate while pushing — at 200 windows this is
514            // ~half the per-element cost.
515            let mut v = Vec::with_capacity(windows.len());
516            v.extend(windows.iter().map(|w| (*w).clone()));
517            *excluded = v;
518        }
519        self
520    }
521
522    /// Include only specific windows in the display capture
523    #[must_use]
524    pub fn with_including_windows(mut self, windows: &[&SCWindow]) -> Self {
525        if let FilterType::DisplayExcluding { display, .. } = self.filter_type {
526            let mut v = Vec::with_capacity(windows.len());
527            v.extend(windows.iter().map(|w| (*w).clone()));
528            self.filter_type = FilterType::DisplayIncluding {
529                display,
530                windows: v,
531            };
532        }
533        self
534    }
535
536    /// Include specific applications and optionally except certain windows
537    #[must_use]
538    pub fn with_including_applications(
539        mut self,
540        applications: &[&SCRunningApplication],
541        excepting_windows: &[&SCWindow],
542    ) -> Self {
543        if let FilterType::DisplayExcluding { display, .. }
544        | FilterType::DisplayIncluding { display, .. } = self.filter_type
545        {
546            let mut apps = Vec::with_capacity(applications.len());
547            apps.extend(applications.iter().map(|a| (*a).clone()));
548            let mut wins = Vec::with_capacity(excepting_windows.len());
549            wins.extend(excepting_windows.iter().map(|w| (*w).clone()));
550            self.filter_type = FilterType::DisplayIncludingApplications {
551                display,
552                applications: apps,
553                excepting_windows: wins,
554            };
555        }
556        self
557    }
558
559    /// Exclude specific applications and optionally except certain windows
560    ///
561    /// Captures everything on the display except the specified applications.
562    /// Windows in `excepting_windows` will still be captured even if their
563    /// owning application is excluded.
564    #[must_use]
565    pub fn with_excluding_applications(
566        mut self,
567        applications: &[&SCRunningApplication],
568        excepting_windows: &[&SCWindow],
569    ) -> Self {
570        if let FilterType::DisplayExcluding { display, .. }
571        | FilterType::DisplayIncluding { display, .. } = self.filter_type
572        {
573            let mut apps = Vec::with_capacity(applications.len());
574            apps.extend(applications.iter().map(|a| (*a).clone()));
575            let mut wins = Vec::with_capacity(excepting_windows.len());
576            wins.extend(excepting_windows.iter().map(|w| (*w).clone()));
577            self.filter_type = FilterType::DisplayExcludingApplications {
578                display,
579                applications: apps,
580                excepting_windows: wins,
581            };
582        }
583        self
584    }
585
586    /// Include or exclude the menu bar in display capture (macOS 14.2+)
587    ///
588    /// This is the only way to set Apple's `SCContentFilter.includeMenuBar`:
589    /// the built filter is immutable, so the value is applied here while the
590    /// underlying object is still uniquely owned by the builder (see the
591    /// [`SCContentFilter`] docs for why a post-construction setter would be
592    /// unsound).
593    ///
594    /// Leaving it unset keeps Apple's per-constructor default — `true` for
595    /// display-excluding filters, `false` for display-including ones. The
596    /// property has no effect on desktop-independent window filters.
597    #[cfg(feature = "macos_14_2")]
598    #[must_use]
599    pub fn with_include_menu_bar(mut self, include: bool) -> Self {
600        self.include_menu_bar = Some(include);
601        self
602    }
603
604    /// Build the content filter.
605    ///
606    /// # Errors
607    ///
608    /// Returns [`SCError::InvalidConfiguration`] if neither `.with_display()` nor
609    /// `.with_window()` was called before building, and
610    /// [`SCError::FeatureNotAvailable`] if `.with_include_menu_bar()` was called
611    /// on a system older than macOS 14.2.
612    #[allow(clippy::too_many_lines)]
613    pub fn build(self) -> SCResult<SCContentFilter> {
614        let filter = match self.filter_type {
615            FilterType::Window(window) => unsafe {
616                let ptr =
617                    ffi::sc_content_filter_create_with_desktop_independent_window(window.as_ptr());
618                SCContentFilter(ptr)
619            },
620            FilterType::DisplayExcluding { display, windows } => {
621                let window_refs: Vec<&SCWindow> = windows.iter().collect();
622                unsafe {
623                    let window_ptrs: Vec<*const c_void> =
624                        window_refs.iter().map(|w| w.as_ptr()).collect();
625
626                    let ptr = if window_ptrs.is_empty() {
627                        ffi::sc_content_filter_create_with_display_excluding_windows(
628                            display.as_ptr(),
629                            std::ptr::null(),
630                            0,
631                        )
632                    } else {
633                        #[allow(clippy::cast_possible_wrap)]
634                        ffi::sc_content_filter_create_with_display_excluding_windows(
635                            display.as_ptr(),
636                            window_ptrs.as_ptr(),
637                            window_ptrs.len() as isize,
638                        )
639                    };
640                    SCContentFilter(ptr)
641                }
642            }
643            FilterType::DisplayIncluding { display, windows } => {
644                let window_refs: Vec<&SCWindow> = windows.iter().collect();
645                unsafe {
646                    let window_ptrs: Vec<*const c_void> =
647                        window_refs.iter().map(|w| w.as_ptr()).collect();
648
649                    let ptr = if window_ptrs.is_empty() {
650                        ffi::sc_content_filter_create_with_display_including_windows(
651                            display.as_ptr(),
652                            std::ptr::null(),
653                            0,
654                        )
655                    } else {
656                        #[allow(clippy::cast_possible_wrap)]
657                        ffi::sc_content_filter_create_with_display_including_windows(
658                            display.as_ptr(),
659                            window_ptrs.as_ptr(),
660                            window_ptrs.len() as isize,
661                        )
662                    };
663                    SCContentFilter(ptr)
664                }
665            }
666            FilterType::DisplayIncludingApplications {
667                display,
668                applications,
669                excepting_windows,
670            } => {
671                let app_refs: Vec<&SCRunningApplication> = applications.iter().collect();
672                let window_refs: Vec<&SCWindow> = excepting_windows.iter().collect();
673                unsafe {
674                    let app_ptrs: Vec<*const c_void> =
675                        app_refs.iter().map(|a| a.as_ptr()).collect();
676
677                    let window_ptrs: Vec<*const c_void> =
678                        window_refs.iter().map(|w| w.as_ptr()).collect();
679
680                    #[allow(clippy::cast_possible_wrap)]
681                    let ptr = ffi::sc_content_filter_create_with_display_including_applications_excepting_windows(
682                        display.as_ptr(),
683                        if app_ptrs.is_empty() { std::ptr::null() } else { app_ptrs.as_ptr() },
684                        app_ptrs.len() as isize,
685                        if window_ptrs.is_empty() { std::ptr::null() } else { window_ptrs.as_ptr() },
686                        window_ptrs.len() as isize,
687                    );
688                    SCContentFilter(ptr)
689                }
690            }
691            FilterType::DisplayExcludingApplications {
692                display,
693                applications,
694                excepting_windows,
695            } => {
696                let app_refs: Vec<&SCRunningApplication> = applications.iter().collect();
697                let window_refs: Vec<&SCWindow> = excepting_windows.iter().collect();
698                unsafe {
699                    let app_ptrs: Vec<*const c_void> =
700                        app_refs.iter().map(|a| a.as_ptr()).collect();
701
702                    let window_ptrs: Vec<*const c_void> =
703                        window_refs.iter().map(|w| w.as_ptr()).collect();
704
705                    #[allow(clippy::cast_possible_wrap)]
706                    let ptr = ffi::sc_content_filter_create_with_display_excluding_applications_excepting_windows(
707                        display.as_ptr(),
708                        if app_ptrs.is_empty() { std::ptr::null() } else { app_ptrs.as_ptr() },
709                        app_ptrs.len() as isize,
710                        if window_ptrs.is_empty() { std::ptr::null() } else { window_ptrs.as_ptr() },
711                        window_ptrs.len() as isize,
712                    );
713                    SCContentFilter(ptr)
714                }
715            }
716            FilterType::None => {
717                return Err(SCError::invalid_config(
718                    "SCContentFilterBuilder: No filter type set. \
719                     Call .with_display() or .with_window() before building.",
720                ));
721            }
722        };
723
724        // The only mutation of an SCContentFilter this crate performs, and the
725        // only point at which it is sound: the object was created moments ago
726        // by the `sc_content_filter_create_*` call above, no other handle or
727        // clone exists yet, and it has not been shared with another thread.
728        // Once `filter` is returned it is immutable for the rest of its life,
729        // which is what `SCContentFilter`'s `Clone`/`Send`/`Sync` rely on.
730        #[cfg(feature = "macos_14_2")]
731        if let Some(include) = self.include_menu_bar {
732            if !unsafe { ffi::sc_content_filter_set_include_menu_bar(filter.0, include) } {
733                return Err(SCError::feature_not_available(
734                    "SCContentFilter.includeMenuBar",
735                    "14.2",
736                ));
737            }
738        }
739
740        Ok(filter)
741    }
742}
743
744impl std::fmt::Debug for SCContentFilterBuilder {
745    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746        let filter_type_name = match &self.filter_type {
747            FilterType::None => "None",
748            FilterType::Window(_) => "Window",
749            FilterType::DisplayExcluding { .. } => "DisplayExcluding",
750            FilterType::DisplayIncluding { .. } => "DisplayIncluding",
751            FilterType::DisplayIncludingApplications { .. } => "DisplayIncludingApplications",
752            FilterType::DisplayExcludingApplications { .. } => "DisplayExcludingApplications",
753        };
754
755        let mut debug = f.debug_struct("SCContentFilterBuilder");
756        debug.field("filter_type", &filter_type_name);
757
758        #[cfg(feature = "macos_14_2")]
759        debug.field("include_menu_bar", &self.include_menu_bar);
760
761        debug.finish()
762    }
763}