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