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