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