screencapturekit/shareable_content/mod.rs
1//! Shareable content types - displays, windows, and applications
2//!
3//! This module provides access to the system's displays, windows, and running
4//! applications that can be captured by `ScreenCaptureKit`.
5//!
6//! ## Main Types
7//!
8//! - [`SCShareableContent`] - Container for all available content (displays, windows, apps)
9//! - [`SCDisplay`] - A physical or virtual display that can be captured
10//! - [`SCWindow`] - A window that can be captured
11//! - [`SCRunningApplication`] - A running application whose windows can be captured
12//!
13//! ## Workflow
14//!
15//! 1. Call [`SCShareableContent::get()`] to retrieve available content
16//! 2. Select displays/windows/apps to capture
17//! 3. Create an [`SCContentFilter`](crate::stream::content_filter::SCContentFilter) from the selection
18//!
19//! # Examples
20//!
21//! ## List All Content
22//!
23//! ```no_run
24//! use screencapturekit::shareable_content::SCShareableContent;
25//!
26//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! // Get all shareable content
28//! let content = SCShareableContent::get()?;
29//!
30//! // List displays
31//! for display in content.displays() {
32//! println!("Display {}: {}x{}",
33//! display.display_id(),
34//! display.width(),
35//! display.height()
36//! );
37//! }
38//!
39//! // List windows
40//! for window in content.windows() {
41//! if let Some(title) = window.title() {
42//! println!("Window: {}", title);
43//! }
44//! }
45//!
46//! // List applications
47//! for app in content.applications() {
48//! println!("App: {} ({})", app.application_name(), app.bundle_identifier());
49//! }
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ## Filter On-Screen Windows Only
55//!
56//! ```no_run
57//! use screencapturekit::shareable_content::SCShareableContent;
58//!
59//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
60//! let content = SCShareableContent::create()
61//! .with_on_screen_windows_only(true)
62//! .with_exclude_desktop_windows(true)
63//! .get()?;
64//!
65//! println!("Found {} on-screen windows", content.windows().len());
66//! # Ok(())
67//! # }
68//! ```
69
70pub mod display;
71pub mod running_application;
72pub mod window;
73pub use display::SCDisplay;
74pub use running_application::SCRunningApplication;
75pub use window::SCWindow;
76
77use crate::error::SCError;
78use crate::utils::completion::{error_from_cstr, SyncCompletion};
79use core::fmt;
80use std::ffi::c_void;
81
82#[repr(transparent)]
83pub struct SCShareableContent(*const c_void);
84
85unsafe impl Send for SCShareableContent {}
86unsafe impl Sync for SCShareableContent {}
87
88/// Callback for shareable content retrieval
89extern "C" fn shareable_content_callback(
90 content_ptr: *const c_void,
91 error_ptr: *const i8,
92 user_data: *mut c_void,
93) {
94 if !error_ptr.is_null() {
95 let error = unsafe { error_from_cstr(error_ptr) };
96 unsafe { SyncCompletion::<SCShareableContent>::complete_err(user_data, error) };
97 } else if !content_ptr.is_null() {
98 let content = unsafe { SCShareableContent::from_ptr(content_ptr) };
99 unsafe { SyncCompletion::complete_ok(user_data, content) };
100 } else {
101 unsafe {
102 SyncCompletion::<SCShareableContent>::complete_err(
103 user_data,
104 "Unknown error".to_string(),
105 );
106 };
107 }
108}
109
110impl PartialEq for SCShareableContent {
111 fn eq(&self, other: &Self) -> bool {
112 self.0 == other.0
113 }
114}
115
116impl Eq for SCShareableContent {}
117
118impl std::hash::Hash for SCShareableContent {
119 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
120 self.0.hash(state);
121 }
122}
123
124impl Clone for SCShareableContent {
125 fn clone(&self) -> Self {
126 unsafe { Self(crate::ffi::sc_shareable_content_retain(self.0)) }
127 }
128}
129
130impl SCShareableContent {
131 /// Create from raw pointer (used internally)
132 ///
133 /// # Safety
134 /// The pointer must be a valid retained `SCShareableContent` pointer from Swift FFI.
135 pub(crate) unsafe fn from_ptr(ptr: *const c_void) -> Self {
136 Self(ptr)
137 }
138
139 /// Get shareable content (displays, windows, and applications)
140 ///
141 /// # Examples
142 ///
143 /// ```no_run
144 /// use screencapturekit::shareable_content::SCShareableContent;
145 ///
146 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
147 /// let content = SCShareableContent::get()?;
148 /// println!("Found {} displays", content.displays().len());
149 /// println!("Found {} windows", content.windows().len());
150 /// println!("Found {} apps", content.applications().len());
151 /// # Ok(())
152 /// # }
153 /// ```
154 ///
155 /// # Errors
156 ///
157 /// Returns an error if screen recording permission is not granted.
158 pub fn get() -> Result<Self, SCError> {
159 SCShareableContentOptions::default().get()
160 }
161
162 /// Create options builder for customizing shareable content retrieval
163 ///
164 /// # Examples
165 ///
166 /// ```no_run
167 /// use screencapturekit::shareable_content::SCShareableContent;
168 ///
169 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
170 /// let content = SCShareableContent::create()
171 /// .with_on_screen_windows_only(true)
172 /// .with_exclude_desktop_windows(true)
173 /// .get()?;
174 /// # Ok(())
175 /// # }
176 /// ```
177 #[must_use]
178 pub fn create() -> SCShareableContentOptions {
179 SCShareableContentOptions::default()
180 }
181
182 /// Get all available displays
183 ///
184 /// # Examples
185 ///
186 /// ```no_run
187 /// use screencapturekit::shareable_content::SCShareableContent;
188 ///
189 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
190 /// let content = SCShareableContent::get()?;
191 /// for display in content.displays() {
192 /// println!("Display: {}x{}", display.width(), display.height());
193 /// }
194 /// # Ok(())
195 /// # }
196 /// ```
197 pub fn displays(&self) -> Vec<SCDisplay> {
198 unsafe {
199 let count = crate::ffi::sc_shareable_content_get_displays_count(self.0);
200 // FFI returns isize but count is always positive
201 #[allow(clippy::cast_sign_loss)]
202 let mut displays = Vec::with_capacity(count as usize);
203
204 for i in 0..count {
205 let display_ptr = crate::ffi::sc_shareable_content_get_display_at(self.0, i);
206 if !display_ptr.is_null() {
207 displays.push(SCDisplay::from_ptr(display_ptr));
208 }
209 }
210
211 displays
212 }
213 }
214
215 /// Get all available windows
216 ///
217 /// # Examples
218 ///
219 /// ```no_run
220 /// use screencapturekit::shareable_content::SCShareableContent;
221 ///
222 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
223 /// let content = SCShareableContent::get()?;
224 /// for window in content.windows() {
225 /// if let Some(title) = window.title() {
226 /// println!("Window: {}", title);
227 /// }
228 /// }
229 /// # Ok(())
230 /// # }
231 /// ```
232 pub fn windows(&self) -> Vec<SCWindow> {
233 unsafe {
234 let count = crate::ffi::sc_shareable_content_get_windows_count(self.0);
235 // FFI returns isize but count is always positive
236 #[allow(clippy::cast_sign_loss)]
237 let mut windows = Vec::with_capacity(count as usize);
238
239 for i in 0..count {
240 let window_ptr = crate::ffi::sc_shareable_content_get_window_at(self.0, i);
241 if !window_ptr.is_null() {
242 windows.push(SCWindow::from_ptr(window_ptr));
243 }
244 }
245
246 windows
247 }
248 }
249
250 /// Get all available running applications
251 ///
252 /// # Examples
253 ///
254 /// ```no_run
255 /// use screencapturekit::shareable_content::SCShareableContent;
256 ///
257 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
258 /// let content = SCShareableContent::get()?;
259 /// for app in content.applications() {
260 /// println!("App: {} (PID: {})", app.application_name(), app.process_id());
261 /// }
262 /// # Ok(())
263 /// # }
264 /// ```
265 pub fn applications(&self) -> Vec<SCRunningApplication> {
266 unsafe {
267 let count = crate::ffi::sc_shareable_content_get_applications_count(self.0);
268 // FFI returns isize but count is always positive
269 #[allow(clippy::cast_sign_loss)]
270 let mut apps = Vec::with_capacity(count as usize);
271
272 for i in 0..count {
273 let app_ptr = crate::ffi::sc_shareable_content_get_application_at(self.0, i);
274 if !app_ptr.is_null() {
275 apps.push(SCRunningApplication::from_ptr(app_ptr));
276 }
277 }
278
279 apps
280 }
281 }
282
283 #[allow(dead_code)]
284 pub(crate) fn as_ptr(&self) -> *const c_void {
285 self.0
286 }
287}
288
289impl Drop for SCShareableContent {
290 fn drop(&mut self) {
291 if !self.0.is_null() {
292 unsafe {
293 crate::ffi::sc_shareable_content_release(self.0);
294 }
295 }
296 }
297}
298
299impl fmt::Debug for SCShareableContent {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 f.debug_struct("SCShareableContent")
302 .field("displays", &self.displays().len())
303 .field("windows", &self.windows().len())
304 .field("applications", &self.applications().len())
305 .finish()
306 }
307}
308
309impl fmt::Display for SCShareableContent {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 write!(
312 f,
313 "SCShareableContent ({} displays, {} windows, {} applications)",
314 self.displays().len(),
315 self.windows().len(),
316 self.applications().len()
317 )
318 }
319}
320
321#[derive(Default, Debug, Clone, PartialEq, Eq)]
322pub struct SCShareableContentOptions {
323 exclude_desktop_windows: bool,
324 on_screen_windows_only: bool,
325}
326
327impl SCShareableContentOptions {
328 /// Exclude desktop windows from the shareable content.
329 ///
330 /// When set to `true`, desktop-level windows (like the desktop background)
331 /// are excluded from the returned window list.
332 #[must_use]
333 pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
334 self.exclude_desktop_windows = exclude;
335 self
336 }
337
338 /// Include only on-screen windows in the shareable content.
339 ///
340 /// When set to `true`, only windows that are currently visible on screen
341 /// are included. Minimized or off-screen windows are excluded.
342 #[must_use]
343 pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
344 self.on_screen_windows_only = on_screen_only;
345 self
346 }
347
348 // =========================================================================
349 // Deprecated methods - use with_* versions instead
350 // =========================================================================
351
352 /// Exclude desktop windows from the shareable content.
353 #[must_use]
354 #[deprecated(since = "1.5.0", note = "Use with_exclude_desktop_windows() instead")]
355 pub fn exclude_desktop_windows(self, exclude: bool) -> Self {
356 self.with_exclude_desktop_windows(exclude)
357 }
358
359 /// Include only on-screen windows in the shareable content.
360 #[must_use]
361 #[deprecated(since = "1.5.0", note = "Use with_on_screen_windows_only() instead")]
362 pub fn on_screen_windows_only(self, on_screen_only: bool) -> Self {
363 self.with_on_screen_windows_only(on_screen_only)
364 }
365
366 /// Get shareable content synchronously
367 ///
368 /// This blocks until the content is retrieved.
369 ///
370 /// # Errors
371 ///
372 /// Returns an error if screen recording permission is not granted or retrieval fails.
373 pub fn get(self) -> Result<SCShareableContent, SCError> {
374 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
375
376 unsafe {
377 crate::ffi::sc_shareable_content_get_with_options(
378 self.exclude_desktop_windows,
379 self.on_screen_windows_only,
380 shareable_content_callback,
381 context,
382 );
383 }
384
385 completion.wait().map_err(SCError::NoShareableContent)
386 }
387
388 /// Get shareable content with only windows below a reference window
389 ///
390 /// This returns windows that are stacked below the specified reference window
391 /// in the window layering order.
392 ///
393 /// # Arguments
394 ///
395 /// * `reference_window` - The window to use as the reference point
396 ///
397 /// # Errors
398 ///
399 /// Returns an error if screen recording permission is not granted or retrieval fails.
400 pub fn below_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
401 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
402
403 unsafe {
404 crate::ffi::sc_shareable_content_get_below_window(
405 self.exclude_desktop_windows,
406 reference_window.as_ptr(),
407 shareable_content_callback,
408 context,
409 );
410 }
411
412 completion.wait().map_err(SCError::NoShareableContent)
413 }
414
415 /// Get shareable content with only windows above a reference window
416 ///
417 /// This returns windows that are stacked above the specified reference window
418 /// in the window layering order.
419 ///
420 /// # Arguments
421 ///
422 /// * `reference_window` - The window to use as the reference point
423 ///
424 /// # Errors
425 ///
426 /// Returns an error if screen recording permission is not granted or retrieval fails.
427 pub fn above_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
428 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
429
430 unsafe {
431 crate::ffi::sc_shareable_content_get_above_window(
432 self.exclude_desktop_windows,
433 reference_window.as_ptr(),
434 shareable_content_callback,
435 context,
436 );
437 }
438
439 completion.wait().map_err(SCError::NoShareableContent)
440 }
441}
442
443impl SCShareableContent {
444 /// Get shareable content for the current process only (macOS 14.4+)
445 ///
446 /// This retrieves content that the current process can capture without
447 /// requiring user authorization via TCC (Transparency, Consent, and Control).
448 ///
449 /// # Errors
450 ///
451 /// Returns an error if retrieval fails.
452 #[cfg(feature = "macos_14_4")]
453 pub fn current_process() -> Result<Self, SCError> {
454 let (completion, context) = SyncCompletion::<Self>::new();
455
456 unsafe {
457 crate::ffi::sc_shareable_content_get_current_process_displays(
458 shareable_content_callback,
459 context,
460 );
461 }
462
463 completion.wait().map_err(SCError::NoShareableContent)
464 }
465}
466
467// MARK: - SCShareableContentInfo (macOS 14.0+)
468
469/// Information about shareable content from a filter (macOS 14.0+)
470///
471/// Provides metadata about the content being captured, including dimensions and scale factor.
472#[cfg(feature = "macos_14_0")]
473pub struct SCShareableContentInfo(*const c_void);
474
475#[cfg(feature = "macos_14_0")]
476impl SCShareableContentInfo {
477 /// Get content info for a filter
478 ///
479 /// Returns information about the content described by the given filter.
480 pub fn for_filter(filter: &crate::stream::content_filter::SCContentFilter) -> Option<Self> {
481 let ptr = unsafe { crate::ffi::sc_shareable_content_info_for_filter(filter.as_ptr()) };
482 if ptr.is_null() {
483 None
484 } else {
485 Some(Self(ptr))
486 }
487 }
488
489 /// Get the content style
490 pub fn style(&self) -> crate::stream::content_filter::SCShareableContentStyle {
491 let value = unsafe { crate::ffi::sc_shareable_content_info_get_style(self.0) };
492 crate::stream::content_filter::SCShareableContentStyle::from(value)
493 }
494
495 /// Get the point-to-pixel scale factor
496 ///
497 /// Typically 2.0 for Retina displays.
498 pub fn point_pixel_scale(&self) -> f32 {
499 unsafe { crate::ffi::sc_shareable_content_info_get_point_pixel_scale(self.0) }
500 }
501
502 /// Get the content rectangle in points
503 pub fn content_rect(&self) -> crate::cg::CGRect {
504 let mut x = 0.0;
505 let mut y = 0.0;
506 let mut width = 0.0;
507 let mut height = 0.0;
508 unsafe {
509 crate::ffi::sc_shareable_content_info_get_content_rect(
510 self.0,
511 &mut x,
512 &mut y,
513 &mut width,
514 &mut height,
515 );
516 }
517 crate::cg::CGRect::new(x, y, width, height)
518 }
519
520 /// Get the content size in pixels
521 ///
522 /// Convenience method that multiplies `content_rect` dimensions by `point_pixel_scale`.
523 pub fn pixel_size(&self) -> (u32, u32) {
524 let rect = self.content_rect();
525 let scale = self.point_pixel_scale();
526 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
527 let width = (rect.width * f64::from(scale)) as u32;
528 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
529 let height = (rect.height * f64::from(scale)) as u32;
530 (width, height)
531 }
532}
533
534#[cfg(feature = "macos_14_0")]
535impl Drop for SCShareableContentInfo {
536 fn drop(&mut self) {
537 if !self.0.is_null() {
538 unsafe {
539 crate::ffi::sc_shareable_content_info_release(self.0);
540 }
541 }
542 }
543}
544
545#[cfg(feature = "macos_14_0")]
546impl Clone for SCShareableContentInfo {
547 fn clone(&self) -> Self {
548 unsafe { Self(crate::ffi::sc_shareable_content_info_retain(self.0)) }
549 }
550}
551
552#[cfg(feature = "macos_14_0")]
553impl fmt::Debug for SCShareableContentInfo {
554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555 f.debug_struct("SCShareableContentInfo")
556 .field("style", &self.style())
557 .field("point_pixel_scale", &self.point_pixel_scale())
558 .field("content_rect", &self.content_rect())
559 .finish()
560 }
561}
562
563#[cfg(feature = "macos_14_0")]
564impl fmt::Display for SCShareableContentInfo {
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 let (width, height) = self.pixel_size();
567 write!(
568 f,
569 "ContentInfo({:?}, {}x{} px, scale: {})",
570 self.style(),
571 width,
572 height,
573 self.point_pixel_scale()
574 )
575 }
576}
577
578#[cfg(feature = "macos_14_0")]
579unsafe impl Send for SCShareableContentInfo {}
580#[cfg(feature = "macos_14_0")]
581unsafe impl Sync for SCShareableContentInfo {}