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