Skip to main content

screencapturekit/
content_sharing_picker.rs

1//! `SCContentSharingPicker` - UI for selecting content to share
2//!
3//! Available on macOS 14.0+.
4//! Provides a system UI for users to select displays, windows, or applications to share.
5//!
6//! ## When to Use
7//!
8//! Use the content sharing picker when:
9//! - You want users to choose what to capture via a native macOS UI
10//! - You need consistent UX with other screen sharing apps
11//! - You want to avoid manually listing and presenting content options
12//!
13//! ## APIs
14//!
15//! | Method | Returns | Use Case |
16//! |--------|---------|----------|
17//! | [`SCContentSharingPicker::show()`] | callback with [`SCPickerOutcome`] | Get filter + metadata (dimensions, picked content) |
18//! | [`SCContentSharingPicker::show_filter()`] | callback with [`SCPickerFilterOutcome`] | Just get the filter |
19//!
20//! For async/await, use [`AsyncSCContentSharingPicker`](crate::async_api::AsyncSCContentSharingPicker) from the `async_api` module.
21//!
22//! # Examples
23//!
24//! ## Callback API: Get filter with metadata
25//! ```no_run
26//! use screencapturekit::content_sharing_picker::*;
27//! use screencapturekit::prelude::*;
28//!
29//! let config = SCContentSharingPickerConfiguration::new();
30//! SCContentSharingPicker::show(&config, |outcome| {
31//!     match outcome {
32//!         SCPickerOutcome::Picked(result) => {
33//!             let (width, height) = result.pixel_size();
34//!             let filter = result.filter();
35//!             println!("Selected content: {}x{}", width, height);
36//!             // Create stream with the filter...
37//!         }
38//!         SCPickerOutcome::Cancelled => println!("Cancelled"),
39//!         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
40//!     }
41//! });
42//! ```
43//!
44//! ## Async API
45//! ```no_run
46//! use screencapturekit::async_api::AsyncSCContentSharingPicker;
47//! use screencapturekit::content_sharing_picker::*;
48//!
49//! async fn example() {
50//!     let config = SCContentSharingPickerConfiguration::new();
51//!     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
52//!         let (width, height) = result.pixel_size();
53//!         let filter = result.filter();
54//!         println!("Selected: {}x{}", width, height);
55//!     }
56//! }
57//! ```
58//!
59//! ## Configure Picker Modes
60//! ```no_run
61//! use screencapturekit::content_sharing_picker::*;
62//!
63//! let mut config = SCContentSharingPickerConfiguration::new();
64//! // Only allow single display selection
65//! config.set_allowed_picker_modes(&[SCContentSharingPickerMode::SingleDisplay]);
66//! // Exclude specific apps from the picker
67//! config.set_excluded_bundle_ids(&["com.apple.finder", "com.apple.dock"]);
68//! ```
69
70use crate::stream::content_filter::SCContentFilter;
71use std::ffi::c_void;
72use std::sync::atomic::{AtomicBool, Ordering};
73
74/// Represents the type of content selected in the picker
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum SCPickedSource {
77    /// A window was selected, with its title
78    Window(String),
79    /// A display was selected, with its ID
80    Display(u32),
81    /// An application was selected, with its name
82    Application(String),
83    /// No specific source identified
84    Unknown,
85}
86
87/// Picker mode determines what content types can be selected
88///
89/// These modes can be combined to allow users to pick from different source types.
90#[repr(i32)]
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92pub enum SCContentSharingPickerMode {
93    /// Allow selection of a single window
94    #[default]
95    SingleWindow = 0,
96    /// Allow selection of multiple windows
97    MultipleWindows = 1,
98    /// Allow selection of a single display/screen
99    SingleDisplay = 2,
100    /// Allow selection of a single application
101    SingleApplication = 3,
102    /// Allow selection of multiple applications
103    MultipleApplications = 4,
104}
105
106/// Configuration for the content sharing picker
107pub struct SCContentSharingPickerConfiguration {
108    ptr: *const c_void,
109}
110
111impl SCContentSharingPickerConfiguration {
112    #[must_use]
113    pub fn new() -> Self {
114        let ptr = unsafe { crate::ffi::sc_content_sharing_picker_configuration_create() };
115        Self { ptr }
116    }
117
118    /// Construct a configuration initialised with the system's default values
119    /// (the equivalent of Apple's `SCContentSharingPicker.shared.defaultConfiguration`).
120    ///
121    /// Use this when you want "system defaults plus my one tweak" — call this
122    /// to get the baseline, then mutate the fields you care about. Compared
123    /// to [`SCContentSharingPickerConfiguration::new()`], which starts from a
124    /// blank-slate `SCContentSharingPickerConfiguration()`, this preserves
125    /// any system-wide picker preferences the OS applies to fresh
126    /// configurations (e.g. allowed picker modes, default exclusion lists).
127    ///
128    /// # Examples
129    ///
130    /// ```no_run
131    /// use screencapturekit::content_sharing_picker::*;
132    ///
133    /// // Start from the system defaults, then override only what you need.
134    /// let mut config = SCContentSharingPickerConfiguration::default_from_system();
135    /// config.set_excluded_bundle_ids(&["com.apple.dock"]);
136    /// ```
137    #[must_use]
138    pub fn default_from_system() -> Self {
139        let ptr = unsafe { crate::ffi::sc_content_sharing_picker_create_default_configuration() };
140        Self { ptr }
141    }
142
143    /// Set allowed picker modes
144    pub fn set_allowed_picker_modes(&mut self, modes: &[SCContentSharingPickerMode]) {
145        let mode_values: Vec<i32> = modes.iter().map(|m| *m as i32).collect();
146        unsafe {
147            crate::ffi::sc_content_sharing_picker_configuration_set_allowed_picker_modes(
148                self.ptr,
149                mode_values.as_ptr(),
150                mode_values.len(),
151            );
152        }
153    }
154
155    /// Get the currently allowed picker modes.
156    pub fn allowed_picker_modes(&self) -> Vec<SCContentSharingPickerMode> {
157        let mask = unsafe {
158            crate::ffi::sc_content_sharing_picker_configuration_get_allowed_picker_modes_mask(
159                self.ptr,
160            )
161        };
162        let mut modes = Vec::new();
163        for (raw_value, mode) in [
164            (1_u64, SCContentSharingPickerMode::SingleWindow),
165            (2_u64, SCContentSharingPickerMode::MultipleWindows),
166            (16_u64, SCContentSharingPickerMode::SingleDisplay),
167            (4_u64, SCContentSharingPickerMode::SingleApplication),
168            (8_u64, SCContentSharingPickerMode::MultipleApplications),
169        ] {
170            if mask & raw_value != 0 {
171                modes.push(mode);
172            }
173        }
174        modes
175    }
176
177    /// Set whether the user can change the selected content while sharing
178    ///
179    /// When `true`, the user can modify their selection during an active session.
180    pub fn set_allows_changing_selected_content(&mut self, allows: bool) {
181        unsafe {
182            crate::ffi::sc_content_sharing_picker_configuration_set_allows_changing_selected_content(
183                self.ptr,
184                allows,
185            );
186        }
187    }
188
189    /// Get whether changing selected content is allowed
190    pub fn allows_changing_selected_content(&self) -> bool {
191        unsafe {
192            crate::ffi::sc_content_sharing_picker_configuration_get_allows_changing_selected_content(
193                self.ptr,
194            )
195        }
196    }
197
198    /// Set bundle identifiers to exclude from the picker
199    ///
200    /// Applications with these bundle IDs will not appear in the picker.
201    pub fn set_excluded_bundle_ids(&mut self, bundle_ids: &[&str]) {
202        let c_strings: Vec<std::ffi::CString> = bundle_ids
203            .iter()
204            .filter_map(|s| std::ffi::CString::new(*s).ok())
205            .collect();
206        let ptrs: Vec<*const i8> = c_strings.iter().map(|s| s.as_ptr()).collect();
207        unsafe {
208            crate::ffi::sc_content_sharing_picker_configuration_set_excluded_bundle_ids(
209                self.ptr,
210                ptrs.as_ptr(),
211                ptrs.len(),
212            );
213        }
214    }
215
216    /// Get the list of excluded bundle identifiers
217    pub fn excluded_bundle_ids(&self) -> Vec<String> {
218        let count = unsafe {
219            crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_ids_count(
220                self.ptr,
221            )
222        };
223        let mut result = Vec::with_capacity(count);
224        for i in 0..count {
225            let mut buffer = vec![0i8; 256];
226            let success = unsafe {
227                crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_id_at(
228                    self.ptr,
229                    i,
230                    buffer.as_mut_ptr(),
231                    buffer.len(),
232                )
233            };
234            if success {
235                let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
236                if let Ok(s) = c_str.to_str() {
237                    result.push(s.to_string());
238                }
239            }
240        }
241        result
242    }
243
244    /// Set window IDs to exclude from the picker
245    ///
246    /// Windows with these IDs will not appear in the picker.
247    pub fn set_excluded_window_ids(&mut self, window_ids: &[u32]) {
248        unsafe {
249            crate::ffi::sc_content_sharing_picker_configuration_set_excluded_window_ids(
250                self.ptr,
251                window_ids.as_ptr(),
252                window_ids.len(),
253            );
254        }
255    }
256
257    /// Get the list of excluded window IDs
258    pub fn excluded_window_ids(&self) -> Vec<u32> {
259        let count = unsafe {
260            crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_ids_count(
261                self.ptr,
262            )
263        };
264        let mut result = Vec::with_capacity(count);
265        for i in 0..count {
266            let id = unsafe {
267                crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_id_at(
268                    self.ptr, i,
269                )
270            };
271            result.push(id);
272        }
273        result
274    }
275
276    #[must_use]
277    pub const fn as_ptr(&self) -> *const c_void {
278        self.ptr
279    }
280}
281
282impl Default for SCContentSharingPickerConfiguration {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288crate::utils::retained::sc_retained!(
289    SCContentSharingPickerConfiguration,
290    field = ptr,
291    retain = crate::ffi::sc_content_sharing_picker_configuration_retain,
292    release = crate::ffi::sc_content_sharing_picker_configuration_release,
293);
294
295impl std::fmt::Debug for SCContentSharingPickerConfiguration {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        f.debug_struct("SCContentSharingPickerConfiguration")
298            .field("ptr", &self.ptr)
299            .finish()
300    }
301}
302
303// ============================================================================
304// Simple API: Returns SCContentFilter directly
305// ============================================================================
306
307/// Result from the simple `show_filter()` API
308#[derive(Debug)]
309pub enum SCPickerFilterOutcome {
310    /// User selected content - contains the filter to use with `SCStream`
311    Filter(SCContentFilter),
312    /// User cancelled the picker
313    Cancelled,
314    /// An error occurred
315    Error(String),
316}
317
318// ============================================================================
319// Main API: Returns SCPickerResult with metadata
320// ============================================================================
321
322/// Result from the main `show()` API - contains filter and content metadata
323///
324/// Provides access to:
325/// - The `SCContentFilter` for use with `SCStream`
326/// - Content dimensions and scale factor
327/// - The picked windows, displays, and applications for custom filter creation
328pub struct SCPickerResult {
329    ptr: *const c_void,
330}
331
332impl SCPickerResult {
333    /// Create from raw pointer (used by async API)
334    #[cfg(feature = "async")]
335    #[must_use]
336    pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
337        Self { ptr }
338    }
339
340    /// Get the content filter for use with `SCStream::new()`
341    #[must_use]
342    pub fn filter(&self) -> SCContentFilter {
343        let filter_ptr = unsafe { crate::ffi::sc_picker_result_get_filter(self.ptr) };
344        SCContentFilter::from_picker_ptr(filter_ptr)
345    }
346
347    /// Get the content size in points (width, height)
348    #[must_use]
349    pub fn size(&self) -> (f64, f64) {
350        let mut x = 0.0;
351        let mut y = 0.0;
352        let mut width = 0.0;
353        let mut height = 0.0;
354        unsafe {
355            crate::ffi::sc_picker_result_get_content_rect(
356                self.ptr,
357                &mut x,
358                &mut y,
359                &mut width,
360                &mut height,
361            );
362        }
363        (width, height)
364    }
365
366    /// Get the content rect (x, y, width, height) in points
367    #[must_use]
368    pub fn rect(&self) -> (f64, f64, f64, f64) {
369        let mut x = 0.0;
370        let mut y = 0.0;
371        let mut width = 0.0;
372        let mut height = 0.0;
373        unsafe {
374            crate::ffi::sc_picker_result_get_content_rect(
375                self.ptr,
376                &mut x,
377                &mut y,
378                &mut width,
379                &mut height,
380            );
381        }
382        (x, y, width, height)
383    }
384
385    /// Get the point-to-pixel scale factor (typically 2.0 for Retina displays)
386    #[must_use]
387    pub fn scale(&self) -> f64 {
388        unsafe { crate::ffi::sc_picker_result_get_scale(self.ptr) }
389    }
390
391    /// Get the pixel dimensions (size * scale)
392    #[must_use]
393    pub fn pixel_size(&self) -> (u32, u32) {
394        let (w, h) = self.size();
395        let scale = self.scale();
396        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
397        let width = (w * scale) as u32;
398        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
399        let height = (h * scale) as u32;
400        (width, height)
401    }
402
403    /// Get the windows selected by the user
404    ///
405    /// Returns the picked windows that can be used to create a custom `SCContentFilter`.
406    ///
407    /// # Example
408    /// ```no_run
409    /// use screencapturekit::content_sharing_picker::*;
410    /// use screencapturekit::prelude::*;
411    ///
412    /// let config = SCContentSharingPickerConfiguration::new();
413    /// SCContentSharingPicker::show(&config, |outcome| {
414    ///     if let SCPickerOutcome::Picked(result) = outcome {
415    ///         let windows = result.windows();
416    ///         if let Some(window) = windows.first() {
417    ///             // Create custom filter with a picked window
418    ///             let filter = SCContentFilter::create()
419    ///                 .with_window(window)
420    ///                 .build();
421    ///         }
422    ///     }
423    /// });
424    /// ```
425    #[must_use]
426    pub fn windows(&self) -> Vec<crate::shareable_content::SCWindow> {
427        let count = unsafe { crate::ffi::sc_picker_result_get_windows_count(self.ptr) };
428        (0..count)
429            .filter_map(|i| {
430                let ptr = unsafe { crate::ffi::sc_picker_result_get_window_at(self.ptr, i) };
431                unsafe { crate::shareable_content::SCWindow::from_retained_ptr(ptr) }
432            })
433            .collect()
434    }
435
436    /// Get the displays selected by the user
437    ///
438    /// Returns the picked displays that can be used to create a custom `SCContentFilter`.
439    ///
440    /// # Example
441    /// ```no_run
442    /// use screencapturekit::content_sharing_picker::*;
443    /// use screencapturekit::prelude::*;
444    ///
445    /// let config = SCContentSharingPickerConfiguration::new();
446    /// SCContentSharingPicker::show(&config, |outcome| {
447    ///     if let SCPickerOutcome::Picked(result) = outcome {
448    ///         let displays = result.displays();
449    ///         if let Some(display) = displays.first() {
450    ///             // Create custom filter with the picked display
451    ///             let filter = SCContentFilter::create()
452    ///                 .with_display(display)
453    ///                 .with_excluding_windows(&[])
454    ///                 .build();
455    ///         }
456    ///     }
457    /// });
458    /// ```
459    #[must_use]
460    pub fn displays(&self) -> Vec<crate::shareable_content::SCDisplay> {
461        let count = unsafe { crate::ffi::sc_picker_result_get_displays_count(self.ptr) };
462        (0..count)
463            .filter_map(|i| {
464                let ptr = unsafe { crate::ffi::sc_picker_result_get_display_at(self.ptr, i) };
465                unsafe { crate::shareable_content::SCDisplay::from_retained_ptr(ptr) }
466            })
467            .collect()
468    }
469
470    /// Get the applications selected by the user
471    ///
472    /// Returns the picked applications that can be used to create a custom `SCContentFilter`.
473    #[must_use]
474    pub fn applications(&self) -> Vec<crate::shareable_content::SCRunningApplication> {
475        let count = unsafe { crate::ffi::sc_picker_result_get_applications_count(self.ptr) };
476        (0..count)
477            .filter_map(|i| {
478                let ptr = unsafe { crate::ffi::sc_picker_result_get_application_at(self.ptr, i) };
479                unsafe { crate::shareable_content::SCRunningApplication::from_retained_ptr(ptr) }
480            })
481            .collect()
482    }
483
484    /// Get the source type that was picked
485    ///
486    /// Returns information about what the user selected: window, display, or application.
487    ///
488    /// # Example
489    /// ```no_run
490    /// use screencapturekit::content_sharing_picker::*;
491    ///
492    /// fn example() {
493    ///     let config = SCContentSharingPickerConfiguration::new();
494    ///     SCContentSharingPicker::show(&config, |outcome| {
495    ///         if let SCPickerOutcome::Picked(result) = outcome {
496    ///             match result.source() {
497    ///                 SCPickedSource::Window(title) => println!("[W] {}", title),
498    ///                 SCPickedSource::Display(id) => println!("[D] Display {}", id),
499    ///                 SCPickedSource::Application(name) => println!("[A] {}", name),
500    ///                 SCPickedSource::Unknown => println!("Unknown source"),
501    ///             }
502    ///         }
503    ///     });
504    /// }
505    /// ```
506    #[must_use]
507    #[allow(clippy::option_if_let_else)]
508    pub fn source(&self) -> SCPickedSource {
509        if let Some(window) = self.windows().first() {
510            SCPickedSource::Window(window.title().unwrap_or_else(|| "Untitled".to_string()))
511        } else if let Some(display) = self.displays().first() {
512            SCPickedSource::Display(display.display_id())
513        } else if let Some(app) = self.applications().first() {
514            SCPickedSource::Application(app.application_name())
515        } else {
516            SCPickedSource::Unknown
517        }
518    }
519}
520
521crate::utils::retained::sc_retained!(
522    SCPickerResult,
523    field = ptr,
524    release = crate::ffi::sc_picker_result_release,
525);
526
527impl std::fmt::Debug for SCPickerResult {
528    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529        let (w, h) = self.size();
530        let scale = self.scale();
531        f.debug_struct("SCPickerResult")
532            .field("size", &(w, h))
533            .field("scale", &scale)
534            .field("pixel_size", &self.pixel_size())
535            .finish()
536    }
537}
538
539/// Outcome from the main `show()` API
540#[derive(Debug)]
541pub enum SCPickerOutcome {
542    /// User selected content - contains result with filter and metadata
543    Picked(SCPickerResult),
544    /// User cancelled the picker
545    Cancelled,
546    /// An error occurred
547    Error(String),
548}
549
550// ============================================================================
551// SCContentSharingPicker
552// ============================================================================
553
554/// System UI for selecting content to share
555///
556/// Available on macOS 14.0+
557///
558/// The picker requires user interaction and cannot block the calling thread.
559/// Use one of these approaches:
560///
561/// - **Callback-based**: `show()` / `show_filter()` - pass a callback closure
562/// - **Async/await**: `AsyncSCContentSharingPicker` from the `async_api` module
563///
564/// # Example (callback)
565/// ```no_run
566/// use screencapturekit::content_sharing_picker::*;
567///
568/// let config = SCContentSharingPickerConfiguration::new();
569/// SCContentSharingPicker::show(&config, |outcome| {
570///     if let SCPickerOutcome::Picked(result) = outcome {
571///         let (width, height) = result.pixel_size();
572///         let filter = result.filter();
573///         // ... create stream
574///     }
575/// });
576/// ```
577///
578/// # Example (async)
579/// ```no_run
580/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
581/// use screencapturekit::content_sharing_picker::*;
582///
583/// async fn example() {
584///     let config = SCContentSharingPickerConfiguration::new();
585///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
586///         let (width, height) = result.pixel_size();
587///         let filter = result.filter();
588///         // ... create stream
589///     }
590/// }
591/// ```
592#[derive(Debug)]
593pub struct SCContentSharingPicker;
594
595impl SCContentSharingPicker {
596    /// Show the picker UI with a callback for the result
597    ///
598    /// This is non-blocking - the callback is invoked when the user makes a selection
599    /// or cancels the picker.
600    ///
601    /// # Example
602    /// ```no_run
603    /// use screencapturekit::content_sharing_picker::*;
604    ///
605    /// let config = SCContentSharingPickerConfiguration::new();
606    /// SCContentSharingPicker::show(&config, |outcome| {
607    ///     match outcome {
608    ///         SCPickerOutcome::Picked(result) => {
609    ///             let (width, height) = result.pixel_size();
610    ///             let filter = result.filter();
611    ///             println!("Selected {}x{}", width, height);
612    ///         }
613    ///         SCPickerOutcome::Cancelled => println!("Cancelled"),
614    ///         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
615    ///     }
616    /// });
617    /// ```
618    pub fn show<F>(config: &SCContentSharingPickerConfiguration, callback: F)
619    where
620        F: FnOnce(SCPickerOutcome) + Send + 'static,
621    {
622        let context = into_callback_context::<SCPickerOutcome, F>(callback);
623
624        unsafe {
625            crate::ffi::sc_content_sharing_picker_show_with_result(
626                config.as_ptr(),
627                picker_trampoline::<ResultDecoder>,
628                context,
629            );
630        }
631    }
632
633    /// Show the picker UI for an existing stream (to change source while capturing)
634    ///
635    /// Use this when you have an active `SCStream` and want to let the user
636    /// select a new content source. The callback receives the new filter
637    /// which can be used with `stream.update_content_filter()`.
638    ///
639    /// # Example
640    /// ```no_run
641    /// use screencapturekit::content_sharing_picker::*;
642    /// use screencapturekit::stream::SCStream;
643    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
644    /// use screencapturekit::stream::content_filter::SCContentFilter;
645    /// use screencapturekit::shareable_content::SCShareableContent;
646    ///
647    /// fn example() -> Option<()> {
648    ///     let content = SCShareableContent::get().ok()?;
649    ///     let displays = content.displays();
650    ///     let display = displays.first()?;
651    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
652    ///     let stream_config = SCStreamConfiguration::new();
653    ///     let stream = SCStream::new(&filter, &stream_config);
654    ///
655    ///     // When stream is active and user wants to change source
656    ///     let config = SCContentSharingPickerConfiguration::new();
657    ///     SCContentSharingPicker::show_for_stream(&config, &stream, |outcome| {
658    ///         if let SCPickerOutcome::Picked(result) = outcome {
659    ///             // Use result.filter() with stream.update_content_filter()
660    ///             let _ = result.filter();
661    ///         }
662    ///     });
663    ///     Some(())
664    /// }
665    /// ```
666    pub fn show_for_stream<F>(
667        config: &SCContentSharingPickerConfiguration,
668        stream: &crate::stream::SCStream,
669        callback: F,
670    ) where
671        F: FnOnce(SCPickerOutcome) + Send + 'static,
672    {
673        let context = into_callback_context::<SCPickerOutcome, F>(callback);
674
675        unsafe {
676            crate::ffi::sc_content_sharing_picker_show_for_stream(
677                config.as_ptr(),
678                stream.as_ptr(),
679                picker_trampoline::<ResultDecoder>,
680                context,
681            );
682        }
683    }
684
685    /// Show the picker UI with a callback that receives just the filter
686    ///
687    /// This is the simple API - use when you just need the filter without metadata.
688    ///
689    /// # Example
690    /// ```no_run
691    /// use screencapturekit::content_sharing_picker::*;
692    ///
693    /// let config = SCContentSharingPickerConfiguration::new();
694    /// SCContentSharingPicker::show_filter(&config, |outcome| {
695    ///     if let SCPickerFilterOutcome::Filter(filter) = outcome {
696    ///         // Use filter with SCStream
697    ///     }
698    /// });
699    /// ```
700    pub fn show_filter<F>(config: &SCContentSharingPickerConfiguration, callback: F)
701    where
702        F: FnOnce(SCPickerFilterOutcome) + Send + 'static,
703    {
704        let context = into_callback_context::<SCPickerFilterOutcome, F>(callback);
705
706        unsafe {
707            crate::ffi::sc_content_sharing_picker_show(
708                config.as_ptr(),
709                picker_trampoline::<FilterDecoder>,
710                context,
711            );
712        }
713    }
714
715    /// Show the picker UI with a specific content style
716    ///
717    /// Presents the picker pre-filtered to a specific content type.
718    ///
719    /// # Arguments
720    /// * `config` - The picker configuration
721    /// * `style` - The content style to show (Window, Display, Application)
722    /// * `callback` - Called with the picker result
723    pub fn show_using_style<F>(
724        config: &SCContentSharingPickerConfiguration,
725        style: crate::stream::content_filter::SCShareableContentStyle,
726        callback: F,
727    ) where
728        F: FnOnce(SCPickerOutcome) + Send + 'static,
729    {
730        let context = into_callback_context::<SCPickerOutcome, F>(callback);
731
732        unsafe {
733            crate::ffi::sc_content_sharing_picker_show_using_style(
734                config.as_ptr(),
735                style as i32,
736                picker_trampoline::<ResultDecoder>,
737                context,
738            );
739        }
740    }
741
742    /// Show the picker for an existing stream with a specific content style
743    ///
744    /// # Arguments
745    /// * `config` - The picker configuration
746    /// * `stream` - The stream to update
747    /// * `style` - The content style to show (Window, Display, Application)
748    /// * `callback` - Called with the picker result
749    pub fn show_for_stream_using_style<F>(
750        config: &SCContentSharingPickerConfiguration,
751        stream: &crate::stream::SCStream,
752        style: crate::stream::content_filter::SCShareableContentStyle,
753        callback: F,
754    ) where
755        F: FnOnce(SCPickerOutcome) + Send + 'static,
756    {
757        let context = into_callback_context::<SCPickerOutcome, F>(callback);
758
759        unsafe {
760            crate::ffi::sc_content_sharing_picker_show_for_stream_using_style(
761                config.as_ptr(),
762                stream.as_ptr(),
763                style as i32,
764                picker_trampoline::<ResultDecoder>,
765                context,
766            );
767        }
768    }
769
770    /// Set the maximum number of streams that can be created from the picker
771    ///
772    /// Pass 0 to allow unlimited streams.
773    pub fn set_maximum_stream_count(count: usize) {
774        unsafe {
775            crate::ffi::sc_content_sharing_picker_set_maximum_stream_count(count);
776        }
777    }
778
779    /// Get the maximum number of streams allowed
780    ///
781    /// Returns 0 if unlimited streams are allowed.
782    pub fn maximum_stream_count() -> usize {
783        unsafe { crate::ffi::sc_content_sharing_picker_get_maximum_stream_count() }
784    }
785
786    /// Returns whether the shared content-sharing picker is currently
787    /// marked active.
788    ///
789    /// Apple requires `picker.isActive = true` before its UI can appear.
790    /// The various `show*()` trampolines on this type set it implicitly
791    /// before presenting, but this getter is useful for callers that
792    /// want to:
793    ///
794    /// * avoid double-presenting (skip a second `show()` while the first
795    ///   picker session is still up),
796    /// * render UI affordances based on whether the picker is currently
797    ///   visible to the user.
798    #[must_use]
799    pub fn is_active() -> bool {
800        unsafe { crate::ffi::sc_content_sharing_picker_get_active() }
801    }
802
803    /// Mark the shared content-sharing picker active or inactive.
804    ///
805    /// Setting this to `false` hides the picker UI between sessions
806    /// (the recommended hygiene step after a long-running app finishes
807    /// using the picker — leaving it active leaves the system-level
808    /// Control Center entry in a "ready to share" state).
809    ///
810    /// Setting to `true` is required before `present*()` can surface
811    /// the picker; the `show*()` trampolines do this for you. Set it
812    /// manually only if you want to opt into the picker UI without
813    /// immediately presenting it.
814    pub fn set_active(active: bool) {
815        unsafe { crate::ffi::sc_content_sharing_picker_set_active(active) }
816    }
817}
818
819// ============================================================================
820// One-shot callback context + trampoline (shared by all `show*()` methods)
821// ============================================================================
822
823/// Heap context handed to the Swift bridge as the opaque `user_data` pointer.
824///
825/// It owns the user's closure plus a `consumed` guard. Centralising the
826/// `Box::into_raw` / `Box::from_raw` lifecycle here means the one-shot
827/// reclaim happens in exactly one place ([`picker_trampoline`]) instead of
828/// being duplicated across every `show*()` method, and the `consumed` flag
829/// makes a (mis-behaving) double fire from Swift safe: the second invocation
830/// observes `true` and returns without a second `Box::from_raw`.
831struct PickerCallbackContext<O> {
832    consumed: AtomicBool,
833    closure: Box<dyn FnOnce(O) + Send>,
834}
835
836/// Box a user closure into the opaque `*mut c_void` context handed to Swift.
837fn into_callback_context<O, F>(callback: F) -> *mut c_void
838where
839    F: FnOnce(O) + Send + 'static,
840{
841    let context = Box::new(PickerCallbackContext {
842        consumed: AtomicBool::new(false),
843        closure: Box::new(callback),
844    });
845    Box::into_raw(context).cast::<c_void>()
846}
847
848/// Decodes the `(code, ptr)` pair from the Swift bridge into a typed outcome.
849///
850/// Implemented by zero-sized marker types so a single generic trampoline can
851/// serve both the result-bearing and filter-only APIs while keeping the FFI
852/// signature identical.
853trait PickerDecode {
854    type Outcome;
855    fn decode(code: i32, ptr: *const c_void) -> Self::Outcome;
856}
857
858struct ResultDecoder;
859impl PickerDecode for ResultDecoder {
860    type Outcome = SCPickerOutcome;
861    fn decode(code: i32, ptr: *const c_void) -> SCPickerOutcome {
862        match code {
863            1 if !ptr.is_null() => SCPickerOutcome::Picked(SCPickerResult { ptr }),
864            0 => SCPickerOutcome::Cancelled,
865            _ => SCPickerOutcome::Error("Picker failed".to_string()),
866        }
867    }
868}
869
870struct FilterDecoder;
871impl PickerDecode for FilterDecoder {
872    type Outcome = SCPickerFilterOutcome;
873    fn decode(code: i32, ptr: *const c_void) -> SCPickerFilterOutcome {
874        match code {
875            1 if !ptr.is_null() => {
876                SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(ptr))
877            }
878            0 => SCPickerFilterOutcome::Cancelled,
879            _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
880        }
881    }
882}
883
884/// Single trampoline for every picker `show*()` callback.
885///
886/// `code` follows the Swift bridge contract (1 = picked, 0 = cancelled,
887/// anything else = error). A `code` of 0 is also produced by the Swift
888/// replacement path when a pending observer is superseded by a newer
889/// `show*()`, so a replaced picker resolves as `Cancelled` rather than
890/// leaking its context.
891///
892/// The boxed closure is reclaimed here exactly once; a duplicate fire on the
893/// same still-live context is rejected by the atomic `consumed` guard.
894extern "C" fn picker_trampoline<D: PickerDecode>(
895    code: i32,
896    ptr: *const c_void,
897    context: *mut c_void,
898) {
899    if context.is_null() {
900        return;
901    }
902
903    // SAFETY: `context` is a live `PickerCallbackContext<D::Outcome>` created
904    // by `into_callback_context`. We only read `consumed` here without taking
905    // ownership; ownership is taken below only by the winner of the swap.
906    let consumed = unsafe { &(*context.cast::<PickerCallbackContext<D::Outcome>>()).consumed };
907    if consumed.swap(true, Ordering::AcqRel) {
908        return;
909    }
910
911    // SAFETY: we won the `consumed` swap, so this is the unique reclaim of the
912    // box created in `into_callback_context`.
913    let context = unsafe { Box::from_raw(context.cast::<PickerCallbackContext<D::Outcome>>()) };
914    let outcome = D::decode(code, ptr);
915    crate::utils::panic_safe::catch_user_panic("picker callback", move || {
916        (context.closure)(outcome);
917    });
918}
919
920// Safety: Configuration wraps an Objective-C object that is thread-safe
921// SAFETY: `SCContentSharingPickerConfiguration` wraps an Objective-C object
922// whose reference counting is atomic; it is safe to send between and share
923// across threads.
924unsafe impl Send for SCContentSharingPickerConfiguration {}
925unsafe impl Sync for SCContentSharingPickerConfiguration {}
926// SAFETY: `SCPickerResult` holds retained Objective-C objects whose reference
927// counting is atomic; it is safe to send between and share across threads.
928unsafe impl Send for SCPickerResult {}
929unsafe impl Sync for SCPickerResult {}