use crate::stream::content_filter::{SCContentFilter, SCShareableContentStyle};
use std::any::Any;
use std::collections::HashMap;
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, PoisonError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SCPickedSource {
Window(String),
Display(u32),
Application(String),
Unknown,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SCContentSharingPickerMode {
#[default]
SingleWindow = 0,
MultipleWindows = 1,
SingleDisplay = 2,
SingleApplication = 3,
MultipleApplications = 4,
}
pub struct SCContentSharingPickerConfiguration {
ptr: *const c_void,
}
impl SCContentSharingPickerConfiguration {
#[must_use]
pub fn new() -> Self {
Self::try_new().expect("SCContentSharingPicker requires macOS 14.0 or later")
}
#[must_use]
pub fn try_new() -> Option<Self> {
if !SCContentSharingPicker::is_available() {
return None;
}
let ptr = unsafe { crate::ffi::sc_content_sharing_picker_configuration_create() };
(!ptr.is_null()).then_some(Self { ptr })
}
#[must_use]
pub fn default_from_system() -> Self {
assert!(
SCContentSharingPicker::is_available(),
"SCContentSharingPicker requires macOS 14.0 or later"
);
let ptr = unsafe { crate::ffi::sc_content_sharing_picker_create_default_configuration() };
Self { ptr }
}
pub fn set_allowed_picker_modes(&mut self, modes: &[SCContentSharingPickerMode]) {
let mode_values: Vec<i32> = modes.iter().map(|m| *m as i32).collect();
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_set_allowed_picker_modes(
self.ptr,
mode_values.as_ptr(),
mode_values.len(),
);
}
}
pub fn allowed_picker_modes(&self) -> Vec<SCContentSharingPickerMode> {
let mask = unsafe {
crate::ffi::sc_content_sharing_picker_configuration_get_allowed_picker_modes_mask(
self.ptr,
)
};
let mut modes = Vec::new();
for (raw_value, mode) in [
(1_u64, SCContentSharingPickerMode::SingleWindow),
(2_u64, SCContentSharingPickerMode::MultipleWindows),
(16_u64, SCContentSharingPickerMode::SingleDisplay),
(4_u64, SCContentSharingPickerMode::SingleApplication),
(8_u64, SCContentSharingPickerMode::MultipleApplications),
] {
if mask & raw_value != 0 {
modes.push(mode);
}
}
modes
}
pub fn set_allows_changing_selected_content(&mut self, allows: bool) {
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_set_allows_changing_selected_content(
self.ptr,
allows,
);
}
}
pub fn allows_changing_selected_content(&self) -> bool {
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_get_allows_changing_selected_content(
self.ptr,
)
}
}
pub fn set_excluded_bundle_ids(&mut self, bundle_ids: &[&str]) {
let c_strings: Vec<std::ffi::CString> = if let Ok(ids) = bundle_ids
.iter()
.map(|id| std::ffi::CString::new(*id))
.collect()
{
ids
} else {
eprintln!(
"SCContentSharingPickerConfiguration: excluded bundle ID contains an \
interior NUL byte; configuration was not changed"
);
return;
};
let ptrs: Vec<*const i8> = c_strings.iter().map(|s| s.as_ptr()).collect();
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_set_excluded_bundle_ids(
self.ptr,
ptrs.as_ptr(),
ptrs.len(),
);
}
}
#[must_use]
pub fn excluded_bundle_ids(&self) -> Vec<String> {
let count = self.excluded_bundle_ids_count();
let mut result = Vec::with_capacity(count);
for i in 0..count {
let id = unsafe {
crate::utils::ffi_string::ffi_string_from_buffer(
crate::utils::ffi_string::DEFAULT_BUFFER_SIZE,
|buffer, len| {
crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_id_at(
self.ptr,
i,
buffer,
usize::try_from(len).unwrap_or(0),
)
},
)
};
if let Some(id) = id {
result.push(id);
}
}
result
}
#[must_use]
pub fn excluded_bundle_ids_count(&self) -> usize {
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_get_excluded_bundle_ids_count(
self.ptr,
)
}
}
pub fn set_excluded_window_ids(&mut self, window_ids: &[u32]) {
unsafe {
crate::ffi::sc_content_sharing_picker_configuration_set_excluded_window_ids(
self.ptr,
window_ids.as_ptr(),
window_ids.len(),
);
}
}
pub fn excluded_window_ids(&self) -> Vec<u32> {
let count = unsafe {
crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_ids_count(
self.ptr,
)
};
let mut result = Vec::with_capacity(count);
for i in 0..count {
let id = unsafe {
crate::ffi::sc_content_sharing_picker_configuration_get_excluded_window_id_at(
self.ptr, i,
)
};
result.push(id);
}
result
}
#[must_use]
pub const fn as_ptr(&self) -> *const c_void {
self.ptr
}
}
impl Default for SCContentSharingPickerConfiguration {
fn default() -> Self {
Self::new()
}
}
crate::utils::retained::sc_retained!(
SCContentSharingPickerConfiguration,
field = ptr,
release = crate::ffi::sc_content_sharing_picker_configuration_release,
);
impl Clone for SCContentSharingPickerConfiguration {
fn clone(&self) -> Self {
Self {
ptr: unsafe { crate::ffi::sc_content_sharing_picker_configuration_copy(self.ptr) },
}
}
}
impl std::fmt::Debug for SCContentSharingPickerConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SCContentSharingPickerConfiguration")
.field("ptr", &self.ptr)
.finish()
}
}
#[derive(Debug)]
pub enum SCPickerFilterOutcome {
Filter(SCContentFilter),
Cancelled,
Error(String),
}
pub struct SCPickerResult {
ptr: *const c_void,
}
impl SCPickerResult {
#[cfg(feature = "async")]
#[must_use]
pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
Self { ptr }
}
#[must_use]
pub fn filter(&self) -> SCContentFilter {
let filter_ptr = unsafe { crate::ffi::sc_picker_result_get_filter(self.ptr) };
SCContentFilter::from_picker_ptr(filter_ptr)
}
#[must_use]
pub fn size(&self) -> (f64, f64) {
let mut x = 0.0;
let mut y = 0.0;
let mut width = 0.0;
let mut height = 0.0;
unsafe {
crate::ffi::sc_picker_result_get_content_rect(
self.ptr,
&mut x,
&mut y,
&mut width,
&mut height,
);
}
(width, height)
}
#[must_use]
pub fn rect(&self) -> (f64, f64, f64, f64) {
let mut x = 0.0;
let mut y = 0.0;
let mut width = 0.0;
let mut height = 0.0;
unsafe {
crate::ffi::sc_picker_result_get_content_rect(
self.ptr,
&mut x,
&mut y,
&mut width,
&mut height,
);
}
(x, y, width, height)
}
#[must_use]
pub fn scale(&self) -> f64 {
unsafe { crate::ffi::sc_picker_result_get_scale(self.ptr) }
}
#[must_use]
pub fn pixel_size(&self) -> (u32, u32) {
let (w, h) = self.size();
let scale = self.scale();
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let width = (w * scale) as u32;
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let height = (h * scale) as u32;
(width, height)
}
#[must_use]
pub fn windows(&self) -> Vec<crate::shareable_content::SCWindow> {
let count = unsafe { crate::ffi::sc_picker_result_get_windows_count(self.ptr) };
(0..count)
.filter_map(|i| {
let ptr = unsafe { crate::ffi::sc_picker_result_get_window_at(self.ptr, i) };
unsafe { crate::shareable_content::SCWindow::from_retained_ptr(ptr) }
})
.collect()
}
#[must_use]
pub fn displays(&self) -> Vec<crate::shareable_content::SCDisplay> {
let count = unsafe { crate::ffi::sc_picker_result_get_displays_count(self.ptr) };
(0..count)
.filter_map(|i| {
let ptr = unsafe { crate::ffi::sc_picker_result_get_display_at(self.ptr, i) };
unsafe { crate::shareable_content::SCDisplay::from_retained_ptr(ptr) }
})
.collect()
}
#[must_use]
pub fn applications(&self) -> Vec<crate::shareable_content::SCRunningApplication> {
let count = unsafe { crate::ffi::sc_picker_result_get_applications_count(self.ptr) };
(0..count)
.filter_map(|i| {
let ptr = unsafe { crate::ffi::sc_picker_result_get_application_at(self.ptr, i) };
unsafe { crate::shareable_content::SCRunningApplication::from_retained_ptr(ptr) }
})
.collect()
}
#[must_use]
#[allow(clippy::option_if_let_else)]
pub fn source(&self) -> SCPickedSource {
if let Some(window) = self.windows().first() {
SCPickedSource::Window(window.title().unwrap_or_else(|| "Untitled".to_string()))
} else if let Some(display) = self.displays().first() {
SCPickedSource::Display(display.display_id())
} else if let Some(app) = self.applications().first() {
SCPickedSource::Application(app.application_name())
} else {
SCPickedSource::Unknown
}
}
}
crate::utils::retained::sc_retained!(
SCPickerResult,
field = ptr,
release = crate::ffi::sc_picker_result_release,
);
impl std::fmt::Debug for SCPickerResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (w, h) = self.size();
let scale = self.scale();
f.debug_struct("SCPickerResult")
.field("size", &(w, h))
.field("scale", &scale)
.field("pixel_size", &self.pixel_size())
.finish()
}
}
#[derive(Debug)]
pub enum SCPickerOutcome {
Picked(SCPickerResult),
Cancelled,
Error(String),
}
#[derive(Debug)]
pub struct SCContentSharingPicker;
impl SCContentSharingPicker {
fn available_or_log(operation: &str) -> bool {
let available = Self::is_available();
if !available {
eprintln!("{operation} requires macOS 14.0 or later");
}
available
}
#[must_use]
pub fn is_available() -> bool {
unsafe { crate::ffi::sc_content_sharing_picker_is_available() }
}
pub fn show<F>(config: &SCContentSharingPickerConfiguration, callback: F)
where
F: FnOnce(SCPickerOutcome) + Send + 'static,
{
let context = into_callback_context::<SCPickerOutcome, F>(callback);
unsafe {
crate::ffi::sc_content_sharing_picker_show_with_result(
config.as_ptr(),
picker_trampoline::<ResultDecoder>,
context,
);
}
}
pub fn show_for_stream<F>(
config: &SCContentSharingPickerConfiguration,
stream: &crate::stream::SCStream,
callback: F,
) where
F: FnOnce(SCPickerOutcome) + Send + 'static,
{
let context = into_callback_context::<SCPickerOutcome, F>(callback);
unsafe {
crate::ffi::sc_content_sharing_picker_show_for_stream(
config.as_ptr(),
stream.as_ptr(),
picker_trampoline::<ResultDecoder>,
context,
);
}
}
pub fn show_filter<F>(config: &SCContentSharingPickerConfiguration, callback: F)
where
F: FnOnce(SCPickerFilterOutcome) + Send + 'static,
{
let context = into_callback_context::<SCPickerFilterOutcome, F>(callback);
unsafe {
crate::ffi::sc_content_sharing_picker_show(
config.as_ptr(),
picker_trampoline::<FilterDecoder>,
context,
);
}
}
pub fn show_using_style<F>(
config: &SCContentSharingPickerConfiguration,
style: crate::stream::content_filter::SCShareableContentStyle,
callback: F,
) where
F: FnOnce(SCPickerOutcome) + Send + 'static,
{
let context = into_callback_context::<SCPickerOutcome, F>(callback);
unsafe {
crate::ffi::sc_content_sharing_picker_show_using_style(
config.as_ptr(),
style as i32,
picker_trampoline::<ResultDecoder>,
context,
);
}
}
pub fn show_for_stream_using_style<F>(
config: &SCContentSharingPickerConfiguration,
stream: &crate::stream::SCStream,
style: crate::stream::content_filter::SCShareableContentStyle,
callback: F,
) where
F: FnOnce(SCPickerOutcome) + Send + 'static,
{
let context = into_callback_context::<SCPickerOutcome, F>(callback);
unsafe {
crate::ffi::sc_content_sharing_picker_show_for_stream_using_style(
config.as_ptr(),
stream.as_ptr(),
style as i32,
picker_trampoline::<ResultDecoder>,
context,
);
}
}
pub fn set_maximum_stream_count(count: usize) {
if !Self::available_or_log("SCContentSharingPicker::set_maximum_stream_count") {
return;
}
unsafe {
crate::ffi::sc_content_sharing_picker_set_maximum_stream_count(count);
}
}
pub fn maximum_stream_count() -> usize {
if !Self::is_available() {
return 0;
}
unsafe { crate::ffi::sc_content_sharing_picker_get_maximum_stream_count() }
}
#[must_use]
pub fn is_active() -> bool {
if !Self::is_available() {
return false;
}
unsafe { crate::ffi::sc_content_sharing_picker_get_active() }
}
pub fn set_active(active: bool) {
if !Self::available_or_log("SCContentSharingPicker::set_active") {
return;
}
unsafe { crate::ffi::sc_content_sharing_picker_set_active(active) }
}
pub fn deactivate() {
if !Self::is_available() {
return;
}
unsafe { crate::ffi::sc_content_sharing_picker_deactivate() }
}
#[must_use]
pub fn default_configuration() -> SCContentSharingPickerConfiguration {
SCContentSharingPickerConfiguration::default_from_system()
}
pub fn set_default_configuration(config: &SCContentSharingPickerConfiguration) {
unsafe {
crate::ffi::sc_content_sharing_picker_set_default_configuration(config.as_ptr());
}
}
pub fn set_configuration_for_stream(
config: Option<&SCContentSharingPickerConfiguration>,
stream: &crate::stream::SCStream,
) {
if !Self::available_or_log("SCContentSharingPicker::set_configuration_for_stream") {
return;
}
let config_ptr = config.map_or(
std::ptr::null(),
SCContentSharingPickerConfiguration::as_ptr,
);
unsafe {
crate::ffi::sc_content_sharing_picker_set_configuration_for_stream(
config_ptr,
stream.as_ptr(),
);
}
}
#[must_use = "the observer is removed as soon as the subscription is dropped"]
pub fn add_observer<F>(handler: F) -> SCPickerSubscription
where
F: Fn(SCPickerEvent) + Send + Sync + 'static,
{
if !Self::available_or_log("SCContentSharingPicker::add_observer") {
return SCPickerSubscription { token: 0 };
}
let context = SCPickerObserverContext::into_raw(handler);
let token = unsafe {
crate::ffi::sc_content_sharing_picker_add_observer(
observer_trampoline,
observer_context_release,
context,
)
};
if token == 0 {
eprintln!(
"SCContentSharingPicker::add_observer must run on the main thread or while an \
AppKit main run loop is active"
);
observer_context_release(context);
}
SCPickerSubscription { token }
}
pub fn remove_all_observers() -> usize {
if !Self::is_available() {
return 0;
}
unsafe { crate::ffi::sc_content_sharing_picker_remove_all_observers() }
}
pub fn present() {
if !Self::available_or_log("SCContentSharingPicker::present") {
return;
}
unsafe { crate::ffi::sc_content_sharing_picker_present(-1) }
}
pub fn present_using_style(style: SCShareableContentStyle) {
if !Self::available_or_log("SCContentSharingPicker::present_using_style") {
return;
}
unsafe { crate::ffi::sc_content_sharing_picker_present(style as i32) }
}
pub fn present_for_stream(stream: &crate::stream::SCStream) {
if !Self::available_or_log("SCContentSharingPicker::present_for_stream") {
return;
}
unsafe { crate::ffi::sc_content_sharing_picker_present_for_stream(stream.as_ptr(), -1) }
}
pub fn present_for_stream_using_style(
stream: &crate::stream::SCStream,
style: SCShareableContentStyle,
) {
if !Self::available_or_log("SCContentSharingPicker::present_for_stream_using_style") {
return;
}
unsafe {
crate::ffi::sc_content_sharing_picker_present_for_stream(stream.as_ptr(), style as i32);
}
}
}
#[derive(Debug)]
pub enum SCPickerEvent {
Updated(SCPickerResult),
Cancelled,
Failed(String),
}
#[derive(Debug)]
#[must_use = "the observer is removed as soon as the subscription is dropped"]
pub struct SCPickerSubscription {
token: i64,
}
impl SCPickerSubscription {
#[must_use]
pub const fn token(&self) -> i64 {
self.token
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.token != 0
}
pub fn unsubscribe(mut self) -> bool {
let removed = self.remove();
std::mem::forget(self);
removed
}
pub fn detach(self) {
std::mem::forget(self);
}
fn remove(&mut self) -> bool {
if self.token == 0 {
return false;
}
let removed = unsafe { crate::ffi::sc_content_sharing_picker_remove_observer(self.token) };
self.token = 0;
removed
}
}
impl Drop for SCPickerSubscription {
fn drop(&mut self) {
self.remove();
}
}
struct SCPickerObserverContext {
active: AtomicBool,
handler: Box<dyn Fn(SCPickerEvent) + Send + Sync>,
}
impl SCPickerObserverContext {
fn into_raw<F>(handler: F) -> *mut c_void
where
F: Fn(SCPickerEvent) + Send + Sync + 'static,
{
let context = std::sync::Arc::new(Self {
active: AtomicBool::new(true),
handler: Box::new(handler),
});
let mut registry = PICKER_OBSERVER_CONTEXTS
.lock()
.unwrap_or_else(PoisonError::into_inner);
let contexts = registry.get_or_insert_with(HashMap::new);
let id = loop {
let id = NEXT_PICKER_OBSERVER_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
if id != 0 && !contexts.contains_key(&id) {
break id;
}
};
contexts.insert(id, context);
drop(registry);
id as *mut c_void
}
}
static NEXT_PICKER_OBSERVER_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1);
static PICKER_OBSERVER_CONTEXTS: Mutex<
Option<HashMap<usize, std::sync::Arc<SCPickerObserverContext>>>,
> = Mutex::new(None);
fn picker_observer_context(
context: *mut c_void,
) -> Option<std::sync::Arc<SCPickerObserverContext>> {
let id = context as usize;
if id == 0 {
return None;
}
PICKER_OBSERVER_CONTEXTS
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()?
.get(&id)
.cloned()
}
extern "C" fn observer_context_release(context: *mut c_void) {
crate::utils::panic_safe::catch_user_panic("picker observer context release", || {
let id = context as usize;
if id == 0 {
return;
}
let removed = {
let mut contexts = PICKER_OBSERVER_CONTEXTS
.lock()
.unwrap_or_else(PoisonError::into_inner);
contexts.as_mut().and_then(|contexts| contexts.remove(&id))
};
if let Some(context) = removed {
context.active.store(false, Ordering::Release);
}
});
}
extern "C" fn observer_trampoline(
event: i32,
result_ptr: *const c_void,
message: *const i8,
context: *mut c_void,
) {
crate::utils::panic_safe::catch_user_panic("picker observer callback", move || {
let Some(context) = picker_observer_context(context) else {
if !result_ptr.is_null() {
unsafe { crate::ffi::sc_picker_result_release(result_ptr) };
}
return;
};
if !context.active.load(Ordering::Acquire) {
if !result_ptr.is_null() {
unsafe { crate::ffi::sc_picker_result_release(result_ptr) };
}
return;
}
let decoded = match event {
1 if !result_ptr.is_null() => {
SCPickerEvent::Updated(SCPickerResult { ptr: result_ptr })
}
1 => SCPickerEvent::Failed("picker delivered an update without a result".to_string()),
0 => SCPickerEvent::Cancelled,
_ => {
let text = if message.is_null() {
"Content sharing picker failed to start".to_string()
} else {
unsafe { std::ffi::CStr::from_ptr(message) }
.to_string_lossy()
.into_owned()
};
SCPickerEvent::Failed(text)
}
};
(context.handler)(decoded);
});
}
struct PickerCallbackContext<O> {
closure: Box<dyn FnOnce(O) + Send>,
}
static NEXT_PICKER_CALLBACK_ID: AtomicUsize = AtomicUsize::new(1);
static PICKER_CALLBACKS: Mutex<Option<HashMap<usize, Box<dyn Any + Send>>>> = Mutex::new(None);
#[allow(clippy::significant_drop_tightening)]
fn into_callback_context<O, F>(callback: F) -> *mut c_void
where
O: 'static,
F: FnOnce(O) + Send + 'static,
{
let context = Box::new(PickerCallbackContext {
closure: Box::new(callback),
});
let mut callbacks = PICKER_CALLBACKS
.lock()
.unwrap_or_else(PoisonError::into_inner);
let callbacks = callbacks.get_or_insert_with(HashMap::new);
loop {
let id = NEXT_PICKER_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
if id != 0 && !callbacks.contains_key(&id) {
callbacks.insert(id, context);
return id as *mut c_void;
}
}
}
fn take_callback_context<O: 'static>(
context: *mut c_void,
) -> Option<Box<PickerCallbackContext<O>>> {
let id = context as usize;
if id == 0 {
return None;
}
let entry = PICKER_CALLBACKS
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_mut()?
.remove(&id)?;
entry.downcast::<PickerCallbackContext<O>>().ok()
}
trait PickerDecode {
type Outcome: 'static;
fn decode(code: i32, ptr: *const c_void) -> Self::Outcome;
unsafe fn release(ptr: *const c_void);
}
struct ResultDecoder;
impl PickerDecode for ResultDecoder {
type Outcome = SCPickerOutcome;
fn decode(code: i32, ptr: *const c_void) -> SCPickerOutcome {
match code {
1 if !ptr.is_null() => SCPickerOutcome::Picked(SCPickerResult { ptr }),
0 => SCPickerOutcome::Cancelled,
_ => SCPickerOutcome::Error("Picker failed".to_string()),
}
}
unsafe fn release(ptr: *const c_void) {
if !ptr.is_null() {
unsafe { crate::ffi::sc_picker_result_release(ptr) };
}
}
}
struct FilterDecoder;
impl PickerDecode for FilterDecoder {
type Outcome = SCPickerFilterOutcome;
fn decode(code: i32, ptr: *const c_void) -> SCPickerFilterOutcome {
match code {
1 if !ptr.is_null() => {
SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(ptr))
}
0 => SCPickerFilterOutcome::Cancelled,
_ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
}
}
unsafe fn release(ptr: *const c_void) {
if !ptr.is_null() {
unsafe { crate::ffi::sc_content_filter_release(ptr) };
}
}
}
extern "C" fn picker_trampoline<D: PickerDecode>(
code: i32,
ptr: *const c_void,
context: *mut c_void,
) {
crate::utils::panic_safe::catch_user_panic("picker callback", move || {
let Some(context) = take_callback_context::<D::Outcome>(context) else {
unsafe { D::release(ptr) };
return;
};
let outcome = D::decode(code, ptr);
(context.closure)(outcome);
});
}
unsafe impl Send for SCContentSharingPickerConfiguration {}
unsafe impl Sync for SCContentSharingPickerConfiguration {}
unsafe impl Send for SCPickerResult {}
unsafe impl Sync for SCPickerResult {}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[test]
fn duplicate_one_shot_callback_is_ignored_after_context_drop() {
let calls = Arc::new(AtomicUsize::new(0));
let observed = Arc::clone(&calls);
let context = into_callback_context::<SCPickerFilterOutcome, _>(move |_| {
observed.fetch_add(1, Ordering::SeqCst);
});
picker_trampoline::<FilterDecoder>(0, std::ptr::null(), context);
picker_trampoline::<FilterDecoder>(0, std::ptr::null(), context);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn released_repeating_observer_ignores_late_callback() {
let calls = Arc::new(AtomicUsize::new(0));
let observed = Arc::clone(&calls);
let context = SCPickerObserverContext::into_raw(move |_| {
observed.fetch_add(1, Ordering::SeqCst);
});
observer_context_release(context);
observer_trampoline(0, std::ptr::null(), std::ptr::null(), context);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
}