1
//! Window state types for azul-layout
2
//!
3
//! These types are defined here (rather than azul-core) because CallbackInfo
4
//! needs to reference them, and CallbackInfo must live in azul-layout (since
5
//! it requires LayoutWindow).
6

            
7
use alloc::collections::BTreeMap;
8

            
9
use azul_core::{
10
    callbacks::LayoutCallback,
11
    dom::DomId,
12
    window::{
13
        DebugState, ImePosition, KeyboardState, Monitor, MouseState, PlatformSpecificOptions,
14
        RendererOptions, TouchState, WindowFlags, WindowPosition, WindowSize, WindowTheme,
15
    },
16
};
17
use azul_css::{
18
    corety::OptionU32, impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug,
19
    impl_vec_mut, impl_vec_partialeq, props::basic::OptionColorU, AzString,
20
};
21

            
22
use crate::callbacks::OptionCallback;
23

            
24
/// Options for creating a new window
25
#[derive(Debug, Clone, PartialEq)]
26
#[repr(C)]
27
pub struct WindowCreateOptions {
28
    /// Initial state for the new window
29
    pub window_state: FullWindowState,
30
    /// Optional callback invoked after the window is created
31
    pub create_callback: OptionCallback,
32
    /// Optional renderer configuration (e.g., `VSync`, SRGB)
33
    pub renderer: azul_core::window::OptionRendererOptions,
34
    /// Optional window theme override (light/dark)
35
    pub theme: azul_core::window::OptionWindowTheme,
36
    /// If true, the window is resized to fit its content after the first layout
37
    pub size_to_content: bool,
38
    /// If true, enables hot-reloading of CSS and resources
39
    pub hot_reload: bool,
40
    /// Parent window's platform id (the window-registry key: X Window id on X11,
41
    /// `wl_surface` ptr on Wayland, HWND on Windows, `NSWindow` ptr on macOS), or 0
42
    /// for a top-level window with no parent. Child windows (menus, dropdowns,
43
    /// dialogs) set this so the backend can position them relative to the parent
44
    /// and, on X11, reuse the parent's display connection for the single shared
45
    /// event pump. 0 = no parent.
46
    pub parent_window_id: u64,
47
}
48

            
49
impl Default for WindowCreateOptions {
50
1907
    fn default() -> Self {
51
1907
        Self {
52
1907
            window_state: FullWindowState::default(),
53
1907
            create_callback: OptionCallback::None,
54
1907
            renderer: azul_core::window::OptionRendererOptions::None,
55
1907
            theme: azul_core::window::OptionWindowTheme::None,
56
1907
            size_to_content: false,
57
1907
            hot_reload: false,
58
1907
            parent_window_id: 0,
59
1907
        }
60
1907
    }
61
}
62

            
63
impl WindowCreateOptions {
64
    /// Create a new `WindowCreateOptions` with a layout callback
65
1879
    pub fn create(layout_callback: impl Into<LayoutCallback>) -> Self {
66
1879
        let mut options = Self::default();
67
1879
        options.window_state.layout_callback = layout_callback.into();
68
1879
        options
69
1879
    }
70
}
71

            
72
impl_option!(WindowCreateOptions, OptionWindowCreateOptions, copy = false, [Debug, Clone, PartialEq]);
73
impl_vec!(WindowCreateOptions, WindowCreateOptionsVec, WindowCreateOptionsVecDestructor, WindowCreateOptionsVecDestructorType, WindowCreateOptionsVecSlice, OptionWindowCreateOptions);
74
impl_vec_clone!(
75
    WindowCreateOptions,
76
    WindowCreateOptionsVec,
77
    WindowCreateOptionsVecDestructor
78
);
79
impl_vec_partialeq!(WindowCreateOptions, WindowCreateOptionsVec);
80
impl_vec_debug!(WindowCreateOptions, WindowCreateOptionsVec);
81
impl_vec_mut!(WindowCreateOptions, WindowCreateOptionsVec);
82

            
83
/// Full window state including internal fields not exposed to callbacks
84
#[derive(Debug, Clone, PartialEq)]
85
#[repr(C)]
86
pub struct FullWindowState {
87
    /// Platform-specific window options
88
    pub platform_specific_options: PlatformSpecificOptions,
89
    /// Current keyboard state (pressed keys, modifiers)
90
    pub keyboard_state: KeyboardState,
91
    /// Semantic window identifier for multi-window debugging.
92
    /// Can be set by the user to identify specific windows (e.g., "main", "settings", "popup-1")
93
    pub window_id: AzString,
94
    /// Window title bar text
95
    pub title: AzString,
96
    /// Optional callback invoked when the user requests the window to close
97
    pub close_callback: OptionCallback,
98
    /// Callback that returns the DOM for this window
99
    pub layout_callback: LayoutCallback,
100
    /// Window position on screen
101
    pub position: WindowPosition,
102
    /// Current touch/gesture input state
103
    pub touch_state: TouchState,
104
    /// Window dimensions (logical and physical)
105
    pub size: WindowSize,
106
    /// Window flags (minimized, maximized, fullscreen, etc.)
107
    pub flags: WindowFlags,
108
    /// Current mouse cursor state (position, buttons)
109
    pub mouse_state: MouseState,
110
    /// Active window theme (light/dark)
111
    pub theme: WindowTheme,
112
    /// Position of the IME candidate window
113
    pub ime_position: ImePosition,
114
    /// GPU renderer options (`VSync`, SRGB, hardware acceleration)
115
    pub renderer_options: RendererOptions,
116
    /// Monitor ID (not the full Monitor struct - just the identifier)
117
    pub monitor_id: OptionU32,
118
    /// Debug visualization state (layout borders, repaints, etc.)
119
    pub debug_state: DebugState,
120
    /// Window background color. If None, uses system window background color.
121
    pub background_color: OptionColorU,
122
    /// Whether this window currently has input focus
123
    pub window_focused: bool,
124
    /// Active route match (pattern + extracted parameters).
125
    /// Set by `CallbackInfo::switch_route()` or by the web server on URL match.
126
    /// Layout callbacks read this via `LayoutCallbackInfo::get_route_param()`.
127
    pub active_route: azul_core::resources::OptionRouteMatch,
128
}
129

            
130
impl_option!(
131
    FullWindowState,
132
    OptionFullWindowState,
133
    copy = false,
134
    [Debug, Clone, PartialEq]
135
);
136

            
137
// C-ABI vec so that `CallbackInfo::queue_window_state_sequence()` can be
138
// exposed over FFI (a `Vec<FullWindowState>` is not repr(C)).
139
impl_vec!(
140
    FullWindowState,
141
    FullWindowStateVec,
142
    FullWindowStateVecDestructor,
143
    FullWindowStateVecDestructorType,
144
    FullWindowStateVecSlice,
145
    OptionFullWindowState
146
);
147
impl_vec_clone!(
148
    FullWindowState,
149
    FullWindowStateVec,
150
    FullWindowStateVecDestructor
151
);
152
impl_vec_partialeq!(FullWindowState, FullWindowStateVec);
153
impl_vec_debug!(FullWindowState, FullWindowStateVec);
154

            
155
impl Default for FullWindowState {
156
14522
    fn default() -> Self {
157
14522
        Self {
158
14522
            platform_specific_options: PlatformSpecificOptions::default(),
159
14522
            keyboard_state: KeyboardState::default(),
160
14522
            window_id: AzString::from_const_str("azul-window"),
161
14522
            title: AzString::from_const_str("Azul Window"),
162
14522
            close_callback: OptionCallback::None,
163
14522
            layout_callback: LayoutCallback::default(),
164
14522
            position: WindowPosition::default(),
165
14522
            touch_state: TouchState::default(),
166
14522
            size: WindowSize::default(),
167
14522
            flags: WindowFlags::default(),
168
14522
            mouse_state: MouseState::default(),
169
14522
            theme: WindowTheme::default(),
170
14522
            ime_position: ImePosition::default(),
171
14522
            renderer_options: RendererOptions::default(),
172
14522
            monitor_id: OptionU32::None,
173
14522
            debug_state: DebugState::default(),
174
14522
            background_color: OptionColorU::None,
175
14522
            window_focused: true,
176
14522
            active_route: azul_core::resources::OptionRouteMatch::None,
177
14522
        }
178
14522
    }
179
}
180

            
181
#[cfg(test)]
182
mod autotest_generated {
183
    use azul_core::{
184
        callbacks::{LayoutCallbackInfo, LayoutCallbackType, Update},
185
        dom::Dom,
186
        geom::{LogicalSize, PhysicalPositionI32},
187
        refany::RefAny,
188
        resources::{OptionRouteMatch, RouteMatch},
189
        window::{AzStringPair, OptionWindowTheme, StringPairVec},
190
    };
191
    use azul_css::props::basic::ColorU;
192

            
193
    use super::*;
194
    use crate::callbacks::{Callback, CallbackInfo};
195

            
196
    // ------------------------------------------------------------------
197
    // Harness
198
    // ------------------------------------------------------------------
199

            
200
    // The four layout callbacks below deliberately have DIFFERENT bodies:
201
    // identical-body `extern "C"` functions can be folded onto a single symbol
202
    // by the linker (identical code folding), which would silently make two
203
    // "distinct" callbacks compare equal. Every assertion here is still written
204
    // so it holds either way — the pointer that goes in is the pointer that
205
    // must come out — but distinct bodies keep the tests meaningful.
206

            
207
    extern "C" fn cb_alpha(_: RefAny, _: LayoutCallbackInfo) -> Dom {
208
        Dom::create_body()
209
    }
210

            
211
    extern "C" fn cb_beta(_: RefAny, _: LayoutCallbackInfo) -> Dom {
212
        Dom::create_text_do_not_use_without_block_level_wrapper("beta")
213
    }
214

            
215
    extern "C" fn cb_gamma(_: RefAny, _: LayoutCallbackInfo) -> Dom {
216
        Dom::create_text_do_not_use_without_block_level_wrapper("gamma-gamma-gamma")
217
    }
218

            
219
    /// Stored by `create` but never invoked by it — invoking it fails the test.
220
    extern "C" fn cb_never_called(_: RefAny, _: LayoutCallbackInfo) -> Dom {
221
        unreachable!("WindowCreateOptions::create must not invoke the layout callback")
222
    }
223

            
224
    extern "C" fn close_cb(_: RefAny, _: CallbackInfo) -> Update {
225
        Update::DoNothing
226
    }
227

            
228
    fn ptr_of(f: LayoutCallbackType) -> usize {
229
        f as usize
230
    }
231

            
232
    fn stored_cb(o: &WindowCreateOptions) -> usize {
233
        o.window_state.layout_callback.cb as usize
234
    }
235

            
236
    /// `FullWindowState::default()` with one mutation applied. Written as a
237
    /// helper so the tests do not trip `field_reassign_with_default`.
238
    fn state_with(mutate: impl FnOnce(&mut FullWindowState)) -> FullWindowState {
239
        let mut s = FullWindowState::default();
240
        mutate(&mut s);
241
        s
242
    }
243

            
244
    fn options_with(mutate: impl FnOnce(&mut WindowCreateOptions)) -> WindowCreateOptions {
245
        let mut o = WindowCreateOptions::default();
246
        mutate(&mut o);
247
        o
248
    }
249

            
250
    fn opts_with_id(id: u64) -> WindowCreateOptions {
251
        let mut o = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
252
        o.parent_window_id = id;
253
        o
254
    }
255

            
256
    fn ids_of(v: &WindowCreateOptionsVec) -> Vec<u64> {
257
        v.as_slice().iter().map(|o| o.parent_window_id).collect()
258
    }
259

            
260
    /// Strings that break naive string handling: empty, interior NUL, astral
261
    /// planes, ZWJ sequences, RTL, combining marks, bidi/zero-width controls,
262
    /// fullwidth forms, and a 128 KiB blob.
263
    fn nasty_strings() -> Vec<String> {
264
        vec![
265
            String::new(),
266
            "\0".to_string(),
267
            "a\0b\0".to_string(),
268
            "🦀🐉👨‍👩‍👧‍👦".to_string(),
269
            "مرحبا بالعالم".to_string(),
270
            "e\u{0301}\u{0301}\u{0301}".to_string(),
271
            "\u{200B}\u{FEFF}\u{202E}drowssap".to_string(),
272
            "fullwidth".to_string(),
273
            "ß".repeat(65_536),
274
        ]
275
    }
276

            
277
    // ------------------------------------------------------------------
278
    // WindowCreateOptions::create
279
    // ------------------------------------------------------------------
280

            
281
    #[test]
282
    fn create_stores_the_callback_and_leaves_every_other_field_at_default() {
283
        let opts = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
284
        let def = WindowCreateOptions::default();
285

            
286
        assert_eq!(stored_cb(&opts), ptr_of(cb_alpha));
287
        assert!(opts.window_state.layout_callback.ctx.is_none());
288

            
289
        assert_eq!(opts.create_callback, def.create_callback);
290
        assert_eq!(opts.renderer, def.renderer);
291
        assert_eq!(opts.theme, def.theme);
292
        assert_eq!(opts.size_to_content, def.size_to_content);
293
        assert_eq!(opts.hot_reload, def.hot_reload);
294
        assert_eq!(opts.parent_window_id, def.parent_window_id);
295
        assert_eq!(opts.window_state.title, def.window_state.title);
296
        assert_eq!(opts.window_state.window_id, def.window_state.window_id);
297
        assert_eq!(
298
            opts.window_state.close_callback,
299
            def.window_state.close_callback
300
        );
301
        assert_eq!(
302
            opts.window_state.window_focused,
303
            def.window_state.window_focused
304
        );
305
    }
306

            
307
    #[test]
308
    fn create_records_exactly_the_pointer_it_was_given() {
309
        let all: [LayoutCallbackType; 4] = [cb_alpha, cb_beta, cb_gamma, cb_never_called];
310
        for cb in all.iter().copied() {
311
            let opts = WindowCreateOptions::create(cb);
312
            assert_eq!(stored_cb(&opts), cb as usize);
313
        }
314
    }
315

            
316
    #[test]
317
    fn create_does_not_invoke_the_stored_callback() {
318
        // `cb_never_called` panics if it ever runs; reaching the assert proves
319
        // `create` only *stores* the pointer.
320
        let opts = WindowCreateOptions::create(cb_never_called as LayoutCallbackType);
321
        assert_eq!(stored_cb(&opts), ptr_of(cb_never_called));
322
    }
323

            
324
    #[test]
325
    fn create_with_the_default_callback_equals_default_options() {
326
        let opts = WindowCreateOptions::create(LayoutCallback::default());
327
        assert_eq!(opts, WindowCreateOptions::default());
328
    }
329

            
330
    #[test]
331
    fn create_accepts_a_layout_callback_value_and_preserves_its_ctx() {
332
        // The `impl Into<LayoutCallback>` arg may already carry an FFI ctx
333
        // payload; `create` must not drop it.
334
        let cb = LayoutCallback {
335
            cb: cb_beta,
336
            ctx: Some(RefAny::new(vec![0xAAu8; 4096])).into(),
337
        };
338
        let opts = WindowCreateOptions::create(cb);
339
        assert_eq!(stored_cb(&opts), ptr_of(cb_beta));
340
        assert!(opts.window_state.layout_callback.ctx.is_some());
341

            
342
        // Same, routed through `LayoutCallback::create`.
343
        let opts =
344
            WindowCreateOptions::create(LayoutCallback::create(cb_gamma as LayoutCallbackType));
345
        assert_eq!(stored_cb(&opts), ptr_of(cb_gamma));
346
        assert!(opts.window_state.layout_callback.ctx.is_none());
347
    }
348

            
349
    #[test]
350
    fn create_is_deterministic_and_independent_across_calls() {
351
        let first = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
352
        for _ in 0..500 {
353
            let next = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
354
            assert_eq!(next, first);
355
        }
356

            
357
        // Mutating one result must not disturb a later one.
358
        let mut a = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
359
        a.parent_window_id = u64::MAX;
360
        a.window_state.title = AzString::from("mutated");
361
        let b = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
362
        assert_eq!(b, first);
363
        assert_ne!(a, b);
364
    }
365

            
366
    // ------------------------------------------------------------------
367
    // Default invariants
368
    // ------------------------------------------------------------------
369

            
370
    #[test]
371
    fn full_window_state_default_invariants() {
372
        let s = FullWindowState::default();
373

            
374
        assert_eq!(s.window_id.as_str(), "azul-window");
375
        assert_eq!(s.title.as_str(), "Azul Window");
376
        assert!(s.window_focused);
377
        assert!(s.monitor_id.is_none());
378
        assert!(s.background_color.is_none());
379
        assert!(s.active_route.is_none());
380
        assert_eq!(s.close_callback, OptionCallback::None);
381
        assert_eq!(s.theme, WindowTheme::default());
382
        assert_eq!(s.position, WindowPosition::default());
383
        assert_eq!(s.ime_position, ImePosition::default());
384

            
385
        // Default is a pure value: stable, self-equal, and clone-equal.
386
        assert_eq!(s, FullWindowState::default());
387
        assert_eq!(s.clone(), s);
388
    }
389

            
390
    #[test]
391
    fn window_create_options_default_invariants() {
392
        let o = WindowCreateOptions::default();
393

            
394
        assert_eq!(o.parent_window_id, 0, "0 means 'no parent window'");
395
        assert!(!o.size_to_content);
396
        assert!(!o.hot_reload);
397
        assert!(o.renderer.is_none());
398
        assert!(o.theme.is_none());
399
        assert_eq!(o.create_callback, OptionCallback::None);
400
        assert_eq!(o.window_state, FullWindowState::default());
401

            
402
        assert_eq!(o, WindowCreateOptions::default());
403
        assert_eq!(o.clone(), o);
404
    }
405

            
406
    // ------------------------------------------------------------------
407
    // Equality / predicate invariants
408
    // ------------------------------------------------------------------
409

            
410
    #[test]
411
    fn mutating_any_scalar_field_breaks_equality() {
412
        let base = WindowCreateOptions::default();
413

            
414
        assert_ne!(options_with(|o| o.parent_window_id = u64::MAX), base);
415
        assert_ne!(options_with(|o| o.parent_window_id = 1), base);
416
        assert_ne!(options_with(|o| o.size_to_content = true), base);
417
        assert_ne!(options_with(|o| o.hot_reload = true), base);
418
        assert_ne!(
419
            options_with(|o| o.theme = OptionWindowTheme::Some(WindowTheme::DarkMode)),
420
            base
421
        );
422
        assert_ne!(
423
            options_with(|o| o.create_callback =
424
                OptionCallback::Some(Callback::from(close_cb as crate::callbacks::CallbackType))),
425
            base
426
        );
427
        assert_ne!(
428
            options_with(|o| o.window_state.window_focused = false),
429
            base
430
        );
431
        assert_ne!(
432
            options_with(|o| o.window_state.monitor_id = OptionU32::Some(u32::MAX)),
433
            base
434
        );
435
        assert_ne!(
436
            options_with(|o| o.window_state.monitor_id = OptionU32::Some(0)),
437
            base
438
        );
439
        assert_ne!(
440
            options_with(
441
                |o| o.window_state.background_color = OptionColorU::Some(ColorU {
442
                    r: 0,
443
                    g: 0,
444
                    b: 0,
445
                    a: 0,
446
                })
447
            ),
448
            base
449
        );
450
        assert_ne!(
451
            options_with(|o| o.window_state.title = AzString::from("")),
452
            base
453
        );
454
    }
455

            
456
    #[test]
457
    #[allow(clippy::eq_op)] // `a == a` IS the reflexivity check being asserted
458
    fn equality_is_reflexive_symmetric_and_transitive_for_extreme_values() {
459
        let a = options_with(|o| {
460
            o.parent_window_id = u64::MAX;
461
            o.window_state.title = AzString::from("🦀\0🦀");
462
            o.window_state.size.dimensions = LogicalSize::new(f32::NAN, -0.0);
463
        });
464
        let b = a.clone();
465
        let c = b.clone();
466

            
467
        assert_eq!(a, a); // reflexive, even with a NaN inside
468
        assert_eq!(a, b);
469
        assert_eq!(b, a); // symmetric
470
        assert_eq!(b, c);
471
        assert_eq!(a, c); // transitive
472
        assert_ne!(a, WindowCreateOptions::default());
473
    }
474

            
475
    #[test]
476
    fn callback_ctx_is_deliberately_not_part_of_equality() {
477
        // `impl_callback!` compares callbacks by function pointer only, so two
478
        // otherwise-identical states that differ ONLY in the FFI ctx payload
479
        // compare equal. Pinned so a change to that rule is loud.
480
        let a = WindowCreateOptions::create(cb_alpha as LayoutCallbackType);
481
        let mut b = a.clone();
482
        b.window_state.layout_callback.ctx = Some(RefAny::new(0xDEAD_BEEF_u32)).into();
483

            
484
        assert!(a.window_state.layout_callback.ctx.is_none());
485
        assert!(b.window_state.layout_callback.ctx.is_some());
486
        assert_eq!(a, b);
487
    }
488

            
489
    // ------------------------------------------------------------------
490
    // Numeric limits / NaN / saturation
491
    // ------------------------------------------------------------------
492

            
493
    #[test]
494
    fn nan_dimensions_stay_reflexive_and_do_not_alias_the_origin() {
495
        let nan = state_with(|s| s.size.dimensions = LogicalSize::new(f32::NAN, f32::NAN));
496
        let zero = state_with(|s| s.size.dimensions = LogicalSize::new(0.0, 0.0));
497

            
498
        // `LogicalSize`'s PartialEq quantizes, mapping NaN to a fixed sentinel,
499
        // so equality stays reflexive (unlike raw f32) and NaN != origin.
500
        assert_eq!(nan, nan.clone());
501
        assert_ne!(nan, zero);
502
        assert_ne!(nan, FullWindowState::default());
503
    }
504

            
505
    #[test]
506
    fn out_of_range_dimensions_saturate_rather_than_wrap() {
507
        // Quantization multiplies by 1000 and saturates the f32->i64 cast, so
508
        // every coordinate at or beyond the overflow point collapses onto the
509
        // same bound. Pinned as behaviour, not as a claim that it is ideal.
510
        let inf = state_with(|s| {
511
            s.size.dimensions = LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY);
512
        });
513
        let max = state_with(|s| s.size.dimensions = LogicalSize::new(f32::MAX, -f32::MAX));
514

            
515
        assert_eq!(inf, inf.clone());
516
        assert_eq!(max, max.clone());
517
        assert_eq!(
518
            inf, max,
519
            "f32::MAX * 1000 overflows to inf, so both saturate"
520
        );
521

            
522
        // Tiny magnitudes quantize to 0 and are therefore indistinguishable
523
        // from the origin — no panic, no wraparound.
524
        let tiny = state_with(|s| {
525
            s.size.dimensions = LogicalSize::new(f32::MIN_POSITIVE, -f32::MIN_POSITIVE);
526
        });
527
        assert_eq!(
528
            tiny,
529
            state_with(|s| s.size.dimensions = LogicalSize::new(0.0, -0.0))
530
        );
531
    }
532

            
533
    #[test]
534
    fn integer_window_state_limits_survive_clone_and_containers() {
535
        let extreme = state_with(|s| {
536
            s.size.dpi = 0; // platform reported no DPI at all
537
            s.size.min_dimensions = Some(LogicalSize::new(-0.0, 0.0)).into();
538
            s.size.max_dimensions = Some(LogicalSize::new(f32::INFINITY, f32::INFINITY)).into();
539
            s.monitor_id = OptionU32::Some(u32::MAX);
540
            s.position = WindowPosition::Initialized(PhysicalPositionI32 {
541
                x: i32::MIN,
542
                y: i32::MAX,
543
            });
544
            s.background_color = OptionColorU::Some(ColorU {
545
                r: u8::MAX,
546
                g: 0,
547
                b: u8::MAX,
548
                a: 0,
549
            });
550
        });
551

            
552
        let cloned = extreme.clone();
553
        assert_eq!(cloned, extreme);
554
        assert_eq!(cloned.size.dpi, 0);
555
        assert_eq!(cloned.monitor_id.into_option(), Some(u32::MAX));
556
        assert_eq!(
557
            cloned.position,
558
            WindowPosition::Initialized(PhysicalPositionI32 {
559
                x: i32::MIN,
560
                y: i32::MAX,
561
            })
562
        );
563

            
564
        let round_tripped: Option<FullWindowState> = OptionFullWindowState::Some(cloned).into();
565
        assert_eq!(round_tripped, Some(extreme.clone()));
566

            
567
        let v = FullWindowStateVec::from_vec(vec![extreme.clone()]);
568
        assert_eq!(v.as_slice()[0], extreme);
569
    }
570

            
571
    #[test]
572
    fn parent_window_id_boundaries_survive_every_round_trip() {
573
        let ids: [u64; 5] = [0, 1, u64::MAX / 2, u64::MAX - 1, u64::MAX];
574
        for id in ids.iter().copied() {
575
            let o = opts_with_id(id);
576
            assert_eq!(o.parent_window_id, id);
577
            assert_eq!(o.clone().parent_window_id, id);
578

            
579
            let opt: OptionWindowCreateOptions = Some(o.clone()).into();
580
            assert_eq!(opt.as_ref().map(|x| x.parent_window_id), Some(id));
581

            
582
            let v = WindowCreateOptionsVec::from_vec(vec![o.clone()]);
583
            assert_eq!(v.as_slice()[0].parent_window_id, id);
584

            
585
            let out: Vec<WindowCreateOptions> = v.into();
586
            assert_eq!(out[0], o);
587
        }
588
    }
589

            
590
    // ------------------------------------------------------------------
591
    // Unicode / huge strings
592
    // ------------------------------------------------------------------
593

            
594
    #[test]
595
    fn unicode_and_huge_strings_round_trip_through_state_and_containers() {
596
        for s in nasty_strings() {
597
            let st = state_with(|w| {
598
                w.title = AzString::from(s.clone());
599
                w.window_id = AzString::from(s.clone());
600
            });
601

            
602
            assert_eq!(st.title.as_str(), s.as_str());
603
            assert_eq!(st.title.as_str().len(), s.len(), "byte length preserved");
604
            assert_eq!(st.window_id.as_str(), s.as_str());
605

            
606
            let cloned = st.clone();
607
            assert_eq!(cloned, st);
608
            assert_eq!(cloned.title.as_str(), s.as_str());
609

            
610
            // encode == decode through the FFI Option wrapper
611
            let opt: OptionFullWindowState = Some(st.clone()).into();
612
            let back: Option<FullWindowState> = opt.into();
613
            assert_eq!(back.as_ref().map(|x| x.title.as_str()), Some(s.as_str()));
614

            
615
            // ... and through the FFI Vec wrapper
616
            let v = FullWindowStateVec::from_vec(vec![st.clone(), st.clone()]);
617
            assert_eq!(v.len(), 2);
618
            assert_eq!(v.as_slice()[1].title.as_str(), s.as_str());
619
            assert_eq!(v.clone(), v);
620

            
621
            let out: Vec<FullWindowState> = v.into_library_owned_vec();
622
            assert_eq!(out.len(), 2);
623
            assert_eq!(out[0].title.as_str(), s.as_str());
624
            assert_eq!(out[0], st);
625
        }
626
    }
627

            
628
    #[test]
629
    fn debug_rendering_survives_nasty_strings() {
630
        for s in nasty_strings().into_iter().filter(|s| s.len() < 4096) {
631
            let o = options_with(|w| {
632
                w.window_state.title = AzString::from(s.clone());
633
                w.parent_window_id = u64::MAX;
634
            });
635
            let rendered = format!("{o:?}");
636
            assert!(rendered.starts_with("WindowCreateOptions"));
637
            assert!(rendered.contains("18446744073709551615"));
638
        }
639
    }
640

            
641
    #[test]
642
    fn active_route_round_trips_with_unicode_pattern_and_params() {
643
        let route = RouteMatch {
644
            pattern: AzString::from("/user/:id/🦀"),
645
            params: StringPairVec::from_vec(vec![AzStringPair {
646
                key: AzString::from("id"),
647
                value: AzString::from("𝟜𝟚"),
648
            }]),
649
        };
650
        let st = state_with(|s| s.active_route = OptionRouteMatch::Some(route.clone()));
651

            
652
        assert!(st.active_route.is_some());
653
        assert_ne!(st, FullWindowState::default());
654

            
655
        let opt: OptionFullWindowState = Some(st.clone()).into();
656
        let back = opt.into_option().expect("just constructed as Some");
657
        assert_eq!(back, st);
658
        assert_eq!(
659
            back.active_route.as_ref().map(|r| r.pattern.as_str()),
660
            Some("/user/:id/🦀")
661
        );
662
        assert_eq!(
663
            back.active_route
664
                .as_ref()
665
                .and_then(|r| r.get_param("id"))
666
                .map(|v| v.as_str()),
667
            Some("𝟜𝟚")
668
        );
669
        assert!(back
670
            .active_route
671
            .as_ref()
672
            .and_then(|r| r.get_param("missing"))
673
            .is_none());
674
    }
675

            
676
    // ------------------------------------------------------------------
677
    // OptionWindowCreateOptions / OptionFullWindowState
678
    // ------------------------------------------------------------------
679

            
680
    #[test]
681
    fn option_wrappers_agree_with_std_option() {
682
        let opts = WindowCreateOptions::create(cb_beta as LayoutCallbackType);
683

            
684
        let none: OptionWindowCreateOptions = Option::<WindowCreateOptions>::None.into();
685
        assert!(none.is_none());
686
        assert!(!none.is_some());
687
        assert!(none.as_ref().is_none());
688
        assert!(none.as_option().is_none());
689
        assert_eq!(none, OptionWindowCreateOptions::default());
690
        let back: Option<WindowCreateOptions> = none.into();
691
        assert_eq!(back, None);
692

            
693
        let some: OptionWindowCreateOptions = Some(opts.clone()).into();
694
        assert!(some.is_some());
695
        assert!(!some.is_none());
696
        assert_eq!(some.as_ref(), Some(&opts));
697
        assert_eq!(some.into_option(), Some(opts.clone()));
698
        assert_eq!(some.clone().map(|o| o.parent_window_id), Some(0));
699
        assert_eq!(
700
            some.clone()
701
                .and_then(|o| o.window_state.title.as_str().chars().next()),
702
            Some('A')
703
        );
704
        let back: Option<WindowCreateOptions> = some.into();
705
        assert_eq!(back, Some(opts));
706
    }
707

            
708
    #[test]
709
    fn option_replace_returns_the_previous_value() {
710
        let a = opts_with_id(1);
711
        let b = opts_with_id(2);
712

            
713
        let mut o = OptionWindowCreateOptions::None;
714
        assert!(o.replace(a.clone()).is_none());
715
        assert_eq!(o.as_ref(), Some(&a));
716

            
717
        let prev = o.replace(b.clone());
718
        assert_eq!(prev.as_ref(), Some(&a));
719
        assert_eq!(o.as_ref(), Some(&b));
720

            
721
        if let Some(inner) = o.as_mut() {
722
            inner.parent_window_id = u64::MAX;
723
        }
724
        assert_eq!(o.as_ref().map(|x| x.parent_window_id), Some(u64::MAX));
725
        // the value handed back by `replace` is independent of `o`
726
        assert_eq!(prev.as_ref().map(|x| x.parent_window_id), Some(1));
727
    }
728

            
729
    #[test]
730
    fn option_into_option_hands_back_an_independent_clone() {
731
        let mut o: OptionFullWindowState = Some(FullWindowState::default()).into();
732

            
733
        let mut taken = o.into_option().expect("constructed as Some");
734
        taken.title = AzString::from("changed");
735
        assert_eq!(
736
            o.as_ref().map(|s| s.title.as_str()),
737
            Some("Azul Window"),
738
            "into_option must clone, not alias"
739
        );
740

            
741
        if let Some(inner) = o.as_mut() {
742
            inner.title = AzString::from("also changed");
743
        }
744
        assert_eq!(taken.title.as_str(), "changed");
745
    }
746

            
747
    // ------------------------------------------------------------------
748
    // WindowCreateOptionsVec
749
    // ------------------------------------------------------------------
750

            
751
    #[test]
752
    fn vec_round_trips_empty_single_and_large() {
753
        for n in [0usize, 1, 2, 1000].iter().copied() {
754
            let src: Vec<WindowCreateOptions> = (0..n as u64).map(opts_with_id).collect();
755
            let v = WindowCreateOptionsVec::from_vec(src.clone());
756

            
757
            assert_eq!(v.len(), n);
758
            assert_eq!(v.is_empty(), n == 0);
759
            assert!(v.capacity() >= v.len());
760
            assert_eq!(v.as_slice(), src.as_slice());
761
            assert_eq!(v.iter().count(), n);
762
            assert_eq!(v.as_c_slice().len(), n);
763
            assert_eq!(v.as_c_slice().as_slice(), src.as_slice());
764

            
765
            let out: Vec<WindowCreateOptions> = v.into_library_owned_vec();
766
            assert_eq!(out, src, "encode == decode");
767
        }
768
    }
769

            
770
    #[test]
771
    fn vec_get_is_none_out_of_bounds() {
772
        let v = WindowCreateOptionsVec::from_vec(vec![opts_with_id(7)]);
773

            
774
        assert_eq!(v.get(0).map(|o| o.parent_window_id), Some(7));
775
        assert!(v.get(1).is_none());
776
        assert!(v.get(usize::MAX).is_none());
777
        assert!(v.c_get(1).is_none());
778
        assert!(v.c_get(usize::MAX).is_none());
779
        assert_eq!(
780
            v.c_get(0).into_option().map(|o| o.parent_window_id),
781
            Some(7)
782
        );
783

            
784
        let empty = WindowCreateOptionsVec::new();
785
        assert!(empty.is_empty());
786
        assert!(empty.get(0).is_none());
787
        assert!(empty.get(usize::MAX).is_none());
788
        assert!(empty.c_get(0).is_none());
789
        assert_eq!(empty.as_slice(), &[] as &[WindowCreateOptions]);
790
    }
791

            
792
    #[test]
793
    fn vec_c_slice_range_clamps_instead_of_panicking() {
794
        let v = WindowCreateOptionsVec::from_vec((0..4u64).map(opts_with_id).collect());
795

            
796
        assert_eq!(v.as_c_slice_range(0, 4).len(), 4);
797
        assert_eq!(v.as_c_slice_range(1, 3).len(), 2);
798
        assert_eq!(v.as_c_slice_range(1, 3).as_slice()[0].parent_window_id, 1);
799
        assert_eq!(v.as_c_slice_range(3, 1).len(), 0, "inverted range -> empty");
800
        assert_eq!(v.as_c_slice_range(4, 4).len(), 0);
801
        assert_eq!(
802
            v.as_c_slice_range(0, usize::MAX).len(),
803
            4,
804
            "end clamps to len"
805
        );
806
        assert_eq!(v.as_c_slice_range(2, usize::MAX).as_slice().len(), 2);
807
        assert_eq!(v.as_c_slice_range(usize::MAX, usize::MAX).len(), 0);
808
        assert_eq!(v.as_c_slice_range(usize::MAX, 0).len(), 0);
809

            
810
        let empty = WindowCreateOptionsVec::new();
811
        assert!(empty.as_c_slice_range(0, usize::MAX).is_empty());
812
        assert!(empty.as_c_slice_range(usize::MAX, usize::MAX).is_empty());
813
        assert!(empty.as_c_slice().as_slice().is_empty());
814
    }
815

            
816
    #[test]
817
    fn vec_insert_and_remove_are_noops_out_of_bounds() {
818
        let mut v = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1), opts_with_id(2)]);
819

            
820
        v.insert(3, opts_with_id(99)); // index > len
821
        assert_eq!(ids_of(&v), vec![1, 2]);
822
        v.insert(usize::MAX, opts_with_id(99));
823
        assert_eq!(ids_of(&v), vec![1, 2]);
824

            
825
        v.remove(2); // index == len
826
        assert_eq!(ids_of(&v), vec![1, 2]);
827
        v.remove(usize::MAX);
828
        assert_eq!(ids_of(&v), vec![1, 2]);
829

            
830
        v.insert(0, opts_with_id(0)); // prepend
831
        v.insert(3, opts_with_id(3)); // index == len -> append
832
        assert_eq!(ids_of(&v), vec![0, 1, 2, 3]);
833

            
834
        v.remove(0);
835
        assert_eq!(ids_of(&v), vec![1, 2, 3]);
836
        v.remove(2);
837
        assert_eq!(ids_of(&v), vec![1, 2]);
838
    }
839

            
840
    #[test]
841
    fn vec_push_pop_truncate_retain_keep_len_and_capacity_consistent() {
842
        let mut v = WindowCreateOptionsVec::new();
843
        assert!(v.pop().is_none(), "pop on an empty vec must not panic");
844

            
845
        for i in 0..256u64 {
846
            v.push(opts_with_id(i));
847
            assert_eq!(v.len(), i as usize + 1);
848
            assert!(v.capacity() >= v.len());
849
        }
850

            
851
        assert_eq!(v.get(255).map(|o| o.parent_window_id), Some(255));
852
        assert_eq!(v.pop().map(|o| o.parent_window_id), Some(255));
853
        assert_eq!(v.len(), 255);
854

            
855
        v.truncate(1000); // larger than len -> no-op
856
        assert_eq!(v.len(), 255);
857

            
858
        v.retain(|o| o.parent_window_id % 2 == 0);
859
        assert_eq!(v.len(), 128);
860
        assert!(v.as_slice().iter().all(|o| o.parent_window_id % 2 == 0));
861

            
862
        v.truncate(0);
863
        assert!(v.is_empty());
864
        assert!(v.pop().is_none());
865
        assert_eq!(v.as_slice(), &[] as &[WindowCreateOptions]);
866
    }
867

            
868
    #[test]
869
    fn vec_append_moves_everything_and_empties_the_source() {
870
        let mut a = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1)]);
871
        let mut b = WindowCreateOptionsVec::from_vec(vec![opts_with_id(2), opts_with_id(3)]);
872

            
873
        a.append(&mut b);
874
        assert_eq!(ids_of(&a), vec![1, 2, 3]);
875
        assert!(b.is_empty());
876

            
877
        let mut empty = WindowCreateOptionsVec::new();
878
        a.append(&mut empty); // appending nothing changes nothing
879
        assert_eq!(ids_of(&a), vec![1, 2, 3]);
880

            
881
        empty.append(&mut a); // appending into a zero-capacity vec
882
        assert_eq!(ids_of(&empty), vec![1, 2, 3]);
883
        assert!(a.is_empty());
884
    }
885

            
886
    #[test]
887
    fn vec_clone_is_deep_and_growth_does_not_alias_the_original() {
888
        let original = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1), opts_with_id(2)]);
889
        let mut cloned = original.clone();
890

            
891
        assert_eq!(cloned, original);
892
        assert_ne!(
893
            cloned.as_ptr(),
894
            original.as_ptr(),
895
            "a clone must own a separate buffer"
896
        );
897

            
898
        for i in 0..64u64 {
899
            cloned.push(opts_with_id(100 + i));
900
        }
901
        assert_eq!(cloned.len(), 66);
902
        assert_eq!(ids_of(&original), vec![1, 2], "original must be untouched");
903

            
904
        for o in cloned.iter_mut() {
905
            o.hot_reload = true;
906
        }
907
        assert!(!original.as_slice()[0].hot_reload);
908
    }
909

            
910
    #[test]
911
    fn vec_from_iterator_extend_and_sort_are_consistent() {
912
        let mut v: WindowCreateOptionsVec = (0..8u64).rev().map(opts_with_id).collect();
913
        assert_eq!(v.len(), 8);
914
        assert_eq!(ids_of(&v), vec![7, 6, 5, 4, 3, 2, 1, 0]);
915

            
916
        v.extend((8..12u64).map(opts_with_id));
917
        assert_eq!(v.len(), 12);
918

            
919
        v.sort_by(|a, b| a.parent_window_id.cmp(&b.parent_window_id));
920
        assert_eq!(ids_of(&v), (0..12u64).collect::<Vec<_>>());
921

            
922
        let back: Vec<WindowCreateOptions> = v.into();
923
        assert_eq!(back.len(), 12);
924
        let again: WindowCreateOptionsVec = back.into();
925
        assert_eq!(ids_of(&again), (0..12u64).collect::<Vec<_>>());
926

            
927
        let single = WindowCreateOptionsVec::from_item(opts_with_id(42));
928
        assert_eq!(ids_of(&single), vec![42]);
929

            
930
        let reserved = WindowCreateOptionsVec::with_capacity(0);
931
        assert!(reserved.is_empty());
932
    }
933

            
934
    #[test]
935
    fn vec_equality_and_debug_are_well_behaved() {
936
        let a = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1)]);
937
        let b = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1)]);
938
        let c = WindowCreateOptionsVec::from_vec(vec![opts_with_id(2)]);
939
        let longer = WindowCreateOptionsVec::from_vec(vec![opts_with_id(1), opts_with_id(1)]);
940
        let empty = WindowCreateOptionsVec::new();
941

            
942
        assert_eq!(a, b);
943
        assert_ne!(a, c);
944
        assert_ne!(a, longer, "length is part of equality");
945
        assert_ne!(a, empty);
946
        assert_eq!(empty, WindowCreateOptionsVec::default());
947
        assert_eq!(format!("{empty:?}"), "[]");
948
        assert!(format!("{a:?}").starts_with('['));
949
    }
950

            
951
    // ------------------------------------------------------------------
952
    // FullWindowStateVec (no mutation API — read-only surface)
953
    // ------------------------------------------------------------------
954

            
955
    #[test]
956
    fn full_window_state_vec_round_trips_and_clones_deeply() {
957
        let extreme = state_with(|s| {
958
            s.title = AzString::from("t".repeat(4096));
959
            s.size.dimensions = LogicalSize::new(f32::NAN, f32::INFINITY);
960
            s.monitor_id = OptionU32::Some(u32::MAX);
961
            s.window_focused = false;
962
        });
963

            
964
        let v = FullWindowStateVec::from_vec(vec![FullWindowState::default(), extreme.clone()]);
965
        assert_eq!(v.len(), 2);
966
        assert!(!v.is_empty());
967
        assert!(v.get(2).is_none());
968
        assert!(v.get(usize::MAX).is_none());
969
        assert!(v.c_get(2).is_none());
970
        assert_eq!(v.c_get(1).into_option().as_ref(), Some(&extreme));
971
        assert_eq!(v.iter().count(), 2);
972
        assert_eq!(v.as_c_slice_range(1, usize::MAX).len(), 1);
973

            
974
        let cloned = v.clone();
975
        assert_eq!(cloned, v);
976
        assert_ne!(cloned.as_ptr(), v.as_ptr());
977

            
978
        let out: Vec<FullWindowState> = cloned.into_library_owned_vec();
979
        assert_eq!(out.len(), 2);
980
        assert_eq!(out[0], FullWindowState::default());
981
        assert_eq!(out[1], extreme);
982
        assert_eq!(v.len(), 2, "original survives the clone being consumed");
983

            
984
        let single = FullWindowStateVec::from_item(extreme.clone());
985
        assert_eq!(single.len(), 1);
986
        assert_eq!(single.as_slice()[0], extreme);
987

            
988
        let empty = FullWindowStateVec::new();
989
        assert!(empty.is_empty());
990
        assert!(empty.get(0).is_none());
991
        assert_eq!(empty, FullWindowStateVec::default());
992
        assert_eq!(format!("{empty:?}"), "[]");
993
    }
994

            
995
    #[test]
996
    fn state_vec_survives_a_large_batch_of_extreme_states() {
997
        let src: Vec<FullWindowState> = (0..512u32)
998
            .map(|i| {
999
                state_with(|s| {
                    s.monitor_id = OptionU32::Some(i);
                    s.title = AzString::from(format!("win-{i}-🦀"));
                    s.size.dimensions = LogicalSize::new(i as f32, f32::NAN);
                })
            })
            .collect();
        let v = FullWindowStateVec::from_vec(src.clone());
        assert_eq!(v.len(), 512);
        assert_eq!(v.clone(), v);
        assert_eq!(v.as_slice()[511].title.as_str(), "win-511-🦀");
        let out: Vec<FullWindowState> = v.into_library_owned_vec();
        assert_eq!(out, src, "encode == decode for 512 extreme states");
    }
}