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