Skip to main content

azul_core/
screencap.rs

1//! POD types for the screen-capture surface
2//! (SUPER_PLAN_2 §4 Priority 6 + research/01).
3//!
4//! Symmetric to the camera surface: screen capture is a "dumb widget"
5//! (`azul_layout::widgets::screencap::ScreenCaptureWidget`) that owns a
6//! background capture thread + a GL-texture `ImageRef`, identical to the
7//! camera widget — only the *source* differs (a display / window instead of
8//! a camera). Defined here in `azul-core` so the config types cross the FFI
9//! without `azul-layout` (or ScreenCaptureKit / MediaProjection / PipeWire)
10//! as a dependency.
11//!
12//! Reuses the camera surface's generic capture status types
13//! ([`crate::camera::StreamState`], `CaptureStats`, `CaptureStreamId`,
14//! `CaptureErrorCode`) — those are capture-agnostic.
15
16use crate::resources::RawImageFormat;
17
18/// What to capture.
19#[repr(C, u8)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[derive(Default)]
22pub enum ScreenCaptureSource {
23    /// The primary display (the default).
24    #[default]
25    PrimaryDisplay,
26    /// A specific display by index (0-based).
27    Display(u32),
28    /// A specific window by its platform id / handle.
29    Window(u64),
30}
31
32
33/// Requested screen-capture configuration — the input to the screencap
34/// widget. Zero `fps` means "let the backend pick its default".
35#[repr(C)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct ScreenCaptureConfig {
38    /// What to capture (display / window).
39    pub source: ScreenCaptureSource,
40    /// Preferred frame rate (0 = backend default).
41    pub fps: u32,
42    /// Texture format the backend should deliver. `BGRA8` is the portable
43    /// default; `Nv12` (a later `RawImageFormat` addition) is the zero-copy
44    /// path on platforms that produce it natively.
45    pub output_format: RawImageFormat,
46}
47
48impl Default for ScreenCaptureConfig {
49    fn default() -> Self {
50        Self {
51            source: ScreenCaptureSource::PrimaryDisplay,
52            fps: 0,
53            output_format: RawImageFormat::BGRA8,
54        }
55    }
56}
57
58impl ScreenCaptureConfig {
59    /// A default config for the given `source` (backend-chosen fps, `BGRA8`).
60    #[must_use] pub fn new(source: ScreenCaptureSource) -> Self {
61        Self {
62            source,
63            ..Self::default()
64        }
65    }
66}
67
68#[cfg(test)]
69mod autotest_generated {
70    use core::mem::size_of;
71
72    use super::*;
73
74    /// Representative + extreme sources: both payload boundaries (0, MAX) for
75    /// each carrying variant, so a truncating/aliasing constructor cannot pass.
76    const ALL_SOURCES: [ScreenCaptureSource; 7] = [
77        ScreenCaptureSource::PrimaryDisplay,
78        ScreenCaptureSource::Display(0),
79        ScreenCaptureSource::Display(1),
80        ScreenCaptureSource::Display(u32::MAX),
81        ScreenCaptureSource::Window(0),
82        ScreenCaptureSource::Window(1),
83        ScreenCaptureSource::Window(u64::MAX),
84    ];
85
86    // ---- ScreenCaptureConfig::new — constructor: no_panic ----
87
88    #[test]
89    fn new_does_not_panic_for_representative_or_extreme_sources() {
90        for &source in &ALL_SOURCES {
91            let _ = ScreenCaptureConfig::new(source);
92        }
93    }
94
95    #[test]
96    fn new_is_deterministic() {
97        // Same argument twice => observably identical configs (no hidden state,
98        // no backend probing at construction time).
99        for &source in &ALL_SOURCES {
100            assert_eq!(
101                ScreenCaptureConfig::new(source),
102                ScreenCaptureConfig::new(source)
103            );
104        }
105    }
106
107    // ---- ScreenCaptureConfig::new — constructor: invariants_hold ----
108
109    #[test]
110    fn new_sets_source_and_leaves_everything_else_at_default() {
111        // Documented contract: "a default config for the given `source`
112        // (backend-chosen fps, BGRA8)" — so new(s) may differ from Default
113        // only in `source`.
114        let def = ScreenCaptureConfig::default();
115        for &source in &ALL_SOURCES {
116            let cfg = ScreenCaptureConfig::new(source);
117            assert_eq!(cfg.source, source, "source must equal the argument");
118            assert_eq!(cfg.fps, def.fps);
119            assert_eq!(cfg.output_format, def.output_format);
120            // Absolute invariants, not merely "== whatever Default says":
121            assert_eq!(cfg.fps, 0, "0 => backend picks the frame rate");
122            assert_eq!(
123                cfg.output_format,
124                RawImageFormat::BGRA8,
125                "BGRA8 is the documented portable default"
126            );
127        }
128    }
129
130    #[test]
131    fn new_equals_struct_update_syntax() {
132        for &source in &ALL_SOURCES {
133            let via_new = ScreenCaptureConfig::new(source);
134            let via_update = ScreenCaptureConfig {
135                source,
136                ..ScreenCaptureConfig::default()
137            };
138            assert_eq!(via_new, via_update);
139        }
140    }
141
142    #[test]
143    fn default_config_is_new_of_default_source() {
144        // The two `Default` impls (derived on the enum, hand-written on the
145        // struct) must not drift apart.
146        assert_eq!(
147            ScreenCaptureConfig::default(),
148            ScreenCaptureConfig::new(ScreenCaptureSource::default())
149        );
150        assert_eq!(
151            ScreenCaptureSource::default(),
152            ScreenCaptureSource::PrimaryDisplay
153        );
154        assert_eq!(
155            ScreenCaptureConfig::default().source,
156            ScreenCaptureSource::PrimaryDisplay
157        );
158    }
159
160    // ---- source payload: round-trip through the constructor ----
161
162    #[test]
163    fn new_preserves_full_payload_width() {
164        // A u64 window handle must survive intact: a `as u32` anywhere in the
165        // pipeline would collapse u64::MAX to u32::MAX-in-a-u64.
166        let cfg = ScreenCaptureConfig::new(ScreenCaptureSource::Window(u64::MAX));
167        match cfg.source {
168            ScreenCaptureSource::Window(h) => assert_eq!(h, u64::MAX),
169            other => panic!("expected Window(u64::MAX), got {other:?}"),
170        }
171
172        let cfg = ScreenCaptureConfig::new(ScreenCaptureSource::Display(u32::MAX));
173        match cfg.source {
174            ScreenCaptureSource::Display(i) => assert_eq!(i, u32::MAX),
175            other => panic!("expected Display(u32::MAX), got {other:?}"),
176        }
177
178        // Enough room for the widest payload; guards against a narrowed repr.
179        assert!(size_of::<ScreenCaptureSource>() >= size_of::<u64>());
180    }
181
182    #[test]
183    fn distinct_sources_stay_distinct() {
184        // Equality must compare the tag too, not just the payload bits.
185        assert_ne!(
186            ScreenCaptureSource::Display(1),
187            ScreenCaptureSource::Window(1)
188        );
189        // `PrimaryDisplay` is its own variant, NOT a spelling of `Display(0)`.
190        assert_ne!(
191            ScreenCaptureSource::PrimaryDisplay,
192            ScreenCaptureSource::Display(0)
193        );
194        assert_ne!(
195            ScreenCaptureConfig::new(ScreenCaptureSource::PrimaryDisplay),
196            ScreenCaptureConfig::new(ScreenCaptureSource::Display(0))
197        );
198
199        // Pairwise: no two entries of ALL_SOURCES may collide.
200        for (i, a) in ALL_SOURCES.iter().enumerate() {
201            for (j, b) in ALL_SOURCES.iter().enumerate() {
202                let cfg_a = ScreenCaptureConfig::new(*a);
203                let cfg_b = ScreenCaptureConfig::new(*b);
204                if i == j {
205                    assert_eq!(cfg_a, cfg_b, "Eq must be reflexive for {a:?}");
206                } else {
207                    assert_ne!(cfg_a, cfg_b, "{a:?} and {b:?} must not compare equal");
208                }
209            }
210        }
211    }
212
213    #[test]
214    fn config_is_copy_and_fields_are_independently_settable() {
215        // The struct is a POD crossing the FFI: mutating a copy must not
216        // disturb the original, and overwriting `fps`/`output_format` must not
217        // corrupt the (differently aligned) `source` payload next to it.
218        let original = ScreenCaptureConfig::new(ScreenCaptureSource::Window(u64::MAX));
219        let mut copy = original;
220        copy.fps = u32::MAX;
221        copy.output_format = RawImageFormat::RGBAF32;
222
223        assert_eq!(original.fps, 0, "original must be untouched (Copy)");
224        assert_eq!(original.output_format, RawImageFormat::BGRA8);
225        assert_eq!(copy.source, original.source, "source must survive the writes");
226        assert_eq!(copy.fps, u32::MAX);
227        assert_eq!(copy.output_format, RawImageFormat::RGBAF32);
228    }
229}