Skip to main content

azul_layout/
window_state.rs

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
7use alloc::collections::BTreeMap;
8
9use 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};
17use 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
22use crate::callbacks::OptionCallback;
23
24/// Options for creating a new window
25#[derive(Debug, Clone, PartialEq)]
26#[repr(C)]
27pub 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
49impl Default for WindowCreateOptions {
50    fn default() -> Self {
51        Self {
52            window_state: FullWindowState::default(),
53            create_callback: OptionCallback::None,
54            renderer: azul_core::window::OptionRendererOptions::None,
55            theme: azul_core::window::OptionWindowTheme::None,
56            size_to_content: false,
57            hot_reload: false,
58            parent_window_id: 0,
59        }
60    }
61}
62
63impl WindowCreateOptions {
64    /// Create a new `WindowCreateOptions` with a layout callback
65    pub fn create(layout_callback: impl Into<LayoutCallback>) -> Self {
66        let mut options = Self::default();
67        options.window_state.layout_callback = layout_callback.into();
68        options
69    }
70}
71
72impl_option!(WindowCreateOptions, OptionWindowCreateOptions, copy = false, [Debug, Clone, PartialEq]);
73impl_vec!(WindowCreateOptions, WindowCreateOptionsVec, WindowCreateOptionsVecDestructor, WindowCreateOptionsVecDestructorType, WindowCreateOptionsVecSlice, OptionWindowCreateOptions);
74impl_vec_clone!(
75    WindowCreateOptions,
76    WindowCreateOptionsVec,
77    WindowCreateOptionsVecDestructor
78);
79impl_vec_partialeq!(WindowCreateOptions, WindowCreateOptionsVec);
80impl_vec_debug!(WindowCreateOptions, WindowCreateOptionsVec);
81impl_vec_mut!(WindowCreateOptions, WindowCreateOptionsVec);
82
83/// Full window state including internal fields not exposed to callbacks
84#[derive(Debug, Clone, PartialEq)]
85#[repr(C)]
86pub 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
130impl_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)).
139impl_vec!(
140    FullWindowState,
141    FullWindowStateVec,
142    FullWindowStateVecDestructor,
143    FullWindowStateVecDestructorType,
144    FullWindowStateVecSlice,
145    OptionFullWindowState
146);
147impl_vec_clone!(
148    FullWindowState,
149    FullWindowStateVec,
150    FullWindowStateVecDestructor
151);
152impl_vec_partialeq!(FullWindowState, FullWindowStateVec);
153impl_vec_debug!(FullWindowState, FullWindowStateVec);
154
155impl Default for FullWindowState {
156    fn default() -> Self {
157        Self {
158            platform_specific_options: PlatformSpecificOptions::default(),
159            keyboard_state: KeyboardState::default(),
160            window_id: AzString::from_const_str("azul-window"),
161            title: AzString::from_const_str("Azul Window"),
162            close_callback: OptionCallback::None,
163            layout_callback: LayoutCallback::default(),
164            position: WindowPosition::default(),
165            touch_state: TouchState::default(),
166            size: WindowSize::default(),
167            flags: WindowFlags::default(),
168            mouse_state: MouseState::default(),
169            theme: WindowTheme::default(),
170            ime_position: ImePosition::default(),
171            renderer_options: RendererOptions::default(),
172            monitor_id: OptionU32::None,
173            debug_state: DebugState::default(),
174            background_color: OptionColorU::None,
175            window_focused: true,
176            active_route: azul_core::resources::OptionRouteMatch::None,
177        }
178    }
179}
180
181#[cfg(test)]
182mod 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("beta")
213    }
214
215    extern "C" fn cb_gamma(_: RefAny, _: LayoutCallbackInfo) -> Dom {
216        Dom::create_text("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            "๏ฝ†๏ฝ•๏ฝŒ๏ฝŒ๏ฝ—๏ฝ‰๏ฝ„๏ฝ”๏ฝˆ".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| {
1000                    s.monitor_id = OptionU32::Some(i);
1001                    s.title = AzString::from(format!("win-{i}-๐Ÿฆ€"));
1002                    s.size.dimensions = LogicalSize::new(i as f32, f32::NAN);
1003                })
1004            })
1005            .collect();
1006
1007        let v = FullWindowStateVec::from_vec(src.clone());
1008        assert_eq!(v.len(), 512);
1009        assert_eq!(v.clone(), v);
1010        assert_eq!(v.as_slice()[511].title.as_str(), "win-511-๐Ÿฆ€");
1011
1012        let out: Vec<FullWindowState> = v.into_library_owned_vec();
1013        assert_eq!(out, src, "encode == decode for 512 extreme states");
1014    }
1015}