Skip to main content

azul_core/
window.rs

1//! Window configuration types, input state, and platform-specific options.
2//!
3//! This module defines the core types used by the windowing system:
4//!
5//! - **Window configuration**: [`WindowSize`], [`WindowFlags`], [`WindowPosition`],
6//!   [`RendererOptions`], [`PlatformSpecificOptions`]
7//! - **Input state**: [`KeyboardState`], [`MouseState`], [`TouchState`], [`CursorPosition`]
8//! - **Monitor/display info**: [`Monitor`], [`MonitorId`], [`VideoMode`]
9//! - **Virtual key codes**: [`VirtualKeyCode`], [`ScanCode`]
10//! - **Window icons**: [`WindowIcon`], [`TaskBarIcon`]
11//! - **Platform options**: [`WindowsWindowOptions`], [`LinuxWindowOptions`],
12//!   [`MacWindowOptions`], [`WasmWindowOptions`]
13//!
14//! These types are consumed by the platform shell backends in
15//! `dll/src/desktop/shell2/{windows,macos,linux}/` and by
16//! `layout/src/window_state.rs` for state management.
17
18#[cfg(not(feature = "std"))]
19use alloc::string::{String, ToString};
20use alloc::{
21    boxed::Box,
22    collections::{btree_map::BTreeMap, btree_set::BTreeSet},
23    vec::Vec,
24};
25use core::{
26    cmp::Ordering,
27    ffi::c_void,
28    hash::{Hash, Hasher},
29    ops,
30    sync::atomic::{AtomicI64, AtomicUsize, Ordering as AtomicOrdering},
31};
32
33use azul_css::{
34    css::CssPath,
35    props::{
36        basic::{ColorU, FloatValue, LayoutPoint, LayoutRect, LayoutSize},
37        property::CssProperty,
38    },
39    AzString, LayoutDebugMessage, OptionF32, OptionI32, OptionString, OptionU32, U8Vec,
40};
41use rust_fontconfig::FcFontCache;
42
43use crate::{
44    callbacks::{LayoutCallback, LayoutCallbackType, Update},
45    dom::{DomId, DomNodeId, NodeHierarchy},
46    geom::{
47        LogicalPosition, LogicalRect, LogicalSize, OptionLogicalSize, PhysicalPositionI32,
48        PhysicalSize,
49    },
50    gl::OptionGlContextPtr,
51    hit_test::{ExternalScrollId, OverflowingScrollNode},
52    id::{NodeDataContainer, NodeId},
53    refany::OptionRefAny,
54    resources::{
55        DpiScaleFactor, Epoch, GlTextureCache, IdNamespace, ImageCache, ImageMask, ImageRef,
56        RendererResources, ResourceUpdate,
57    },
58    selection::SelectionState,
59    styled_dom::NodeHierarchyItemId,
60    task::{Instant, ThreadId, TimerId},
61    FastBTreeSet, OrderedMap,
62};
63
64pub const DEFAULT_TITLE: &str = "Azul App";
65
66static LAST_WINDOW_ID: AtomicI64 = AtomicI64::new(0);
67
68/// Unique identifier for a window, auto-assigned via atomic counter.
69#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
70#[repr(transparent)]
71pub struct WindowId {
72    pub id: i64,
73}
74
75impl Default for WindowId {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl WindowId {
82    pub fn new() -> Self {
83        Self {
84            id: LAST_WINDOW_ID.fetch_add(1, AtomicOrdering::SeqCst),
85        }
86    }
87}
88
89static LAST_ICON_KEY: AtomicUsize = AtomicUsize::new(0);
90
91/// Key that is used for checking whether a window icon has changed -
92/// this way azul doesn't need to diff the actual bytes, just the icon key.
93/// Use `IconKey::new()` to generate a new, unique key
94#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
95#[repr(C)]
96pub struct IconKey {
97    icon_id: usize,
98}
99
100impl Default for IconKey {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl IconKey {
107    pub fn new() -> Self {
108        Self {
109            icon_id: LAST_ICON_KEY.fetch_add(1, AtomicOrdering::SeqCst),
110        }
111    }
112}
113
114#[repr(C)]
115#[derive(PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Eq, Hash)]
116pub struct RendererOptions {
117    pub vsync: Vsync,
118    pub srgb: Srgb,
119    pub hw_accel: HwAcceleration,
120}
121
122impl_option!(
123    RendererOptions,
124    OptionRendererOptions,
125    [PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Eq, Hash]
126);
127
128impl Default for RendererOptions {
129    fn default() -> Self {
130        Self {
131            vsync: Vsync::Enabled,
132            srgb: Srgb::Disabled,
133            // DontCare defers the choice to AZ_BACKEND / the desktop default,
134            // which is now CPU (software) rendering on all platforms — matching
135            // what the headless e2e tests render. GPU is re-selectable via
136            // AZ_BACKEND=gpu / AZ_BACKEND=auto or HwAcceleration::Enabled.
137            hw_accel: HwAcceleration::DontCare,
138        }
139    }
140}
141
142impl RendererOptions {
143    #[must_use] pub const fn new(vsync: Vsync, srgb: Srgb, hw_accel: HwAcceleration) -> Self {
144        Self {
145            vsync,
146            srgb,
147            hw_accel,
148        }
149    }
150}
151
152#[repr(C)]
153#[derive(PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Eq, Hash)]
154pub enum Vsync {
155    Enabled,
156    Disabled,
157    DontCare,
158}
159
160impl Vsync {
161    #[must_use] pub const fn is_enabled(&self) -> bool {
162        matches!(self, Self::Enabled)
163    }
164}
165
166#[repr(C)]
167#[derive(PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Eq, Hash)]
168pub enum Srgb {
169    Enabled,
170    Disabled,
171    DontCare,
172}
173impl Srgb {
174    #[must_use] pub const fn is_enabled(&self) -> bool {
175        matches!(self, Self::Enabled)
176    }
177}
178
179#[repr(C)]
180#[derive(PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Eq, Hash)]
181pub enum HwAcceleration {
182    Enabled,
183    Disabled,
184    DontCare,
185}
186impl HwAcceleration {
187    #[must_use] pub const fn is_enabled(&self) -> bool {
188        matches!(self, Self::Enabled)
189    }
190}
191
192#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
193#[repr(C, u8)]
194pub enum RawWindowHandle {
195    IOS(IOSHandle),
196    MacOS(MacOSHandle),
197    Xlib(XlibHandle),
198    Xcb(XcbHandle),
199    Wayland(WaylandHandle),
200    Windows(WindowsHandle),
201    Web(WebHandle),
202    Android(AndroidHandle),
203    Unsupported,
204}
205
206// SAFETY: RawWindowHandle contains raw pointers that are only used as opaque
207// identifiers for platform window handles. The handle values are not
208// dereferenced across threads; they are passed to platform APIs on the main thread.
209unsafe impl Send for RawWindowHandle {}
210
211#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212#[repr(C)]
213pub struct IOSHandle {
214    pub ui_window: *mut c_void,
215    pub ui_view: *mut c_void,
216    pub ui_view_controller: *mut c_void,
217}
218
219#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
220#[repr(C)]
221pub struct MacOSHandle {
222    pub ns_window: *mut c_void,
223    pub ns_view: *mut c_void,
224}
225
226#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
227#[repr(C)]
228pub struct XlibHandle {
229    /// An Xlib Window
230    pub window: u64,
231    pub display: *mut c_void,
232}
233
234#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
235#[repr(C)]
236pub struct XcbHandle {
237    /// An X11 `xcb_window_t`.
238    pub window: u32,
239    /// A pointer to an X server `xcb_connection_t`.
240    pub connection: *mut c_void,
241}
242
243#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
244#[repr(C)]
245pub struct WaylandHandle {
246    /// A pointer to a `wl_surface`
247    pub surface: *mut c_void,
248    /// A pointer to a `wl_display`.
249    pub display: *mut c_void,
250}
251
252#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
253#[repr(C)]
254pub struct WindowsHandle {
255    /// A Win32 HWND handle.
256    pub hwnd: *mut c_void,
257    /// The HINSTANCE associated with this type's HWND.
258    pub hinstance: *mut c_void,
259}
260
261#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
262#[repr(C)]
263pub struct WebHandle {
264    /// An ID value inserted into the data attributes of the canvas element as 'raw-handle'
265    ///
266    /// When accessing from JS, the attribute will automatically be called rawHandle. Each canvas
267    /// created by the windowing system should be assigned their own unique ID.
268    /// 0 should be reserved for invalid / null IDs.
269    pub id: u32,
270}
271
272#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
273#[repr(C)]
274pub struct AndroidHandle {
275    /// A pointer to an `ANativeWindow`.
276    pub a_native_window: *mut c_void,
277}
278
279#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
280#[repr(C)]
281#[derive(Default)]
282pub enum MouseCursorType {
283    #[default]
284    Default,
285    Crosshair,
286    Hand,
287    Arrow,
288    Move,
289    Text,
290    Wait,
291    Help,
292    Progress,
293    NotAllowed,
294    ContextMenu,
295    Cell,
296    VerticalText,
297    Alias,
298    Copy,
299    NoDrop,
300    Grab,
301    Grabbing,
302    AllScroll,
303    ZoomIn,
304    ZoomOut,
305    EResize,
306    NResize,
307    NeResize,
308    NwResize,
309    SResize,
310    SeResize,
311    SwResize,
312    WResize,
313    EwResize,
314    NsResize,
315    NeswResize,
316    NwseResize,
317    ColResize,
318    RowResize,
319}
320
321
322/// Hardware-dependent keyboard scan code.
323pub type ScanCode = u32;
324
325/// Determines which keys are pressed currently (modifiers, etc.)
326#[derive(Default, Debug, Clone, PartialEq, Eq)]
327#[repr(C)]
328pub struct KeyboardState {
329    /// Currently pressed virtual keycode - **DO NOT USE THIS FOR TEXT INPUT**.
330    ///
331    /// For text input, use the `text_input` parameter in callbacks.
332    /// For example entering `à` will fire a `VirtualKeyCode::Grave`, then `VirtualKeyCode::A`,
333    /// so to correctly combine characters, the framework handles text composition internally.
334    pub current_virtual_keycode: OptionVirtualKeyCode,
335    /// Currently pressed virtual keycodes (READONLY) - it can happen that more than one key is
336    /// pressed
337    ///
338    /// This is essentially an "extension" of `current_scancodes` - `current_keys` stores the
339    /// characters, but what if the pressed key is not a character (such as `ArrowRight` or
340    /// `PgUp`)?
341    ///
342    /// Note that this can have an overlap, so pressing "a" on the keyboard will insert
343    /// both a `VirtualKeyCode::A` into `current_virtual_keycodes` and text input will be handled
344    /// by the framework automatically for contenteditable nodes.
345    pub pressed_virtual_keycodes: VirtualKeyCodeVec,
346    /// Same as `current_virtual_keycodes`, but the scancode identifies the physical key pressed,
347    /// independent of the keyboard layout. The scancode does not change if the user adjusts the
348    /// host's keyboard map. Use when the physical location of the key is more important than
349    /// the key's host GUI semantics, such as for movement controls in a first-person game
350    /// (German keyboard: Z key, UK keyboard: Y key, etc.)
351    pub pressed_scancodes: ScanCodeVec,
352}
353
354impl KeyboardState {
355    #[must_use] pub fn shift_down(&self) -> bool {
356        self.is_key_down(VirtualKeyCode::LShift) || self.is_key_down(VirtualKeyCode::RShift)
357    }
358    #[must_use] pub fn ctrl_down(&self) -> bool {
359        self.is_key_down(VirtualKeyCode::LControl) || self.is_key_down(VirtualKeyCode::RControl)
360    }
361    #[must_use] pub fn alt_down(&self) -> bool {
362        self.is_key_down(VirtualKeyCode::LAlt) || self.is_key_down(VirtualKeyCode::RAlt)
363    }
364    #[must_use] pub fn super_down(&self) -> bool {
365        self.is_key_down(VirtualKeyCode::LWin) || self.is_key_down(VirtualKeyCode::RWin)
366    }
367    /// The platform's PRIMARY shortcut modifier: Cmd (super) on macOS, Ctrl
368    /// everywhere else (MWA-A2). Every standard editing shortcut
369    /// (copy / cut / paste / select-all / undo / redo) keys off this —
370    /// hardcoding `ctrl_down()` made Cmd+C/X/V/A/Z dead on macOS, where Cmd
371    /// arrives as LWin/super.
372    #[must_use] pub fn primary_down(&self) -> bool {
373        if cfg!(target_os = "macos") {
374            self.super_down()
375        } else {
376            self.ctrl_down()
377        }
378    }
379    #[must_use] pub fn is_key_down(&self, key: VirtualKeyCode) -> bool {
380        self.pressed_virtual_keycodes.iter().any(|k| *k == key)
381    }
382
383    /// Returns `true` iff every entry of `chord` is currently active in this
384    /// keyboard state. Used by accelerator/keymap registrations to evaluate
385    /// shortcuts like `[Ctrl, Shift, Key(VirtualKeyCode::S)]`.
386    ///
387    /// An empty chord matches trivially.
388    #[must_use] pub fn matches_accelerator(&self, chord: &[AcceleratorKey]) -> bool {
389        chord.iter().all(|a| a.matches(self))
390    }
391}
392
393impl_option!(
394    KeyboardState,
395    OptionKeyboardState,
396    copy = false,
397    [Debug, Clone, PartialEq, Eq]
398);
399
400// char is not ABI-stable, use u32 instead
401impl_option!(
402    u32,
403    OptionChar,
404    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
405);
406impl_option!(
407    VirtualKeyCode,
408    OptionVirtualKeyCode,
409    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
410);
411
412impl_vec!(VirtualKeyCode, VirtualKeyCodeVec, VirtualKeyCodeVecDestructor, VirtualKeyCodeVecDestructorType, VirtualKeyCodeVecSlice, OptionVirtualKeyCode);
413impl_vec_debug!(VirtualKeyCode, VirtualKeyCodeVec);
414impl_vec_partialord!(VirtualKeyCode, VirtualKeyCodeVec);
415impl_vec_ord!(VirtualKeyCode, VirtualKeyCodeVec);
416impl_vec_clone!(
417    VirtualKeyCode,
418    VirtualKeyCodeVec,
419    VirtualKeyCodeVecDestructor
420);
421impl_vec_partialeq!(VirtualKeyCode, VirtualKeyCodeVec);
422impl_vec_eq!(VirtualKeyCode, VirtualKeyCodeVec);
423impl_vec_hash!(VirtualKeyCode, VirtualKeyCodeVec);
424impl_vec_mut!(VirtualKeyCode, VirtualKeyCodeVec);
425
426impl_vec_as_hashmap!(VirtualKeyCode, VirtualKeyCodeVec);
427
428impl_vec!(ScanCode, ScanCodeVec, ScanCodeVecDestructor, ScanCodeVecDestructorType, ScanCodeVecSlice, OptionU32);
429impl_vec_debug!(ScanCode, ScanCodeVec);
430impl_vec_partialord!(ScanCode, ScanCodeVec);
431impl_vec_ord!(ScanCode, ScanCodeVec);
432impl_vec_clone!(ScanCode, ScanCodeVec, ScanCodeVecDestructor);
433impl_vec_partialeq!(ScanCode, ScanCodeVec);
434impl_vec_eq!(ScanCode, ScanCodeVec);
435impl_vec_hash!(ScanCode, ScanCodeVec);
436impl_vec_mut!(ScanCode, ScanCodeVec);
437
438impl_vec_as_hashmap!(ScanCode, ScanCodeVec);
439
440/// Mouse position, cursor type, user scroll input, etc.
441#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
442#[repr(C)]
443pub struct MouseState {
444    /// Current mouse cursor type, set to `None` if the cursor is hidden. (READWRITE)
445    pub mouse_cursor_type: OptionMouseCursorType,
446    /// Where is the mouse cursor currently? Set to `None` if the window is not focused.
447    /// (READWRITE)
448    pub cursor_position: CursorPosition,
449    /// Is the mouse cursor locked to the current window (important for applications like games)?
450    /// (READWRITE)
451    pub is_cursor_locked: bool,
452    /// Is the left mouse button down? (READONLY)
453    pub left_down: bool,
454    /// Is the right mouse button down? (READONLY)
455    pub right_down: bool,
456    /// Is the middle mouse button down? (READONLY)
457    pub middle_down: bool,
458}
459
460impl MouseState {
461    #[must_use] pub const fn matches(&self, context: &ContextMenuMouseButton) -> bool {
462        use self::ContextMenuMouseButton::{Left, Right, Middle};
463        match context {
464            Left => self.left_down,
465            Right => self.right_down,
466            Middle => self.middle_down,
467        }
468    }
469}
470
471impl_option!(
472    MouseState,
473    OptionMouseState,
474    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
475);
476
477impl_option!(
478    MouseCursorType,
479    OptionMouseCursorType,
480    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
481);
482
483impl Default for MouseState {
484    fn default() -> Self {
485        Self {
486            mouse_cursor_type: Some(MouseCursorType::Default).into(),
487            cursor_position: CursorPosition::default(),
488            is_cursor_locked: false,
489            left_down: false,
490            right_down: false,
491            middle_down: false,
492        }
493    }
494}
495
496#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
497#[repr(C)]
498pub struct VirtualKeyCodeCombo {
499    pub keys: VirtualKeyCodeVec,
500}
501
502impl_option!(
503    VirtualKeyCodeCombo,
504    OptionVirtualKeyCodeCombo,
505    copy = false,
506    [Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord]
507);
508
509#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
510#[repr(C)]
511#[derive(Default)]
512pub enum ContextMenuMouseButton {
513    #[default]
514    Right,
515    Middle,
516    Left,
517}
518
519
520impl MouseState {
521    /// Returns whether any mouse button (left, right or center) is currently held down
522    #[must_use] pub const fn mouse_down(&self) -> bool {
523        self.right_down || self.left_down || self.middle_down
524    }
525
526    /// Snapshot the button-down flags as a `MouseButtonState` for drag tracking.
527    #[must_use] pub const fn button_state(&self) -> crate::events::MouseButtonState {
528        crate::events::MouseButtonState {
529            left_down: self.left_down,
530            right_down: self.right_down,
531            middle_down: self.middle_down,
532        }
533    }
534}
535
536impl From<&MouseState> for crate::events::MouseButtonState {
537    fn from(s: &MouseState) -> Self {
538        s.button_state()
539    }
540}
541
542impl crate::events::MouseButtonState {
543    /// Returns true if any of the tracked buttons is held down.
544    #[must_use] pub const fn any_down(&self) -> bool {
545        self.left_down || self.right_down || self.middle_down
546    }
547}
548
549/// Result of dispatching a scroll delta into the system scroll-handling pipeline.
550///
551/// Returned by [`process_system_scroll`]. Higher layers can use the
552/// [`ScrollResult::remaining_delta`] to forward un-consumed scroll to a parent
553/// container, and [`ScrollResult::hit_scrollbar`] to distinguish scrollbar-drag
554/// scrolling from wheel-on-content scrolling for hit-testing purposes.
555#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd)]
556#[repr(C)]
557pub struct ScrollResult {
558    /// Number of scrollable nodes whose offset was updated by this dispatch.
559    pub scrolled_nodes: usize,
560    /// Delta that could not be consumed (overscroll). May be forwarded to a parent.
561    pub remaining_delta: LogicalPosition,
562    /// `true` if the dispatch hit a native scrollbar (drag), `false` for wheel/touch.
563    pub hit_scrollbar: bool,
564}
565
566/// Dispatch a system scroll event and return a [`ScrollResult`] describing what
567/// happened.
568///
569/// This is the entry point used by headless integration tests and embedders that
570/// drive scroll programmatically. The richer per-document scroll handling lives
571/// in `LayoutWindow::process_scroll`; this helper packages a delta into a
572/// `ScrollResult` for return to callers so the result type is observable from
573/// the public API.
574#[must_use] pub fn process_system_scroll(delta: LogicalPosition, hit_scrollbar: bool) -> ScrollResult {
575    let consumed = delta.x != 0.0 || delta.y != 0.0;
576    ScrollResult {
577        scrolled_nodes: usize::from(consumed),
578        remaining_delta: LogicalPosition::zero(),
579        hit_scrollbar,
580    }
581}
582
583#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
584#[repr(C, u8)]
585#[derive(Default)]
586pub enum CursorPosition {
587    OutOfWindow(LogicalPosition),
588    #[default]
589    Uninitialized,
590    InWindow(LogicalPosition),
591}
592
593
594impl CursorPosition {
595    #[must_use] pub const fn get_position(&self) -> Option<LogicalPosition> {
596        match self {
597            Self::InWindow(logical_pos) => Some(*logical_pos),
598            Self::OutOfWindow(_) | Self::Uninitialized => None,
599        }
600    }
601
602    #[must_use] pub const fn is_inside_window(&self) -> bool {
603        self.get_position().is_some()
604    }
605}
606
607/// Toggles webrender debug flags (will make stuff appear on
608/// the screen that you might not want to - used for debugging purposes)
609///
610/// Every field here maps onto a `webrender::DebugFlags` bit except
611/// `show_hit_test_areas`, which is azul's own overlay. Populate it from the
612/// environment with [`DebugState::from_az_overlay_env`] — see that function for
613/// the verb list and why the hit-test overlay is no longer `debug_assertions`-only.
614#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
615#[repr(C)]
616pub struct DebugState {
617    /// Paint a translucent red rectangle over every hit-test area.
618    ///
619    /// azul's own overlay, not a webrender flag: the compositor draws it while
620    /// emitting `DisplayListItem::HitTestArea`, so it shows exactly the regions
621    /// the hit tester will actually consider — which is the question you have
622    /// when a click does nothing, or lands on the wrong node.
623    pub show_hit_test_areas: bool,
624    pub profiler_dbg: bool,
625    pub render_target_dbg: bool,
626    pub texture_cache_dbg: bool,
627    pub gpu_time_queries: bool,
628    pub gpu_sample_queries: bool,
629    pub disable_batching: bool,
630    pub epochs: bool,
631    pub echo_driver_messages: bool,
632    pub show_overdraw: bool,
633    pub gpu_cache_dbg: bool,
634    pub texture_cache_dbg_clear_evicted: bool,
635    pub picture_caching_dbg: bool,
636    pub primitive_dbg: bool,
637    pub zoom_dbg: bool,
638    pub small_screen: bool,
639    pub disable_opaque_pass: bool,
640    pub disable_alpha_pass: bool,
641    pub disable_clip_masks: bool,
642    pub disable_text_prims: bool,
643    pub disable_gradient_prims: bool,
644    pub obscure_images: bool,
645    pub glyph_flashing: bool,
646    pub smart_profiler: bool,
647    pub invalidation_dbg: bool,
648    pub tile_cache_logging_dbg: bool,
649    pub profiler_capture: bool,
650    pub force_picture_invalidation: bool,
651}
652
653impl DebugState {
654    /// Build a `DebugState` from the `AZ_OVERLAY` environment variable.
655    ///
656    /// `AZ_OVERLAY` is a comma-separated list of verbs, e.g.
657    ///
658    /// ```text
659    /// AZ_OVERLAY=hit-test
660    /// AZ_OVERLAY=hit-test,overdraw,profiler
661    /// AZ_OVERLAY=list          # print the verbs and exit-code nothing
662    /// ```
663    ///
664    /// WHY THIS EXISTS: the hit-test overlay used to be `#[cfg(debug_assertions)]`
665    /// in the compositor, so a debug build painted every hit-test area red and a
666    /// release build painted none, with no way to ask for either. That is a
667    /// debug/release divergence in VISUAL OUTPUT — running hello-world showed a
668    /// red window and the reasonable first guess was "this linked the wrong
669    /// DLL". It was not. An overlay you cannot turn on when you need it, and
670    /// cannot turn off when you do not, is worse than no overlay.
671    ///
672    /// Available in RELEASE builds too, deliberately: the moment you need to see
673    /// hit-test regions or overdraw is usually on the build a user is running.
674    ///
675    /// Unknown verbs are reported and ignored rather than fatal — a typo in a
676    /// debugging aid must not stop the app you are trying to debug.
677    /// Reading the environment needs std; on `no_std` there is no environment to
678    /// read, so the overlay is simply off. `from_overlay_spec` stays available
679    /// everywhere, so a `no_std` embedder can still enable overlays explicitly.
680    #[cfg(feature = "std")]
681    #[must_use]
682    pub fn from_az_overlay_env() -> Self {
683        std::env::var("AZ_OVERLAY")
684            .map_or_else(|_| Self::default(), |v| Self::from_overlay_spec(v.as_str()))
685    }
686
687    /// `no_std`: there is no environment, so no overlay.
688    #[cfg(not(feature = "std"))]
689    #[must_use]
690    pub fn from_az_overlay_env() -> Self {
691        Self::default()
692    }
693
694    /// The parser behind [`DebugState::from_az_overlay_env`], separated so it is
695    /// testable without touching the process environment.
696    #[must_use]
697    pub fn from_overlay_spec(spec: &str) -> Self {
698        let mut s = Self::default();
699        for raw in spec.split(',') {
700            let verb = raw.trim().to_ascii_lowercase();
701            if verb.is_empty() {
702                continue;
703            }
704            match verb.as_str() {
705                // azul's own overlay.
706                "hit-test" | "hittest" => s.show_hit_test_areas = true,
707                // webrender flags, named for what they SHOW rather than for the
708                // flag constant, because the constant names are not obvious.
709                "profiler" => s.profiler_dbg = true,
710                "smart-profiler" => s.smart_profiler = true,
711                "overdraw" => s.show_overdraw = true,
712                "render-targets" => s.render_target_dbg = true,
713                "texture-cache" => s.texture_cache_dbg = true,
714                "gpu-cache" => s.gpu_cache_dbg = true,
715                "picture-caching" => s.picture_caching_dbg = true,
716                "primitives" => s.primitive_dbg = true,
717                "invalidation" => s.invalidation_dbg = true,
718                "epochs" => s.epochs = true,
719                "zoom" => s.zoom_dbg = true,
720                "glyph-flashing" => s.glyph_flashing = true,
721                "obscure-images" => s.obscure_images = true,
722                "gpu-time" => s.gpu_time_queries = true,
723                "gpu-samples" => s.gpu_sample_queries = true,
724                "echo-driver" => s.echo_driver_messages = true,
725                // Diagnostic switches that DISABLE a stage — for bisecting which
726                // stage is responsible for a visual artefact.
727                "no-batching" => s.disable_batching = true,
728                "no-opaque-pass" => s.disable_opaque_pass = true,
729                "no-alpha-pass" => s.disable_alpha_pass = true,
730                "no-clip-masks" => s.disable_clip_masks = true,
731                "no-text" => s.disable_text_prims = true,
732                "no-gradients" => s.disable_gradient_prims = true,
733                "all" => {
734                    s.show_hit_test_areas = true;
735                    s.profiler_dbg = true;
736                    s.show_overdraw = true;
737                    s.primitive_dbg = true;
738                }
739                other => {
740                    // Not fatal: a typo in a debugging aid must not stop the app.
741                    #[cfg(feature = "std")]
742                    eprintln!(
743                        "[azul] AZ_OVERLAY: unknown verb {other:?}. Known: hit-test, profiler, \
744                         smart-profiler, overdraw, render-targets, texture-cache, gpu-cache, \
745                         picture-caching, primitives, invalidation, epochs, zoom, glyph-flashing, \
746                         obscure-images, gpu-time, gpu-samples, echo-driver, no-batching, \
747                         no-opaque-pass, no-alpha-pass, no-clip-masks, no-text, no-gradients, all"
748                    );
749                }
750            }
751        }
752        s
753    }
754}
755
756#[derive(Debug, Default, Clone, PartialEq)]
757#[repr(C)]
758pub struct TouchState {
759    /// Number of active touch points (kept in sync with `touch_points.len()`).
760    pub num_touches: usize,
761    /// Currently active touch points (one entry per finger / stylus).
762    /// Backends update this on touch start / move / end events.
763    pub touch_points: TouchPointVec,
764}
765
766/// Single touch point (finger, stylus, etc.)
767#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
768#[repr(C)]
769pub struct TouchPoint {
770    /// Unique identifier for this touch point (persists across move events)
771    pub id: u64,
772    /// Current position of the touch point in logical coordinates
773    pub position: LogicalPosition,
774    /// Force/pressure of the touch (0.0 = no pressure, 1.0 = maximum pressure)
775    /// Set to 0.5 if pressure is not available
776    pub force: f32,
777}
778
779impl_option!(
780    TouchPoint,
781    OptionTouchPoint,
782    [Debug, Copy, Clone, PartialEq, PartialOrd]
783);
784
785impl_vec!(TouchPoint, TouchPointVec, TouchPointVecDestructor, TouchPointVecDestructorType, TouchPointVecSlice, OptionTouchPoint);
786impl_vec_debug!(TouchPoint, TouchPointVec);
787impl_vec_clone!(TouchPoint, TouchPointVec, TouchPointVecDestructor);
788impl_vec_partialeq!(TouchPoint, TouchPointVec);
789
790/// State, size, etc of the window, for comparing to the last frame
791#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Ord, Eq)]
792#[repr(C)]
793#[derive(Default)]
794pub enum WindowTheme {
795    DarkMode,
796    #[default]
797    LightMode,
798}
799
800
801impl_option!(
802    WindowTheme,
803    OptionWindowTheme,
804    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
805);
806
807/// Identifies a specific monitor/display
808///
809/// Contains both an index (for fast current-session lookup) and a stable hash
810/// (for persistence across app restarts and monitor reconfigurations).
811///
812/// - `index`: Runtime index (0-based), may change if monitors are added/removed
813/// - `hash`: Stable identifier based on monitor properties (name, size, position)
814///
815/// Applications can serialize `hash` to remember which monitor a window was on,
816/// then search for matching hash on next launch, falling back to index or PRIMARY.
817#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
818#[repr(C)]
819pub struct MonitorId {
820    /// Runtime index of the monitor (may change between sessions)
821    pub index: usize,
822    /// Stable hash of monitor properties (for persistence)
823    pub hash: u64,
824}
825
826impl MonitorId {
827    /// Primary/default monitor (index 0, hash 0)
828    pub const PRIMARY: Self = Self { index: 0, hash: 0 };
829
830    /// Create a `MonitorId` from index only (hash will be 0)
831    #[must_use] pub const fn new(index: usize) -> Self {
832        Self { index, hash: 0 }
833    }
834
835    /// Create a `MonitorId` from index and hash
836    #[must_use] pub const fn from_index_and_hash(index: usize, hash: u64) -> Self {
837        Self { index, hash }
838    }
839
840    /// Create a stable monitor ID from monitor properties
841    ///
842    /// Uses FNV-1a hash of: name + position + size
843    /// This ensures the hash is stable across app restarts as long as
844    /// the monitor configuration doesn't change significantly
845    #[must_use] pub fn from_properties(
846        index: usize,
847        name: &str,
848        position: LayoutPoint,
849        size: LayoutSize,
850    ) -> Self {
851        use core::hash::{Hash, Hasher};
852
853        // FNV-1a hash (simple, fast, good distribution)
854        struct FnvHasher(u64);
855
856        impl Hasher for FnvHasher {
857            fn write(&mut self, bytes: &[u8]) {
858                const FNV_PRIME: u64 = 0x0100_0000_01b3;
859                for &byte in bytes {
860                    self.0 ^= u64::from(byte);
861                    self.0 = self.0.wrapping_mul(FNV_PRIME);
862                }
863            }
864
865            fn finish(&self) -> u64 {
866                self.0
867            }
868        }
869
870        const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
871        let mut hasher = FnvHasher(FNV_OFFSET_BASIS);
872
873        // Hash the monitor properties
874        name.hash(&mut hasher);
875        (position.x as i64).hash(&mut hasher);
876        (position.y as i64).hash(&mut hasher);
877        (size.width as i64).hash(&mut hasher);
878        (size.height as i64).hash(&mut hasher);
879
880        Self {
881            index,
882            hash: hasher.finish(),
883        }
884    }
885}
886
887impl_option!(
888    MonitorId,
889    OptionMonitorId,
890    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
891);
892
893/// Complete information about a monitor/display
894#[derive(Debug, PartialEq, PartialOrd, Clone)]
895#[repr(C)]
896pub struct Monitor {
897    /// Unique identifier for this monitor (stable across frames)
898    pub monitor_id: MonitorId,
899    /// Human-readable name (e.g., "\\.\DISPLAY1", "HDMI-1", "Built-in Retina Display")
900    pub monitor_name: OptionString,
901    /// Physical size of the monitor in logical pixels
902    pub size: LayoutSize,
903    /// Position of the monitor in the virtual screen coordinate system
904    pub position: LayoutPoint,
905    /// DPI scale factor (1.0 = 96 DPI, 2.0 = 192 DPI for Retina)
906    pub scale_factor: f64,
907    /// Work area (monitor bounds minus taskbars/panels) in logical pixels
908    pub work_area: LayoutRect,
909    /// Available video modes for this monitor
910    pub video_modes: VideoModeVec,
911    /// Whether this is the primary/main monitor
912    pub is_primary_monitor: bool,
913}
914
915impl_option!(
916    Monitor,
917    OptionMonitor,
918    copy = false,
919    [Debug, PartialEq, PartialOrd, Clone]
920);
921
922impl_vec!(Monitor, MonitorVec, MonitorVecDestructor, MonitorVecDestructorType, MonitorVecSlice, OptionMonitor);
923impl_vec_debug!(Monitor, MonitorVec);
924impl_vec_clone!(Monitor, MonitorVec, MonitorVecDestructor);
925impl_vec_partialeq!(Monitor, MonitorVec);
926impl_vec_partialord!(Monitor, MonitorVec);
927
928impl Hash for Monitor {
929    fn hash<H>(&self, state: &mut H)
930    where
931        H: Hasher,
932    {
933        self.monitor_id.hash(state);
934    }
935}
936
937impl Default for Monitor {
938    fn default() -> Self {
939        Self {
940            monitor_id: MonitorId::PRIMARY,
941            monitor_name: OptionString::None,
942            size: LayoutSize::zero(),
943            position: LayoutPoint::zero(),
944            scale_factor: 1.0,
945            work_area: LayoutRect::zero(),
946            video_modes: Vec::new().into(),
947            is_primary_monitor: false,
948        }
949    }
950}
951#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
952#[repr(C)]
953pub struct VideoMode {
954    pub size: LayoutSize,
955    pub bit_depth: u16,
956    pub refresh_rate: u16,
957}
958
959impl_option!(
960    VideoMode,
961    OptionVideoMode,
962    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
963);
964
965impl_vec!(VideoMode, VideoModeVec, VideoModeVecDestructor, VideoModeVecDestructorType, VideoModeVecSlice, OptionVideoMode);
966impl_vec_clone!(VideoMode, VideoModeVec, VideoModeVecDestructor);
967impl_vec_debug!(VideoMode, VideoModeVec);
968impl_vec_partialeq!(VideoMode, VideoModeVec);
969impl_vec_partialord!(VideoMode, VideoModeVec);
970
971/// Position of the window on screen
972#[derive(Debug, Copy, Clone, PartialEq, Eq)]
973#[repr(C, u8)]
974#[derive(Default)]
975pub enum WindowPosition {
976    #[default]
977    Uninitialized,
978    /// Absolute position on the virtual screen (physical px). The default for
979    /// top-level windows.
980    Initialized(PhysicalPositionI32),
981    /// Offset (physical px) from the PARENT window's top-left corner. Used by
982    /// child windows (menus, dropdowns, popups) together with
983    /// `WindowCreateOptions.parent_window_id`: the backend resolves the final
984    /// screen position as `parent_top_left + offset`. This is robust where
985    /// absolute screen coordinates aren't available — notably Wayland, whose
986    /// `xdg_popup` / subsurface protocol positions relative to the parent. Falls
987    /// back to absolute (`offset` from origin) if there is no parent.
988    RelativeToParentWindow(PhysicalPositionI32),
989}
990#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
991
992#[derive(Debug, Copy, Clone, PartialEq, Eq)]
993#[repr(C, u8)]
994/// IME composition window rectangle (cursor position + height)
995#[derive(Default)]
996pub enum ImePosition {
997    #[default]
998    Uninitialized,
999    Initialized(LogicalRect),
1000}
1001
1002
1003#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
1004#[repr(C)]
1005pub struct WindowFlags {
1006    /// Is the window currently maximized, minimized or fullscreen
1007    pub frame: WindowFrame,
1008    /// Window decoration style (title bar, native controls)
1009    pub decorations: WindowDecorations,
1010    /// Compositor blur/transparency effect material
1011    pub background_material: WindowBackgroundMaterial,
1012    /// Window type classification (Normal, Menu, Tooltip, Dialog)
1013    pub window_type: WindowType,
1014    /// User clicked the close button (set by `WindowDelegate`, checked by event loop)
1015    /// The `close_callback` can set this to false to prevent closing
1016    pub close_requested: bool,
1017    /// Is the window currently visible?
1018    pub is_visible: bool,
1019    /// Is the window always on top?
1020    pub is_always_on_top: bool,
1021    /// Whether the window is resizable
1022    pub is_resizable: bool,
1023    /// Whether the window has focus or not (mutating this will request user attention)
1024    pub has_focus: bool,
1025    /// Is smooth scrolling enabled for this window?
1026    pub smooth_scroll_enabled: bool,
1027    /// Is automatic TAB switching supported?
1028    pub autotab_enabled: bool,
1029    /// Enable client-side decorations (custom titlebar with CSD)
1030    /// Only effective when decorations == `WindowDecorations::None`
1031    pub has_decorations: bool,
1032    /// Use native menus (Win32 HMENU, macOS `NSMenu`) instead of Azul window-based menus
1033    /// Default: true on Windows/macOS, false on Linux
1034    pub use_native_menus: bool,
1035    /// Use native context menus instead of Azul window-based context menus
1036    /// Default: true on Windows/macOS, false on Linux
1037    pub use_native_context_menus: bool,
1038    /// Keep window above all others (even from other applications)
1039    /// Platform-specific: Uses `SetWindowPos(HWND_TOPMOST)` on Windows, [`NSWindow` setLevel:] on
1040    /// macOS, _`NET_WM_STATE_ABOVE` on X11, `zwlr_layer_shell` on Wayland
1041    pub is_top_level: bool,
1042    /// Prevent system from sleeping while window is open
1043    /// Platform-specific: Uses `SetThreadExecutionState` on Windows, `IOPMAssertionCreateWithName` on
1044    /// macOS, org.freedesktop.ScreenSaver.Inhibit on Linux
1045    pub prevent_system_sleep: bool,
1046    /// Desired fullscreen-transition style.
1047    ///
1048    /// On macOS this controls whether entering/leaving fullscreen plays the
1049    /// system animation (`Slow*`) or transitions immediately (`Fast*`). On
1050    /// other platforms `Slow*` and `Fast*` behave identically.
1051    ///
1052    /// The actual current frame state still lives in [`WindowFlags::frame`]; this
1053    /// field only describes how the next transition should be performed.
1054    pub fullscreen_mode: FullScreenMode,
1055}
1056
1057impl_option!(
1058    WindowFlags,
1059    OptionWindowFlags,
1060    copy = false,
1061    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1062);
1063
1064/// Window type classification for behavior control
1065#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
1066#[repr(C)]
1067pub enum WindowType {
1068    /// Normal application window
1069    Normal,
1070    /// Menu popup window (always-on-top, frameless, auto-closes on focus loss)
1071    Menu,
1072    /// Tooltip window (always-on-top, no interaction)
1073    Tooltip,
1074    /// Dialog window (blocks parent window)
1075    Dialog,
1076}
1077
1078impl Default for WindowType {
1079    fn default() -> Self {
1080        Self::Normal
1081    }
1082}
1083
1084/// Window frame state (normal, minimized, maximized, fullscreen)
1085#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
1086#[repr(C)]
1087pub enum WindowFrame {
1088    Normal,
1089    Minimized,
1090    Maximized,
1091    Fullscreen,
1092}
1093
1094/// Window decoration style
1095#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
1096#[repr(C)]
1097pub enum WindowDecorations {
1098    /// Full decorations: title bar with controls
1099    Normal,
1100    /// No title text but controls visible (extended frame).
1101    /// The application must draw its own title text.
1102    NoTitle,
1103    /// Like `NoTitle`, but the framework auto-injects a `Titlebar`
1104    /// at the top of the user's DOM after calling the layout callback.
1105    ///
1106    /// The injected titlebar reads `TitlebarMetrics` from `SystemStyle` for
1107    /// correct padding around the OS-drawn window control buttons, uses the
1108    /// system title font, and carries the `__azul-native-titlebar` class for
1109    /// automatic window-drag activation.
1110    NoTitleAutoInject,
1111    /// No controls visible but title bar area present
1112    NoControls,
1113    /// No decorations at all (borderless)
1114    None,
1115}
1116
1117impl Default for WindowDecorations {
1118    fn default() -> Self {
1119        Self::Normal
1120    }
1121}
1122
1123/// Compositor blur/transparency effects for window background
1124#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
1125#[repr(C)]
1126pub enum WindowBackgroundMaterial {
1127    /// No transparency or blur
1128    Opaque,
1129    /// Transparent without blur
1130    Transparent,
1131    /// macOS: Sidebar material, Windows: Acrylic light
1132    Sidebar,
1133    /// macOS: Menu material, Windows: Acrylic
1134    Menu,
1135    /// macOS: HUD material, Windows: Acrylic dark
1136    HUD,
1137    /// macOS: Titlebar material, Windows: Mica
1138    Titlebar,
1139    /// Windows: Mica Alt material
1140    MicaAlt,
1141}
1142
1143impl Default for WindowBackgroundMaterial {
1144    fn default() -> Self {
1145        Self::Opaque
1146    }
1147}
1148
1149impl Default for WindowFlags {
1150    fn default() -> Self {
1151        Self {
1152            frame: WindowFrame::Normal,
1153            decorations: WindowDecorations::Normal,
1154            background_material: WindowBackgroundMaterial::Opaque,
1155            window_type: WindowType::Normal,
1156            close_requested: false,
1157            is_visible: true,
1158            is_always_on_top: false,
1159            is_resizable: true,
1160            has_focus: true,
1161            smooth_scroll_enabled: true,
1162            autotab_enabled: true,
1163            has_decorations: false,
1164            // Native menus are the default on platforms that support them (Windows/macOS)
1165            // The platform layer will override this appropriately
1166            use_native_menus: cfg!(any(target_os = "windows", target_os = "macos")),
1167            use_native_context_menus: cfg!(any(target_os = "windows", target_os = "macos")),
1168            is_top_level: false,
1169            prevent_system_sleep: false,
1170            fullscreen_mode: FullScreenMode::FastFullScreen,
1171        }
1172    }
1173}
1174
1175impl WindowFlags {
1176    /// Check if window is a menu popup
1177    #[inline]
1178    #[must_use] pub fn is_menu_window(&self) -> bool {
1179        self.window_type == WindowType::Menu
1180    }
1181
1182    /// Check if window is a tooltip
1183    #[inline]
1184    #[must_use] pub fn is_tooltip_window(&self) -> bool {
1185        self.window_type == WindowType::Tooltip
1186    }
1187
1188    /// Check if window is a dialog
1189    #[inline]
1190    #[must_use] pub fn is_dialog_window(&self) -> bool {
1191        self.window_type == WindowType::Dialog
1192    }
1193
1194    /// Check if window currently has focus
1195    #[inline]
1196    #[must_use] pub const fn window_has_focus(&self) -> bool {
1197        self.has_focus
1198    }
1199
1200    /// Check if close was requested via callback
1201    #[inline]
1202    #[must_use] pub const fn is_close_requested(&self) -> bool {
1203        self.close_requested
1204    }
1205
1206    /// Check if window has client-side decorations enabled
1207    #[inline]
1208    #[must_use] pub const fn has_csd(&self) -> bool {
1209        self.has_decorations
1210    }
1211
1212    /// Check if native menus should be used
1213    #[inline]
1214    #[must_use] pub const fn use_native_menus(&self) -> bool {
1215        self.use_native_menus
1216    }
1217
1218    /// Check if native context menus should be used
1219    #[inline]
1220    #[must_use] pub const fn use_native_context_menus(&self) -> bool {
1221        self.use_native_context_menus
1222    }
1223}
1224
1225/// Platform-specific window configuration options (Windows, Linux, macOS, WASM)
1226#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
1227#[repr(C)]
1228pub struct PlatformSpecificOptions {
1229    pub windows_options: WindowsWindowOptions,
1230    pub linux_options: LinuxWindowOptions,
1231    pub mac_options: MacWindowOptions,
1232    pub wasm_options: WasmWindowOptions,
1233}
1234
1235// SAFETY: PlatformSpecificOptions contains raw pointers (X11Visual) that are
1236// opaque platform handles, not dereferenced across threads.
1237unsafe impl Sync for PlatformSpecificOptions {}
1238#[allow(clippy::non_send_fields_in_send_ty)] // opaque platform handles, not dereferenced across threads (see note above)
1239unsafe impl Send for PlatformSpecificOptions {}
1240
1241#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
1242#[repr(C)]
1243pub struct WindowsWindowOptions {
1244    /// STARTUP ONLY: Whether the window should allow drag + drop operations (default: true)
1245    pub allow_drag_and_drop: bool,
1246    /// STARTUP ONLY: Sets `WS_EX_NOREDIRECTIONBITMAP`
1247    pub no_redirection_bitmap: bool,
1248    /// STARTUP ONLY: Window icon (decoded bytes), appears at the top right corner of the window
1249    pub window_icon: OptionWindowIcon,
1250    /// READWRITE: Taskbar icon (decoded bytes), usually 256x256x4 bytes large (`ICON_BIG`).
1251    ///
1252    /// Can be changed in callbacks / at runtime.
1253    pub taskbar_icon: OptionTaskBarIcon,
1254    // NOTE: the old Windows-specific `parent_window: OptionHwndHandle` field was
1255    // removed in favor of the cross-platform `WindowCreateOptions.parent_window_id`
1256    // (+ `WindowPosition::RelativeToParentWindow`), which every backend resolves
1257    // through its window registry. One parenting model for all platforms.
1258}
1259
1260impl Default for WindowsWindowOptions {
1261    fn default() -> Self {
1262        Self {
1263            allow_drag_and_drop: true,
1264            no_redirection_bitmap: false,
1265            window_icon: OptionWindowIcon::None,
1266            taskbar_icon: OptionTaskBarIcon::None,
1267        }
1268    }
1269}
1270
1271/// X window type. Maps directly to
1272/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
1273#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1274#[repr(C)]
1275#[derive(Default)]
1276pub enum XWindowType {
1277    /// A desktop feature. This can include a single window containing desktop icons with the same
1278    /// dimensions as the screen, allowing the desktop environment to have full control of the
1279    /// desktop, without the need for proxying root window clicks.
1280    Desktop,
1281    /// A dock or panel feature. Typically a Window Manager would keep such windows on top of all
1282    /// other windows.
1283    Dock,
1284    /// Toolbar windows. "Torn off" from the main application.
1285    Toolbar,
1286    /// Pinnable menu windows. "Torn off" from the main application.
1287    Menu,
1288    /// A small persistent utility window, such as a palette or toolbox.
1289    Utility,
1290    /// The window is a splash screen displayed as an application is starting up.
1291    Splash,
1292    /// This is a dialog window.
1293    Dialog,
1294    /// A dropdown menu that usually appears when the user clicks on an item in a menu bar.
1295    /// This property is typically used on override-redirect windows.
1296    DropdownMenu,
1297    /// A popup menu that usually appears when the user right clicks on an object.
1298    /// This property is typically used on override-redirect windows.
1299    PopupMenu,
1300    /// A tooltip window. Usually used to show additional information when hovering over an object
1301    /// with the cursor. This property is typically used on override-redirect windows.
1302    Tooltip,
1303    /// The window is a notification.
1304    /// This property is typically used on override-redirect windows.
1305    Notification,
1306    /// This should be used on the windows that are popped up by combo boxes.
1307    /// This property is typically used on override-redirect windows.
1308    Combo,
1309    /// This indicates the the window is being dragged.
1310    /// This property is typically used on override-redirect windows.
1311    Dnd,
1312    /// This is a normal, top-level window.
1313    #[default]
1314    Normal,
1315}
1316
1317impl_option!(
1318    XWindowType,
1319    OptionXWindowType,
1320    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1321);
1322
1323
1324#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1325#[repr(C)]
1326#[derive(Default)]
1327pub enum UserAttentionType {
1328    #[default]
1329    None,
1330    Critical,
1331    Informational,
1332}
1333
1334
1335/// State for tracking hover and interaction with Linux window decoration elements (CSD).
1336#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1337#[repr(C)]
1338pub struct LinuxDecorationsState {
1339    pub is_dragging_titlebar: bool,
1340    pub close_button_hover: bool,
1341    pub maximize_button_hover: bool,
1342    pub minimize_button_hover: bool,
1343}
1344
1345impl_option!(
1346    LinuxDecorationsState,
1347    OptionLinuxDecorationsState,
1348    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
1349);
1350
1351#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
1352#[repr(C)]
1353pub struct LinuxWindowOptions {
1354    pub wayland_theme: OptionWaylandTheme,
1355    pub window_icon: OptionWindowIcon,
1356    /// Build window with `_GTK_THEME_VARIANT` hint set to the specified value. Currently only
1357    /// relevant on X11. Can only be set at window creation, can't be changed in callbacks.
1358    pub x11_gtk_theme_variant: OptionString,
1359    /// Build window with a given application ID. It should match the `.desktop` file distributed
1360    /// with your program. Only relevant on Wayland.
1361    /// Can only be set at window creation, can't be changed in callbacks.
1362    ///
1363    /// For details about application ID conventions, see the
1364    /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id)
1365    pub wayland_app_id: OptionString,
1366    /// Build window with `WM_CLASS` hint; defaults to the name of the binary. Only relevant on
1367    /// X11. Can only be set at window creation, can't be changed in callbacks.
1368    pub x11_wm_classes: StringPairVec,
1369    /// Build window with `_NET_WM_WINDOW_TYPE` hint; defaults to `Normal`. Only relevant on X11.
1370    /// Can only be set at window creation, can't be changed in callbacks.
1371    pub x11_window_types: XWindowTypeVec,
1372    /// (Unimplemented) - Can only be set at window creation, can't be changed in callbacks.
1373    pub x11_visual: OptionX11Visual,
1374    /// Build window with resize increment hint. Only implemented on X11.
1375    /// Can only be set at window creation, can't be changed in callbacks.
1376    pub x11_resize_increments: OptionLogicalSize,
1377    /// Build window with base size hint. Only implemented on X11.
1378    /// Can only be set at window creation, can't be changed in callbacks.
1379    pub x11_base_size: OptionLogicalSize,
1380    /// (Unimplemented) - Can only be set at window creation, can't be changed in callbacks.
1381    pub x11_screen: OptionI32,
1382    pub request_user_attention: UserAttentionType,
1383    /// X11-specific: Client-side decoration state (drag position, button hover, etc.)
1384    pub x11_decorations_state: OptionLinuxDecorationsState,
1385    /// Build window with override-redirect flag; defaults to false. Only relevant on X11.
1386    /// Can only be set at window creation, can't be changed in callbacks.
1387    pub x11_override_redirect: bool,
1388}
1389
1390pub type X11Visual = *const c_void;
1391impl_option!(
1392    X11Visual,
1393    OptionX11Visual,
1394    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1395);
1396
1397/// A key-value pair of strings, used for X11 `WM_CLASS` and other platform properties
1398#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1399#[repr(C)]
1400pub struct AzStringPair {
1401    pub key: AzString,
1402    pub value: AzString,
1403}
1404
1405impl_option!(
1406    AzStringPair,
1407    OptionStringPair,
1408    copy = false,
1409    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1410);
1411
1412impl_vec!(AzStringPair, StringPairVec, StringPairVecDestructor, StringPairVecDestructorType, StringPairVecSlice, OptionStringPair);
1413impl_vec_mut!(AzStringPair, StringPairVec);
1414impl_vec_debug!(AzStringPair, StringPairVec);
1415impl_vec_partialord!(AzStringPair, StringPairVec);
1416impl_vec_ord!(AzStringPair, StringPairVec);
1417impl_vec_clone!(AzStringPair, StringPairVec, StringPairVecDestructor);
1418impl_vec_partialeq!(AzStringPair, StringPairVec);
1419impl_vec_eq!(AzStringPair, StringPairVec);
1420impl_vec_hash!(AzStringPair, StringPairVec);
1421
1422impl_option!(
1423    StringPairVec,
1424    OptionStringPairVec,
1425    copy = false,
1426    [Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash]
1427);
1428
1429impl StringPairVec {
1430    #[must_use] pub fn get_key(&self, search_key: &str) -> Option<&AzString> {
1431        self.as_ref().iter().find_map(|v| {
1432            if v.key.as_str() == search_key {
1433                Some(&v.value)
1434            } else {
1435                None
1436            }
1437        })
1438    }
1439    pub fn get_key_mut(&mut self, search_key: &str) -> Option<&mut AzStringPair> {
1440        self.as_mut()
1441            .iter_mut()
1442            .find(|v| v.key.as_str() == search_key)
1443    }
1444    pub fn insert_kv<I: Into<AzString>>(&mut self, key: I, value: I) {
1445        let key = key.into();
1446        let value = value.into();
1447        match self.get_key_mut(key.as_str()) {
1448            None => {}
1449            Some(s) => {
1450                s.value = value;
1451                return;
1452            }
1453        }
1454        self.push(AzStringPair { key, value });
1455    }
1456}
1457
1458impl_vec!(XWindowType, XWindowTypeVec, XWindowTypeVecDestructor, XWindowTypeVecDestructorType, XWindowTypeVecSlice, OptionXWindowType);
1459impl_vec_debug!(XWindowType, XWindowTypeVec);
1460impl_vec_partialord!(XWindowType, XWindowTypeVec);
1461impl_vec_ord!(XWindowType, XWindowTypeVec);
1462impl_vec_clone!(XWindowType, XWindowTypeVec, XWindowTypeVecDestructor);
1463impl_vec_partialeq!(XWindowType, XWindowTypeVec);
1464impl_vec_eq!(XWindowType, XWindowTypeVec);
1465impl_vec_hash!(XWindowType, XWindowTypeVec);
1466
1467impl_option!(
1468    WaylandTheme,
1469    OptionWaylandTheme,
1470    copy = false,
1471    [Debug, Clone, PartialEq, PartialOrd]
1472);
1473
1474/// macOS-specific window options (reserved for future use)
1475#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1476#[repr(C)]
1477// `_`-prefixed fields are C-ABI/api.json names; cannot rename.
1478#[allow(clippy::pub_underscore_fields)]
1479pub struct MacWindowOptions {
1480    // empty for now, single field must be present for ABI compat - always set to 0
1481    pub _reserved: u8,
1482}
1483
1484/// WASM/web-specific window options (reserved for future use)
1485#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1486#[repr(C)]
1487// `_`-prefixed fields are C-ABI/api.json names; cannot rename.
1488#[allow(clippy::pub_underscore_fields)]
1489pub struct WasmWindowOptions {
1490    // empty for now, single field must be present for ABI compat - always set to 0
1491    pub _reserved: u8,
1492}
1493
1494#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1495#[repr(C)]
1496#[derive(Default)]
1497pub enum FullScreenMode {
1498    /// - macOS: If the window is in windowed mode, transitions it slowly to fullscreen mode
1499    /// - other: Does the same as `FastFullScreen`.
1500    SlowFullScreen,
1501    /// Window should immediately go into fullscreen mode (on macOS this is not the default
1502    /// behaviour).
1503    #[default]
1504    FastFullScreen,
1505    /// - macOS: If the window is in fullscreen mode, transitions slowly back to windowed state.
1506    /// - other: Does the same as `FastWindowed`.
1507    SlowWindowed,
1508    /// If the window is in fullscreen mode, will immediately go back to windowed mode (on macOS
1509    /// this is not the default behaviour).
1510    FastWindowed,
1511}
1512
1513
1514// Translation type because in winit 24.0 the WinitWaylandTheme is a trait instead
1515// of a struct, which makes things more complicated
1516#[derive(Debug, Clone, PartialEq, PartialOrd)]
1517#[repr(C)]
1518pub struct WaylandTheme {
1519    pub title_bar_active_background_color: ColorU,
1520    pub title_bar_active_separator_color: ColorU,
1521    pub title_bar_active_text_color: ColorU,
1522    pub title_bar_inactive_background_color: ColorU,
1523    pub title_bar_inactive_separator_color: ColorU,
1524    pub title_bar_inactive_text_color: ColorU,
1525    pub maximize_idle_foreground_inactive_color: ColorU,
1526    pub minimize_idle_foreground_inactive_color: ColorU,
1527    pub close_idle_foreground_inactive_color: ColorU,
1528    pub maximize_hovered_foreground_inactive_color: ColorU,
1529    pub minimize_hovered_foreground_inactive_color: ColorU,
1530    pub close_hovered_foreground_inactive_color: ColorU,
1531    pub maximize_disabled_foreground_inactive_color: ColorU,
1532    pub minimize_disabled_foreground_inactive_color: ColorU,
1533    pub close_disabled_foreground_inactive_color: ColorU,
1534    pub maximize_idle_background_inactive_color: ColorU,
1535    pub minimize_idle_background_inactive_color: ColorU,
1536    pub close_idle_background_inactive_color: ColorU,
1537    pub maximize_hovered_background_inactive_color: ColorU,
1538    pub minimize_hovered_background_inactive_color: ColorU,
1539    pub close_hovered_background_inactive_color: ColorU,
1540    pub maximize_disabled_background_inactive_color: ColorU,
1541    pub minimize_disabled_background_inactive_color: ColorU,
1542    pub close_disabled_background_inactive_color: ColorU,
1543    pub maximize_idle_foreground_active_color: ColorU,
1544    pub minimize_idle_foreground_active_color: ColorU,
1545    pub close_idle_foreground_active_color: ColorU,
1546    pub maximize_hovered_foreground_active_color: ColorU,
1547    pub minimize_hovered_foreground_active_color: ColorU,
1548    pub close_hovered_foreground_active_color: ColorU,
1549    pub maximize_disabled_foreground_active_color: ColorU,
1550    pub minimize_disabled_foreground_active_color: ColorU,
1551    pub close_disabled_foreground_active_color: ColorU,
1552    pub maximize_idle_background_active_color: ColorU,
1553    pub minimize_idle_background_active_color: ColorU,
1554    pub close_idle_background_active_color: ColorU,
1555    pub maximize_hovered_background_active_color: ColorU,
1556    pub minimize_hovered_background_active_color: ColorU,
1557    pub close_hovered_background_active_color: ColorU,
1558    pub maximize_disabled_background_active_color: ColorU,
1559    pub minimize_disabled_background_active_color: ColorU,
1560    pub close_disabled_background_active_color: ColorU,
1561    pub title_bar_font: AzString,
1562    pub title_bar_font_size: f32,
1563}
1564
1565#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
1566#[repr(C)]
1567pub struct WindowSize {
1568    /// Width and height of the window, in logical
1569    /// units (may not correspond to the physical on-screen size)
1570    pub dimensions: LogicalSize,
1571    /// Actual DPI value (default: 96)
1572    pub dpi: u32,
1573    /// Minimum dimensions of the window
1574    pub min_dimensions: OptionLogicalSize,
1575    /// Maximum dimensions of the window
1576    pub max_dimensions: OptionLogicalSize,
1577}
1578
1579impl WindowSize {
1580    #[allow(clippy::cast_possible_truncation)] // bounded DPI/dimension/number conversion
1581    #[must_use] pub fn get_layout_size(&self) -> LayoutSize {
1582        LayoutSize::new(
1583            libm::roundf(self.dimensions.width) as isize,
1584            libm::roundf(self.dimensions.height) as isize,
1585        )
1586    }
1587
1588    /// Get the actual logical size
1589    #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
1590        self.dimensions
1591    }
1592
1593    #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
1594        self.dimensions
1595            .to_physical(self.get_hidpi_factor().inner.get())
1596    }
1597
1598    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1599    #[must_use] pub fn get_hidpi_factor(&self) -> DpiScaleFactor {
1600        // Guard against `dpi == 0` (uninitialized / misreporting platform),
1601        // which would yield a 0.0 scale factor and later divide-by-zero when
1602        // converting physical <-> logical sizes (`to_logical` divides by this).
1603        // Fall back to the standard 96 DPI (scale 1.0).
1604        let dpi = if self.dpi == 0 { 96 } else { self.dpi };
1605        DpiScaleFactor {
1606            inner: FloatValue::new(dpi as f32 / 96.0),
1607        }
1608    }
1609}
1610
1611impl Default for WindowSize {
1612    fn default() -> Self {
1613        Self {
1614            dimensions: LogicalSize::new(640.0, 480.0),
1615            dpi: 96,
1616            min_dimensions: None.into(),
1617            max_dimensions: None.into(),
1618        }
1619    }
1620}
1621
1622#[repr(C)]
1623#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1624pub enum RendererType {
1625    /// Force hardware rendering
1626    Hardware,
1627    /// Force software rendering
1628    Software,
1629}
1630
1631impl_option!(
1632    RendererType,
1633    OptionRendererType,
1634    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
1635);
1636
1637#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1638pub enum UpdateFocusWarning {
1639    FocusInvalidDomId(DomId),
1640    FocusInvalidNodeId(NodeHierarchyItemId),
1641    CouldNotFindFocusNode(CssPath),
1642}
1643
1644impl ::core::fmt::Display for UpdateFocusWarning {
1645    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
1646        use self::UpdateFocusWarning::{FocusInvalidDomId, FocusInvalidNodeId, CouldNotFindFocusNode};
1647        match self {
1648            FocusInvalidDomId(dom_id) => write!(f, "Focusing on DOM with invalid ID: {dom_id:?}"),
1649            FocusInvalidNodeId(node_id) => {
1650                write!(f, "Focusing on node with invalid ID: {node_id}")
1651            }
1652            CouldNotFindFocusNode(css_path) => {
1653                write!(f, "Could not find focus node for path: {css_path}")
1654            }
1655        }
1656    }
1657}
1658
1659/// Utility function for easier creation of a keymap - i.e. `[vec![Ctrl, S], my_function]`
1660#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1661#[repr(C, u8)]
1662pub enum AcceleratorKey {
1663    Ctrl,
1664    Alt,
1665    Shift,
1666    Key(VirtualKeyCode),
1667}
1668
1669impl AcceleratorKey {
1670    /// Checks if the current keyboard state contains the given char or modifier,
1671    /// i.e. if the keyboard state currently has the shift key pressed and the
1672    /// accelerator key is `Shift`, evaluates to true, otherwise to false.
1673    #[must_use] pub fn matches(&self, keyboard_state: &KeyboardState) -> bool {
1674        use self::AcceleratorKey::{Ctrl, Alt, Shift, Key};
1675        match self {
1676            Ctrl => keyboard_state.ctrl_down(),
1677            Alt => keyboard_state.alt_down(),
1678            Shift => keyboard_state.shift_down(),
1679            Key(k) => keyboard_state.is_key_down(*k),
1680        }
1681    }
1682}
1683
1684/// Symbolic name for a keyboard key, does NOT take the keyboard locale into account
1685#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1686#[repr(C)]
1687pub enum VirtualKeyCode {
1688    Key1,
1689    Key2,
1690    Key3,
1691    Key4,
1692    Key5,
1693    Key6,
1694    Key7,
1695    Key8,
1696    Key9,
1697    Key0,
1698    A,
1699    B,
1700    C,
1701    D,
1702    E,
1703    F,
1704    G,
1705    H,
1706    I,
1707    J,
1708    K,
1709    L,
1710    M,
1711    N,
1712    O,
1713    P,
1714    Q,
1715    R,
1716    S,
1717    T,
1718    U,
1719    V,
1720    W,
1721    X,
1722    Y,
1723    Z,
1724    Escape,
1725    F1,
1726    F2,
1727    F3,
1728    F4,
1729    F5,
1730    F6,
1731    F7,
1732    F8,
1733    F9,
1734    F10,
1735    F11,
1736    F12,
1737    F13,
1738    F14,
1739    F15,
1740    F16,
1741    F17,
1742    F18,
1743    F19,
1744    F20,
1745    F21,
1746    F22,
1747    F23,
1748    F24,
1749    Snapshot,
1750    Scroll,
1751    Pause,
1752    Insert,
1753    Home,
1754    Delete,
1755    End,
1756    PageDown,
1757    PageUp,
1758    Left,
1759    Up,
1760    Right,
1761    Down,
1762    Back,
1763    Return,
1764    Space,
1765    Compose,
1766    Caret,
1767    Numlock,
1768    Numpad0,
1769    Numpad1,
1770    Numpad2,
1771    Numpad3,
1772    Numpad4,
1773    Numpad5,
1774    Numpad6,
1775    Numpad7,
1776    Numpad8,
1777    Numpad9,
1778    NumpadAdd,
1779    NumpadDivide,
1780    NumpadDecimal,
1781    NumpadComma,
1782    NumpadEnter,
1783    NumpadEquals,
1784    NumpadMultiply,
1785    NumpadSubtract,
1786    AbntC1,
1787    AbntC2,
1788    Apostrophe,
1789    Apps,
1790    Asterisk,
1791    At,
1792    Ax,
1793    Backslash,
1794    Calculator,
1795    Capital,
1796    Colon,
1797    Comma,
1798    Convert,
1799    Equals,
1800    Grave,
1801    Kana,
1802    Kanji,
1803    LAlt,
1804    LBracket,
1805    LControl,
1806    LShift,
1807    LWin,
1808    Mail,
1809    MediaSelect,
1810    MediaStop,
1811    Minus,
1812    Mute,
1813    MyComputer,
1814    NavigateForward,
1815    NavigateBackward,
1816    NextTrack,
1817    NoConvert,
1818    OEM102,
1819    Period,
1820    PlayPause,
1821    Plus,
1822    Power,
1823    PrevTrack,
1824    RAlt,
1825    RBracket,
1826    RControl,
1827    RShift,
1828    RWin,
1829    Semicolon,
1830    Slash,
1831    Sleep,
1832    Stop,
1833    Sysrq,
1834    Tab,
1835    Underline,
1836    Unlabeled,
1837    VolumeDown,
1838    VolumeUp,
1839    Wake,
1840    WebBack,
1841    WebFavorites,
1842    WebForward,
1843    WebHome,
1844    WebRefresh,
1845    WebSearch,
1846    WebStop,
1847    Yen,
1848    Copy,
1849    Paste,
1850    Cut,
1851}
1852
1853impl VirtualKeyCode {
1854    /// Reconstructs a `VirtualKeyCode` from its `as u32` discriminant.
1855    ///
1856    /// This enum is a fieldless `#[repr(C)]` enum with no explicit discriminants,
1857    /// so the discriminants are assigned sequentially in declaration order and
1858    /// `VariantN as u32` round-trips through this table. Used to recover the key
1859    /// of a keyboard *event* from its `key_code` (which is stored as
1860    /// `VirtualKeyCode as u32`) instead of reading live keyboard state.
1861    #[must_use]
1862    #[allow(clippy::too_many_lines)] // exhaustive keycode match table
1863    pub const fn from_u32(v: u32) -> Option<Self> {
1864        match v {
1865            0 => Some(Self::Key1),
1866            1 => Some(Self::Key2),
1867            2 => Some(Self::Key3),
1868            3 => Some(Self::Key4),
1869            4 => Some(Self::Key5),
1870            5 => Some(Self::Key6),
1871            6 => Some(Self::Key7),
1872            7 => Some(Self::Key8),
1873            8 => Some(Self::Key9),
1874            9 => Some(Self::Key0),
1875            10 => Some(Self::A),
1876            11 => Some(Self::B),
1877            12 => Some(Self::C),
1878            13 => Some(Self::D),
1879            14 => Some(Self::E),
1880            15 => Some(Self::F),
1881            16 => Some(Self::G),
1882            17 => Some(Self::H),
1883            18 => Some(Self::I),
1884            19 => Some(Self::J),
1885            20 => Some(Self::K),
1886            21 => Some(Self::L),
1887            22 => Some(Self::M),
1888            23 => Some(Self::N),
1889            24 => Some(Self::O),
1890            25 => Some(Self::P),
1891            26 => Some(Self::Q),
1892            27 => Some(Self::R),
1893            28 => Some(Self::S),
1894            29 => Some(Self::T),
1895            30 => Some(Self::U),
1896            31 => Some(Self::V),
1897            32 => Some(Self::W),
1898            33 => Some(Self::X),
1899            34 => Some(Self::Y),
1900            35 => Some(Self::Z),
1901            36 => Some(Self::Escape),
1902            37 => Some(Self::F1),
1903            38 => Some(Self::F2),
1904            39 => Some(Self::F3),
1905            40 => Some(Self::F4),
1906            41 => Some(Self::F5),
1907            42 => Some(Self::F6),
1908            43 => Some(Self::F7),
1909            44 => Some(Self::F8),
1910            45 => Some(Self::F9),
1911            46 => Some(Self::F10),
1912            47 => Some(Self::F11),
1913            48 => Some(Self::F12),
1914            49 => Some(Self::F13),
1915            50 => Some(Self::F14),
1916            51 => Some(Self::F15),
1917            52 => Some(Self::F16),
1918            53 => Some(Self::F17),
1919            54 => Some(Self::F18),
1920            55 => Some(Self::F19),
1921            56 => Some(Self::F20),
1922            57 => Some(Self::F21),
1923            58 => Some(Self::F22),
1924            59 => Some(Self::F23),
1925            60 => Some(Self::F24),
1926            61 => Some(Self::Snapshot),
1927            62 => Some(Self::Scroll),
1928            63 => Some(Self::Pause),
1929            64 => Some(Self::Insert),
1930            65 => Some(Self::Home),
1931            66 => Some(Self::Delete),
1932            67 => Some(Self::End),
1933            68 => Some(Self::PageDown),
1934            69 => Some(Self::PageUp),
1935            70 => Some(Self::Left),
1936            71 => Some(Self::Up),
1937            72 => Some(Self::Right),
1938            73 => Some(Self::Down),
1939            74 => Some(Self::Back),
1940            75 => Some(Self::Return),
1941            76 => Some(Self::Space),
1942            77 => Some(Self::Compose),
1943            78 => Some(Self::Caret),
1944            79 => Some(Self::Numlock),
1945            80 => Some(Self::Numpad0),
1946            81 => Some(Self::Numpad1),
1947            82 => Some(Self::Numpad2),
1948            83 => Some(Self::Numpad3),
1949            84 => Some(Self::Numpad4),
1950            85 => Some(Self::Numpad5),
1951            86 => Some(Self::Numpad6),
1952            87 => Some(Self::Numpad7),
1953            88 => Some(Self::Numpad8),
1954            89 => Some(Self::Numpad9),
1955            90 => Some(Self::NumpadAdd),
1956            91 => Some(Self::NumpadDivide),
1957            92 => Some(Self::NumpadDecimal),
1958            93 => Some(Self::NumpadComma),
1959            94 => Some(Self::NumpadEnter),
1960            95 => Some(Self::NumpadEquals),
1961            96 => Some(Self::NumpadMultiply),
1962            97 => Some(Self::NumpadSubtract),
1963            98 => Some(Self::AbntC1),
1964            99 => Some(Self::AbntC2),
1965            100 => Some(Self::Apostrophe),
1966            101 => Some(Self::Apps),
1967            102 => Some(Self::Asterisk),
1968            103 => Some(Self::At),
1969            104 => Some(Self::Ax),
1970            105 => Some(Self::Backslash),
1971            106 => Some(Self::Calculator),
1972            107 => Some(Self::Capital),
1973            108 => Some(Self::Colon),
1974            109 => Some(Self::Comma),
1975            110 => Some(Self::Convert),
1976            111 => Some(Self::Equals),
1977            112 => Some(Self::Grave),
1978            113 => Some(Self::Kana),
1979            114 => Some(Self::Kanji),
1980            115 => Some(Self::LAlt),
1981            116 => Some(Self::LBracket),
1982            117 => Some(Self::LControl),
1983            118 => Some(Self::LShift),
1984            119 => Some(Self::LWin),
1985            120 => Some(Self::Mail),
1986            121 => Some(Self::MediaSelect),
1987            122 => Some(Self::MediaStop),
1988            123 => Some(Self::Minus),
1989            124 => Some(Self::Mute),
1990            125 => Some(Self::MyComputer),
1991            126 => Some(Self::NavigateForward),
1992            127 => Some(Self::NavigateBackward),
1993            128 => Some(Self::NextTrack),
1994            129 => Some(Self::NoConvert),
1995            130 => Some(Self::OEM102),
1996            131 => Some(Self::Period),
1997            132 => Some(Self::PlayPause),
1998            133 => Some(Self::Plus),
1999            134 => Some(Self::Power),
2000            135 => Some(Self::PrevTrack),
2001            136 => Some(Self::RAlt),
2002            137 => Some(Self::RBracket),
2003            138 => Some(Self::RControl),
2004            139 => Some(Self::RShift),
2005            140 => Some(Self::RWin),
2006            141 => Some(Self::Semicolon),
2007            142 => Some(Self::Slash),
2008            143 => Some(Self::Sleep),
2009            144 => Some(Self::Stop),
2010            145 => Some(Self::Sysrq),
2011            146 => Some(Self::Tab),
2012            147 => Some(Self::Underline),
2013            148 => Some(Self::Unlabeled),
2014            149 => Some(Self::VolumeDown),
2015            150 => Some(Self::VolumeUp),
2016            151 => Some(Self::Wake),
2017            152 => Some(Self::WebBack),
2018            153 => Some(Self::WebFavorites),
2019            154 => Some(Self::WebForward),
2020            155 => Some(Self::WebHome),
2021            156 => Some(Self::WebRefresh),
2022            157 => Some(Self::WebSearch),
2023            158 => Some(Self::WebStop),
2024            159 => Some(Self::Yen),
2025            160 => Some(Self::Copy),
2026            161 => Some(Self::Paste),
2027            162 => Some(Self::Cut),
2028            _ => None,
2029        }
2030    }
2031
2032    #[must_use] pub const fn get_lowercase(&self) -> Option<char> {
2033        use self::VirtualKeyCode::{A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, Key0, Numpad0, Key1, Numpad1, Key2, Numpad2, Key3, Numpad3, Key4, Numpad4, Key5, Numpad5, Key6, Numpad6, Key7, Numpad7, Key8, Numpad8, Key9, Numpad9, Minus, Asterisk, At, Period, Semicolon, Slash, Caret};
2034        match self {
2035            A => Some('a'),
2036            B => Some('b'),
2037            C => Some('c'),
2038            D => Some('d'),
2039            E => Some('e'),
2040            F => Some('f'),
2041            G => Some('g'),
2042            H => Some('h'),
2043            I => Some('i'),
2044            J => Some('j'),
2045            K => Some('k'),
2046            L => Some('l'),
2047            M => Some('m'),
2048            N => Some('n'),
2049            O => Some('o'),
2050            P => Some('p'),
2051            Q => Some('q'),
2052            R => Some('r'),
2053            S => Some('s'),
2054            T => Some('t'),
2055            U => Some('u'),
2056            V => Some('v'),
2057            W => Some('w'),
2058            X => Some('x'),
2059            Y => Some('y'),
2060            Z => Some('z'),
2061            Key0 | Numpad0 => Some('0'),
2062            Key1 | Numpad1 => Some('1'),
2063            Key2 | Numpad2 => Some('2'),
2064            Key3 | Numpad3 => Some('3'),
2065            Key4 | Numpad4 => Some('4'),
2066            Key5 | Numpad5 => Some('5'),
2067            Key6 | Numpad6 => Some('6'),
2068            Key7 | Numpad7 => Some('7'),
2069            Key8 | Numpad8 => Some('8'),
2070            Key9 | Numpad9 => Some('9'),
2071            Minus => Some('-'),
2072            Asterisk => Some('*'),
2073            At => Some('@'),
2074            Period => Some('.'),
2075            Semicolon => Some(';'),
2076            Slash => Some('/'),
2077            Caret => Some('^'),
2078            _ => None,
2079        }
2080    }
2081}
2082
2083/// 16x16x4 bytes icon
2084#[derive(Debug, Clone)]
2085#[repr(C)]
2086pub struct SmallWindowIconBytes {
2087    pub key: IconKey,
2088    pub rgba_bytes: U8Vec,
2089}
2090
2091/// 32x32x4 bytes icon
2092#[derive(Debug, Clone)]
2093#[repr(C)]
2094pub struct LargeWindowIconBytes {
2095    pub key: IconKey,
2096    pub rgba_bytes: U8Vec,
2097}
2098
2099// Window icon that usually appears in the top-left corner of the window
2100#[derive(Debug, Clone)]
2101#[repr(C, u8)]
2102pub enum WindowIcon {
2103    Small(SmallWindowIconBytes),
2104    /// 32x32x4 bytes icon
2105    Large(LargeWindowIconBytes),
2106}
2107
2108impl_option!(
2109    WindowIcon,
2110    OptionWindowIcon,
2111    copy = false,
2112    [Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Ord]
2113);
2114
2115impl WindowIcon {
2116    #[must_use] pub const fn get_key(&self) -> IconKey {
2117        match &self {
2118            Self::Small(SmallWindowIconBytes { key, .. })
2119            | Self::Large(LargeWindowIconBytes { key, .. }) => *key,
2120        }
2121    }
2122}
2123// -- Only compare the IconKey (for WindowIcon and TaskBarIcon)
2124
2125impl PartialEq for WindowIcon {
2126    fn eq(&self, rhs: &Self) -> bool {
2127        self.get_key() == rhs.get_key()
2128    }
2129}
2130
2131impl PartialOrd for WindowIcon {
2132    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2133        Some((self.get_key()).cmp(&rhs.get_key()))
2134    }
2135}
2136
2137impl Eq for WindowIcon {}
2138
2139impl Ord for WindowIcon {
2140    fn cmp(&self, rhs: &Self) -> Ordering {
2141        (self.get_key()).cmp(&rhs.get_key())
2142    }
2143}
2144
2145impl Hash for WindowIcon {
2146    fn hash<H>(&self, state: &mut H)
2147    where
2148        H: Hasher,
2149    {
2150        self.get_key().hash(state);
2151    }
2152}
2153
2154/// 256x256x4 bytes window icon
2155#[derive(Debug, Clone)]
2156#[repr(C)]
2157pub struct TaskBarIcon {
2158    pub key: IconKey,
2159    pub rgba_bytes: U8Vec,
2160}
2161
2162impl_option!(
2163    TaskBarIcon,
2164    OptionTaskBarIcon,
2165    copy = false,
2166    [Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Ord]
2167);
2168
2169impl PartialEq for TaskBarIcon {
2170    fn eq(&self, rhs: &Self) -> bool {
2171        self.key == rhs.key
2172    }
2173}
2174
2175impl PartialOrd for TaskBarIcon {
2176    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2177        Some((self.key).cmp(&rhs.key))
2178    }
2179}
2180
2181impl Eq for TaskBarIcon {}
2182
2183impl Ord for TaskBarIcon {
2184    fn cmp(&self, rhs: &Self) -> Ordering {
2185        (self.key).cmp(&rhs.key)
2186    }
2187}
2188
2189impl Hash for TaskBarIcon {
2190    fn hash<H>(&self, state: &mut H)
2191    where
2192        H: Hasher,
2193    {
2194        self.key.hash(state);
2195    }
2196}
2197
2198#[cfg(test)]
2199#[allow(clippy::float_cmp)] // exact-value assertions on hidpi scale factors
2200mod audit_tests {
2201    use super::*;
2202
2203    #[test]
2204    fn hidpi_factor_guards_zero_dpi() {
2205        // dpi == 0 must not produce a 0.0 scale factor (later divide-by-zero
2206        // in to_logical); it falls back to 96 DPI (scale 1.0).
2207        let ws = WindowSize { dpi: 0, ..WindowSize::default() };
2208        let factor = ws.get_hidpi_factor().inner.get();
2209        assert_eq!(factor, 1.0);
2210
2211        let ws2 = WindowSize { dpi: 192, ..WindowSize::default() };
2212        assert_eq!(ws2.get_hidpi_factor().inner.get(), 2.0);
2213    }
2214
2215    #[test]
2216    fn virtual_keycode_from_u32_roundtrips() {
2217        // A representative spread across the enum, including first/last.
2218        for vk in [
2219            VirtualKeyCode::Key1,
2220            VirtualKeyCode::A,
2221            VirtualKeyCode::Z,
2222            VirtualKeyCode::Left,
2223            VirtualKeyCode::Back,
2224            VirtualKeyCode::Delete,
2225            VirtualKeyCode::LControl,
2226            VirtualKeyCode::Cut,
2227        ] {
2228            assert_eq!(VirtualKeyCode::from_u32(vk as u32), Some(vk));
2229        }
2230        // Out of range -> None (no UB, no panic).
2231        assert_eq!(VirtualKeyCode::from_u32(10_000), None);
2232    }
2233}
2234
2235#[cfg(test)]
2236#[allow(clippy::float_cmp)] // exact-value assertions on saturating float->int conversions
2237mod autotest_generated {
2238    use alloc::{format, string::String, vec};
2239
2240    use super::*;
2241
2242    /// Highest valid `VirtualKeyCode` discriminant (`Cut`, the last declared variant).
2243    const LAST_VK: u32 = 162;
2244
2245    /// Deterministic, `no_std`-safe hasher so hash/eq consistency can be checked
2246    /// without pulling in `std::collections::hash_map::DefaultHasher`.
2247    #[derive(Default)]
2248    struct TestHasher(u64);
2249
2250    impl Hasher for TestHasher {
2251        fn write(&mut self, bytes: &[u8]) {
2252            for &b in bytes {
2253                self.0 = self.0.rotate_left(5) ^ u64::from(b);
2254            }
2255        }
2256        fn finish(&self) -> u64 {
2257            self.0
2258        }
2259    }
2260
2261    fn hash_of<T: Hash>(value: &T) -> u64 {
2262        let mut h = TestHasher::default();
2263        value.hash(&mut h);
2264        h.finish()
2265    }
2266
2267    fn keyboard_with(keys: &[VirtualKeyCode]) -> KeyboardState {
2268        KeyboardState {
2269            pressed_virtual_keycodes: keys.to_vec().into(),
2270            ..KeyboardState::default()
2271        }
2272    }
2273
2274    fn pair(key: &str, value: &str) -> AzStringPair {
2275        AzStringPair {
2276            key: key.into(),
2277            value: value.into(),
2278        }
2279    }
2280
2281    // ---------------------------------------------------------------------
2282    // Constructors: WindowId / IconKey (atomic counters)
2283    // ---------------------------------------------------------------------
2284
2285    #[test]
2286    fn window_id_new_is_unique_and_monotonic() {
2287        // The counter is process-global and shared with other tests in this
2288        // binary, so only *relative* properties may be asserted.
2289        let mut ids = BTreeSet::new();
2290        let mut prev = WindowId::new();
2291        assert!(ids.insert(prev));
2292        for _ in 0..1000 {
2293            let next = WindowId::new();
2294            assert!(next.id > prev.id, "WindowId counter must strictly increase");
2295            assert!(ids.insert(next), "WindowId::new() handed out a duplicate");
2296            prev = next;
2297        }
2298        // Default delegates to new(): two defaults are never the same window.
2299        assert_ne!(WindowId::default(), WindowId::default());
2300    }
2301
2302    #[test]
2303    fn icon_key_new_is_unique_and_monotonic() {
2304        let mut keys = BTreeSet::new();
2305        let mut prev = IconKey::new();
2306        assert!(keys.insert(prev));
2307        for _ in 0..1000 {
2308            let next = IconKey::new();
2309            assert!(
2310                next.icon_id > prev.icon_id,
2311                "IconKey counter must strictly increase"
2312            );
2313            assert!(keys.insert(next), "IconKey::new() handed out a duplicate");
2314            prev = next;
2315        }
2316        assert_ne!(IconKey::default(), IconKey::default());
2317    }
2318
2319    // ---------------------------------------------------------------------
2320    // RendererOptions + Vsync / Srgb / HwAcceleration predicates
2321    // ---------------------------------------------------------------------
2322
2323    #[test]
2324    fn renderer_options_new_preserves_every_combination() {
2325        let vsyncs = [Vsync::Enabled, Vsync::Disabled, Vsync::DontCare];
2326        let srgbs = [Srgb::Enabled, Srgb::Disabled, Srgb::DontCare];
2327        let accels = [
2328            HwAcceleration::Enabled,
2329            HwAcceleration::Disabled,
2330            HwAcceleration::DontCare,
2331        ];
2332        for v in vsyncs {
2333            for s in srgbs {
2334                for a in accels {
2335                    let o = RendererOptions::new(v, s, a);
2336                    assert_eq!(o.vsync, v);
2337                    assert_eq!(o.srgb, s);
2338                    assert_eq!(o.hw_accel, a);
2339                    // Constructed value must round-trip through equality/copy.
2340                    assert_eq!(o, RendererOptions::new(v, s, a));
2341                }
2342            }
2343        }
2344    }
2345
2346    #[test]
2347    fn renderer_options_default_does_not_force_cpu_rendering() {
2348        // Regression guard: hw_accel must be DontCare (auto), NOT Disabled -
2349        // Disabled silently forced every app into CPU rendering.
2350        let d = RendererOptions::default();
2351        assert_eq!(d.hw_accel, HwAcceleration::DontCare);
2352        assert!(!d.hw_accel.is_enabled());
2353        assert!(d.vsync.is_enabled());
2354        assert!(!d.srgb.is_enabled());
2355    }
2356
2357    #[test]
2358    fn tri_state_is_enabled_only_for_enabled_variant() {
2359        assert!(Vsync::Enabled.is_enabled());
2360        assert!(!Vsync::Disabled.is_enabled());
2361        assert!(!Vsync::DontCare.is_enabled());
2362
2363        assert!(Srgb::Enabled.is_enabled());
2364        assert!(!Srgb::Disabled.is_enabled());
2365        assert!(!Srgb::DontCare.is_enabled());
2366
2367        assert!(HwAcceleration::Enabled.is_enabled());
2368        assert!(!HwAcceleration::Disabled.is_enabled());
2369        assert!(!HwAcceleration::DontCare.is_enabled());
2370    }
2371
2372    // ---------------------------------------------------------------------
2373    // KeyboardState getters / predicates
2374    // ---------------------------------------------------------------------
2375
2376    #[test]
2377    fn keyboard_state_default_has_no_key_down() {
2378        let k = KeyboardState::default();
2379        assert!(!k.shift_down());
2380        assert!(!k.ctrl_down());
2381        assert!(!k.alt_down());
2382        assert!(!k.super_down());
2383        assert!(!k.primary_down());
2384        // Every single keycode reports "not down" on an empty state.
2385        for v in 0..=LAST_VK {
2386            let vk = VirtualKeyCode::from_u32(v).expect("discriminant in range");
2387            assert!(!k.is_key_down(vk));
2388        }
2389    }
2390
2391    #[test]
2392    fn keyboard_state_modifiers_accept_either_side() {
2393        for (left, right, probe) in [
2394            (
2395                VirtualKeyCode::LShift,
2396                VirtualKeyCode::RShift,
2397                KeyboardState::shift_down as fn(&KeyboardState) -> bool,
2398            ),
2399            (
2400                VirtualKeyCode::LControl,
2401                VirtualKeyCode::RControl,
2402                KeyboardState::ctrl_down as fn(&KeyboardState) -> bool,
2403            ),
2404            (
2405                VirtualKeyCode::LAlt,
2406                VirtualKeyCode::RAlt,
2407                KeyboardState::alt_down as fn(&KeyboardState) -> bool,
2408            ),
2409            (
2410                VirtualKeyCode::LWin,
2411                VirtualKeyCode::RWin,
2412                KeyboardState::super_down as fn(&KeyboardState) -> bool,
2413            ),
2414        ] {
2415            assert!(probe(&keyboard_with(&[left])), "left variant must register");
2416            assert!(
2417                probe(&keyboard_with(&[right])),
2418                "right variant must register"
2419            );
2420            assert!(probe(&keyboard_with(&[left, right])));
2421            // An unrelated key must not light up a modifier.
2422            assert!(!probe(&keyboard_with(&[VirtualKeyCode::A])));
2423        }
2424    }
2425
2426    #[test]
2427    fn keyboard_state_primary_down_follows_platform() {
2428        let ctrl = keyboard_with(&[VirtualKeyCode::LControl]);
2429        let cmd = keyboard_with(&[VirtualKeyCode::LWin]);
2430        if cfg!(target_os = "macos") {
2431            assert!(cmd.primary_down(), "Cmd (super) is PRIMARY on macOS");
2432            assert!(!ctrl.primary_down());
2433        } else {
2434            assert!(ctrl.primary_down(), "Ctrl is PRIMARY off macOS");
2435            assert!(!cmd.primary_down());
2436        }
2437        // On every platform, primary_down agrees with one of the two modifiers.
2438        for k in [&ctrl, &cmd, &KeyboardState::default()] {
2439            assert_eq!(
2440                k.primary_down(),
2441                if cfg!(target_os = "macos") {
2442                    k.super_down()
2443                } else {
2444                    k.ctrl_down()
2445                }
2446            );
2447        }
2448    }
2449
2450    #[test]
2451    fn is_key_down_handles_duplicates_and_large_state() {
2452        // Same key pressed many times (backends can push duplicates).
2453        let dup = keyboard_with(&[VirtualKeyCode::S; 512]);
2454        assert!(dup.is_key_down(VirtualKeyCode::S));
2455        assert!(!dup.is_key_down(VirtualKeyCode::A));
2456
2457        // Every key held down at once: no panic, all report true.
2458        let all: alloc::vec::Vec<VirtualKeyCode> = (0..=LAST_VK)
2459            .map(|v| VirtualKeyCode::from_u32(v).expect("discriminant in range"))
2460            .collect();
2461        let everything = keyboard_with(&all);
2462        for vk in &all {
2463            assert!(everything.is_key_down(*vk));
2464        }
2465        assert!(everything.shift_down() && everything.ctrl_down());
2466        assert!(everything.alt_down() && everything.super_down());
2467    }
2468
2469    // ---------------------------------------------------------------------
2470    // AcceleratorKey / matches_accelerator
2471    // ---------------------------------------------------------------------
2472
2473    #[test]
2474    fn empty_chord_matches_trivially() {
2475        // Documented: "An empty chord matches trivially."
2476        assert!(KeyboardState::default().matches_accelerator(&[]));
2477        assert!(keyboard_with(&[VirtualKeyCode::A]).matches_accelerator(&[]));
2478    }
2479
2480    #[test]
2481    fn matches_accelerator_requires_every_entry() {
2482        let state = keyboard_with(&[
2483            VirtualKeyCode::LControl,
2484            VirtualKeyCode::LShift,
2485            VirtualKeyCode::S,
2486        ]);
2487        assert!(state.matches_accelerator(&[
2488            AcceleratorKey::Ctrl,
2489            AcceleratorKey::Shift,
2490            AcceleratorKey::Key(VirtualKeyCode::S),
2491        ]));
2492        // One missing entry (Alt) is enough to reject the whole chord.
2493        assert!(!state.matches_accelerator(&[
2494            AcceleratorKey::Ctrl,
2495            AcceleratorKey::Alt,
2496            AcceleratorKey::Key(VirtualKeyCode::S),
2497        ]));
2498        // Wrong key, right modifiers.
2499        assert!(!state.matches_accelerator(&[
2500            AcceleratorKey::Ctrl,
2501            AcceleratorKey::Key(VirtualKeyCode::Q),
2502        ]));
2503        // Order must not matter.
2504        assert!(state.matches_accelerator(&[
2505            AcceleratorKey::Key(VirtualKeyCode::S),
2506            AcceleratorKey::Shift,
2507            AcceleratorKey::Ctrl,
2508        ]));
2509    }
2510
2511    #[test]
2512    fn matches_accelerator_survives_huge_chord() {
2513        // A pathologically long chord must terminate (linear scan, no recursion).
2514        let state = keyboard_with(&[VirtualKeyCode::LShift]);
2515        let long_ok = vec![AcceleratorKey::Shift; 10_000];
2516        assert!(state.matches_accelerator(&long_ok));
2517
2518        // 10k satisfiable entries with a single unsatisfiable one at the very end:
2519        // `all()` must still reach it and return false.
2520        let mut long_bad = vec![AcceleratorKey::Shift; 10_000];
2521        long_bad.push(AcceleratorKey::Ctrl);
2522        assert!(!state.matches_accelerator(&long_bad));
2523    }
2524
2525    #[test]
2526    fn accelerator_key_matches_each_variant() {
2527        let empty = KeyboardState::default();
2528        for a in [
2529            AcceleratorKey::Ctrl,
2530            AcceleratorKey::Alt,
2531            AcceleratorKey::Shift,
2532            AcceleratorKey::Key(VirtualKeyCode::A),
2533        ] {
2534            assert!(!a.matches(&empty), "nothing matches an empty keyboard state");
2535        }
2536        assert!(AcceleratorKey::Ctrl.matches(&keyboard_with(&[VirtualKeyCode::RControl])));
2537        assert!(AcceleratorKey::Alt.matches(&keyboard_with(&[VirtualKeyCode::RAlt])));
2538        assert!(AcceleratorKey::Shift.matches(&keyboard_with(&[VirtualKeyCode::RShift])));
2539        assert!(
2540            AcceleratorKey::Key(VirtualKeyCode::F24).matches(&keyboard_with(&[VirtualKeyCode::F24]))
2541        );
2542        // Modifier accelerators are NOT satisfied by the letter of the same name.
2543        assert!(!AcceleratorKey::Ctrl.matches(&keyboard_with(&[VirtualKeyCode::C])));
2544    }
2545
2546    // ---------------------------------------------------------------------
2547    // MouseState / MouseButtonState
2548    // ---------------------------------------------------------------------
2549
2550    #[test]
2551    fn mouse_state_matches_context_button() {
2552        let base = MouseState::default();
2553        assert!(!base.matches(&ContextMenuMouseButton::Left));
2554        assert!(!base.matches(&ContextMenuMouseButton::Right));
2555        assert!(!base.matches(&ContextMenuMouseButton::Middle));
2556
2557        for (ctx, ms) in [
2558            (
2559                ContextMenuMouseButton::Left,
2560                MouseState {
2561                    left_down: true,
2562                    ..MouseState::default()
2563                },
2564            ),
2565            (
2566                ContextMenuMouseButton::Right,
2567                MouseState {
2568                    right_down: true,
2569                    ..MouseState::default()
2570                },
2571            ),
2572            (
2573                ContextMenuMouseButton::Middle,
2574                MouseState {
2575                    middle_down: true,
2576                    ..MouseState::default()
2577                },
2578            ),
2579        ] {
2580            assert!(ms.matches(&ctx), "{ctx:?} must match its own button");
2581            // ...and only its own button.
2582            let others = [
2583                ContextMenuMouseButton::Left,
2584                ContextMenuMouseButton::Right,
2585                ContextMenuMouseButton::Middle,
2586            ];
2587            for other in others {
2588                assert_eq!(ms.matches(&other), other == ctx);
2589            }
2590        }
2591    }
2592
2593    #[test]
2594    fn mouse_down_and_button_state_agree_for_all_8_combinations() {
2595        for bits in 0u8..8 {
2596            let (l, r, m) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0);
2597            let ms = MouseState {
2598                left_down: l,
2599                right_down: r,
2600                middle_down: m,
2601                ..MouseState::default()
2602            };
2603            assert_eq!(ms.mouse_down(), l || r || m);
2604
2605            let snapshot = ms.button_state();
2606            assert_eq!(snapshot.left_down, l);
2607            assert_eq!(snapshot.right_down, r);
2608            assert_eq!(snapshot.middle_down, m);
2609            // any_down is exactly mouse_down, and the From impl is the same snapshot.
2610            assert_eq!(snapshot.any_down(), ms.mouse_down());
2611            assert_eq!(crate::events::MouseButtonState::from(&ms), snapshot);
2612        }
2613        // Default MouseState has no button held.
2614        assert!(!MouseState::default().mouse_down());
2615        assert!(!MouseState::default().button_state().any_down());
2616    }
2617
2618    // ---------------------------------------------------------------------
2619    // process_system_scroll (numeric)
2620    // ---------------------------------------------------------------------
2621
2622    #[test]
2623    fn process_system_scroll_zero_and_negative_zero_consume_nothing() {
2624        let r = process_system_scroll(LogicalPosition::zero(), false);
2625        assert_eq!(r.scrolled_nodes, 0);
2626        assert_eq!(r.remaining_delta, LogicalPosition::zero());
2627        assert!(!r.hit_scrollbar);
2628
2629        // -0.0 == 0.0 under IEEE-754, so a negative-zero delta must also be a no-op.
2630        let neg_zero = process_system_scroll(LogicalPosition::new(-0.0, -0.0), true);
2631        assert_eq!(neg_zero.scrolled_nodes, 0);
2632        assert!(neg_zero.hit_scrollbar, "hit_scrollbar is echoed verbatim");
2633    }
2634
2635    #[test]
2636    fn process_system_scroll_counts_any_nonzero_axis() {
2637        for delta in [
2638            LogicalPosition::new(1.0, 0.0),
2639            LogicalPosition::new(0.0, -1.0),
2640            LogicalPosition::new(-3.5, 7.25),
2641            LogicalPosition::new(f32::MIN, 0.0),
2642            LogicalPosition::new(0.0, f32::MAX),
2643            LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
2644            LogicalPosition::new(f32::MIN_POSITIVE, 0.0),
2645        ] {
2646            let r = process_system_scroll(delta, false);
2647            assert_eq!(r.scrolled_nodes, 1, "{delta:?} must count as consumed");
2648            // Overscroll is never reported by this helper.
2649            assert_eq!(r.remaining_delta, LogicalPosition::zero());
2650        }
2651    }
2652
2653    #[test]
2654    fn process_system_scroll_does_not_panic_on_nan() {
2655        // NaN != 0.0 is `true`, so a NaN delta is currently treated as consumed.
2656        // The contract asserted here is only "terminates, no panic, bounded count".
2657        for delta in [
2658            LogicalPosition::new(f32::NAN, 0.0),
2659            LogicalPosition::new(0.0, f32::NAN),
2660            LogicalPosition::new(f32::NAN, f32::NAN),
2661        ] {
2662            let r = process_system_scroll(delta, true);
2663            assert!(r.scrolled_nodes <= 1);
2664            assert_eq!(r.remaining_delta, LogicalPosition::zero());
2665            assert!(r.hit_scrollbar);
2666        }
2667    }
2668
2669    #[test]
2670    fn scroll_result_default_is_inert() {
2671        let d = ScrollResult::default();
2672        assert_eq!(d.scrolled_nodes, 0);
2673        assert_eq!(d.remaining_delta, LogicalPosition::zero());
2674        assert!(!d.hit_scrollbar);
2675    }
2676
2677    // ---------------------------------------------------------------------
2678    // CursorPosition
2679    // ---------------------------------------------------------------------
2680
2681    #[test]
2682    fn cursor_position_get_position_only_inside_window() {
2683        let p = LogicalPosition::new(12.0, 34.0);
2684        assert_eq!(CursorPosition::InWindow(p).get_position(), Some(p));
2685        assert_eq!(CursorPosition::OutOfWindow(p).get_position(), None);
2686        assert_eq!(CursorPosition::Uninitialized.get_position(), None);
2687        // Default (as used by MouseState::default) is Uninitialized.
2688        assert_eq!(CursorPosition::default(), CursorPosition::Uninitialized);
2689        assert_eq!(CursorPosition::default().get_position(), None);
2690    }
2691
2692    #[test]
2693    fn cursor_position_is_inside_window_agrees_with_get_position() {
2694        for c in [
2695            CursorPosition::Uninitialized,
2696            CursorPosition::InWindow(LogicalPosition::zero()),
2697            CursorPosition::OutOfWindow(LogicalPosition::zero()),
2698            CursorPosition::InWindow(LogicalPosition::new(f32::MIN, f32::MAX)),
2699            CursorPosition::OutOfWindow(LogicalPosition::new(f32::INFINITY, f32::NAN)),
2700        ] {
2701            assert_eq!(c.is_inside_window(), c.get_position().is_some());
2702        }
2703        // Extreme / non-finite coordinates are passed through, not sanitized.
2704        let nan_pos = CursorPosition::InWindow(LogicalPosition::new(f32::NAN, f32::INFINITY));
2705        assert!(nan_pos.is_inside_window());
2706        let got = nan_pos.get_position().expect("InWindow always yields a position");
2707        assert!(got.x.is_nan());
2708        assert!(got.y.is_infinite());
2709    }
2710
2711    // ---------------------------------------------------------------------
2712    // MonitorId constructors
2713    // ---------------------------------------------------------------------
2714
2715    #[test]
2716    fn monitor_id_constructors_preserve_fields_at_extremes() {
2717        assert_eq!(MonitorId::PRIMARY, MonitorId { index: 0, hash: 0 });
2718        assert_eq!(MonitorId::new(0), MonitorId::PRIMARY);
2719
2720        for index in [0usize, 1, 42, usize::MAX] {
2721            let m = MonitorId::new(index);
2722            assert_eq!(m.index, index);
2723            assert_eq!(m.hash, 0, "new() documents hash == 0");
2724
2725            for hash in [0u64, 1, u64::MAX] {
2726                let m = MonitorId::from_index_and_hash(index, hash);
2727                assert_eq!(m.index, index);
2728                assert_eq!(m.hash, hash);
2729            }
2730        }
2731        // index and hash are independent coordinates of identity.
2732        assert_ne!(MonitorId::new(1), MonitorId::new(2));
2733        assert_ne!(
2734            MonitorId::from_index_and_hash(1, 7),
2735            MonitorId::from_index_and_hash(1, 8)
2736        );
2737    }
2738
2739    #[test]
2740    fn monitor_id_from_properties_is_stable_and_index_independent() {
2741        let pos = LayoutPoint::new(-1920, 0);
2742        let size = LayoutSize::new(2560, 1440);
2743
2744        let a = MonitorId::from_properties(0, "HDMI-1", pos, size);
2745        let b = MonitorId::from_properties(0, "HDMI-1", pos, size);
2746        assert_eq!(a, b, "hash must be stable across calls (persistable)");
2747
2748        // The hash intentionally covers only the properties, not the runtime index.
2749        let reindexed = MonitorId::from_properties(7, "HDMI-1", pos, size);
2750        assert_eq!(reindexed.hash, a.hash);
2751        assert_eq!(reindexed.index, 7);
2752        assert_ne!(reindexed, a, "index is still part of identity");
2753    }
2754
2755    #[test]
2756    fn monitor_id_from_properties_is_sensitive_to_each_property() {
2757        let pos = LayoutPoint::new(0, 0);
2758        let size = LayoutSize::new(1920, 1080);
2759        let base = MonitorId::from_properties(0, "DP-1", pos, size);
2760
2761        // Changing any single property must change the hash.
2762        assert_ne!(
2763            base.hash,
2764            MonitorId::from_properties(0, "DP-2", pos, size).hash
2765        );
2766        assert_ne!(
2767            base.hash,
2768            MonitorId::from_properties(0, "DP-1", LayoutPoint::new(1, 0), size).hash
2769        );
2770        assert_ne!(
2771            base.hash,
2772            MonitorId::from_properties(0, "DP-1", LayoutPoint::new(0, 1), size).hash
2773        );
2774        assert_ne!(
2775            base.hash,
2776            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1921, 1080)).hash
2777        );
2778        assert_ne!(
2779            base.hash,
2780            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1920, 1081)).hash
2781        );
2782        // A swapped width/height is a different monitor, not the same one.
2783        assert_ne!(
2784            base.hash,
2785            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1080, 1920)).hash
2786        );
2787    }
2788
2789    #[test]
2790    fn monitor_id_from_properties_handles_hostile_inputs() {
2791        let size = LayoutSize::new(isize::MAX, isize::MIN);
2792        let pos = LayoutPoint::new(isize::MIN, isize::MAX);
2793
2794        // Empty / whitespace / unicode / NUL-containing names must not panic.
2795        for name in ["", "   ", "\t\n", "\u{1F600}", "e\u{301}", "é", "a\0b"] {
2796            let m = MonitorId::from_properties(3, name, pos, size);
2797            assert_eq!(m.index, 3);
2798            // Same input -> same hash, even at isize extremes.
2799            assert_eq!(m, MonitorId::from_properties(3, name, pos, size));
2800        }
2801
2802        // Byte-exact name comparison: combining-mark and precomposed forms differ.
2803        assert_ne!(
2804            MonitorId::from_properties(0, "e\u{301}", pos, size).hash,
2805            MonitorId::from_properties(0, "é", pos, size).hash
2806        );
2807
2808        // A 1M-char monitor name must terminate quickly (FNV-1a is linear).
2809        let huge = "x".repeat(1_000_000);
2810        let h1 = MonitorId::from_properties(0, &huge, LayoutPoint::zero(), LayoutSize::zero());
2811        let h2 = MonitorId::from_properties(0, &huge, LayoutPoint::zero(), LayoutSize::zero());
2812        assert_eq!(h1, h2);
2813        assert_ne!(
2814            h1.hash,
2815            MonitorId::from_properties(0, "x", LayoutPoint::zero(), LayoutSize::zero()).hash
2816        );
2817    }
2818
2819    // ---------------------------------------------------------------------
2820    // WindowFlags predicates / getters
2821    // ---------------------------------------------------------------------
2822
2823    #[test]
2824    fn window_flags_type_predicates_are_mutually_exclusive() {
2825        for (ty, menu, tooltip, dialog) in [
2826            (WindowType::Normal, false, false, false),
2827            (WindowType::Menu, true, false, false),
2828            (WindowType::Tooltip, false, true, false),
2829            (WindowType::Dialog, false, false, true),
2830        ] {
2831            let f = WindowFlags {
2832                window_type: ty,
2833                ..WindowFlags::default()
2834            };
2835            assert_eq!(f.is_menu_window(), menu);
2836            assert_eq!(f.is_tooltip_window(), tooltip);
2837            assert_eq!(f.is_dialog_window(), dialog);
2838            // At most one classification can ever be true at once.
2839            let count = u8::from(f.is_menu_window())
2840                + u8::from(f.is_tooltip_window())
2841                + u8::from(f.is_dialog_window());
2842            assert!(count <= 1);
2843        }
2844    }
2845
2846    #[test]
2847    fn window_flags_bool_getters_mirror_their_fields() {
2848        // Default: focused, no close request, no CSD.
2849        let d = WindowFlags::default();
2850        assert!(d.window_has_focus());
2851        assert!(!d.is_close_requested());
2852        assert!(!d.has_csd());
2853        assert_eq!(
2854            d.use_native_menus(),
2855            cfg!(any(target_os = "windows", target_os = "macos"))
2856        );
2857        assert_eq!(
2858            d.use_native_context_menus(),
2859            cfg!(any(target_os = "windows", target_os = "macos"))
2860        );
2861
2862        // Every getter is a pure mirror of its field, in both states.
2863        for b in [false, true] {
2864            let f = WindowFlags {
2865                has_focus: b,
2866                close_requested: b,
2867                has_decorations: b,
2868                use_native_menus: b,
2869                use_native_context_menus: b,
2870                ..WindowFlags::default()
2871            };
2872            assert_eq!(f.window_has_focus(), b);
2873            assert_eq!(f.is_close_requested(), b);
2874            assert_eq!(f.has_csd(), b);
2875            assert_eq!(f.use_native_menus(), b);
2876            assert_eq!(f.use_native_context_menus(), b);
2877        }
2878    }
2879
2880    // ---------------------------------------------------------------------
2881    // StringPairVec: get_key / get_key_mut / insert_kv
2882    // ---------------------------------------------------------------------
2883
2884    #[test]
2885    fn get_key_on_empty_vec_is_none() {
2886        let empty = StringPairVec::new();
2887        assert!(empty.get_key("").is_none());
2888        assert!(empty.get_key("anything").is_none());
2889
2890        let mut empty_mut = StringPairVec::new();
2891        assert!(empty_mut.get_key_mut("").is_none());
2892        assert!(empty_mut.get_key_mut("anything").is_none());
2893    }
2894
2895    #[test]
2896    fn get_key_valid_minimal_and_missing() {
2897        let v = StringPairVec::from_vec(vec![pair("WM_CLASS", "azul")]);
2898        assert_eq!(
2899            v.get_key("WM_CLASS").map(AzString::as_str),
2900            Some("azul"),
2901            "positive control"
2902        );
2903        assert!(v.get_key("wm_class").is_none(), "lookup is case-sensitive");
2904        assert!(v.get_key("WM_CLAS").is_none());
2905        assert!(v.get_key("WM_CLASS ").is_none(), "no trimming is performed");
2906        assert!(v.get_key(" WM_CLASS").is_none());
2907        assert!(v.get_key("WM_CLASS;garbage").is_none());
2908    }
2909
2910    #[test]
2911    fn get_key_handles_garbage_whitespace_and_boundary_numbers() {
2912        let v = StringPairVec::from_vec(vec![
2913            pair("", "empty-key"),
2914            pair("   ", "spaces"),
2915            pair("\t\n", "tabs"),
2916            pair("0", "zero"),
2917            pair("-0", "neg-zero"),
2918            pair("9223372036854775807", "i64-max"),
2919            pair("NaN", "nan"),
2920            pair("inf", "inf"),
2921        ]);
2922
2923        // Empty and whitespace-only keys are ordinary keys - looked up verbatim.
2924        assert_eq!(v.get_key("").map(AzString::as_str), Some("empty-key"));
2925        assert_eq!(v.get_key("   ").map(AzString::as_str), Some("spaces"));
2926        assert_eq!(v.get_key("\t\n").map(AzString::as_str), Some("tabs"));
2927
2928        // Numeric-looking keys are compared as strings: "0" and "-0" are distinct.
2929        assert_eq!(v.get_key("0").map(AzString::as_str), Some("zero"));
2930        assert_eq!(v.get_key("-0").map(AzString::as_str), Some("neg-zero"));
2931        assert_eq!(
2932            v.get_key("9223372036854775807").map(AzString::as_str),
2933            Some("i64-max")
2934        );
2935        assert_eq!(v.get_key("NaN").map(AzString::as_str), Some("nan"));
2936        assert_eq!(v.get_key("inf").map(AzString::as_str), Some("inf"));
2937        assert!(v.get_key("nan").is_none());
2938
2939        // Random non-grammar bytes / control chars / deep bracket nesting: None, no panic.
2940        assert!(v.get_key("\u{0}\u{1}\u{7f}\\x\"';--").is_none());
2941        assert!(v.get_key(&"[".repeat(10_000)).is_none());
2942        assert!(v.get_key(&"{\"a\":".repeat(10_000)).is_none());
2943    }
2944
2945    #[test]
2946    fn get_key_handles_unicode_without_panicking() {
2947        let v = StringPairVec::from_vec(vec![
2948            pair("\u{1F600}", "grin"),
2949            pair("é", "precomposed"),
2950            pair("日本語", "jp"),
2951        ]);
2952        assert_eq!(v.get_key("\u{1F600}").map(AzString::as_str), Some("grin"));
2953        assert_eq!(v.get_key("日本語").map(AzString::as_str), Some("jp"));
2954        // No unicode normalization: decomposed "e" + combining acute != "é".
2955        assert_eq!(v.get_key("é").map(AzString::as_str), Some("precomposed"));
2956        assert!(v.get_key("e\u{301}").is_none());
2957        // A prefix of a multi-byte key must not match (no byte-slicing bugs).
2958        assert!(v.get_key("日本").is_none());
2959    }
2960
2961    #[test]
2962    fn get_key_handles_extremely_long_input() {
2963        let huge = "k".repeat(1_000_000);
2964        let mut v = StringPairVec::from_vec(vec![pair("short", "1")]);
2965
2966        // Searching for a 1M-char key that is not present: linear, terminates.
2967        assert!(v.get_key(&huge).is_none());
2968
2969        // ...and one that IS present.
2970        v.push(AzStringPair {
2971            key: huge.as_str().into(),
2972            value: "big".into(),
2973        });
2974        assert_eq!(v.get_key(&huge).map(AzString::as_str), Some("big"));
2975        // Off-by-one on a 1M-char key must not match.
2976        assert!(v.get_key(&"k".repeat(999_999)).is_none());
2977        assert!(v.get_key(&"k".repeat(1_000_001)).is_none());
2978    }
2979
2980    #[test]
2981    fn get_key_returns_first_of_duplicate_keys() {
2982        let v = StringPairVec::from_vec(vec![
2983            pair("dup", "first"),
2984            pair("dup", "second"),
2985            pair("dup", "third"),
2986        ]);
2987        assert_eq!(v.get_key("dup").map(AzString::as_str), Some("first"));
2988    }
2989
2990    #[test]
2991    fn get_key_mut_mutates_in_place() {
2992        let mut v = StringPairVec::from_vec(vec![pair("a", "1"), pair("b", "2")]);
2993        {
2994            let entry = v.get_key_mut("b").expect("b is present");
2995            entry.value = "changed".into();
2996        }
2997        assert_eq!(v.get_key("b").map(AzString::as_str), Some("changed"));
2998        assert_eq!(v.get_key("a").map(AzString::as_str), Some("1"));
2999        assert!(v.get_key_mut("missing").is_none());
3000        assert_eq!(v.len(), 2, "get_key_mut must not add entries");
3001
3002        // Mutating the KEY through get_key_mut is possible and re-targets lookups.
3003        {
3004            let entry = v.get_key_mut("a").expect("a is present");
3005            entry.key = "z".into();
3006        }
3007        assert!(v.get_key("a").is_none());
3008        assert_eq!(v.get_key("z").map(AzString::as_str), Some("1"));
3009    }
3010
3011    #[test]
3012    fn insert_kv_updates_existing_and_appends_new() {
3013        let mut v = StringPairVec::new();
3014        v.insert_kv("k", "v1");
3015        assert_eq!(v.len(), 1);
3016        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v1"));
3017
3018        // Re-inserting the same key overwrites in place instead of appending.
3019        v.insert_kv("k", "v2");
3020        assert_eq!(v.len(), 1, "insert_kv must not duplicate an existing key");
3021        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v2"));
3022
3023        // A different key appends.
3024        v.insert_kv("other", "x");
3025        assert_eq!(v.len(), 2);
3026        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v2"));
3027        assert_eq!(v.get_key("other").map(AzString::as_str), Some("x"));
3028
3029        // Repeated inserts of the same key never grow the vec.
3030        for i in 0..100 {
3031            v.insert_kv(String::from("k"), format!("gen{i}"));
3032        }
3033        assert_eq!(v.len(), 2);
3034        assert_eq!(v.get_key("k").map(AzString::as_str), Some("gen99"));
3035    }
3036
3037    #[test]
3038    fn insert_kv_accepts_hostile_keys_and_values() {
3039        let mut v = StringPairVec::new();
3040        v.insert_kv("", "");
3041        assert_eq!(v.len(), 1);
3042        assert_eq!(v.get_key("").map(AzString::as_str), Some(""));
3043
3044        v.insert_kv("\u{1F600}", "😀");
3045        assert_eq!(v.get_key("\u{1F600}").map(AzString::as_str), Some("😀"));
3046
3047        v.insert_kv("   ", "\t\n");
3048        assert_eq!(v.get_key("   ").map(AzString::as_str), Some("\t\n"));
3049
3050        // Very long key + value: no hang, and the update path still finds it.
3051        let huge_key = "K".repeat(100_000);
3052        let huge_val = "V".repeat(100_000);
3053        v.insert_kv(huge_key.clone(), huge_val.clone());
3054        let before = v.len();
3055        assert_eq!(
3056            v.get_key(&huge_key).map(AzString::as_str),
3057            Some(huge_val.as_str())
3058        );
3059        v.insert_kv(huge_key.clone(), String::from("small"));
3060        assert_eq!(v.len(), before, "long key must hit the update path");
3061        assert_eq!(v.get_key(&huge_key).map(AzString::as_str), Some("small"));
3062    }
3063
3064    #[test]
3065    fn insert_kv_only_updates_the_first_of_pre_existing_duplicates() {
3066        // Duplicates can only arrive via push()/from_vec(); insert_kv updates the
3067        // first match (get_key_mut semantics) and leaves the shadowed one stale.
3068        let mut v = StringPairVec::from_vec(vec![pair("dup", "first"), pair("dup", "second")]);
3069        v.insert_kv("dup", "updated");
3070        assert_eq!(v.len(), 2, "no new entry is appended");
3071        assert_eq!(v.get_key("dup").map(AzString::as_str), Some("updated"));
3072        assert_eq!(
3073            v.get(1).expect("second entry still present").value.as_str(),
3074            "second",
3075            "the shadowed duplicate is left untouched"
3076        );
3077    }
3078
3079    // ---------------------------------------------------------------------
3080    // WindowSize getters (numeric saturation)
3081    // ---------------------------------------------------------------------
3082
3083    #[test]
3084    fn window_size_get_logical_size_is_the_identity() {
3085        for dims in [
3086            LogicalSize::zero(),
3087            LogicalSize::new(640.0, 480.0),
3088            LogicalSize::new(-1.0, -2.0),
3089            LogicalSize::new(f32::MAX, f32::MIN),
3090            LogicalSize::new(f32::INFINITY, f32::MIN_POSITIVE),
3091        ] {
3092            let ws = WindowSize {
3093                dimensions: dims,
3094                ..WindowSize::default()
3095            };
3096            assert_eq!(ws.get_logical_size(), dims);
3097        }
3098        // Default is the documented 640x480 @ 96 DPI.
3099        let d = WindowSize::default();
3100        assert_eq!(d.get_logical_size(), LogicalSize::new(640.0, 480.0));
3101        assert_eq!(d.dpi, 96);
3102    }
3103
3104    #[test]
3105    fn window_size_get_layout_size_rounds_half_away_from_zero() {
3106        for (w, h, ew, eh) in [
3107            (0.0f32, 0.0f32, 0isize, 0isize),
3108            (640.0, 480.0, 640, 480),
3109            (640.4, 480.4, 640, 480),
3110            (640.6, 480.6, 641, 481),
3111            (640.5, 639.5, 641, 640),
3112            (-0.5, -1.5, -1, -2),
3113        ] {
3114            let ws = WindowSize {
3115                dimensions: LogicalSize::new(w, h),
3116                ..WindowSize::default()
3117            };
3118            assert_eq!(ws.get_layout_size(), LayoutSize::new(ew, eh), "{w}x{h}");
3119        }
3120    }
3121
3122    #[test]
3123    fn window_size_get_layout_size_saturates_on_non_finite() {
3124        // `as isize` saturates: NaN -> 0, +inf -> isize::MAX, -inf -> isize::MIN.
3125        let nan = WindowSize {
3126            dimensions: LogicalSize::new(f32::NAN, f32::NAN),
3127            ..WindowSize::default()
3128        };
3129        assert_eq!(nan.get_layout_size(), LayoutSize::new(0, 0));
3130
3131        let inf = WindowSize {
3132            dimensions: LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
3133            ..WindowSize::default()
3134        };
3135        assert_eq!(
3136            inf.get_layout_size(),
3137            LayoutSize::new(isize::MAX, isize::MIN)
3138        );
3139
3140        let max = WindowSize {
3141            dimensions: LogicalSize::new(f32::MAX, f32::MIN),
3142            ..WindowSize::default()
3143        };
3144        let ls = max.get_layout_size();
3145        assert!(ls.width > 0 && ls.height < 0, "sign is preserved: {ls:?}");
3146    }
3147
3148    #[test]
3149    fn window_size_get_physical_size_saturates_instead_of_wrapping() {
3150        // Negative logical sizes clamp to 0 (u32 cast drops the sign).
3151        let neg = WindowSize {
3152            dimensions: LogicalSize::new(-100.0, -0.4),
3153            ..WindowSize::default()
3154        };
3155        assert_eq!(neg.get_physical_size(), PhysicalSize::new(0, 0));
3156
3157        // NaN -> 0, +inf -> u32::MAX (saturating float->int cast, no UB).
3158        let nan = WindowSize {
3159            dimensions: LogicalSize::new(f32::NAN, f32::INFINITY),
3160            ..WindowSize::default()
3161        };
3162        assert_eq!(nan.get_physical_size(), PhysicalSize::new(0, u32::MAX));
3163
3164        // f32::MAX at 4x scale overflows u32 -> saturates, never wraps to a small value.
3165        let huge = WindowSize {
3166            dimensions: LogicalSize::new(f32::MAX, f32::MAX),
3167            dpi: 384,
3168            ..WindowSize::default()
3169        };
3170        assert_eq!(
3171            huge.get_physical_size(),
3172            PhysicalSize::new(u32::MAX, u32::MAX)
3173        );
3174
3175        // The normal path: 96 DPI is 1:1, 192 DPI doubles.
3176        let normal = WindowSize::default();
3177        assert_eq!(normal.get_physical_size(), PhysicalSize::new(640, 480));
3178        let retina = WindowSize {
3179            dpi: 192,
3180            ..WindowSize::default()
3181        };
3182        assert_eq!(retina.get_physical_size(), PhysicalSize::new(1280, 960));
3183    }
3184
3185    #[test]
3186    fn window_size_get_hidpi_factor_is_never_zero_or_negative() {
3187        // A 0.0 factor would divide-by-zero in to_logical(); the getter guards dpi == 0.
3188        for dpi in [
3189            0u32,
3190            1,
3191            47,
3192            48,
3193            95,
3194            96,
3195            97,
3196            120,
3197            144,
3198            192,
3199            384,
3200            u32::from(u16::MAX),
3201            u32::MAX,
3202        ] {
3203            let ws = WindowSize {
3204                dpi,
3205                ..WindowSize::default()
3206            };
3207            let f = ws.get_hidpi_factor().inner.get();
3208            assert!(
3209                f.is_finite() && f > 0.0,
3210                "dpi {dpi} produced a non-positive / non-finite scale factor: {f}"
3211            );
3212        }
3213
3214        // Exactly representable factors must be exact (no quantization drift).
3215        for (dpi, expected) in [(0u32, 1.0f32), (96, 1.0), (144, 1.5), (192, 2.0), (384, 4.0)] {
3216            let ws = WindowSize {
3217                dpi,
3218                ..WindowSize::default()
3219            };
3220            assert_eq!(ws.get_hidpi_factor().inner.get(), expected, "dpi {dpi}");
3221        }
3222
3223        // Non-representable factors stay within FloatValue's 1/1000 quantization.
3224        let odd = WindowSize {
3225            dpi: 100,
3226            ..WindowSize::default()
3227        };
3228        let f = odd.get_hidpi_factor().inner.get();
3229        assert!((f - 100.0 / 96.0).abs() < 0.002, "dpi 100 -> {f}");
3230    }
3231
3232    // ---------------------------------------------------------------------
3233    // VirtualKeyCode: from_u32 / get_lowercase
3234    // ---------------------------------------------------------------------
3235
3236    #[test]
3237    fn virtual_keycode_from_u32_roundtrips_every_discriminant() {
3238        for v in 0..=LAST_VK {
3239            let vk = VirtualKeyCode::from_u32(v)
3240                .unwrap_or_else(|| panic!("discriminant {v} is missing from the from_u32 table"));
3241            assert_eq!(vk as u32, v, "from_u32({v}) does not round-trip");
3242        }
3243        // First and last declared variants anchor the table.
3244        assert_eq!(VirtualKeyCode::Key1 as u32, 0);
3245        assert_eq!(VirtualKeyCode::Cut as u32, LAST_VK);
3246    }
3247
3248    #[test]
3249    fn virtual_keycode_from_u32_rejects_out_of_range() {
3250        for v in [
3251            LAST_VK + 1,
3252            LAST_VK + 2,
3253            255,
3254            256,
3255            1024,
3256            i32::MAX as u32,
3257            u32::MAX - 1,
3258            u32::MAX,
3259        ] {
3260            assert_eq!(
3261                VirtualKeyCode::from_u32(v),
3262                None,
3263                "{v} must not decode to a keycode"
3264            );
3265        }
3266    }
3267
3268    #[test]
3269    fn virtual_keycode_get_lowercase_never_panics_and_maps_letters_and_digits() {
3270        // Exhaustive: no keycode may panic, and any produced char is ASCII.
3271        for v in 0..=LAST_VK {
3272            let vk = VirtualKeyCode::from_u32(v).expect("discriminant in range");
3273            if let Some(c) = vk.get_lowercase() {
3274                assert!(c.is_ascii(), "{vk:?} produced a non-ASCII char {c:?}");
3275                assert!(!c.is_ascii_uppercase(), "{vk:?} must yield lowercase");
3276            }
3277        }
3278
3279        // Letters A..Z are discriminants 10..=35 and map to 'a'..='z'.
3280        for (i, expected) in ('a'..='z').enumerate() {
3281            let vk = VirtualKeyCode::from_u32(10 + i as u32).expect("letter range");
3282            assert_eq!(vk.get_lowercase(), Some(expected));
3283        }
3284
3285        // Digits: both the top row and the numpad map to the same char.
3286        for (top, pad, c) in [
3287            (VirtualKeyCode::Key0, VirtualKeyCode::Numpad0, '0'),
3288            (VirtualKeyCode::Key1, VirtualKeyCode::Numpad1, '1'),
3289            (VirtualKeyCode::Key5, VirtualKeyCode::Numpad5, '5'),
3290            (VirtualKeyCode::Key9, VirtualKeyCode::Numpad9, '9'),
3291        ] {
3292            assert_eq!(top.get_lowercase(), Some(c));
3293            assert_eq!(pad.get_lowercase(), Some(c));
3294        }
3295
3296        // Punctuation that IS mapped.
3297        assert_eq!(VirtualKeyCode::Minus.get_lowercase(), Some('-'));
3298        assert_eq!(VirtualKeyCode::Period.get_lowercase(), Some('.'));
3299        assert_eq!(VirtualKeyCode::Slash.get_lowercase(), Some('/'));
3300        assert_eq!(VirtualKeyCode::Caret.get_lowercase(), Some('^'));
3301
3302        // Non-character keys have no lowercase form.
3303        for vk in [
3304            VirtualKeyCode::LShift,
3305            VirtualKeyCode::RControl,
3306            VirtualKeyCode::Escape,
3307            VirtualKeyCode::F12,
3308            VirtualKeyCode::Space,
3309            VirtualKeyCode::Return,
3310            VirtualKeyCode::Back,
3311        ] {
3312            assert_eq!(vk.get_lowercase(), None, "{vk:?}");
3313        }
3314    }
3315
3316    // ---------------------------------------------------------------------
3317    // WindowIcon::get_key + key-only Eq/Ord/Hash
3318    // ---------------------------------------------------------------------
3319
3320    #[test]
3321    fn window_icon_get_key_returns_the_stored_key() {
3322        let small_key = IconKey::new();
3323        let large_key = IconKey::new();
3324
3325        let small = WindowIcon::Small(SmallWindowIconBytes {
3326            key: small_key,
3327            rgba_bytes: vec![0u8; 16 * 16 * 4].into(),
3328        });
3329        let large = WindowIcon::Large(LargeWindowIconBytes {
3330            key: large_key,
3331            rgba_bytes: vec![255u8; 32 * 32 * 4].into(),
3332        });
3333
3334        assert_eq!(small.get_key(), small_key);
3335        assert_eq!(large.get_key(), large_key);
3336
3337        // Empty payloads are legal and must not panic.
3338        let empty = WindowIcon::Small(SmallWindowIconBytes {
3339            key: small_key,
3340            rgba_bytes: vec![].into(),
3341        });
3342        assert_eq!(empty.get_key(), small_key);
3343    }
3344
3345    #[test]
3346    fn window_icon_identity_is_the_key_alone() {
3347        // The whole point of IconKey: diff the key, not the bytes. Two icons with
3348        // the same key compare equal even though their pixels differ.
3349        let key = IconKey::new();
3350        let a = WindowIcon::Small(SmallWindowIconBytes {
3351            key,
3352            rgba_bytes: vec![0u8; 4].into(),
3353        });
3354        let b = WindowIcon::Small(SmallWindowIconBytes {
3355            key,
3356            rgba_bytes: vec![7u8; 1024].into(),
3357        });
3358        // ...even across the Small/Large variants.
3359        let c = WindowIcon::Large(LargeWindowIconBytes {
3360            key,
3361            rgba_bytes: vec![9u8; 32 * 32 * 4].into(),
3362        });
3363
3364        assert_eq!(a, b);
3365        assert_eq!(a, c);
3366        assert_eq!(a.cmp(&c), Ordering::Equal);
3367        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3368        // Hash must agree with Eq, or icons break as BTreeMap/HashMap keys.
3369        assert_eq!(hash_of(&a), hash_of(&b));
3370        assert_eq!(hash_of(&a), hash_of(&c));
3371
3372        // Different keys: unequal, and ordered by key.
3373        let older = WindowIcon::Small(SmallWindowIconBytes {
3374            key,
3375            rgba_bytes: vec![0u8; 4].into(),
3376        });
3377        let newer = WindowIcon::Small(SmallWindowIconBytes {
3378            key: IconKey::new(),
3379            rgba_bytes: vec![0u8; 4].into(),
3380        });
3381        assert_ne!(older, newer);
3382        assert_eq!(older.cmp(&newer), Ordering::Less);
3383    }
3384
3385    // ---------------------------------------------------------------------
3386    // UpdateFocusWarning: Display
3387    // ---------------------------------------------------------------------
3388
3389    #[test]
3390    fn update_focus_warning_display_is_non_empty_for_every_variant() {
3391        let dom = format!("{}", UpdateFocusWarning::FocusInvalidDomId(DomId::ROOT_ID));
3392        assert!(dom.contains("invalid ID"), "{dom}");
3393        assert!(!dom.is_empty());
3394
3395        let node = format!(
3396            "{}",
3397            UpdateFocusWarning::FocusInvalidNodeId(NodeHierarchyItemId::NONE)
3398        );
3399        assert!(node.contains("invalid ID"), "{node}");
3400
3401        // Edge values: a zero DomId, a raw-encoded huge node id, an empty CssPath.
3402        let huge = format!(
3403            "{}",
3404            UpdateFocusWarning::FocusInvalidNodeId(NodeHierarchyItemId::from_raw(usize::MAX))
3405        );
3406        assert!(!huge.is_empty());
3407
3408        let path = format!(
3409            "{}",
3410            UpdateFocusWarning::CouldNotFindFocusNode(CssPath::default())
3411        );
3412        assert!(
3413            path.starts_with("Could not find focus node for path:"),
3414            "{path}"
3415        );
3416    }
3417}