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 snapshot;
73pub mod window;
74pub use display::SCDisplay;
75pub use running_application::SCRunningApplication;
76pub use snapshot::{ApplicationSnapshot, ContentSnapshot, DisplaySnapshot, WindowSnapshot};
77pub use window::SCWindow;
78
79use crate::error::SCError;
80use crate::utils::completion::{error_from_cstr, SyncCompletion};
81use core::fmt;
82use std::ffi::c_void;
83
84#[repr(transparent)]
85pub struct SCShareableContent(*const c_void);
86
87unsafe impl Send for SCShareableContent {}
88unsafe impl Sync for SCShareableContent {}
89
90/// Callback for shareable content retrieval
91extern "C" fn shareable_content_callback(
92 content_ptr: *const c_void,
93 error_ptr: *const i8,
94 user_data: *mut c_void,
95) {
96 if !error_ptr.is_null() {
97 let error = unsafe { error_from_cstr(error_ptr) };
98 unsafe { SyncCompletion::<SCShareableContent>::complete_err(user_data, error) };
99 } else if !content_ptr.is_null() {
100 let content = unsafe { SCShareableContent::from_ptr(content_ptr) };
101 unsafe { SyncCompletion::complete_ok(user_data, content) };
102 } else {
103 unsafe {
104 SyncCompletion::<SCShareableContent>::complete_err(
105 user_data,
106 "Unknown error".to_string(),
107 );
108 };
109 }
110}
111
112impl PartialEq for SCShareableContent {
113 fn eq(&self, other: &Self) -> bool {
114 self.0 == other.0
115 }
116}
117
118impl Eq for SCShareableContent {}
119
120impl std::hash::Hash for SCShareableContent {
121 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
122 self.0.hash(state);
123 }
124}
125
126impl Clone for SCShareableContent {
127 fn clone(&self) -> Self {
128 unsafe { Self(crate::ffi::sc_shareable_content_retain(self.0)) }
129 }
130}
131
132impl SCShareableContent {
133 /// Create from raw pointer (used internally)
134 ///
135 /// # Safety
136 /// The pointer must be a valid retained `SCShareableContent` pointer from Swift FFI.
137 pub(crate) unsafe fn from_ptr(ptr: *const c_void) -> Self {
138 Self(ptr)
139 }
140
141 /// Get shareable content (displays, windows, and applications)
142 ///
143 /// # Examples
144 ///
145 /// ```no_run
146 /// use screencapturekit::shareable_content::SCShareableContent;
147 ///
148 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
149 /// let content = SCShareableContent::get()?;
150 /// println!("Found {} displays", content.displays().len());
151 /// println!("Found {} windows", content.windows().len());
152 /// println!("Found {} apps", content.applications().len());
153 /// # Ok(())
154 /// # }
155 /// ```
156 ///
157 /// # Errors
158 ///
159 /// Returns an error if screen recording permission is not granted.
160 pub fn get() -> Result<Self, SCError> {
161 SCShareableContentOptions::default().get()
162 }
163
164 /// Create options builder for customizing shareable content retrieval
165 ///
166 /// # Examples
167 ///
168 /// ```no_run
169 /// use screencapturekit::shareable_content::SCShareableContent;
170 ///
171 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
172 /// let content = SCShareableContent::create()
173 /// .with_on_screen_windows_only(true)
174 /// .with_exclude_desktop_windows(true)
175 /// .get()?;
176 /// # Ok(())
177 /// # }
178 /// ```
179 #[must_use]
180 pub fn create() -> SCShareableContentOptions {
181 SCShareableContentOptions::default()
182 }
183
184 /// Get all available displays
185 ///
186 /// # Examples
187 ///
188 /// ```no_run
189 /// use screencapturekit::shareable_content::SCShareableContent;
190 ///
191 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
192 /// let content = SCShareableContent::get()?;
193 /// for display in content.displays() {
194 /// println!("Display: {}x{}", display.width(), display.height());
195 /// }
196 /// # Ok(())
197 /// # }
198 /// ```
199 pub fn displays(&self) -> Vec<SCDisplay> {
200 unsafe {
201 let count = crate::ffi::sc_shareable_content_get_displays_count(self.0);
202 // FFI returns isize but count is always positive
203 #[allow(clippy::cast_sign_loss)]
204 let mut displays = Vec::with_capacity(count as usize);
205
206 for i in 0..count {
207 let display_ptr = crate::ffi::sc_shareable_content_get_display_at(self.0, i);
208 if !display_ptr.is_null() {
209 displays.push(SCDisplay::from_ptr(display_ptr));
210 }
211 }
212
213 displays
214 }
215 }
216
217 /// Get all available windows
218 ///
219 /// # Examples
220 ///
221 /// ```no_run
222 /// use screencapturekit::shareable_content::SCShareableContent;
223 ///
224 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
225 /// let content = SCShareableContent::get()?;
226 /// for window in content.windows() {
227 /// if let Some(title) = window.title() {
228 /// println!("Window: {}", title);
229 /// }
230 /// }
231 /// # Ok(())
232 /// # }
233 /// ```
234 pub fn windows(&self) -> Vec<SCWindow> {
235 unsafe {
236 let count = crate::ffi::sc_shareable_content_get_windows_count(self.0);
237 // FFI returns isize but count is always positive
238 #[allow(clippy::cast_sign_loss)]
239 let mut windows = Vec::with_capacity(count as usize);
240
241 for i in 0..count {
242 let window_ptr = crate::ffi::sc_shareable_content_get_window_at(self.0, i);
243 if !window_ptr.is_null() {
244 windows.push(SCWindow::from_ptr(window_ptr));
245 }
246 }
247
248 windows
249 }
250 }
251
252 /// Get all available running applications
253 ///
254 /// # Examples
255 ///
256 /// ```no_run
257 /// use screencapturekit::shareable_content::SCShareableContent;
258 ///
259 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
260 /// let content = SCShareableContent::get()?;
261 /// for app in content.applications() {
262 /// println!("App: {} (PID: {})", app.application_name(), app.process_id());
263 /// }
264 /// # Ok(())
265 /// # }
266 /// ```
267 pub fn applications(&self) -> Vec<SCRunningApplication> {
268 unsafe {
269 let count = crate::ffi::sc_shareable_content_get_applications_count(self.0);
270 // FFI returns isize but count is always positive
271 #[allow(clippy::cast_sign_loss)]
272 let mut apps = Vec::with_capacity(count as usize);
273
274 for i in 0..count {
275 let app_ptr = crate::ffi::sc_shareable_content_get_application_at(self.0, i);
276 if !app_ptr.is_null() {
277 apps.push(SCRunningApplication::from_ptr(app_ptr));
278 }
279 }
280
281 apps
282 }
283 }
284
285 #[allow(dead_code)]
286 pub(crate) fn as_ptr(&self) -> *const c_void {
287 self.0
288 }
289
290 /// Fetch every display, window, and running application in a single batched
291 /// FFI round-trip — see [`ContentSnapshot`] for what's returned.
292 ///
293 /// **Use this in any code path that reads more than one attribute per
294 /// window / display / app.** The per-element accessors ([`displays`],
295 /// [`windows`], [`applications`] + per-attribute methods like
296 /// [`SCWindow::title`] / [`SCWindow::frame`]) issue one FFI call each
297 /// — a typical "list windows with title and frame" walk on a 220-window
298 /// system measured at ~73 µs. `snapshot()` collapses that to ~5 µs (~15×
299 /// faster) by going through the bridge's packed FFI surface and copying
300 /// every attribute into Rust-side plain data structs in a single pass.
301 ///
302 /// Returns `None` if the bridge couldn't fit the data into the static
303 /// scratch buffers (extremely unlikely — limits are 64 displays, 4096
304 /// windows, 1024 apps with a 256 KiB string pool).
305 ///
306 /// [`displays`]: Self::displays
307 /// [`windows`]: Self::windows
308 /// [`applications`]: Self::applications
309 /// [`SCWindow::title`]: crate::shareable_content::SCWindow::title
310 /// [`SCWindow::frame`]: crate::shareable_content::SCWindow::frame
311 #[must_use]
312 pub fn snapshot(&self) -> Option<ContentSnapshot> {
313 ContentSnapshot::collect(self.0)
314 }
315}
316
317impl Drop for SCShareableContent {
318 fn drop(&mut self) {
319 if !self.0.is_null() {
320 unsafe {
321 crate::ffi::sc_shareable_content_release(self.0);
322 }
323 }
324 }
325}
326
327impl fmt::Debug for SCShareableContent {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 // Use the cheap _count FFIs instead of full enumerations. The previous
330 // implementation called `.windows()` / `.displays()` / `.applications()`
331 // which each issue 1 + N FFI calls and allocate a Vec — measured at
332 // ~47 µs total on a system with ~220 windows. The count FFIs are O(1)
333 // each.
334 unsafe {
335 f.debug_struct("SCShareableContent")
336 .field(
337 "displays",
338 &crate::ffi::sc_shareable_content_get_displays_count(self.0),
339 )
340 .field(
341 "windows",
342 &crate::ffi::sc_shareable_content_get_windows_count(self.0),
343 )
344 .field(
345 "applications",
346 &crate::ffi::sc_shareable_content_get_applications_count(self.0),
347 )
348 .finish()
349 }
350 }
351}
352
353impl fmt::Display for SCShareableContent {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 // O(1) count FFIs instead of full enumerations — see the Debug impl.
356 unsafe {
357 write!(
358 f,
359 "SCShareableContent ({} displays, {} windows, {} applications)",
360 crate::ffi::sc_shareable_content_get_displays_count(self.0),
361 crate::ffi::sc_shareable_content_get_windows_count(self.0),
362 crate::ffi::sc_shareable_content_get_applications_count(self.0),
363 )
364 }
365 }
366}
367
368#[derive(Default, Debug, Clone, PartialEq, Eq)]
369pub struct SCShareableContentOptions {
370 exclude_desktop_windows: bool,
371 on_screen_windows_only: bool,
372}
373
374impl SCShareableContentOptions {
375 /// Exclude desktop windows from the shareable content.
376 ///
377 /// When set to `true`, desktop-level windows (like the desktop background)
378 /// are excluded from the returned window list.
379 #[must_use]
380 pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
381 self.exclude_desktop_windows = exclude;
382 self
383 }
384
385 /// Include only on-screen windows in the shareable content.
386 ///
387 /// When set to `true`, only windows that are currently visible on screen
388 /// are included. Minimized or off-screen windows are excluded.
389 #[must_use]
390 pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
391 self.on_screen_windows_only = on_screen_only;
392 self
393 }
394
395 // =========================================================================
396 // Deprecated methods - use with_* versions instead
397 // =========================================================================
398
399 /// Exclude desktop windows from the shareable content.
400 #[must_use]
401 #[deprecated(since = "1.5.0", note = "Use with_exclude_desktop_windows() instead")]
402 pub fn exclude_desktop_windows(self, exclude: bool) -> Self {
403 self.with_exclude_desktop_windows(exclude)
404 }
405
406 /// Include only on-screen windows in the shareable content.
407 #[must_use]
408 #[deprecated(since = "1.5.0", note = "Use with_on_screen_windows_only() instead")]
409 pub fn on_screen_windows_only(self, on_screen_only: bool) -> Self {
410 self.with_on_screen_windows_only(on_screen_only)
411 }
412
413 /// Get shareable content synchronously
414 ///
415 /// This blocks until the content is retrieved.
416 ///
417 /// # Errors
418 ///
419 /// Returns an error if screen recording permission is not granted or retrieval fails.
420 pub fn get(self) -> Result<SCShareableContent, SCError> {
421 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
422
423 unsafe {
424 crate::ffi::sc_shareable_content_get_with_options(
425 self.exclude_desktop_windows,
426 self.on_screen_windows_only,
427 shareable_content_callback,
428 context,
429 );
430 }
431
432 completion.wait().map_err(SCError::NoShareableContent)
433 }
434
435 /// Get shareable content with only windows below a reference window
436 ///
437 /// This returns windows that are stacked below the specified reference window
438 /// in the window layering order.
439 ///
440 /// # Arguments
441 ///
442 /// * `reference_window` - The window to use as the reference point
443 ///
444 /// # Errors
445 ///
446 /// Returns an error if screen recording permission is not granted or retrieval fails.
447 pub fn below_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
448 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
449
450 unsafe {
451 crate::ffi::sc_shareable_content_get_below_window(
452 self.exclude_desktop_windows,
453 reference_window.as_ptr(),
454 shareable_content_callback,
455 context,
456 );
457 }
458
459 completion.wait().map_err(SCError::NoShareableContent)
460 }
461
462 /// Get shareable content with only windows above a reference window
463 ///
464 /// This returns windows that are stacked above the specified reference window
465 /// in the window layering order.
466 ///
467 /// # Arguments
468 ///
469 /// * `reference_window` - The window to use as the reference point
470 ///
471 /// # Errors
472 ///
473 /// Returns an error if screen recording permission is not granted or retrieval fails.
474 pub fn above_window(self, reference_window: &SCWindow) -> Result<SCShareableContent, SCError> {
475 let (completion, context) = SyncCompletion::<SCShareableContent>::new();
476
477 unsafe {
478 crate::ffi::sc_shareable_content_get_above_window(
479 self.exclude_desktop_windows,
480 reference_window.as_ptr(),
481 shareable_content_callback,
482 context,
483 );
484 }
485
486 completion.wait().map_err(SCError::NoShareableContent)
487 }
488}
489
490impl SCShareableContent {
491 /// Get shareable content for the current process only (macOS 14.4+)
492 ///
493 /// This retrieves content that the current process can capture without
494 /// requiring user authorization via TCC (Transparency, Consent, and Control).
495 ///
496 /// # Errors
497 ///
498 /// Returns an error if retrieval fails.
499 #[cfg(feature = "macos_14_4")]
500 pub fn current_process() -> Result<Self, SCError> {
501 let (completion, context) = SyncCompletion::<Self>::new();
502
503 unsafe {
504 crate::ffi::sc_shareable_content_get_current_process_displays(
505 shareable_content_callback,
506 context,
507 );
508 }
509
510 completion.wait().map_err(SCError::NoShareableContent)
511 }
512}
513
514// MARK: - SCShareableContentInfo (macOS 14.0+)
515
516/// Information about shareable content from a filter (macOS 14.0+)
517///
518/// Provides metadata about the content being captured, including dimensions and scale factor.
519#[cfg(feature = "macos_14_0")]
520pub struct SCShareableContentInfo(*const c_void);
521
522#[cfg(feature = "macos_14_0")]
523impl SCShareableContentInfo {
524 /// Get content info for a filter
525 ///
526 /// Returns information about the content described by the given filter.
527 pub fn for_filter(filter: &crate::stream::content_filter::SCContentFilter) -> Option<Self> {
528 let ptr = unsafe { crate::ffi::sc_shareable_content_info_for_filter(filter.as_ptr()) };
529 if ptr.is_null() {
530 None
531 } else {
532 Some(Self(ptr))
533 }
534 }
535
536 /// Get the content style
537 pub fn style(&self) -> crate::stream::content_filter::SCShareableContentStyle {
538 let value = unsafe { crate::ffi::sc_shareable_content_info_get_style(self.0) };
539 crate::stream::content_filter::SCShareableContentStyle::from(value)
540 }
541
542 /// Get the point-to-pixel scale factor
543 ///
544 /// Typically 2.0 for Retina displays.
545 pub fn point_pixel_scale(&self) -> f32 {
546 unsafe { crate::ffi::sc_shareable_content_info_get_point_pixel_scale(self.0) }
547 }
548
549 /// Get the content rectangle in points
550 pub fn content_rect(&self) -> crate::cg::CGRect {
551 let mut x = 0.0;
552 let mut y = 0.0;
553 let mut width = 0.0;
554 let mut height = 0.0;
555 unsafe {
556 crate::ffi::sc_shareable_content_info_get_content_rect(
557 self.0,
558 &mut x,
559 &mut y,
560 &mut width,
561 &mut height,
562 );
563 }
564 crate::cg::CGRect::new(x, y, width, height)
565 }
566
567 /// Get the content size in pixels
568 ///
569 /// Convenience method that multiplies `content_rect` dimensions by `point_pixel_scale`.
570 pub fn pixel_size(&self) -> (u32, u32) {
571 let rect = self.content_rect();
572 let scale = self.point_pixel_scale();
573 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
574 let width = (rect.width * f64::from(scale)) as u32;
575 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
576 let height = (rect.height * f64::from(scale)) as u32;
577 (width, height)
578 }
579}
580
581#[cfg(feature = "macos_14_0")]
582impl Drop for SCShareableContentInfo {
583 fn drop(&mut self) {
584 if !self.0.is_null() {
585 unsafe {
586 crate::ffi::sc_shareable_content_info_release(self.0);
587 }
588 }
589 }
590}
591
592#[cfg(feature = "macos_14_0")]
593impl Clone for SCShareableContentInfo {
594 fn clone(&self) -> Self {
595 unsafe { Self(crate::ffi::sc_shareable_content_info_retain(self.0)) }
596 }
597}
598
599#[cfg(feature = "macos_14_0")]
600impl fmt::Debug for SCShareableContentInfo {
601 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
602 f.debug_struct("SCShareableContentInfo")
603 .field("style", &self.style())
604 .field("point_pixel_scale", &self.point_pixel_scale())
605 .field("content_rect", &self.content_rect())
606 .finish()
607 }
608}
609
610#[cfg(feature = "macos_14_0")]
611impl fmt::Display for SCShareableContentInfo {
612 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 let (width, height) = self.pixel_size();
614 write!(
615 f,
616 "ContentInfo({:?}, {}x{} px, scale: {})",
617 self.style(),
618 width,
619 height,
620 self.point_pixel_scale()
621 )
622 }
623}
624
625#[cfg(feature = "macos_14_0")]
626unsafe impl Send for SCShareableContentInfo {}
627#[cfg(feature = "macos_14_0")]
628unsafe impl Sync for SCShareableContentInfo {}