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