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

            
16
use crate::resources::RawImageFormat;
17

            
18
/// What to capture.
19
#[repr(C, u8)]
20
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21
#[derive(Default)]
22
pub 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)]
37
pub 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

            
48
impl Default for ScreenCaptureConfig {
49
149
    fn default() -> Self {
50
149
        Self {
51
149
            source: ScreenCaptureSource::PrimaryDisplay,
52
149
            fps: 0,
53
149
            output_format: RawImageFormat::BGRA8,
54
149
        }
55
149
    }
56
}
57

            
58
impl ScreenCaptureConfig {
59
    /// A default config for the given `source` (backend-chosen fps, `BGRA8`).
60
139
    #[must_use] pub fn new(source: ScreenCaptureSource) -> Self {
61
139
        Self {
62
139
            source,
63
139
            ..Self::default()
64
139
        }
65
139
    }
66
}
67

            
68
#[cfg(test)]
69
mod 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
}