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` from the optional
21//! `async_api` module.
22//!
23//! # Examples
24//!
25//! ## Callback API: Get filter with metadata
26//! ```no_run
27//! use screencapturekit::content_sharing_picker::*;
28//! use screencapturekit::prelude::*;
29//!
30//! let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
31//! SCContentSharingPicker::show(&config, |outcome| {
32//!     match outcome {
33//!         SCPickerOutcome::Picked(result) => {
34//!             let (width, height) = result.pixel_size();
35//!             let filter = result.filter();
36//!             println!("Selected content: {}x{}", width, height);
37//!             // Create stream with the filter...
38//!         }
39//!         SCPickerOutcome::Cancelled => println!("Cancelled"),
40//!         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
41//!     }
42//! });
43//! ```
44//!
45//! ## Async API
46//! ```no_run
47//! use screencapturekit::async_api::AsyncSCContentSharingPicker;
48//! use screencapturekit::content_sharing_picker::*;
49//!
50//! async fn example() {
51//!     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
52//!     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
53//!         let (width, height) = result.pixel_size();
54//!         let filter = result.filter();
55//!         println!("Selected: {}x{}", width, height);
56//!     }
57//! }
58//! ```
59//!
60//! ## Configure Picker Modes
61//! ```no_run
62//! use screencapturekit::content_sharing_picker::*;
63//!
64//! let mut config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
65//! // Only allow single display selection
66//! config.set_allowed_picker_modes(&[SCContentSharingPickerMode::SingleDisplay]);
67//! // Exclude specific apps from the picker
68//! config.set_excluded_bundle_ids(&["com.apple.finder", "com.apple.dock"]).expect("bundle IDs have no NUL byte");
69//! ```
70
71use crate::error::SCError;
72use crate::stream::configuration::InteriorNulError;
73use crate::stream::content_filter::{SCContentFilter, SCShareableContentStyle};
74pub use crate::stream::StreamIdentity;
75use std::any::Any;
76use std::collections::HashMap;
77use std::ffi::c_void;
78use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
79use std::sync::{Mutex, PoisonError};
80
81/// Represents the type of content selected in the picker
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum SCPickedSource {
84    /// A window was selected, with its title
85    Window(String),
86    /// A display was selected, with its ID
87    Display(u32),
88    /// An application was selected, with its name
89    Application(String),
90    /// No specific source identified
91    Unknown,
92}
93
94/// Picker mode determines what content types can be selected
95///
96/// These modes can be combined to allow users to pick from different source types.
97#[repr(i32)]
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
99pub enum SCContentSharingPickerMode {
100    /// Allow selection of a single window
101    #[default]
102    SingleWindow = 0,
103    /// Allow selection of multiple windows
104    MultipleWindows = 1,
105    /// Allow selection of a single display/screen
106    SingleDisplay = 2,
107    /// Allow selection of a single application
108    SingleApplication = 3,
109    /// Allow selection of multiple applications
110    MultipleApplications = 4,
111}
112
113/// Configuration for the content sharing picker
114pub struct SCContentSharingPickerConfiguration {
115    ptr: *const c_void,
116}
117
118impl SCContentSharingPickerConfiguration {
119    /// # Errors
120    ///
121    /// Returns [`SCError::FeatureNotAvailable`] when run on macOS older than
122    /// 14.0.
123    pub fn new() -> Result<Self, SCError> {
124        if !SCContentSharingPicker::is_available() {
125            return Err(SCError::feature_not_available(
126                "SCContentSharingPicker",
127                "14.0",
128            ));
129        }
130        let ptr = unsafe { crate::ffi::sc_content_sharing_picker_configuration_create() };
131        if ptr.is_null() {
132            return Err(SCError::null_pointer("SCContentSharingPickerConfiguration"));
133        }
134        Ok(Self { ptr })
135    }
136
137    /// Construct a configuration initialised with the system's default values
138    /// (the equivalent of Apple's `SCContentSharingPicker.shared.defaultConfiguration`).
139    ///
140    /// Use this when you want "system defaults plus my one tweak" — call this
141    /// to get the baseline, then mutate the fields you care about. Compared
142    /// to [`SCContentSharingPickerConfiguration::new()`], which starts from a
143    /// blank-slate `SCContentSharingPickerConfiguration()`, this preserves
144    /// any system-wide picker preferences the OS applies to fresh
145    /// configurations (e.g. allowed picker modes, default exclusion lists).
146    ///
147    /// # Errors
148    ///
149    /// Returns [`SCError::FeatureNotAvailable`] when run on macOS older than
150    /// 14.0.
151    ///
152    /// # Examples
153    ///
154    /// ```no_run
155    /// use screencapturekit::content_sharing_picker::*;
156    ///
157    /// // Start from the system defaults, then override only what you need.
158    /// let mut config = SCContentSharingPickerConfiguration::default_from_system()
159    ///     .expect("read the default picker configuration");
160    /// config.set_excluded_bundle_ids(&["com.apple.dock"]).expect("bundle IDs have no NUL byte");
161    /// ```
162    pub fn default_from_system() -> Result<Self, SCError> {
163        if !SCContentSharingPicker::is_available() {
164            return Err(SCError::feature_not_available(
165                "SCContentSharingPicker",
166                "14.0",
167            ));
168        }
169        let ptr = unsafe { crate::ffi::sc_content_sharing_picker_create_default_configuration() };
170        if ptr.is_null() {
171            return Err(SCError::null_pointer("SCContentSharingPickerConfiguration"));
172        }
173        Ok(Self { ptr })
174    }
175
176    /// Set allowed picker modes
177    pub fn set_allowed_picker_modes(&mut self, modes: &[SCContentSharingPickerMode]) {
178        let mode_values: Vec<i32> = modes.iter().map(|m| *m as i32).collect();
179        unsafe {
180            crate::ffi::sc_content_sharing_picker_configuration_set_allowed_picker_modes(
181                self.ptr,
182                mode_values.as_ptr(),
183                mode_values.len(),
184            );
185        }
186    }
187
188    /// Get the currently allowed picker modes.
189    pub fn allowed_picker_modes(&self) -> Vec<SCContentSharingPickerMode> {
190        let mask = unsafe {
191            crate::ffi::sc_content_sharing_picker_configuration_get_allowed_picker_modes_mask(
192                self.ptr,
193            )
194        };
195        let mut modes = Vec::new();
196        for (raw_value, mode) in [
197            (1_u64, SCContentSharingPickerMode::SingleWindow),
198            (2_u64, SCContentSharingPickerMode::MultipleWindows),
199            (16_u64, SCContentSharingPickerMode::SingleDisplay),
200            (4_u64, SCContentSharingPickerMode::SingleApplication),
201            (8_u64, SCContentSharingPickerMode::MultipleApplications),
202        ] {
203            if mask & raw_value != 0 {
204                modes.push(mode);
205            }
206        }
207        modes
208    }
209
210    /// Set whether the user can change the selected content while sharing
211    ///
212    /// When `true`, the user can modify their selection during an active session.
213    pub fn set_allows_changing_selected_content(&mut self, allows: bool) {
214        unsafe {
215            crate::ffi::sc_content_sharing_picker_configuration_set_allows_changing_selected_content(
216                self.ptr,
217                allows,
218            );
219        }
220    }
221
222    /// Get whether changing selected content is allowed
223    pub fn allows_changing_selected_content(&self) -> bool {
224        unsafe {
225            crate::ffi::sc_content_sharing_picker_configuration_get_allows_changing_selected_content(
226                self.ptr,
227            )
228        }
229    }
230
231    /// Set bundle identifiers to exclude from the picker
232    ///
233    /// Applications with these bundle IDs will not appear in the picker.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`InteriorNulError`] — leaving the configuration unchanged — if
238    /// any bundle ID contains an interior NUL byte.
239    pub fn set_excluded_bundle_ids(&mut self, bundle_ids: &[&str]) -> Result<(), InteriorNulError> {
240        let c_strings = bundle_ids
241            .iter()
242            .map(|id| std::ffi::CString::new(*id))
243            .collect::<Result<Vec<_>, _>>()
244            .map_err(|_| InteriorNulError)?;
245        let ptrs: Vec<*const i8> = c_strings.iter().map(|s| s.as_ptr()).collect();
246        unsafe {
247            crate::ffi::sc_content_sharing_picker_configuration_set_excluded_bundle_ids(
248                self.ptr,
249                ptrs.as_ptr(),
250                ptrs.len(),
251            );
252        }
253        Ok(())
254    }
255
256    /// Get the list of excluded bundle identifiers
257    ///
258    /// A bundle ID that does not fit in the transfer buffer is skipped rather
259    /// than returned truncated, so the result can be shorter than
260    /// [`excluded_bundle_ids_count`](Self::excluded_bundle_ids_count).
261    #[must_use]
262    pub fn excluded_bundle_ids(&self) -> Vec<String> {
263        let count = self.excluded_bundle_ids_count();
264        let mut result = Vec::with_capacity(count);
265        for i in 0..count {
266            let id = unsafe {
267                crate::utils::ffi_string::ffi_string_from_buffer(
268                    crate::utils::ffi_string::DEFAULT_BUFFER_SIZE,
269                    |buffer, len| {
270                        crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_id_at(
271                            self.ptr,
272                            i,
273                            buffer,
274                            usize::try_from(len).unwrap_or(0),
275                        )
276                    },
277                )
278            };
279            if let Some(id) = id {
280                result.push(id);
281            }
282        }
283        result
284    }
285
286    /// Number of excluded bundle identifiers configured.
287    #[must_use]
288    pub fn excluded_bundle_ids_count(&self) -> usize {
289        unsafe {
290            crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_ids_count(
291                self.ptr,
292            )
293        }
294    }
295
296    /// Set window IDs to exclude from the picker
297    ///
298    /// Windows with these IDs will not appear in the picker.
299    pub fn set_excluded_window_ids(&mut self, window_ids: &[u32]) {
300        unsafe {
301            crate::ffi::sc_content_sharing_picker_configuration_set_excluded_window_ids(
302                self.ptr,
303                window_ids.as_ptr(),
304                window_ids.len(),
305            );
306        }
307    }
308
309    /// Get the list of excluded window IDs
310    pub fn excluded_window_ids(&self) -> Vec<u32> {
311        let count = unsafe {
312            crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_ids_count(
313                self.ptr,
314            )
315        };
316        let mut result = Vec::with_capacity(count);
317        for i in 0..count {
318            let id = unsafe {
319                crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_id_at(
320                    self.ptr, i,
321                )
322            };
323            result.push(id);
324        }
325        result
326    }
327
328    #[must_use]
329    pub const fn as_ptr(&self) -> *const c_void {
330        self.ptr
331    }
332}
333
334crate::utils::retained::sc_retained!(
335    SCContentSharingPickerConfiguration,
336    field = ptr,
337    release = crate::ffi::sc_content_sharing_picker_configuration_release,
338);
339
340impl Clone for SCContentSharingPickerConfiguration {
341    /// Produce an independent configuration with the same values.
342    ///
343    /// Deliberately **not** the retain-based clone the other wrappers in this
344    /// crate use. Those wrap immutable Objective-C objects, where sharing one
345    /// instance between handles is unobservable. This type is different: it
346    /// wraps a mutable Swift box and exposes `&mut self` setters, so a
347    /// refcount-only clone would let a `&mut` on one handle mutate state that
348    /// another handle observes through a shared `&` — and, with `Send + Sync`,
349    /// from another thread at the same time.
350    fn clone(&self) -> Self {
351        Self {
352            ptr: unsafe { crate::ffi::sc_content_sharing_picker_configuration_copy(self.ptr) },
353        }
354    }
355}
356
357impl std::fmt::Debug for SCContentSharingPickerConfiguration {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        f.debug_struct("SCContentSharingPickerConfiguration")
360            .field("ptr", &self.ptr)
361            .finish()
362    }
363}
364
365// ============================================================================
366// Simple API: Returns SCContentFilter directly
367// ============================================================================
368
369/// Result from the simple `show_filter()` API
370#[derive(Debug)]
371pub enum SCPickerFilterOutcome {
372    /// User selected content - contains the filter to use with `SCStream`
373    Filter(SCContentFilter),
374    /// User cancelled the picker
375    Cancelled,
376    /// An error occurred
377    Error(String),
378}
379
380// ============================================================================
381// Main API: Returns SCPickerResult with metadata
382// ============================================================================
383
384/// Result from the main `show()` API - contains filter and content metadata
385///
386/// Provides access to:
387/// - The `SCContentFilter` for use with `SCStream`
388/// - Content dimensions and scale factor
389/// - The picked windows, displays, and applications for custom filter creation
390pub struct SCPickerResult {
391    ptr: *const c_void,
392}
393
394impl SCPickerResult {
395    /// Create from raw pointer (used by async API)
396    #[cfg(feature = "async")]
397    #[must_use]
398    pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
399        Self { ptr }
400    }
401
402    /// Get the content filter for use with `SCStream::new()`
403    #[must_use]
404    pub fn filter(&self) -> SCContentFilter {
405        let filter_ptr = unsafe { crate::ffi::sc_picker_result_get_filter(self.ptr) };
406        SCContentFilter::from_picker_ptr(filter_ptr)
407    }
408
409    /// Get the content size in points (width, height)
410    #[must_use]
411    pub fn size(&self) -> (f64, f64) {
412        let mut x = 0.0;
413        let mut y = 0.0;
414        let mut width = 0.0;
415        let mut height = 0.0;
416        unsafe {
417            crate::ffi::sc_picker_result_get_content_rect(
418                self.ptr,
419                &raw mut x,
420                &raw mut y,
421                &raw mut width,
422                &raw mut height,
423            );
424        }
425        (width, height)
426    }
427
428    /// Get the content rect (x, y, width, height) in points
429    #[must_use]
430    pub fn rect(&self) -> (f64, f64, f64, f64) {
431        let mut x = 0.0;
432        let mut y = 0.0;
433        let mut width = 0.0;
434        let mut height = 0.0;
435        unsafe {
436            crate::ffi::sc_picker_result_get_content_rect(
437                self.ptr,
438                &raw mut x,
439                &raw mut y,
440                &raw mut width,
441                &raw mut height,
442            );
443        }
444        (x, y, width, height)
445    }
446
447    /// Get the point-to-pixel scale factor (typically 2.0 for Retina displays)
448    #[must_use]
449    pub fn scale(&self) -> f64 {
450        unsafe { crate::ffi::sc_picker_result_get_scale(self.ptr) }
451    }
452
453    /// Get the pixel dimensions (size * scale)
454    #[must_use]
455    pub fn pixel_size(&self) -> (u32, u32) {
456        let (w, h) = self.size();
457        let scale = self.scale();
458        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
459        let width = (w * scale) as u32;
460        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
461        let height = (h * scale) as u32;
462        (width, height)
463    }
464
465    /// Get the windows selected by the user
466    ///
467    /// Returns the picked windows that can be used to create a custom `SCContentFilter`.
468    ///
469    /// # Example
470    /// ```no_run
471    /// use screencapturekit::content_sharing_picker::*;
472    /// use screencapturekit::prelude::*;
473    ///
474    /// let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
475    /// SCContentSharingPicker::show(&config, |outcome| {
476    ///     if let SCPickerOutcome::Picked(result) = outcome {
477    ///         let windows = result.windows();
478    ///         if let Some(window) = windows.first() {
479    ///             // Create custom filter with a picked window
480    ///             let filter = SCContentFilter::create()
481    ///                 .with_window(window)
482    ///                 .build().expect("failed to build content filter");
483    ///         }
484    ///     }
485    /// });
486    /// ```
487    #[must_use]
488    pub fn windows(&self) -> Vec<crate::shareable_content::SCWindow> {
489        let count = unsafe { crate::ffi::sc_picker_result_get_windows_count(self.ptr) };
490        (0..count)
491            .filter_map(|i| {
492                let ptr = unsafe { crate::ffi::sc_picker_result_get_window_at(self.ptr, i) };
493                unsafe { crate::shareable_content::SCWindow::from_retained_ptr(ptr) }
494            })
495            .collect()
496    }
497
498    /// Get the displays selected by the user
499    ///
500    /// Returns the picked displays that can be used to create a custom `SCContentFilter`.
501    ///
502    /// # Example
503    /// ```no_run
504    /// use screencapturekit::content_sharing_picker::*;
505    /// use screencapturekit::prelude::*;
506    ///
507    /// let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
508    /// SCContentSharingPicker::show(&config, |outcome| {
509    ///     if let SCPickerOutcome::Picked(result) = outcome {
510    ///         let displays = result.displays();
511    ///         if let Some(display) = displays.first() {
512    ///             // Create custom filter with the picked display
513    ///             let filter = SCContentFilter::create()
514    ///                 .with_display(display)
515    ///                 .with_excluding_windows(&[])
516    ///                 .build().expect("failed to build content filter");
517    ///         }
518    ///     }
519    /// });
520    /// ```
521    #[must_use]
522    pub fn displays(&self) -> Vec<crate::shareable_content::SCDisplay> {
523        let count = unsafe { crate::ffi::sc_picker_result_get_displays_count(self.ptr) };
524        (0..count)
525            .filter_map(|i| {
526                let ptr = unsafe { crate::ffi::sc_picker_result_get_display_at(self.ptr, i) };
527                unsafe { crate::shareable_content::SCDisplay::from_retained_ptr(ptr) }
528            })
529            .collect()
530    }
531
532    /// Get the applications selected by the user
533    ///
534    /// Returns the picked applications that can be used to create a custom `SCContentFilter`.
535    #[must_use]
536    pub fn applications(&self) -> Vec<crate::shareable_content::SCRunningApplication> {
537        let count = unsafe { crate::ffi::sc_picker_result_get_applications_count(self.ptr) };
538        (0..count)
539            .filter_map(|i| {
540                let ptr = unsafe { crate::ffi::sc_picker_result_get_application_at(self.ptr, i) };
541                unsafe { crate::shareable_content::SCRunningApplication::from_retained_ptr(ptr) }
542            })
543            .collect()
544    }
545
546    /// Get the source type that was picked
547    ///
548    /// Returns information about what the user selected: window, display, or application.
549    ///
550    /// # Example
551    /// ```no_run
552    /// use screencapturekit::content_sharing_picker::*;
553    ///
554    /// fn example() {
555    ///     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
556    ///     SCContentSharingPicker::show(&config, |outcome| {
557    ///         if let SCPickerOutcome::Picked(result) = outcome {
558    ///             match result.source() {
559    ///                 SCPickedSource::Window(title) => println!("[W] {}", title),
560    ///                 SCPickedSource::Display(id) => println!("[D] Display {}", id),
561    ///                 SCPickedSource::Application(name) => println!("[A] {}", name),
562    ///                 SCPickedSource::Unknown => println!("Unknown source"),
563    ///             }
564    ///         }
565    ///     });
566    /// }
567    /// ```
568    #[must_use]
569    #[allow(clippy::option_if_let_else)]
570    pub fn source(&self) -> SCPickedSource {
571        if let Some(window) = self.windows().first() {
572            SCPickedSource::Window(window.title().unwrap_or_else(|| "Untitled".to_string()))
573        } else if let Some(display) = self.displays().first() {
574            SCPickedSource::Display(display.display_id())
575        } else if let Some(app) = self.applications().first() {
576            SCPickedSource::Application(app.application_name())
577        } else {
578            SCPickedSource::Unknown
579        }
580    }
581}
582
583crate::utils::retained::sc_retained!(
584    SCPickerResult,
585    field = ptr,
586    release = crate::ffi::sc_picker_result_release,
587);
588
589impl std::fmt::Debug for SCPickerResult {
590    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591        let (w, h) = self.size();
592        let scale = self.scale();
593        f.debug_struct("SCPickerResult")
594            .field("size", &(w, h))
595            .field("scale", &scale)
596            .field("pixel_size", &self.pixel_size())
597            .finish()
598    }
599}
600
601/// Outcome from the main `show()` API
602#[derive(Debug)]
603pub enum SCPickerOutcome {
604    /// User selected content - contains result with filter and metadata
605    Picked(SCPickerResult),
606    /// User cancelled the picker
607    Cancelled,
608    /// An error occurred
609    Error(String),
610}
611
612/// Error returned by `SCContentSharingPicker` operations.
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614pub enum SCPickerConfigurationError {
615    /// The picker API is unavailable on this system.
616    Unavailable,
617    /// Picker configuration properties must be assigned on the process main
618    /// thread.
619    MainThreadRequired,
620}
621
622impl std::fmt::Display for SCPickerConfigurationError {
623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624        match self {
625            Self::Unavailable => f.write_str("content sharing picker is unavailable"),
626            Self::MainThreadRequired => {
627                f.write_str("content sharing picker configuration requires the main thread")
628            }
629        }
630    }
631}
632
633impl std::error::Error for SCPickerConfigurationError {}
634
635// ============================================================================
636// SCContentSharingPicker
637// ============================================================================
638
639/// System UI for selecting content to share
640///
641/// Available on macOS 14.0+
642///
643/// The picker requires user interaction and cannot block the calling thread.
644/// Use one of these approaches:
645///
646/// - **Callback-based**: `show()` / `show_filter()` - pass a callback closure
647/// - **Async/await**: `AsyncSCContentSharingPicker` from the `async_api` module
648///
649/// # Example (callback)
650/// ```no_run
651/// use screencapturekit::content_sharing_picker::*;
652///
653/// let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
654/// SCContentSharingPicker::show(&config, |outcome| {
655///     if let SCPickerOutcome::Picked(result) = outcome {
656///         let (width, height) = result.pixel_size();
657///         let filter = result.filter();
658///         // ... create stream
659///     }
660/// });
661/// ```
662///
663/// # Example (async)
664/// ```no_run
665/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
666/// use screencapturekit::content_sharing_picker::*;
667///
668/// async fn example() {
669///     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
670///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
671///         let (width, height) = result.pixel_size();
672///         let filter = result.filter();
673///         // ... create stream
674///     }
675/// }
676/// ```
677#[derive(Debug)]
678pub struct SCContentSharingPicker;
679
680impl SCContentSharingPicker {
681    fn require_available() -> Result<(), SCPickerConfigurationError> {
682        if Self::is_available() {
683            Ok(())
684        } else {
685            Err(SCPickerConfigurationError::Unavailable)
686        }
687    }
688
689    /// Whether content-sharing picker APIs are available on this system.
690    #[must_use]
691    pub fn is_available() -> bool {
692        unsafe { crate::ffi::sc_content_sharing_picker_is_available() }
693    }
694
695    /// Show the picker UI with a callback for the result
696    ///
697    /// This is non-blocking - the callback is invoked when the user makes a selection
698    /// or cancels the picker.
699    ///
700    /// # Example
701    /// ```no_run
702    /// use screencapturekit::content_sharing_picker::*;
703    ///
704    /// let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
705    /// SCContentSharingPicker::show(&config, |outcome| {
706    ///     match outcome {
707    ///         SCPickerOutcome::Picked(result) => {
708    ///             let (width, height) = result.pixel_size();
709    ///             let filter = result.filter();
710    ///             println!("Selected {}x{}", width, height);
711    ///         }
712    ///         SCPickerOutcome::Cancelled => println!("Cancelled"),
713    ///         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
714    ///     }
715    /// });
716    /// ```
717    pub fn show<F>(config: &SCContentSharingPickerConfiguration, callback: F)
718    where
719        F: FnOnce(SCPickerOutcome) + Send + 'static,
720    {
721        let context = into_callback_context::<SCPickerOutcome, F>(callback);
722
723        unsafe {
724            crate::ffi::sc_content_sharing_picker_show_with_result(
725                config.as_ptr(),
726                picker_trampoline::<ResultDecoder>,
727                context,
728            );
729        }
730    }
731
732    /// Show the picker UI for an existing stream (to change source while capturing)
733    ///
734    /// Use this when you have an active `SCStream` and want to let the user
735    /// select a new content source. The callback receives the new filter
736    /// which can be used with `stream.update_content_filter()`.
737    ///
738    /// # Example
739    /// ```no_run
740    /// use screencapturekit::content_sharing_picker::*;
741    /// use screencapturekit::stream::SCStream;
742    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
743    /// use screencapturekit::stream::content_filter::SCContentFilter;
744    /// use screencapturekit::shareable_content::SCShareableContent;
745    ///
746    /// fn example() -> Option<()> {
747    ///     let content = SCShareableContent::get().ok()?;
748    ///     let displays = content.displays();
749    ///     let display = displays.first()?;
750    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build().ok()?;
751    ///     let stream_config = SCStreamConfiguration::new();
752    ///     let stream = SCStream::new(&filter, &stream_config).ok()?;
753    ///
754    ///     // When stream is active and user wants to change source
755    ///     let config = SCContentSharingPickerConfiguration::new().ok()?;
756    ///     SCContentSharingPicker::show_for_stream(&config, &stream, |outcome| {
757    ///         if let SCPickerOutcome::Picked(result) = outcome {
758    ///             // Use result.filter() with stream.update_content_filter()
759    ///             let _ = result.filter();
760    ///         }
761    ///     });
762    ///     Some(())
763    /// }
764    /// ```
765    pub fn show_for_stream<F>(
766        config: &SCContentSharingPickerConfiguration,
767        stream: &crate::stream::SCStream,
768        callback: F,
769    ) where
770        F: FnOnce(SCPickerOutcome) + Send + 'static,
771    {
772        let context = into_callback_context::<SCPickerOutcome, F>(callback);
773
774        unsafe {
775            crate::ffi::sc_content_sharing_picker_show_for_stream(
776                config.as_ptr(),
777                stream.as_ptr(),
778                picker_trampoline::<ResultDecoder>,
779                context,
780            );
781        }
782    }
783
784    /// Show the picker UI with a callback that receives just the filter
785    ///
786    /// This is the simple API - use when you just need the filter without metadata.
787    ///
788    /// # Example
789    /// ```no_run
790    /// use screencapturekit::content_sharing_picker::*;
791    ///
792    /// let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
793    /// SCContentSharingPicker::show_filter(&config, |outcome| {
794    ///     if let SCPickerFilterOutcome::Filter(filter) = outcome {
795    ///         // Use filter with SCStream
796    ///     }
797    /// });
798    /// ```
799    pub fn show_filter<F>(config: &SCContentSharingPickerConfiguration, callback: F)
800    where
801        F: FnOnce(SCPickerFilterOutcome) + Send + 'static,
802    {
803        let context = into_callback_context::<SCPickerFilterOutcome, F>(callback);
804
805        unsafe {
806            crate::ffi::sc_content_sharing_picker_show(
807                config.as_ptr(),
808                picker_trampoline::<FilterDecoder>,
809                context,
810            );
811        }
812    }
813
814    /// Show the picker UI with a specific content style
815    ///
816    /// Presents the picker pre-filtered to a specific content type.
817    ///
818    /// # Arguments
819    /// * `config` - The picker configuration
820    /// * `style` - The content style to show (Window, Display, Application)
821    /// * `callback` - Called with the picker result
822    pub fn show_using_style<F>(
823        config: &SCContentSharingPickerConfiguration,
824        style: crate::stream::content_filter::SCShareableContentStyle,
825        callback: F,
826    ) where
827        F: FnOnce(SCPickerOutcome) + Send + 'static,
828    {
829        let context = into_callback_context::<SCPickerOutcome, F>(callback);
830
831        unsafe {
832            crate::ffi::sc_content_sharing_picker_show_using_style(
833                config.as_ptr(),
834                style as i32,
835                picker_trampoline::<ResultDecoder>,
836                context,
837            );
838        }
839    }
840
841    /// Show the picker for an existing stream with a specific content style
842    ///
843    /// # Arguments
844    /// * `config` - The picker configuration
845    /// * `stream` - The stream to update
846    /// * `style` - The content style to show (Window, Display, Application)
847    /// * `callback` - Called with the picker result
848    pub fn show_for_stream_using_style<F>(
849        config: &SCContentSharingPickerConfiguration,
850        stream: &crate::stream::SCStream,
851        style: crate::stream::content_filter::SCShareableContentStyle,
852        callback: F,
853    ) where
854        F: FnOnce(SCPickerOutcome) + Send + 'static,
855    {
856        let context = into_callback_context::<SCPickerOutcome, F>(callback);
857
858        unsafe {
859            crate::ffi::sc_content_sharing_picker_show_for_stream_using_style(
860                config.as_ptr(),
861                stream.as_ptr(),
862                style as i32,
863                picker_trampoline::<ResultDecoder>,
864                context,
865            );
866        }
867    }
868
869    /// Set the maximum number of streams that can be created from the picker
870    ///
871    /// Pass 0 to allow unlimited streams.
872    #[allow(clippy::missing_errors_doc)]
873    pub fn set_maximum_stream_count(count: usize) -> Result<(), SCPickerConfigurationError> {
874        Self::require_available()?;
875        unsafe {
876            crate::ffi::sc_content_sharing_picker_set_maximum_stream_count(count);
877        }
878        Ok(())
879    }
880
881    /// Get the maximum number of streams allowed
882    ///
883    /// Returns 0 if unlimited streams are allowed.
884    pub fn maximum_stream_count() -> usize {
885        if !Self::is_available() {
886            return 0;
887        }
888        unsafe { crate::ffi::sc_content_sharing_picker_get_maximum_stream_count() }
889    }
890
891    /// Returns whether the shared content-sharing picker is currently
892    /// marked active.
893    ///
894    /// Apple requires `picker.isActive = true` before its UI can appear.
895    /// The various `show*()` trampolines on this type set it implicitly
896    /// before presenting, but this getter is useful for callers that
897    /// want to:
898    ///
899    /// * avoid double-presenting (skip a second `show()` while the first
900    ///   picker session is still up),
901    /// * render UI affordances based on whether the picker is currently
902    ///   visible to the user.
903    #[must_use]
904    pub fn is_active() -> bool {
905        if !Self::is_available() {
906            return false;
907        }
908        unsafe { crate::ffi::sc_content_sharing_picker_get_active() }
909    }
910
911    /// Mark the shared content-sharing picker active or inactive.
912    ///
913    /// Setting this to `false` hides the picker UI between sessions
914    /// (the recommended hygiene step after a long-running app finishes
915    /// using the picker — leaving it active leaves the system-level
916    /// Control Center entry in a "ready to share" state).
917    ///
918    /// Setting to `true` is required before `present*()` can surface
919    /// the picker; the `show*()` trampolines do this for you. Set it
920    /// manually only if you want to opt into the picker UI without
921    /// immediately presenting it.
922    #[allow(clippy::missing_errors_doc)]
923    pub fn set_active(active: bool) -> Result<(), SCPickerConfigurationError> {
924        Self::require_available()?;
925        unsafe { crate::ffi::sc_content_sharing_picker_set_active(active) };
926        Ok(())
927    }
928
929    /// Deactivate the picker and undo any activation-policy promotion the
930    /// bridge performed on behalf of a non-UI (`.prohibited`) host process.
931    ///
932    /// Presenting `SCContentSharingPicker` requires the process to be a
933    /// regular, Dock-visible app. Pure-Rust hosts usually are not, so the
934    /// bridge temporarily promotes them; the promotion is reference counted
935    /// and unwound automatically when each one-shot `show*()` resolves. Call
936    /// this after you are done with a *long-lived* observer session to drop
937    /// the promotion immediately and clear the Control Center "ready to
938    /// share" indicator.
939    ///
940    /// Registered observers are **not** removed — drop their
941    /// [`SCPickerSubscription`] for that.
942    pub fn deactivate() {
943        if !Self::is_available() {
944            return;
945        }
946        unsafe { crate::ffi::sc_content_sharing_picker_deactivate() }
947    }
948
949    // ------------------------------------------------------------------
950    // Standalone configuration operations
951    // ------------------------------------------------------------------
952
953    /// Read the picker's process-wide default configuration.
954    ///
955    /// Equivalent to Apple's `SCContentSharingPicker.shared.defaultConfiguration`.
956    /// This is the same value returned by
957    /// [`SCContentSharingPickerConfiguration::default_from_system`].
958    ///
959    /// # Errors
960    ///
961    /// Returns [`SCError::FeatureNotAvailable`] when run on macOS older than
962    /// 14.0.
963    pub fn default_configuration() -> Result<SCContentSharingPickerConfiguration, SCError> {
964        SCContentSharingPickerConfiguration::default_from_system()
965    }
966
967    /// Assign the picker's process-wide default configuration.
968    ///
969    /// Apple's `SCContentSharingPicker.defaultConfiguration` is read-write;
970    /// previously this crate could only set it as a side effect of calling a
971    /// `show*()` helper. Setting it explicitly is what you want when driving
972    /// the picker with a persistent observer plus [`Self::present`].
973    ///
974    /// # Examples
975    ///
976    /// ```no_run
977    /// use screencapturekit::content_sharing_picker::*;
978    ///
979    /// let mut config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
980    /// config.set_allows_changing_selected_content(true);
981    /// SCContentSharingPicker::set_default_configuration(&config)
982    ///     .expect("call from the process main thread");
983    /// ```
984    ///
985    /// The assignment is complete when this method returns, so an immediate
986    /// call to [`Self::default_configuration`] observes the new value.
987    ///
988    /// # Errors
989    ///
990    /// Returns [`SCPickerConfigurationError::Unavailable`] when the API is
991    /// unavailable, or [`SCPickerConfigurationError::MainThreadRequired`]
992    /// when called from any other thread. Failed calls do not enqueue a later
993    /// mutation.
994    pub fn set_default_configuration(
995        config: &SCContentSharingPickerConfiguration,
996    ) -> Result<(), SCPickerConfigurationError> {
997        if !Self::is_available() {
998            return Err(SCPickerConfigurationError::Unavailable);
999        }
1000        if unsafe {
1001            crate::ffi::sc_content_sharing_picker_set_default_configuration(config.as_ptr())
1002        } {
1003            Ok(())
1004        } else {
1005            Err(SCPickerConfigurationError::MainThreadRequired)
1006        }
1007    }
1008
1009    /// Assign a picker configuration scoped to a single stream, mirroring
1010    /// Apple's `setConfiguration(_:for:)`.
1011    ///
1012    /// Pass `None` to clear the stream-specific configuration and fall back to
1013    /// the process-wide default.
1014    ///
1015    /// The assignment is complete when this method returns.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns [`SCPickerConfigurationError::Unavailable`] when the API is
1020    /// unavailable, or [`SCPickerConfigurationError::MainThreadRequired`]
1021    /// when called from any other thread. Failed calls do not enqueue a later
1022    /// mutation.
1023    pub fn set_configuration_for_stream(
1024        config: Option<&SCContentSharingPickerConfiguration>,
1025        stream: &crate::stream::SCStream,
1026    ) -> Result<(), SCPickerConfigurationError> {
1027        if !Self::is_available() {
1028            return Err(SCPickerConfigurationError::Unavailable);
1029        }
1030        let config_ptr = config.map_or(
1031            std::ptr::null(),
1032            SCContentSharingPickerConfiguration::as_ptr,
1033        );
1034        if unsafe {
1035            crate::ffi::sc_content_sharing_picker_set_configuration_for_stream(
1036                config_ptr,
1037                stream.as_ptr(),
1038            )
1039        } {
1040            Ok(())
1041        } else {
1042            Err(SCPickerConfigurationError::MainThreadRequired)
1043        }
1044    }
1045
1046    // ------------------------------------------------------------------
1047    // Persistent observers
1048    // ------------------------------------------------------------------
1049
1050    /// Register a **repeating** observer that receives every picker event for
1051    /// as long as the returned subscription is alive.
1052    ///
1053    /// This is the API to use with
1054    /// [`SCContentSharingPickerConfiguration::set_allows_changing_selected_content`]:
1055    /// Apple re-invokes `contentSharingPicker(_:didUpdateWith:for:)` each time
1056    /// the user re-picks during an active share, and the one-shot
1057    /// [`Self::show`] family deliberately latches after the first event.
1058    ///
1059    /// The subscription unregisters on drop, so bind it to a variable that
1060    /// lives as long as you want events (use [`SCPickerSubscription::detach`]
1061    /// to keep it for the remainder of the process).
1062    ///
1063    /// Pair this with [`Self::present`] / [`Self::present_for_stream`] to
1064    /// surface the UI.
1065    ///
1066    /// Apple marks `SCContentSharingPicker` as `@MainActor`. Call this on the
1067    /// process main thread, or while an `AppKit` main run loop is active so the
1068    /// bridge can synchronously hop to it.
1069    ///
1070    /// # Errors
1071    ///
1072    /// Returns [`SCPickerConfigurationError::Unavailable`] below macOS 14.0,
1073    /// and [`SCPickerConfigurationError::MainThreadRequired`] when called off
1074    /// the main thread without an active main run loop.
1075    ///
1076    /// # Examples
1077    ///
1078    /// ```no_run
1079    /// use screencapturekit::content_sharing_picker::*;
1080    ///
1081    /// let mut config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
1082    /// config.set_allows_changing_selected_content(true);
1083    /// SCContentSharingPicker::set_default_configuration(&config)
1084    ///     .expect("call from the process main thread");
1085    ///
1086    /// let subscription = SCContentSharingPicker::add_observer(|event| match event {
1087    ///     SCPickerEvent::Updated { result, stream } => {
1088    ///         // Fires again every time the user changes their selection.
1089    ///         let _filter = result.filter();
1090    ///         let _existing_stream = stream;
1091    ///     }
1092    ///     SCPickerEvent::Cancelled { .. } => println!("cancelled"),
1093    ///     SCPickerEvent::Failed(err) => eprintln!("picker failed: {err}"),
1094    /// }).expect("register picker observer");
1095    ///
1096    /// SCContentSharingPicker::present().expect("present picker");
1097    /// // ... keep `subscription` alive for as long as you want updates ...
1098    /// drop(subscription);
1099    /// ```
1100    pub fn add_observer<F>(handler: F) -> Result<SCPickerSubscription, SCPickerConfigurationError>
1101    where
1102        F: Fn(SCPickerEvent) + Send + Sync + 'static,
1103    {
1104        Self::require_available()?;
1105        let (context, active) = SCPickerObserverContext::into_raw(handler);
1106        let token = unsafe {
1107            crate::ffi::sc_content_sharing_picker_add_observer(
1108                observer_trampoline,
1109                observer_context_release,
1110                context,
1111            )
1112        };
1113
1114        if token == 0 {
1115            observer_context_release(context);
1116            return Err(SCPickerConfigurationError::MainThreadRequired);
1117        }
1118
1119        Ok(SCPickerSubscription { token, active })
1120    }
1121
1122    /// Remove every repeating observer registered through
1123    /// [`Self::add_observer`], regardless of which subscriptions are still
1124    /// alive. Returns how many were removed.
1125    ///
1126    /// Dropping the corresponding [`SCPickerSubscription`] afterwards is
1127    /// harmless — removal is idempotent.
1128    pub fn remove_all_observers() -> usize {
1129        if !Self::is_available() {
1130            return 0;
1131        }
1132        unsafe { crate::ffi::sc_content_sharing_picker_remove_all_observers() }
1133    }
1134
1135    // ------------------------------------------------------------------
1136    // Standalone presentation
1137    // ------------------------------------------------------------------
1138
1139    /// Present the picker without a content-style hint.
1140    ///
1141    /// Use with [`Self::add_observer`]; the one-shot [`Self::show`] family
1142    /// presents for you.
1143    #[allow(clippy::missing_errors_doc)]
1144    pub fn present() -> Result<(), SCPickerConfigurationError> {
1145        Self::require_available()?;
1146        unsafe { crate::ffi::sc_content_sharing_picker_present(-1) };
1147        Ok(())
1148    }
1149
1150    /// Present the picker preselecting a content style.
1151    #[allow(clippy::missing_errors_doc)]
1152    pub fn present_using_style(
1153        style: SCShareableContentStyle,
1154    ) -> Result<(), SCPickerConfigurationError> {
1155        Self::require_available()?;
1156        unsafe { crate::ffi::sc_content_sharing_picker_present(style as i32) };
1157        Ok(())
1158    }
1159
1160    /// Present the picker targeting an existing stream, so the user can swap
1161    /// the shared source mid-capture.
1162    #[allow(clippy::missing_errors_doc)]
1163    pub fn present_for_stream(
1164        stream: &crate::stream::SCStream,
1165    ) -> Result<(), SCPickerConfigurationError> {
1166        Self::require_available()?;
1167        unsafe { crate::ffi::sc_content_sharing_picker_present_for_stream(stream.as_ptr(), -1) };
1168        Ok(())
1169    }
1170
1171    /// Present the picker targeting an existing stream, preselecting a style.
1172    #[allow(clippy::missing_errors_doc)]
1173    pub fn present_for_stream_using_style(
1174        stream: &crate::stream::SCStream,
1175        style: SCShareableContentStyle,
1176    ) -> Result<(), SCPickerConfigurationError> {
1177        Self::require_available()?;
1178        unsafe {
1179            crate::ffi::sc_content_sharing_picker_present_for_stream(stream.as_ptr(), style as i32);
1180        }
1181        Ok(())
1182    }
1183}
1184
1185// ============================================================================
1186// Persistent observer: events, subscription handle, context, trampoline
1187// ============================================================================
1188
1189/// A single event delivered to a repeating observer registered with
1190/// [`SCContentSharingPicker::add_observer`].
1191///
1192/// Unlike [`SCPickerOutcome`], which resolves a one-shot `show*()` call,
1193/// these arrive as many times as the user interacts with the picker.
1194#[derive(Debug)]
1195pub enum SCPickerEvent {
1196    /// The user selected (or re-selected) content. Mirrors Apple's
1197    /// `contentSharingPicker(_:didUpdateWith:for:)`. `stream` identifies an
1198    /// existing stream being updated; `None` means the user made a new
1199    /// selection rather than replacing a stream's source.
1200    Updated {
1201        /// Selected filter and metadata.
1202        result: SCPickerResult,
1203        /// Non-owning identity of the stream being updated.
1204        stream: Option<StreamIdentity>,
1205    },
1206    /// The user dismissed the picker. Mirrors
1207    /// `contentSharingPicker(_:didCancelFor:)`.
1208    Cancelled {
1209        /// Non-owning identity of the stream whose update was cancelled.
1210        stream: Option<StreamIdentity>,
1211    },
1212    /// The picker failed to start. Mirrors
1213    /// `contentSharingPickerStartDidFailWithError(_:)`.
1214    Failed(String),
1215}
1216
1217/// Handle representing a live repeating-observer registration.
1218///
1219/// The observer is removed when this value is dropped. Call
1220/// [`Self::detach`] to keep the observer alive for the rest of the process.
1221#[derive(Debug)]
1222#[must_use = "the observer is removed as soon as the subscription is dropped"]
1223pub struct SCPickerSubscription {
1224    token: i64,
1225    active: std::sync::Arc<AtomicBool>,
1226}
1227
1228impl SCPickerSubscription {
1229    /// Opaque identifier for this registration. Non-zero when registration
1230    /// succeeded.
1231    #[must_use]
1232    pub const fn token(&self) -> i64 {
1233        self.token
1234    }
1235
1236    /// Whether this subscription refers to a live registration.
1237    #[must_use]
1238    pub fn is_active(&self) -> bool {
1239        self.active.load(Ordering::Acquire)
1240    }
1241
1242    /// Remove the observer now instead of waiting for the drop.
1243    ///
1244    /// Returns `true` if a live observer was removed.
1245    pub fn unsubscribe(mut self) -> bool {
1246        self.remove()
1247    }
1248
1249    /// Give up ownership without removing the observer, keeping it registered
1250    /// for the remainder of the process.
1251    ///
1252    /// Useful for "install once at startup" wiring where there is no natural
1253    /// owner for the handle. The registration can still be torn down with
1254    /// [`SCContentSharingPicker::remove_all_observers`].
1255    pub fn detach(mut self) {
1256        self.token = 0;
1257    }
1258
1259    fn remove(&mut self) -> bool {
1260        if self.token == 0 || !self.is_active() {
1261            self.token = 0;
1262            return false;
1263        }
1264        let removed = unsafe { crate::ffi::sc_content_sharing_picker_remove_observer(self.token) };
1265        self.token = 0;
1266        if !removed {
1267            self.active.store(false, Ordering::Release);
1268        }
1269        removed
1270    }
1271}
1272
1273impl Drop for SCPickerSubscription {
1274    fn drop(&mut self) {
1275        self.remove();
1276    }
1277}
1278
1279struct SCPickerObserverContext {
1280    /// Cleared on unsubscribe so a callback already in flight is dropped
1281    /// rather than delivered after the user asked to stop listening.
1282    active: std::sync::Arc<AtomicBool>,
1283    handler: Box<dyn Fn(SCPickerEvent) + Send + Sync>,
1284}
1285
1286impl SCPickerObserverContext {
1287    fn into_raw<F>(handler: F) -> (*mut c_void, std::sync::Arc<AtomicBool>)
1288    where
1289        F: Fn(SCPickerEvent) + Send + Sync + 'static,
1290    {
1291        let active = std::sync::Arc::new(AtomicBool::new(true));
1292        let context = std::sync::Arc::new(Self {
1293            active: std::sync::Arc::clone(&active),
1294            handler: Box::new(handler),
1295        });
1296        let mut registry = PICKER_OBSERVER_CONTEXTS
1297            .lock()
1298            .unwrap_or_else(PoisonError::into_inner);
1299        let contexts = registry.get_or_insert_with(HashMap::new);
1300        let id = loop {
1301            let id = NEXT_PICKER_OBSERVER_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
1302            if id != 0 && !contexts.contains_key(&id) {
1303                break id;
1304            }
1305        };
1306        contexts.insert(id, context);
1307        drop(registry);
1308        (id as *mut c_void, active)
1309    }
1310}
1311
1312static NEXT_PICKER_OBSERVER_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1);
1313static PICKER_OBSERVER_CONTEXTS: Mutex<
1314    Option<HashMap<usize, std::sync::Arc<SCPickerObserverContext>>>,
1315> = Mutex::new(None);
1316
1317fn picker_observer_context(
1318    context: *mut c_void,
1319) -> Option<std::sync::Arc<SCPickerObserverContext>> {
1320    let id = context as usize;
1321    if id == 0 {
1322        return None;
1323    }
1324    PICKER_OBSERVER_CONTEXTS
1325        .lock()
1326        .unwrap_or_else(PoisonError::into_inner)
1327        .as_ref()?
1328        .get(&id)
1329        .cloned()
1330}
1331
1332extern "C" fn observer_context_release(context: *mut c_void) {
1333    crate::utils::panic_safe::catch_user_panic("picker observer context release", || {
1334        let id = context as usize;
1335        if id == 0 {
1336            return;
1337        }
1338        let removed = {
1339            let mut contexts = PICKER_OBSERVER_CONTEXTS
1340                .lock()
1341                .unwrap_or_else(PoisonError::into_inner);
1342            contexts.as_mut().and_then(|contexts| contexts.remove(&id))
1343        };
1344        if let Some(context) = removed {
1345            context.active.store(false, Ordering::Release);
1346        }
1347    });
1348}
1349
1350/// Trampoline for every repeating-observer event.
1351///
1352/// `event` follows the Swift bridge contract: 1 = updated (with a result
1353/// pointer), 0 = cancelled, anything else = start failure (with a message).
1354extern "C" fn observer_trampoline(
1355    event: i32,
1356    result_ptr: *const c_void,
1357    message: *const i8,
1358    stream_ptr: *const c_void,
1359    context: *mut c_void,
1360) {
1361    // The whole body sits inside the barrier: the registry lookup and the
1362    // message decoding both allocate, and an unwind out of an `extern "C"`
1363    // function is undefined behaviour on this crate's MSRV.
1364    crate::utils::panic_safe::catch_user_panic("picker observer callback", move || {
1365        let Some(context) = picker_observer_context(context) else {
1366            if !result_ptr.is_null() {
1367                unsafe { crate::ffi::sc_picker_result_release(result_ptr) };
1368            }
1369            return;
1370        };
1371
1372        if !context.active.load(Ordering::Acquire) {
1373            // Unsubscribed while this callback was in flight; drop the result
1374            // rather than delivering it. Release the retained result first.
1375            if !result_ptr.is_null() {
1376                unsafe { crate::ffi::sc_picker_result_release(result_ptr) };
1377            }
1378            return;
1379        }
1380
1381        let stream = StreamIdentity::from_ptr(stream_ptr);
1382        let decoded = match event {
1383            1 if !result_ptr.is_null() => SCPickerEvent::Updated {
1384                result: SCPickerResult { ptr: result_ptr },
1385                stream,
1386            },
1387            1 => SCPickerEvent::Failed("picker delivered an update without a result".to_string()),
1388            0 => SCPickerEvent::Cancelled { stream },
1389            _ => {
1390                let text = if message.is_null() {
1391                    "Content sharing picker failed to start".to_string()
1392                } else {
1393                    // SAFETY: Swift passes a NUL-terminated UTF-8 buffer that is
1394                    // valid for the duration of this call.
1395                    unsafe { std::ffi::CStr::from_ptr(message) }
1396                        .to_string_lossy()
1397                        .into_owned()
1398                };
1399                SCPickerEvent::Failed(text)
1400            }
1401        };
1402
1403        (context.handler)(decoded);
1404    });
1405}
1406
1407// ============================================================================
1408// One-shot callback context + trampoline (shared by all `show*()` methods)
1409// ============================================================================
1410
1411/// Context owned by the one-shot callback registry.
1412struct PickerCallbackContext<O> {
1413    closure: Box<dyn FnOnce(O) + Send>,
1414}
1415
1416static NEXT_PICKER_CALLBACK_ID: AtomicUsize = AtomicUsize::new(1);
1417static PICKER_CALLBACKS: Mutex<Option<HashMap<usize, Box<dyn Any + Send>>>> = Mutex::new(None);
1418
1419/// Store a user closure and hand Swift a token that is never dereferenced.
1420#[allow(clippy::significant_drop_tightening)]
1421fn into_callback_context<O, F>(callback: F) -> *mut c_void
1422where
1423    O: 'static,
1424    F: FnOnce(O) + Send + 'static,
1425{
1426    let context = Box::new(PickerCallbackContext {
1427        closure: Box::new(callback),
1428    });
1429    let mut callbacks = PICKER_CALLBACKS
1430        .lock()
1431        .unwrap_or_else(PoisonError::into_inner);
1432    let callbacks = callbacks.get_or_insert_with(HashMap::new);
1433    loop {
1434        let id = NEXT_PICKER_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
1435        if id != 0 && !callbacks.contains_key(&id) {
1436            callbacks.insert(id, context);
1437            return id as *mut c_void;
1438        }
1439    }
1440}
1441
1442fn take_callback_context<O: 'static>(
1443    context: *mut c_void,
1444) -> Option<Box<PickerCallbackContext<O>>> {
1445    let id = context as usize;
1446    if id == 0 {
1447        return None;
1448    }
1449    let entry = PICKER_CALLBACKS
1450        .lock()
1451        .unwrap_or_else(PoisonError::into_inner)
1452        .as_mut()?
1453        .remove(&id)?;
1454    entry.downcast::<PickerCallbackContext<O>>().ok()
1455}
1456
1457/// Decodes the `(code, ptr)` pair from the Swift bridge into a typed outcome.
1458///
1459/// Implemented by zero-sized marker types so a single generic trampoline can
1460/// serve both the result-bearing and filter-only APIs while keeping the FFI
1461/// signature identical.
1462trait PickerDecode {
1463    type Outcome: 'static;
1464    fn decode(code: i32, ptr: *const c_void) -> Self::Outcome;
1465    unsafe fn release(ptr: *const c_void);
1466}
1467
1468struct ResultDecoder;
1469impl PickerDecode for ResultDecoder {
1470    type Outcome = SCPickerOutcome;
1471    fn decode(code: i32, ptr: *const c_void) -> SCPickerOutcome {
1472        match code {
1473            1 if !ptr.is_null() => SCPickerOutcome::Picked(SCPickerResult { ptr }),
1474            0 => SCPickerOutcome::Cancelled,
1475            _ => SCPickerOutcome::Error("Picker failed".to_string()),
1476        }
1477    }
1478
1479    unsafe fn release(ptr: *const c_void) {
1480        if !ptr.is_null() {
1481            unsafe { crate::ffi::sc_picker_result_release(ptr) };
1482        }
1483    }
1484}
1485
1486struct FilterDecoder;
1487impl PickerDecode for FilterDecoder {
1488    type Outcome = SCPickerFilterOutcome;
1489    fn decode(code: i32, ptr: *const c_void) -> SCPickerFilterOutcome {
1490        match code {
1491            1 if !ptr.is_null() => {
1492                SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(ptr))
1493            }
1494            0 => SCPickerFilterOutcome::Cancelled,
1495            _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
1496        }
1497    }
1498
1499    unsafe fn release(ptr: *const c_void) {
1500        if !ptr.is_null() {
1501            unsafe { crate::ffi::sc_content_filter_release(ptr) };
1502        }
1503    }
1504}
1505
1506/// Single trampoline for every picker `show*()` callback.
1507///
1508/// `code` follows the Swift bridge contract (1 = picked, 0 = cancelled,
1509/// anything else = error). A `code` of 0 is also produced by the Swift
1510/// replacement path when a pending observer is superseded by a newer
1511/// `show*()`, so a replaced picker resolves as `Cancelled` rather than
1512/// leaking its context.
1513///
1514/// The registry entry is removed exactly once. Duplicate or late callbacks
1515/// find no entry, so they cannot dereference freed memory.
1516extern "C" fn picker_trampoline<D: PickerDecode>(
1517    code: i32,
1518    ptr: *const c_void,
1519    context: *mut c_void,
1520) {
1521    crate::utils::panic_safe::catch_user_panic("picker callback", move || {
1522        let Some(context) = take_callback_context::<D::Outcome>(context) else {
1523            unsafe { D::release(ptr) };
1524            return;
1525        };
1526        let outcome = D::decode(code, ptr);
1527        (context.closure)(outcome);
1528    });
1529}
1530
1531// SAFETY: the wrapper owns its Swift box exclusively — `Clone` copies the box
1532// rather than retaining it, and no constructor hands out a second handle to the
1533// same allocation. Mutation therefore only happens through `&mut self`, which
1534// Rust already makes exclusive, and the `&self` methods are pure getters. The
1535// box itself is a Swift class, so the refcount traffic that `Drop` performs is
1536// atomic.
1537unsafe impl Send for SCContentSharingPickerConfiguration {}
1538unsafe impl Sync for SCContentSharingPickerConfiguration {}
1539// SAFETY: `SCPickerResult` holds retained Objective-C objects whose reference
1540// counting is atomic; it is safe to send between and share across threads.
1541unsafe impl Send for SCPickerResult {}
1542unsafe impl Sync for SCPickerResult {}
1543
1544#[cfg(test)]
1545mod tests {
1546    use super::*;
1547    use std::sync::atomic::{AtomicUsize, Ordering};
1548    use std::sync::Arc;
1549
1550    #[test]
1551    fn duplicate_one_shot_callback_is_ignored_after_context_drop() {
1552        let calls = Arc::new(AtomicUsize::new(0));
1553        let observed = Arc::clone(&calls);
1554        let context = into_callback_context::<SCPickerFilterOutcome, _>(move |_| {
1555            observed.fetch_add(1, Ordering::SeqCst);
1556        });
1557
1558        picker_trampoline::<FilterDecoder>(0, std::ptr::null(), context);
1559        picker_trampoline::<FilterDecoder>(0, std::ptr::null(), context);
1560
1561        assert_eq!(calls.load(Ordering::SeqCst), 1);
1562    }
1563
1564    #[test]
1565    fn released_repeating_observer_ignores_late_callback() {
1566        let calls = Arc::new(AtomicUsize::new(0));
1567        let observed = Arc::clone(&calls);
1568        let (context, active) = SCPickerObserverContext::into_raw(move |_| {
1569            observed.fetch_add(1, Ordering::SeqCst);
1570        });
1571
1572        observer_context_release(context);
1573        observer_trampoline(
1574            0,
1575            std::ptr::null(),
1576            std::ptr::null(),
1577            std::ptr::null(),
1578            context,
1579        );
1580
1581        assert_eq!(calls.load(Ordering::SeqCst), 0);
1582        assert!(!active.load(Ordering::Acquire));
1583    }
1584
1585    #[test]
1586    fn repeating_observer_preserves_stream_identity() {
1587        let observed = Arc::new(Mutex::new(None));
1588        let output = Arc::clone(&observed);
1589        let (context, _) = SCPickerObserverContext::into_raw(move |event| {
1590            if let SCPickerEvent::Cancelled { stream } = event {
1591                *output.lock().unwrap_or_else(PoisonError::into_inner) = stream;
1592            }
1593        });
1594        let stream_ptr = std::ptr::NonNull::<c_void>::dangling()
1595            .as_ptr()
1596            .cast_const();
1597
1598        observer_trampoline(0, std::ptr::null(), std::ptr::null(), stream_ptr, context);
1599        observer_context_release(context);
1600
1601        assert_eq!(
1602            *observed.lock().unwrap_or_else(PoisonError::into_inner),
1603            StreamIdentity::from_ptr(stream_ptr)
1604        );
1605    }
1606}