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/// The global CSS viewport breakpoints for `@media`-style conditions.
1566///
1567/// The dynamic-selector system evaluates against these, and they are one of
1568/// the three signals the resize fast path checks: crossing any of these
1569/// (on either axis) re-invokes the
1570/// app's `layout()`; staying between them re-flows the existing DOM.
1571///
1572/// Lived in `azul-dll`'s shell (`shell2::common::CSS_BREAKPOINTS`, still
1573/// re-exported there) until the headless E2E runner needed the same resize
1574/// decision — the list is engine policy, not shell policy.
1575pub const CSS_BREAKPOINTS: &[f32] = &[320.0, 480.0, 640.0, 768.0, 1024.0, 1280.0, 1440.0, 1920.0];
1576
1577#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
1578#[repr(C)]
1579pub struct WindowSize {
1580    /// Width and height of the window, in logical
1581    /// units (may not correspond to the physical on-screen size)
1582    pub dimensions: LogicalSize,
1583    /// Actual DPI value (default: 96)
1584    pub dpi: u32,
1585    /// Minimum dimensions of the window
1586    pub min_dimensions: OptionLogicalSize,
1587    /// Maximum dimensions of the window
1588    pub max_dimensions: OptionLogicalSize,
1589}
1590
1591impl WindowSize {
1592    #[allow(clippy::cast_possible_truncation)] // bounded DPI/dimension/number conversion
1593    #[must_use] pub fn get_layout_size(&self) -> LayoutSize {
1594        LayoutSize::new(
1595            libm::roundf(self.dimensions.width) as isize,
1596            libm::roundf(self.dimensions.height) as isize,
1597        )
1598    }
1599
1600    /// Get the actual logical size
1601    #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
1602        self.dimensions
1603    }
1604
1605    #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
1606        self.dimensions
1607            .to_physical(self.get_hidpi_factor().inner.get())
1608    }
1609
1610    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1611    #[must_use] pub fn get_hidpi_factor(&self) -> DpiScaleFactor {
1612        // Guard against `dpi == 0` (uninitialized / misreporting platform),
1613        // which would yield a 0.0 scale factor and later divide-by-zero when
1614        // converting physical <-> logical sizes (`to_logical` divides by this).
1615        // Fall back to the standard 96 DPI (scale 1.0).
1616        let dpi = if self.dpi == 0 { 96 } else { self.dpi };
1617        DpiScaleFactor {
1618            inner: FloatValue::new(dpi as f32 / 96.0),
1619        }
1620    }
1621}
1622
1623impl Default for WindowSize {
1624    fn default() -> Self {
1625        Self {
1626            dimensions: LogicalSize::new(640.0, 480.0),
1627            dpi: 96,
1628            min_dimensions: None.into(),
1629            max_dimensions: None.into(),
1630        }
1631    }
1632}
1633
1634#[repr(C)]
1635#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1636pub enum RendererType {
1637    /// Force hardware rendering
1638    Hardware,
1639    /// Force software rendering
1640    Software,
1641}
1642
1643impl_option!(
1644    RendererType,
1645    OptionRendererType,
1646    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
1647);
1648
1649#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
1650pub enum UpdateFocusWarning {
1651    FocusInvalidDomId(DomId),
1652    FocusInvalidNodeId(NodeHierarchyItemId),
1653    CouldNotFindFocusNode(CssPath),
1654}
1655
1656impl ::core::fmt::Display for UpdateFocusWarning {
1657    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
1658        use self::UpdateFocusWarning::{FocusInvalidDomId, FocusInvalidNodeId, CouldNotFindFocusNode};
1659        match self {
1660            FocusInvalidDomId(dom_id) => write!(f, "Focusing on DOM with invalid ID: {dom_id:?}"),
1661            FocusInvalidNodeId(node_id) => {
1662                write!(f, "Focusing on node with invalid ID: {node_id}")
1663            }
1664            CouldNotFindFocusNode(css_path) => {
1665                write!(f, "Could not find focus node for path: {css_path}")
1666            }
1667        }
1668    }
1669}
1670
1671/// Utility function for easier creation of a keymap - i.e. `[vec![Ctrl, S], my_function]`
1672#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1673#[repr(C, u8)]
1674pub enum AcceleratorKey {
1675    Ctrl,
1676    Alt,
1677    Shift,
1678    Key(VirtualKeyCode),
1679}
1680
1681impl AcceleratorKey {
1682    /// Checks if the current keyboard state contains the given char or modifier,
1683    /// i.e. if the keyboard state currently has the shift key pressed and the
1684    /// accelerator key is `Shift`, evaluates to true, otherwise to false.
1685    #[must_use] pub fn matches(&self, keyboard_state: &KeyboardState) -> bool {
1686        use self::AcceleratorKey::{Ctrl, Alt, Shift, Key};
1687        match self {
1688            Ctrl => keyboard_state.ctrl_down(),
1689            Alt => keyboard_state.alt_down(),
1690            Shift => keyboard_state.shift_down(),
1691            Key(k) => keyboard_state.is_key_down(*k),
1692        }
1693    }
1694}
1695
1696/// Symbolic name for a keyboard key, does NOT take the keyboard locale into account
1697#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1698#[repr(C)]
1699pub enum VirtualKeyCode {
1700    Key1,
1701    Key2,
1702    Key3,
1703    Key4,
1704    Key5,
1705    Key6,
1706    Key7,
1707    Key8,
1708    Key9,
1709    Key0,
1710    A,
1711    B,
1712    C,
1713    D,
1714    E,
1715    F,
1716    G,
1717    H,
1718    I,
1719    J,
1720    K,
1721    L,
1722    M,
1723    N,
1724    O,
1725    P,
1726    Q,
1727    R,
1728    S,
1729    T,
1730    U,
1731    V,
1732    W,
1733    X,
1734    Y,
1735    Z,
1736    Escape,
1737    F1,
1738    F2,
1739    F3,
1740    F4,
1741    F5,
1742    F6,
1743    F7,
1744    F8,
1745    F9,
1746    F10,
1747    F11,
1748    F12,
1749    F13,
1750    F14,
1751    F15,
1752    F16,
1753    F17,
1754    F18,
1755    F19,
1756    F20,
1757    F21,
1758    F22,
1759    F23,
1760    F24,
1761    Snapshot,
1762    Scroll,
1763    Pause,
1764    Insert,
1765    Home,
1766    Delete,
1767    End,
1768    PageDown,
1769    PageUp,
1770    Left,
1771    Up,
1772    Right,
1773    Down,
1774    Back,
1775    Return,
1776    Space,
1777    Compose,
1778    Caret,
1779    Numlock,
1780    Numpad0,
1781    Numpad1,
1782    Numpad2,
1783    Numpad3,
1784    Numpad4,
1785    Numpad5,
1786    Numpad6,
1787    Numpad7,
1788    Numpad8,
1789    Numpad9,
1790    NumpadAdd,
1791    NumpadDivide,
1792    NumpadDecimal,
1793    NumpadComma,
1794    NumpadEnter,
1795    NumpadEquals,
1796    NumpadMultiply,
1797    NumpadSubtract,
1798    AbntC1,
1799    AbntC2,
1800    Apostrophe,
1801    Apps,
1802    Asterisk,
1803    At,
1804    Ax,
1805    Backslash,
1806    Calculator,
1807    Capital,
1808    Colon,
1809    Comma,
1810    Convert,
1811    Equals,
1812    Grave,
1813    Kana,
1814    Kanji,
1815    LAlt,
1816    LBracket,
1817    LControl,
1818    LShift,
1819    LWin,
1820    Mail,
1821    MediaSelect,
1822    MediaStop,
1823    Minus,
1824    Mute,
1825    MyComputer,
1826    NavigateForward,
1827    NavigateBackward,
1828    NextTrack,
1829    NoConvert,
1830    OEM102,
1831    Period,
1832    PlayPause,
1833    Plus,
1834    Power,
1835    PrevTrack,
1836    RAlt,
1837    RBracket,
1838    RControl,
1839    RShift,
1840    RWin,
1841    Semicolon,
1842    Slash,
1843    Sleep,
1844    Stop,
1845    Sysrq,
1846    Tab,
1847    Underline,
1848    Unlabeled,
1849    VolumeDown,
1850    VolumeUp,
1851    Wake,
1852    WebBack,
1853    WebFavorites,
1854    WebForward,
1855    WebHome,
1856    WebRefresh,
1857    WebSearch,
1858    WebStop,
1859    Yen,
1860    Copy,
1861    Paste,
1862    Cut,
1863}
1864
1865impl VirtualKeyCode {
1866    /// Reconstructs a `VirtualKeyCode` from its `as u32` discriminant.
1867    ///
1868    /// This enum is a fieldless `#[repr(C)]` enum with no explicit discriminants,
1869    /// so the discriminants are assigned sequentially in declaration order and
1870    /// `VariantN as u32` round-trips through this table. Used to recover the key
1871    /// of a keyboard *event* from its `key_code` (which is stored as
1872    /// `VirtualKeyCode as u32`) instead of reading live keyboard state.
1873    #[must_use]
1874    #[allow(clippy::too_many_lines)] // exhaustive keycode match table
1875    pub const fn from_u32(v: u32) -> Option<Self> {
1876        match v {
1877            0 => Some(Self::Key1),
1878            1 => Some(Self::Key2),
1879            2 => Some(Self::Key3),
1880            3 => Some(Self::Key4),
1881            4 => Some(Self::Key5),
1882            5 => Some(Self::Key6),
1883            6 => Some(Self::Key7),
1884            7 => Some(Self::Key8),
1885            8 => Some(Self::Key9),
1886            9 => Some(Self::Key0),
1887            10 => Some(Self::A),
1888            11 => Some(Self::B),
1889            12 => Some(Self::C),
1890            13 => Some(Self::D),
1891            14 => Some(Self::E),
1892            15 => Some(Self::F),
1893            16 => Some(Self::G),
1894            17 => Some(Self::H),
1895            18 => Some(Self::I),
1896            19 => Some(Self::J),
1897            20 => Some(Self::K),
1898            21 => Some(Self::L),
1899            22 => Some(Self::M),
1900            23 => Some(Self::N),
1901            24 => Some(Self::O),
1902            25 => Some(Self::P),
1903            26 => Some(Self::Q),
1904            27 => Some(Self::R),
1905            28 => Some(Self::S),
1906            29 => Some(Self::T),
1907            30 => Some(Self::U),
1908            31 => Some(Self::V),
1909            32 => Some(Self::W),
1910            33 => Some(Self::X),
1911            34 => Some(Self::Y),
1912            35 => Some(Self::Z),
1913            36 => Some(Self::Escape),
1914            37 => Some(Self::F1),
1915            38 => Some(Self::F2),
1916            39 => Some(Self::F3),
1917            40 => Some(Self::F4),
1918            41 => Some(Self::F5),
1919            42 => Some(Self::F6),
1920            43 => Some(Self::F7),
1921            44 => Some(Self::F8),
1922            45 => Some(Self::F9),
1923            46 => Some(Self::F10),
1924            47 => Some(Self::F11),
1925            48 => Some(Self::F12),
1926            49 => Some(Self::F13),
1927            50 => Some(Self::F14),
1928            51 => Some(Self::F15),
1929            52 => Some(Self::F16),
1930            53 => Some(Self::F17),
1931            54 => Some(Self::F18),
1932            55 => Some(Self::F19),
1933            56 => Some(Self::F20),
1934            57 => Some(Self::F21),
1935            58 => Some(Self::F22),
1936            59 => Some(Self::F23),
1937            60 => Some(Self::F24),
1938            61 => Some(Self::Snapshot),
1939            62 => Some(Self::Scroll),
1940            63 => Some(Self::Pause),
1941            64 => Some(Self::Insert),
1942            65 => Some(Self::Home),
1943            66 => Some(Self::Delete),
1944            67 => Some(Self::End),
1945            68 => Some(Self::PageDown),
1946            69 => Some(Self::PageUp),
1947            70 => Some(Self::Left),
1948            71 => Some(Self::Up),
1949            72 => Some(Self::Right),
1950            73 => Some(Self::Down),
1951            74 => Some(Self::Back),
1952            75 => Some(Self::Return),
1953            76 => Some(Self::Space),
1954            77 => Some(Self::Compose),
1955            78 => Some(Self::Caret),
1956            79 => Some(Self::Numlock),
1957            80 => Some(Self::Numpad0),
1958            81 => Some(Self::Numpad1),
1959            82 => Some(Self::Numpad2),
1960            83 => Some(Self::Numpad3),
1961            84 => Some(Self::Numpad4),
1962            85 => Some(Self::Numpad5),
1963            86 => Some(Self::Numpad6),
1964            87 => Some(Self::Numpad7),
1965            88 => Some(Self::Numpad8),
1966            89 => Some(Self::Numpad9),
1967            90 => Some(Self::NumpadAdd),
1968            91 => Some(Self::NumpadDivide),
1969            92 => Some(Self::NumpadDecimal),
1970            93 => Some(Self::NumpadComma),
1971            94 => Some(Self::NumpadEnter),
1972            95 => Some(Self::NumpadEquals),
1973            96 => Some(Self::NumpadMultiply),
1974            97 => Some(Self::NumpadSubtract),
1975            98 => Some(Self::AbntC1),
1976            99 => Some(Self::AbntC2),
1977            100 => Some(Self::Apostrophe),
1978            101 => Some(Self::Apps),
1979            102 => Some(Self::Asterisk),
1980            103 => Some(Self::At),
1981            104 => Some(Self::Ax),
1982            105 => Some(Self::Backslash),
1983            106 => Some(Self::Calculator),
1984            107 => Some(Self::Capital),
1985            108 => Some(Self::Colon),
1986            109 => Some(Self::Comma),
1987            110 => Some(Self::Convert),
1988            111 => Some(Self::Equals),
1989            112 => Some(Self::Grave),
1990            113 => Some(Self::Kana),
1991            114 => Some(Self::Kanji),
1992            115 => Some(Self::LAlt),
1993            116 => Some(Self::LBracket),
1994            117 => Some(Self::LControl),
1995            118 => Some(Self::LShift),
1996            119 => Some(Self::LWin),
1997            120 => Some(Self::Mail),
1998            121 => Some(Self::MediaSelect),
1999            122 => Some(Self::MediaStop),
2000            123 => Some(Self::Minus),
2001            124 => Some(Self::Mute),
2002            125 => Some(Self::MyComputer),
2003            126 => Some(Self::NavigateForward),
2004            127 => Some(Self::NavigateBackward),
2005            128 => Some(Self::NextTrack),
2006            129 => Some(Self::NoConvert),
2007            130 => Some(Self::OEM102),
2008            131 => Some(Self::Period),
2009            132 => Some(Self::PlayPause),
2010            133 => Some(Self::Plus),
2011            134 => Some(Self::Power),
2012            135 => Some(Self::PrevTrack),
2013            136 => Some(Self::RAlt),
2014            137 => Some(Self::RBracket),
2015            138 => Some(Self::RControl),
2016            139 => Some(Self::RShift),
2017            140 => Some(Self::RWin),
2018            141 => Some(Self::Semicolon),
2019            142 => Some(Self::Slash),
2020            143 => Some(Self::Sleep),
2021            144 => Some(Self::Stop),
2022            145 => Some(Self::Sysrq),
2023            146 => Some(Self::Tab),
2024            147 => Some(Self::Underline),
2025            148 => Some(Self::Unlabeled),
2026            149 => Some(Self::VolumeDown),
2027            150 => Some(Self::VolumeUp),
2028            151 => Some(Self::Wake),
2029            152 => Some(Self::WebBack),
2030            153 => Some(Self::WebFavorites),
2031            154 => Some(Self::WebForward),
2032            155 => Some(Self::WebHome),
2033            156 => Some(Self::WebRefresh),
2034            157 => Some(Self::WebSearch),
2035            158 => Some(Self::WebStop),
2036            159 => Some(Self::Yen),
2037            160 => Some(Self::Copy),
2038            161 => Some(Self::Paste),
2039            162 => Some(Self::Cut),
2040            _ => None,
2041        }
2042    }
2043
2044    #[must_use] pub const fn get_lowercase(&self) -> Option<char> {
2045        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};
2046        match self {
2047            A => Some('a'),
2048            B => Some('b'),
2049            C => Some('c'),
2050            D => Some('d'),
2051            E => Some('e'),
2052            F => Some('f'),
2053            G => Some('g'),
2054            H => Some('h'),
2055            I => Some('i'),
2056            J => Some('j'),
2057            K => Some('k'),
2058            L => Some('l'),
2059            M => Some('m'),
2060            N => Some('n'),
2061            O => Some('o'),
2062            P => Some('p'),
2063            Q => Some('q'),
2064            R => Some('r'),
2065            S => Some('s'),
2066            T => Some('t'),
2067            U => Some('u'),
2068            V => Some('v'),
2069            W => Some('w'),
2070            X => Some('x'),
2071            Y => Some('y'),
2072            Z => Some('z'),
2073            Key0 | Numpad0 => Some('0'),
2074            Key1 | Numpad1 => Some('1'),
2075            Key2 | Numpad2 => Some('2'),
2076            Key3 | Numpad3 => Some('3'),
2077            Key4 | Numpad4 => Some('4'),
2078            Key5 | Numpad5 => Some('5'),
2079            Key6 | Numpad6 => Some('6'),
2080            Key7 | Numpad7 => Some('7'),
2081            Key8 | Numpad8 => Some('8'),
2082            Key9 | Numpad9 => Some('9'),
2083            Minus => Some('-'),
2084            Asterisk => Some('*'),
2085            At => Some('@'),
2086            Period => Some('.'),
2087            Semicolon => Some(';'),
2088            Slash => Some('/'),
2089            Caret => Some('^'),
2090            _ => None,
2091        }
2092    }
2093}
2094
2095/// 16x16x4 bytes icon
2096#[derive(Debug, Clone)]
2097#[repr(C)]
2098pub struct SmallWindowIconBytes {
2099    pub key: IconKey,
2100    pub rgba_bytes: U8Vec,
2101}
2102
2103/// 32x32x4 bytes icon
2104#[derive(Debug, Clone)]
2105#[repr(C)]
2106pub struct LargeWindowIconBytes {
2107    pub key: IconKey,
2108    pub rgba_bytes: U8Vec,
2109}
2110
2111// Window icon that usually appears in the top-left corner of the window
2112#[derive(Debug, Clone)]
2113#[repr(C, u8)]
2114pub enum WindowIcon {
2115    Small(SmallWindowIconBytes),
2116    /// 32x32x4 bytes icon
2117    Large(LargeWindowIconBytes),
2118}
2119
2120impl_option!(
2121    WindowIcon,
2122    OptionWindowIcon,
2123    copy = false,
2124    [Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Ord]
2125);
2126
2127impl WindowIcon {
2128    #[must_use] pub const fn get_key(&self) -> IconKey {
2129        match &self {
2130            Self::Small(SmallWindowIconBytes { key, .. })
2131            | Self::Large(LargeWindowIconBytes { key, .. }) => *key,
2132        }
2133    }
2134}
2135// -- Only compare the IconKey (for WindowIcon and TaskBarIcon)
2136
2137impl PartialEq for WindowIcon {
2138    fn eq(&self, rhs: &Self) -> bool {
2139        self.get_key() == rhs.get_key()
2140    }
2141}
2142
2143impl PartialOrd for WindowIcon {
2144    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2145        Some((self.get_key()).cmp(&rhs.get_key()))
2146    }
2147}
2148
2149impl Eq for WindowIcon {}
2150
2151impl Ord for WindowIcon {
2152    fn cmp(&self, rhs: &Self) -> Ordering {
2153        (self.get_key()).cmp(&rhs.get_key())
2154    }
2155}
2156
2157impl Hash for WindowIcon {
2158    fn hash<H>(&self, state: &mut H)
2159    where
2160        H: Hasher,
2161    {
2162        self.get_key().hash(state);
2163    }
2164}
2165
2166/// 256x256x4 bytes window icon
2167#[derive(Debug, Clone)]
2168#[repr(C)]
2169pub struct TaskBarIcon {
2170    pub key: IconKey,
2171    pub rgba_bytes: U8Vec,
2172}
2173
2174impl_option!(
2175    TaskBarIcon,
2176    OptionTaskBarIcon,
2177    copy = false,
2178    [Debug, Clone, PartialOrd, PartialEq, Eq, Hash, Ord]
2179);
2180
2181impl PartialEq for TaskBarIcon {
2182    fn eq(&self, rhs: &Self) -> bool {
2183        self.key == rhs.key
2184    }
2185}
2186
2187impl PartialOrd for TaskBarIcon {
2188    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2189        Some((self.key).cmp(&rhs.key))
2190    }
2191}
2192
2193impl Eq for TaskBarIcon {}
2194
2195impl Ord for TaskBarIcon {
2196    fn cmp(&self, rhs: &Self) -> Ordering {
2197        (self.key).cmp(&rhs.key)
2198    }
2199}
2200
2201impl Hash for TaskBarIcon {
2202    fn hash<H>(&self, state: &mut H)
2203    where
2204        H: Hasher,
2205    {
2206        self.key.hash(state);
2207    }
2208}
2209
2210#[cfg(test)]
2211#[allow(clippy::float_cmp)] // exact-value assertions on hidpi scale factors
2212mod audit_tests {
2213    use super::*;
2214
2215    #[test]
2216    fn hidpi_factor_guards_zero_dpi() {
2217        // dpi == 0 must not produce a 0.0 scale factor (later divide-by-zero
2218        // in to_logical); it falls back to 96 DPI (scale 1.0).
2219        let ws = WindowSize { dpi: 0, ..WindowSize::default() };
2220        let factor = ws.get_hidpi_factor().inner.get();
2221        assert_eq!(factor, 1.0);
2222
2223        let ws2 = WindowSize { dpi: 192, ..WindowSize::default() };
2224        assert_eq!(ws2.get_hidpi_factor().inner.get(), 2.0);
2225    }
2226
2227    #[test]
2228    fn virtual_keycode_from_u32_roundtrips() {
2229        // A representative spread across the enum, including first/last.
2230        for vk in [
2231            VirtualKeyCode::Key1,
2232            VirtualKeyCode::A,
2233            VirtualKeyCode::Z,
2234            VirtualKeyCode::Left,
2235            VirtualKeyCode::Back,
2236            VirtualKeyCode::Delete,
2237            VirtualKeyCode::LControl,
2238            VirtualKeyCode::Cut,
2239        ] {
2240            assert_eq!(VirtualKeyCode::from_u32(vk as u32), Some(vk));
2241        }
2242        // Out of range -> None (no UB, no panic).
2243        assert_eq!(VirtualKeyCode::from_u32(10_000), None);
2244    }
2245}
2246
2247#[cfg(test)]
2248#[allow(clippy::float_cmp)] // exact-value assertions on saturating float->int conversions
2249mod autotest_generated {
2250    use alloc::{format, string::String, vec};
2251
2252    use super::*;
2253
2254    /// Highest valid `VirtualKeyCode` discriminant (`Cut`, the last declared variant).
2255    const LAST_VK: u32 = 162;
2256
2257    /// Deterministic, `no_std`-safe hasher so hash/eq consistency can be checked
2258    /// without pulling in `std::collections::hash_map::DefaultHasher`.
2259    #[derive(Default)]
2260    struct TestHasher(u64);
2261
2262    impl Hasher for TestHasher {
2263        fn write(&mut self, bytes: &[u8]) {
2264            for &b in bytes {
2265                self.0 = self.0.rotate_left(5) ^ u64::from(b);
2266            }
2267        }
2268        fn finish(&self) -> u64 {
2269            self.0
2270        }
2271    }
2272
2273    fn hash_of<T: Hash>(value: &T) -> u64 {
2274        let mut h = TestHasher::default();
2275        value.hash(&mut h);
2276        h.finish()
2277    }
2278
2279    fn keyboard_with(keys: &[VirtualKeyCode]) -> KeyboardState {
2280        KeyboardState {
2281            pressed_virtual_keycodes: keys.to_vec().into(),
2282            ..KeyboardState::default()
2283        }
2284    }
2285
2286    fn pair(key: &str, value: &str) -> AzStringPair {
2287        AzStringPair {
2288            key: key.into(),
2289            value: value.into(),
2290        }
2291    }
2292
2293    // ---------------------------------------------------------------------
2294    // Constructors: WindowId / IconKey (atomic counters)
2295    // ---------------------------------------------------------------------
2296
2297    #[test]
2298    fn window_id_new_is_unique_and_monotonic() {
2299        // The counter is process-global and shared with other tests in this
2300        // binary, so only *relative* properties may be asserted.
2301        let mut ids = BTreeSet::new();
2302        let mut prev = WindowId::new();
2303        assert!(ids.insert(prev));
2304        for _ in 0..1000 {
2305            let next = WindowId::new();
2306            assert!(next.id > prev.id, "WindowId counter must strictly increase");
2307            assert!(ids.insert(next), "WindowId::new() handed out a duplicate");
2308            prev = next;
2309        }
2310        // Default delegates to new(): two defaults are never the same window.
2311        assert_ne!(WindowId::default(), WindowId::default());
2312    }
2313
2314    #[test]
2315    fn icon_key_new_is_unique_and_monotonic() {
2316        let mut keys = BTreeSet::new();
2317        let mut prev = IconKey::new();
2318        assert!(keys.insert(prev));
2319        for _ in 0..1000 {
2320            let next = IconKey::new();
2321            assert!(
2322                next.icon_id > prev.icon_id,
2323                "IconKey counter must strictly increase"
2324            );
2325            assert!(keys.insert(next), "IconKey::new() handed out a duplicate");
2326            prev = next;
2327        }
2328        assert_ne!(IconKey::default(), IconKey::default());
2329    }
2330
2331    // ---------------------------------------------------------------------
2332    // RendererOptions + Vsync / Srgb / HwAcceleration predicates
2333    // ---------------------------------------------------------------------
2334
2335    #[test]
2336    fn renderer_options_new_preserves_every_combination() {
2337        let vsyncs = [Vsync::Enabled, Vsync::Disabled, Vsync::DontCare];
2338        let srgbs = [Srgb::Enabled, Srgb::Disabled, Srgb::DontCare];
2339        let accels = [
2340            HwAcceleration::Enabled,
2341            HwAcceleration::Disabled,
2342            HwAcceleration::DontCare,
2343        ];
2344        for v in vsyncs {
2345            for s in srgbs {
2346                for a in accels {
2347                    let o = RendererOptions::new(v, s, a);
2348                    assert_eq!(o.vsync, v);
2349                    assert_eq!(o.srgb, s);
2350                    assert_eq!(o.hw_accel, a);
2351                    // Constructed value must round-trip through equality/copy.
2352                    assert_eq!(o, RendererOptions::new(v, s, a));
2353                }
2354            }
2355        }
2356    }
2357
2358    #[test]
2359    fn renderer_options_default_does_not_force_cpu_rendering() {
2360        // Regression guard: hw_accel must be DontCare (auto), NOT Disabled -
2361        // Disabled silently forced every app into CPU rendering.
2362        let d = RendererOptions::default();
2363        assert_eq!(d.hw_accel, HwAcceleration::DontCare);
2364        assert!(!d.hw_accel.is_enabled());
2365        assert!(d.vsync.is_enabled());
2366        assert!(!d.srgb.is_enabled());
2367    }
2368
2369    #[test]
2370    fn tri_state_is_enabled_only_for_enabled_variant() {
2371        assert!(Vsync::Enabled.is_enabled());
2372        assert!(!Vsync::Disabled.is_enabled());
2373        assert!(!Vsync::DontCare.is_enabled());
2374
2375        assert!(Srgb::Enabled.is_enabled());
2376        assert!(!Srgb::Disabled.is_enabled());
2377        assert!(!Srgb::DontCare.is_enabled());
2378
2379        assert!(HwAcceleration::Enabled.is_enabled());
2380        assert!(!HwAcceleration::Disabled.is_enabled());
2381        assert!(!HwAcceleration::DontCare.is_enabled());
2382    }
2383
2384    // ---------------------------------------------------------------------
2385    // KeyboardState getters / predicates
2386    // ---------------------------------------------------------------------
2387
2388    #[test]
2389    fn keyboard_state_default_has_no_key_down() {
2390        let k = KeyboardState::default();
2391        assert!(!k.shift_down());
2392        assert!(!k.ctrl_down());
2393        assert!(!k.alt_down());
2394        assert!(!k.super_down());
2395        assert!(!k.primary_down());
2396        // Every single keycode reports "not down" on an empty state.
2397        for v in 0..=LAST_VK {
2398            let vk = VirtualKeyCode::from_u32(v).expect("discriminant in range");
2399            assert!(!k.is_key_down(vk));
2400        }
2401    }
2402
2403    #[test]
2404    fn keyboard_state_modifiers_accept_either_side() {
2405        for (left, right, probe) in [
2406            (
2407                VirtualKeyCode::LShift,
2408                VirtualKeyCode::RShift,
2409                KeyboardState::shift_down as fn(&KeyboardState) -> bool,
2410            ),
2411            (
2412                VirtualKeyCode::LControl,
2413                VirtualKeyCode::RControl,
2414                KeyboardState::ctrl_down as fn(&KeyboardState) -> bool,
2415            ),
2416            (
2417                VirtualKeyCode::LAlt,
2418                VirtualKeyCode::RAlt,
2419                KeyboardState::alt_down as fn(&KeyboardState) -> bool,
2420            ),
2421            (
2422                VirtualKeyCode::LWin,
2423                VirtualKeyCode::RWin,
2424                KeyboardState::super_down as fn(&KeyboardState) -> bool,
2425            ),
2426        ] {
2427            assert!(probe(&keyboard_with(&[left])), "left variant must register");
2428            assert!(
2429                probe(&keyboard_with(&[right])),
2430                "right variant must register"
2431            );
2432            assert!(probe(&keyboard_with(&[left, right])));
2433            // An unrelated key must not light up a modifier.
2434            assert!(!probe(&keyboard_with(&[VirtualKeyCode::A])));
2435        }
2436    }
2437
2438    #[test]
2439    fn keyboard_state_primary_down_follows_platform() {
2440        let ctrl = keyboard_with(&[VirtualKeyCode::LControl]);
2441        let cmd = keyboard_with(&[VirtualKeyCode::LWin]);
2442        if cfg!(target_os = "macos") {
2443            assert!(cmd.primary_down(), "Cmd (super) is PRIMARY on macOS");
2444            assert!(!ctrl.primary_down());
2445        } else {
2446            assert!(ctrl.primary_down(), "Ctrl is PRIMARY off macOS");
2447            assert!(!cmd.primary_down());
2448        }
2449        // On every platform, primary_down agrees with one of the two modifiers.
2450        for k in [&ctrl, &cmd, &KeyboardState::default()] {
2451            assert_eq!(
2452                k.primary_down(),
2453                if cfg!(target_os = "macos") {
2454                    k.super_down()
2455                } else {
2456                    k.ctrl_down()
2457                }
2458            );
2459        }
2460    }
2461
2462    #[test]
2463    fn is_key_down_handles_duplicates_and_large_state() {
2464        // Same key pressed many times (backends can push duplicates).
2465        let dup = keyboard_with(&[VirtualKeyCode::S; 512]);
2466        assert!(dup.is_key_down(VirtualKeyCode::S));
2467        assert!(!dup.is_key_down(VirtualKeyCode::A));
2468
2469        // Every key held down at once: no panic, all report true.
2470        let all: alloc::vec::Vec<VirtualKeyCode> = (0..=LAST_VK)
2471            .map(|v| VirtualKeyCode::from_u32(v).expect("discriminant in range"))
2472            .collect();
2473        let everything = keyboard_with(&all);
2474        for vk in &all {
2475            assert!(everything.is_key_down(*vk));
2476        }
2477        assert!(everything.shift_down() && everything.ctrl_down());
2478        assert!(everything.alt_down() && everything.super_down());
2479    }
2480
2481    // ---------------------------------------------------------------------
2482    // AcceleratorKey / matches_accelerator
2483    // ---------------------------------------------------------------------
2484
2485    #[test]
2486    fn empty_chord_matches_trivially() {
2487        // Documented: "An empty chord matches trivially."
2488        assert!(KeyboardState::default().matches_accelerator(&[]));
2489        assert!(keyboard_with(&[VirtualKeyCode::A]).matches_accelerator(&[]));
2490    }
2491
2492    #[test]
2493    fn matches_accelerator_requires_every_entry() {
2494        let state = keyboard_with(&[
2495            VirtualKeyCode::LControl,
2496            VirtualKeyCode::LShift,
2497            VirtualKeyCode::S,
2498        ]);
2499        assert!(state.matches_accelerator(&[
2500            AcceleratorKey::Ctrl,
2501            AcceleratorKey::Shift,
2502            AcceleratorKey::Key(VirtualKeyCode::S),
2503        ]));
2504        // One missing entry (Alt) is enough to reject the whole chord.
2505        assert!(!state.matches_accelerator(&[
2506            AcceleratorKey::Ctrl,
2507            AcceleratorKey::Alt,
2508            AcceleratorKey::Key(VirtualKeyCode::S),
2509        ]));
2510        // Wrong key, right modifiers.
2511        assert!(!state.matches_accelerator(&[
2512            AcceleratorKey::Ctrl,
2513            AcceleratorKey::Key(VirtualKeyCode::Q),
2514        ]));
2515        // Order must not matter.
2516        assert!(state.matches_accelerator(&[
2517            AcceleratorKey::Key(VirtualKeyCode::S),
2518            AcceleratorKey::Shift,
2519            AcceleratorKey::Ctrl,
2520        ]));
2521    }
2522
2523    #[test]
2524    fn matches_accelerator_survives_huge_chord() {
2525        // A pathologically long chord must terminate (linear scan, no recursion).
2526        let state = keyboard_with(&[VirtualKeyCode::LShift]);
2527        let long_ok = vec![AcceleratorKey::Shift; 10_000];
2528        assert!(state.matches_accelerator(&long_ok));
2529
2530        // 10k satisfiable entries with a single unsatisfiable one at the very end:
2531        // `all()` must still reach it and return false.
2532        let mut long_bad = vec![AcceleratorKey::Shift; 10_000];
2533        long_bad.push(AcceleratorKey::Ctrl);
2534        assert!(!state.matches_accelerator(&long_bad));
2535    }
2536
2537    #[test]
2538    fn accelerator_key_matches_each_variant() {
2539        let empty = KeyboardState::default();
2540        for a in [
2541            AcceleratorKey::Ctrl,
2542            AcceleratorKey::Alt,
2543            AcceleratorKey::Shift,
2544            AcceleratorKey::Key(VirtualKeyCode::A),
2545        ] {
2546            assert!(!a.matches(&empty), "nothing matches an empty keyboard state");
2547        }
2548        assert!(AcceleratorKey::Ctrl.matches(&keyboard_with(&[VirtualKeyCode::RControl])));
2549        assert!(AcceleratorKey::Alt.matches(&keyboard_with(&[VirtualKeyCode::RAlt])));
2550        assert!(AcceleratorKey::Shift.matches(&keyboard_with(&[VirtualKeyCode::RShift])));
2551        assert!(
2552            AcceleratorKey::Key(VirtualKeyCode::F24).matches(&keyboard_with(&[VirtualKeyCode::F24]))
2553        );
2554        // Modifier accelerators are NOT satisfied by the letter of the same name.
2555        assert!(!AcceleratorKey::Ctrl.matches(&keyboard_with(&[VirtualKeyCode::C])));
2556    }
2557
2558    // ---------------------------------------------------------------------
2559    // MouseState / MouseButtonState
2560    // ---------------------------------------------------------------------
2561
2562    #[test]
2563    fn mouse_state_matches_context_button() {
2564        let base = MouseState::default();
2565        assert!(!base.matches(&ContextMenuMouseButton::Left));
2566        assert!(!base.matches(&ContextMenuMouseButton::Right));
2567        assert!(!base.matches(&ContextMenuMouseButton::Middle));
2568
2569        for (ctx, ms) in [
2570            (
2571                ContextMenuMouseButton::Left,
2572                MouseState {
2573                    left_down: true,
2574                    ..MouseState::default()
2575                },
2576            ),
2577            (
2578                ContextMenuMouseButton::Right,
2579                MouseState {
2580                    right_down: true,
2581                    ..MouseState::default()
2582                },
2583            ),
2584            (
2585                ContextMenuMouseButton::Middle,
2586                MouseState {
2587                    middle_down: true,
2588                    ..MouseState::default()
2589                },
2590            ),
2591        ] {
2592            assert!(ms.matches(&ctx), "{ctx:?} must match its own button");
2593            // ...and only its own button.
2594            let others = [
2595                ContextMenuMouseButton::Left,
2596                ContextMenuMouseButton::Right,
2597                ContextMenuMouseButton::Middle,
2598            ];
2599            for other in others {
2600                assert_eq!(ms.matches(&other), other == ctx);
2601            }
2602        }
2603    }
2604
2605    #[test]
2606    fn mouse_down_and_button_state_agree_for_all_8_combinations() {
2607        for bits in 0u8..8 {
2608            let (l, r, m) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0);
2609            let ms = MouseState {
2610                left_down: l,
2611                right_down: r,
2612                middle_down: m,
2613                ..MouseState::default()
2614            };
2615            assert_eq!(ms.mouse_down(), l || r || m);
2616
2617            let snapshot = ms.button_state();
2618            assert_eq!(snapshot.left_down, l);
2619            assert_eq!(snapshot.right_down, r);
2620            assert_eq!(snapshot.middle_down, m);
2621            // any_down is exactly mouse_down, and the From impl is the same snapshot.
2622            assert_eq!(snapshot.any_down(), ms.mouse_down());
2623            assert_eq!(crate::events::MouseButtonState::from(&ms), snapshot);
2624        }
2625        // Default MouseState has no button held.
2626        assert!(!MouseState::default().mouse_down());
2627        assert!(!MouseState::default().button_state().any_down());
2628    }
2629
2630    // ---------------------------------------------------------------------
2631    // process_system_scroll (numeric)
2632    // ---------------------------------------------------------------------
2633
2634    #[test]
2635    fn process_system_scroll_zero_and_negative_zero_consume_nothing() {
2636        let r = process_system_scroll(LogicalPosition::zero(), false);
2637        assert_eq!(r.scrolled_nodes, 0);
2638        assert_eq!(r.remaining_delta, LogicalPosition::zero());
2639        assert!(!r.hit_scrollbar);
2640
2641        // -0.0 == 0.0 under IEEE-754, so a negative-zero delta must also be a no-op.
2642        let neg_zero = process_system_scroll(LogicalPosition::new(-0.0, -0.0), true);
2643        assert_eq!(neg_zero.scrolled_nodes, 0);
2644        assert!(neg_zero.hit_scrollbar, "hit_scrollbar is echoed verbatim");
2645    }
2646
2647    #[test]
2648    fn process_system_scroll_counts_any_nonzero_axis() {
2649        for delta in [
2650            LogicalPosition::new(1.0, 0.0),
2651            LogicalPosition::new(0.0, -1.0),
2652            LogicalPosition::new(-3.5, 7.25),
2653            LogicalPosition::new(f32::MIN, 0.0),
2654            LogicalPosition::new(0.0, f32::MAX),
2655            LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
2656            LogicalPosition::new(f32::MIN_POSITIVE, 0.0),
2657        ] {
2658            let r = process_system_scroll(delta, false);
2659            assert_eq!(r.scrolled_nodes, 1, "{delta:?} must count as consumed");
2660            // Overscroll is never reported by this helper.
2661            assert_eq!(r.remaining_delta, LogicalPosition::zero());
2662        }
2663    }
2664
2665    #[test]
2666    fn process_system_scroll_does_not_panic_on_nan() {
2667        // NaN != 0.0 is `true`, so a NaN delta is currently treated as consumed.
2668        // The contract asserted here is only "terminates, no panic, bounded count".
2669        for delta in [
2670            LogicalPosition::new(f32::NAN, 0.0),
2671            LogicalPosition::new(0.0, f32::NAN),
2672            LogicalPosition::new(f32::NAN, f32::NAN),
2673        ] {
2674            let r = process_system_scroll(delta, true);
2675            assert!(r.scrolled_nodes <= 1);
2676            assert_eq!(r.remaining_delta, LogicalPosition::zero());
2677            assert!(r.hit_scrollbar);
2678        }
2679    }
2680
2681    #[test]
2682    fn scroll_result_default_is_inert() {
2683        let d = ScrollResult::default();
2684        assert_eq!(d.scrolled_nodes, 0);
2685        assert_eq!(d.remaining_delta, LogicalPosition::zero());
2686        assert!(!d.hit_scrollbar);
2687    }
2688
2689    // ---------------------------------------------------------------------
2690    // CursorPosition
2691    // ---------------------------------------------------------------------
2692
2693    #[test]
2694    fn cursor_position_get_position_only_inside_window() {
2695        let p = LogicalPosition::new(12.0, 34.0);
2696        assert_eq!(CursorPosition::InWindow(p).get_position(), Some(p));
2697        assert_eq!(CursorPosition::OutOfWindow(p).get_position(), None);
2698        assert_eq!(CursorPosition::Uninitialized.get_position(), None);
2699        // Default (as used by MouseState::default) is Uninitialized.
2700        assert_eq!(CursorPosition::default(), CursorPosition::Uninitialized);
2701        assert_eq!(CursorPosition::default().get_position(), None);
2702    }
2703
2704    #[test]
2705    fn cursor_position_is_inside_window_agrees_with_get_position() {
2706        for c in [
2707            CursorPosition::Uninitialized,
2708            CursorPosition::InWindow(LogicalPosition::zero()),
2709            CursorPosition::OutOfWindow(LogicalPosition::zero()),
2710            CursorPosition::InWindow(LogicalPosition::new(f32::MIN, f32::MAX)),
2711            CursorPosition::OutOfWindow(LogicalPosition::new(f32::INFINITY, f32::NAN)),
2712        ] {
2713            assert_eq!(c.is_inside_window(), c.get_position().is_some());
2714        }
2715        // Extreme / non-finite coordinates are passed through, not sanitized.
2716        let nan_pos = CursorPosition::InWindow(LogicalPosition::new(f32::NAN, f32::INFINITY));
2717        assert!(nan_pos.is_inside_window());
2718        let got = nan_pos.get_position().expect("InWindow always yields a position");
2719        assert!(got.x.is_nan());
2720        assert!(got.y.is_infinite());
2721    }
2722
2723    // ---------------------------------------------------------------------
2724    // MonitorId constructors
2725    // ---------------------------------------------------------------------
2726
2727    #[test]
2728    fn monitor_id_constructors_preserve_fields_at_extremes() {
2729        assert_eq!(MonitorId::PRIMARY, MonitorId { index: 0, hash: 0 });
2730        assert_eq!(MonitorId::new(0), MonitorId::PRIMARY);
2731
2732        for index in [0usize, 1, 42, usize::MAX] {
2733            let m = MonitorId::new(index);
2734            assert_eq!(m.index, index);
2735            assert_eq!(m.hash, 0, "new() documents hash == 0");
2736
2737            for hash in [0u64, 1, u64::MAX] {
2738                let m = MonitorId::from_index_and_hash(index, hash);
2739                assert_eq!(m.index, index);
2740                assert_eq!(m.hash, hash);
2741            }
2742        }
2743        // index and hash are independent coordinates of identity.
2744        assert_ne!(MonitorId::new(1), MonitorId::new(2));
2745        assert_ne!(
2746            MonitorId::from_index_and_hash(1, 7),
2747            MonitorId::from_index_and_hash(1, 8)
2748        );
2749    }
2750
2751    #[test]
2752    fn monitor_id_from_properties_is_stable_and_index_independent() {
2753        let pos = LayoutPoint::new(-1920, 0);
2754        let size = LayoutSize::new(2560, 1440);
2755
2756        let a = MonitorId::from_properties(0, "HDMI-1", pos, size);
2757        let b = MonitorId::from_properties(0, "HDMI-1", pos, size);
2758        assert_eq!(a, b, "hash must be stable across calls (persistable)");
2759
2760        // The hash intentionally covers only the properties, not the runtime index.
2761        let reindexed = MonitorId::from_properties(7, "HDMI-1", pos, size);
2762        assert_eq!(reindexed.hash, a.hash);
2763        assert_eq!(reindexed.index, 7);
2764        assert_ne!(reindexed, a, "index is still part of identity");
2765    }
2766
2767    #[test]
2768    fn monitor_id_from_properties_is_sensitive_to_each_property() {
2769        let pos = LayoutPoint::new(0, 0);
2770        let size = LayoutSize::new(1920, 1080);
2771        let base = MonitorId::from_properties(0, "DP-1", pos, size);
2772
2773        // Changing any single property must change the hash.
2774        assert_ne!(
2775            base.hash,
2776            MonitorId::from_properties(0, "DP-2", pos, size).hash
2777        );
2778        assert_ne!(
2779            base.hash,
2780            MonitorId::from_properties(0, "DP-1", LayoutPoint::new(1, 0), size).hash
2781        );
2782        assert_ne!(
2783            base.hash,
2784            MonitorId::from_properties(0, "DP-1", LayoutPoint::new(0, 1), size).hash
2785        );
2786        assert_ne!(
2787            base.hash,
2788            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1921, 1080)).hash
2789        );
2790        assert_ne!(
2791            base.hash,
2792            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1920, 1081)).hash
2793        );
2794        // A swapped width/height is a different monitor, not the same one.
2795        assert_ne!(
2796            base.hash,
2797            MonitorId::from_properties(0, "DP-1", pos, LayoutSize::new(1080, 1920)).hash
2798        );
2799    }
2800
2801    #[test]
2802    fn monitor_id_from_properties_handles_hostile_inputs() {
2803        let size = LayoutSize::new(isize::MAX, isize::MIN);
2804        let pos = LayoutPoint::new(isize::MIN, isize::MAX);
2805
2806        // Empty / whitespace / unicode / NUL-containing names must not panic.
2807        for name in ["", "   ", "\t\n", "\u{1F600}", "e\u{301}", "é", "a\0b"] {
2808            let m = MonitorId::from_properties(3, name, pos, size);
2809            assert_eq!(m.index, 3);
2810            // Same input -> same hash, even at isize extremes.
2811            assert_eq!(m, MonitorId::from_properties(3, name, pos, size));
2812        }
2813
2814        // Byte-exact name comparison: combining-mark and precomposed forms differ.
2815        assert_ne!(
2816            MonitorId::from_properties(0, "e\u{301}", pos, size).hash,
2817            MonitorId::from_properties(0, "é", pos, size).hash
2818        );
2819
2820        // A 1M-char monitor name must terminate quickly (FNV-1a is linear).
2821        let huge = "x".repeat(1_000_000);
2822        let h1 = MonitorId::from_properties(0, &huge, LayoutPoint::zero(), LayoutSize::zero());
2823        let h2 = MonitorId::from_properties(0, &huge, LayoutPoint::zero(), LayoutSize::zero());
2824        assert_eq!(h1, h2);
2825        assert_ne!(
2826            h1.hash,
2827            MonitorId::from_properties(0, "x", LayoutPoint::zero(), LayoutSize::zero()).hash
2828        );
2829    }
2830
2831    // ---------------------------------------------------------------------
2832    // WindowFlags predicates / getters
2833    // ---------------------------------------------------------------------
2834
2835    #[test]
2836    fn window_flags_type_predicates_are_mutually_exclusive() {
2837        for (ty, menu, tooltip, dialog) in [
2838            (WindowType::Normal, false, false, false),
2839            (WindowType::Menu, true, false, false),
2840            (WindowType::Tooltip, false, true, false),
2841            (WindowType::Dialog, false, false, true),
2842        ] {
2843            let f = WindowFlags {
2844                window_type: ty,
2845                ..WindowFlags::default()
2846            };
2847            assert_eq!(f.is_menu_window(), menu);
2848            assert_eq!(f.is_tooltip_window(), tooltip);
2849            assert_eq!(f.is_dialog_window(), dialog);
2850            // At most one classification can ever be true at once.
2851            let count = u8::from(f.is_menu_window())
2852                + u8::from(f.is_tooltip_window())
2853                + u8::from(f.is_dialog_window());
2854            assert!(count <= 1);
2855        }
2856    }
2857
2858    #[test]
2859    fn window_flags_bool_getters_mirror_their_fields() {
2860        // Default: focused, no close request, no CSD.
2861        let d = WindowFlags::default();
2862        assert!(d.window_has_focus());
2863        assert!(!d.is_close_requested());
2864        assert!(!d.has_csd());
2865        assert_eq!(
2866            d.use_native_menus(),
2867            cfg!(any(target_os = "windows", target_os = "macos"))
2868        );
2869        assert_eq!(
2870            d.use_native_context_menus(),
2871            cfg!(any(target_os = "windows", target_os = "macos"))
2872        );
2873
2874        // Every getter is a pure mirror of its field, in both states.
2875        for b in [false, true] {
2876            let f = WindowFlags {
2877                has_focus: b,
2878                close_requested: b,
2879                has_decorations: b,
2880                use_native_menus: b,
2881                use_native_context_menus: b,
2882                ..WindowFlags::default()
2883            };
2884            assert_eq!(f.window_has_focus(), b);
2885            assert_eq!(f.is_close_requested(), b);
2886            assert_eq!(f.has_csd(), b);
2887            assert_eq!(f.use_native_menus(), b);
2888            assert_eq!(f.use_native_context_menus(), b);
2889        }
2890    }
2891
2892    // ---------------------------------------------------------------------
2893    // StringPairVec: get_key / get_key_mut / insert_kv
2894    // ---------------------------------------------------------------------
2895
2896    #[test]
2897    fn get_key_on_empty_vec_is_none() {
2898        let empty = StringPairVec::new();
2899        assert!(empty.get_key("").is_none());
2900        assert!(empty.get_key("anything").is_none());
2901
2902        let mut empty_mut = StringPairVec::new();
2903        assert!(empty_mut.get_key_mut("").is_none());
2904        assert!(empty_mut.get_key_mut("anything").is_none());
2905    }
2906
2907    #[test]
2908    fn get_key_valid_minimal_and_missing() {
2909        let v = StringPairVec::from_vec(vec![pair("WM_CLASS", "azul")]);
2910        assert_eq!(
2911            v.get_key("WM_CLASS").map(AzString::as_str),
2912            Some("azul"),
2913            "positive control"
2914        );
2915        assert!(v.get_key("wm_class").is_none(), "lookup is case-sensitive");
2916        assert!(v.get_key("WM_CLAS").is_none());
2917        assert!(v.get_key("WM_CLASS ").is_none(), "no trimming is performed");
2918        assert!(v.get_key(" WM_CLASS").is_none());
2919        assert!(v.get_key("WM_CLASS;garbage").is_none());
2920    }
2921
2922    #[test]
2923    fn get_key_handles_garbage_whitespace_and_boundary_numbers() {
2924        let v = StringPairVec::from_vec(vec![
2925            pair("", "empty-key"),
2926            pair("   ", "spaces"),
2927            pair("\t\n", "tabs"),
2928            pair("0", "zero"),
2929            pair("-0", "neg-zero"),
2930            pair("9223372036854775807", "i64-max"),
2931            pair("NaN", "nan"),
2932            pair("inf", "inf"),
2933        ]);
2934
2935        // Empty and whitespace-only keys are ordinary keys - looked up verbatim.
2936        assert_eq!(v.get_key("").map(AzString::as_str), Some("empty-key"));
2937        assert_eq!(v.get_key("   ").map(AzString::as_str), Some("spaces"));
2938        assert_eq!(v.get_key("\t\n").map(AzString::as_str), Some("tabs"));
2939
2940        // Numeric-looking keys are compared as strings: "0" and "-0" are distinct.
2941        assert_eq!(v.get_key("0").map(AzString::as_str), Some("zero"));
2942        assert_eq!(v.get_key("-0").map(AzString::as_str), Some("neg-zero"));
2943        assert_eq!(
2944            v.get_key("9223372036854775807").map(AzString::as_str),
2945            Some("i64-max")
2946        );
2947        assert_eq!(v.get_key("NaN").map(AzString::as_str), Some("nan"));
2948        assert_eq!(v.get_key("inf").map(AzString::as_str), Some("inf"));
2949        assert!(v.get_key("nan").is_none());
2950
2951        // Random non-grammar bytes / control chars / deep bracket nesting: None, no panic.
2952        assert!(v.get_key("\u{0}\u{1}\u{7f}\\x\"';--").is_none());
2953        assert!(v.get_key(&"[".repeat(10_000)).is_none());
2954        assert!(v.get_key(&"{\"a\":".repeat(10_000)).is_none());
2955    }
2956
2957    #[test]
2958    fn get_key_handles_unicode_without_panicking() {
2959        let v = StringPairVec::from_vec(vec![
2960            pair("\u{1F600}", "grin"),
2961            pair("é", "precomposed"),
2962            pair("日本語", "jp"),
2963        ]);
2964        assert_eq!(v.get_key("\u{1F600}").map(AzString::as_str), Some("grin"));
2965        assert_eq!(v.get_key("日本語").map(AzString::as_str), Some("jp"));
2966        // No unicode normalization: decomposed "e" + combining acute != "é".
2967        assert_eq!(v.get_key("é").map(AzString::as_str), Some("precomposed"));
2968        assert!(v.get_key("e\u{301}").is_none());
2969        // A prefix of a multi-byte key must not match (no byte-slicing bugs).
2970        assert!(v.get_key("日本").is_none());
2971    }
2972
2973    #[test]
2974    fn get_key_handles_extremely_long_input() {
2975        let huge = "k".repeat(1_000_000);
2976        let mut v = StringPairVec::from_vec(vec![pair("short", "1")]);
2977
2978        // Searching for a 1M-char key that is not present: linear, terminates.
2979        assert!(v.get_key(&huge).is_none());
2980
2981        // ...and one that IS present.
2982        v.push(AzStringPair {
2983            key: huge.as_str().into(),
2984            value: "big".into(),
2985        });
2986        assert_eq!(v.get_key(&huge).map(AzString::as_str), Some("big"));
2987        // Off-by-one on a 1M-char key must not match.
2988        assert!(v.get_key(&"k".repeat(999_999)).is_none());
2989        assert!(v.get_key(&"k".repeat(1_000_001)).is_none());
2990    }
2991
2992    #[test]
2993    fn get_key_returns_first_of_duplicate_keys() {
2994        let v = StringPairVec::from_vec(vec![
2995            pair("dup", "first"),
2996            pair("dup", "second"),
2997            pair("dup", "third"),
2998        ]);
2999        assert_eq!(v.get_key("dup").map(AzString::as_str), Some("first"));
3000    }
3001
3002    #[test]
3003    fn get_key_mut_mutates_in_place() {
3004        let mut v = StringPairVec::from_vec(vec![pair("a", "1"), pair("b", "2")]);
3005        {
3006            let entry = v.get_key_mut("b").expect("b is present");
3007            entry.value = "changed".into();
3008        }
3009        assert_eq!(v.get_key("b").map(AzString::as_str), Some("changed"));
3010        assert_eq!(v.get_key("a").map(AzString::as_str), Some("1"));
3011        assert!(v.get_key_mut("missing").is_none());
3012        assert_eq!(v.len(), 2, "get_key_mut must not add entries");
3013
3014        // Mutating the KEY through get_key_mut is possible and re-targets lookups.
3015        {
3016            let entry = v.get_key_mut("a").expect("a is present");
3017            entry.key = "z".into();
3018        }
3019        assert!(v.get_key("a").is_none());
3020        assert_eq!(v.get_key("z").map(AzString::as_str), Some("1"));
3021    }
3022
3023    #[test]
3024    fn insert_kv_updates_existing_and_appends_new() {
3025        let mut v = StringPairVec::new();
3026        v.insert_kv("k", "v1");
3027        assert_eq!(v.len(), 1);
3028        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v1"));
3029
3030        // Re-inserting the same key overwrites in place instead of appending.
3031        v.insert_kv("k", "v2");
3032        assert_eq!(v.len(), 1, "insert_kv must not duplicate an existing key");
3033        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v2"));
3034
3035        // A different key appends.
3036        v.insert_kv("other", "x");
3037        assert_eq!(v.len(), 2);
3038        assert_eq!(v.get_key("k").map(AzString::as_str), Some("v2"));
3039        assert_eq!(v.get_key("other").map(AzString::as_str), Some("x"));
3040
3041        // Repeated inserts of the same key never grow the vec.
3042        for i in 0..100 {
3043            v.insert_kv(String::from("k"), format!("gen{i}"));
3044        }
3045        assert_eq!(v.len(), 2);
3046        assert_eq!(v.get_key("k").map(AzString::as_str), Some("gen99"));
3047    }
3048
3049    #[test]
3050    fn insert_kv_accepts_hostile_keys_and_values() {
3051        let mut v = StringPairVec::new();
3052        v.insert_kv("", "");
3053        assert_eq!(v.len(), 1);
3054        assert_eq!(v.get_key("").map(AzString::as_str), Some(""));
3055
3056        v.insert_kv("\u{1F600}", "😀");
3057        assert_eq!(v.get_key("\u{1F600}").map(AzString::as_str), Some("😀"));
3058
3059        v.insert_kv("   ", "\t\n");
3060        assert_eq!(v.get_key("   ").map(AzString::as_str), Some("\t\n"));
3061
3062        // Very long key + value: no hang, and the update path still finds it.
3063        let huge_key = "K".repeat(100_000);
3064        let huge_val = "V".repeat(100_000);
3065        v.insert_kv(huge_key.clone(), huge_val.clone());
3066        let before = v.len();
3067        assert_eq!(
3068            v.get_key(&huge_key).map(AzString::as_str),
3069            Some(huge_val.as_str())
3070        );
3071        v.insert_kv(huge_key.clone(), String::from("small"));
3072        assert_eq!(v.len(), before, "long key must hit the update path");
3073        assert_eq!(v.get_key(&huge_key).map(AzString::as_str), Some("small"));
3074    }
3075
3076    #[test]
3077    fn insert_kv_only_updates_the_first_of_pre_existing_duplicates() {
3078        // Duplicates can only arrive via push()/from_vec(); insert_kv updates the
3079        // first match (get_key_mut semantics) and leaves the shadowed one stale.
3080        let mut v = StringPairVec::from_vec(vec![pair("dup", "first"), pair("dup", "second")]);
3081        v.insert_kv("dup", "updated");
3082        assert_eq!(v.len(), 2, "no new entry is appended");
3083        assert_eq!(v.get_key("dup").map(AzString::as_str), Some("updated"));
3084        assert_eq!(
3085            v.get(1).expect("second entry still present").value.as_str(),
3086            "second",
3087            "the shadowed duplicate is left untouched"
3088        );
3089    }
3090
3091    // ---------------------------------------------------------------------
3092    // WindowSize getters (numeric saturation)
3093    // ---------------------------------------------------------------------
3094
3095    #[test]
3096    fn window_size_get_logical_size_is_the_identity() {
3097        for dims in [
3098            LogicalSize::zero(),
3099            LogicalSize::new(640.0, 480.0),
3100            LogicalSize::new(-1.0, -2.0),
3101            LogicalSize::new(f32::MAX, f32::MIN),
3102            LogicalSize::new(f32::INFINITY, f32::MIN_POSITIVE),
3103        ] {
3104            let ws = WindowSize {
3105                dimensions: dims,
3106                ..WindowSize::default()
3107            };
3108            assert_eq!(ws.get_logical_size(), dims);
3109        }
3110        // Default is the documented 640x480 @ 96 DPI.
3111        let d = WindowSize::default();
3112        assert_eq!(d.get_logical_size(), LogicalSize::new(640.0, 480.0));
3113        assert_eq!(d.dpi, 96);
3114    }
3115
3116    #[test]
3117    fn window_size_get_layout_size_rounds_half_away_from_zero() {
3118        for (w, h, ew, eh) in [
3119            (0.0f32, 0.0f32, 0isize, 0isize),
3120            (640.0, 480.0, 640, 480),
3121            (640.4, 480.4, 640, 480),
3122            (640.6, 480.6, 641, 481),
3123            (640.5, 639.5, 641, 640),
3124            (-0.5, -1.5, -1, -2),
3125        ] {
3126            let ws = WindowSize {
3127                dimensions: LogicalSize::new(w, h),
3128                ..WindowSize::default()
3129            };
3130            assert_eq!(ws.get_layout_size(), LayoutSize::new(ew, eh), "{w}x{h}");
3131        }
3132    }
3133
3134    #[test]
3135    fn window_size_get_layout_size_saturates_on_non_finite() {
3136        // `as isize` saturates: NaN -> 0, +inf -> isize::MAX, -inf -> isize::MIN.
3137        let nan = WindowSize {
3138            dimensions: LogicalSize::new(f32::NAN, f32::NAN),
3139            ..WindowSize::default()
3140        };
3141        assert_eq!(nan.get_layout_size(), LayoutSize::new(0, 0));
3142
3143        let inf = WindowSize {
3144            dimensions: LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
3145            ..WindowSize::default()
3146        };
3147        assert_eq!(
3148            inf.get_layout_size(),
3149            LayoutSize::new(isize::MAX, isize::MIN)
3150        );
3151
3152        let max = WindowSize {
3153            dimensions: LogicalSize::new(f32::MAX, f32::MIN),
3154            ..WindowSize::default()
3155        };
3156        let ls = max.get_layout_size();
3157        assert!(ls.width > 0 && ls.height < 0, "sign is preserved: {ls:?}");
3158    }
3159
3160    #[test]
3161    fn window_size_get_physical_size_saturates_instead_of_wrapping() {
3162        // Negative logical sizes clamp to 0 (u32 cast drops the sign).
3163        let neg = WindowSize {
3164            dimensions: LogicalSize::new(-100.0, -0.4),
3165            ..WindowSize::default()
3166        };
3167        assert_eq!(neg.get_physical_size(), PhysicalSize::new(0, 0));
3168
3169        // NaN -> 0, +inf -> u32::MAX (saturating float->int cast, no UB).
3170        let nan = WindowSize {
3171            dimensions: LogicalSize::new(f32::NAN, f32::INFINITY),
3172            ..WindowSize::default()
3173        };
3174        assert_eq!(nan.get_physical_size(), PhysicalSize::new(0, u32::MAX));
3175
3176        // f32::MAX at 4x scale overflows u32 -> saturates, never wraps to a small value.
3177        let huge = WindowSize {
3178            dimensions: LogicalSize::new(f32::MAX, f32::MAX),
3179            dpi: 384,
3180            ..WindowSize::default()
3181        };
3182        assert_eq!(
3183            huge.get_physical_size(),
3184            PhysicalSize::new(u32::MAX, u32::MAX)
3185        );
3186
3187        // The normal path: 96 DPI is 1:1, 192 DPI doubles.
3188        let normal = WindowSize::default();
3189        assert_eq!(normal.get_physical_size(), PhysicalSize::new(640, 480));
3190        let retina = WindowSize {
3191            dpi: 192,
3192            ..WindowSize::default()
3193        };
3194        assert_eq!(retina.get_physical_size(), PhysicalSize::new(1280, 960));
3195    }
3196
3197    #[test]
3198    fn window_size_get_hidpi_factor_is_never_zero_or_negative() {
3199        // A 0.0 factor would divide-by-zero in to_logical(); the getter guards dpi == 0.
3200        for dpi in [
3201            0u32,
3202            1,
3203            47,
3204            48,
3205            95,
3206            96,
3207            97,
3208            120,
3209            144,
3210            192,
3211            384,
3212            u32::from(u16::MAX),
3213            u32::MAX,
3214        ] {
3215            let ws = WindowSize {
3216                dpi,
3217                ..WindowSize::default()
3218            };
3219            let f = ws.get_hidpi_factor().inner.get();
3220            assert!(
3221                f.is_finite() && f > 0.0,
3222                "dpi {dpi} produced a non-positive / non-finite scale factor: {f}"
3223            );
3224        }
3225
3226        // Exactly representable factors must be exact (no quantization drift).
3227        for (dpi, expected) in [(0u32, 1.0f32), (96, 1.0), (144, 1.5), (192, 2.0), (384, 4.0)] {
3228            let ws = WindowSize {
3229                dpi,
3230                ..WindowSize::default()
3231            };
3232            assert_eq!(ws.get_hidpi_factor().inner.get(), expected, "dpi {dpi}");
3233        }
3234
3235        // Non-representable factors stay within FloatValue's 1/1000 quantization.
3236        let odd = WindowSize {
3237            dpi: 100,
3238            ..WindowSize::default()
3239        };
3240        let f = odd.get_hidpi_factor().inner.get();
3241        assert!((f - 100.0 / 96.0).abs() < 0.002, "dpi 100 -> {f}");
3242    }
3243
3244    // ---------------------------------------------------------------------
3245    // VirtualKeyCode: from_u32 / get_lowercase
3246    // ---------------------------------------------------------------------
3247
3248    #[test]
3249    fn virtual_keycode_from_u32_roundtrips_every_discriminant() {
3250        for v in 0..=LAST_VK {
3251            let vk = VirtualKeyCode::from_u32(v)
3252                .unwrap_or_else(|| panic!("discriminant {v} is missing from the from_u32 table"));
3253            assert_eq!(vk as u32, v, "from_u32({v}) does not round-trip");
3254        }
3255        // First and last declared variants anchor the table.
3256        assert_eq!(VirtualKeyCode::Key1 as u32, 0);
3257        assert_eq!(VirtualKeyCode::Cut as u32, LAST_VK);
3258    }
3259
3260    #[test]
3261    fn virtual_keycode_from_u32_rejects_out_of_range() {
3262        for v in [
3263            LAST_VK + 1,
3264            LAST_VK + 2,
3265            255,
3266            256,
3267            1024,
3268            i32::MAX as u32,
3269            u32::MAX - 1,
3270            u32::MAX,
3271        ] {
3272            assert_eq!(
3273                VirtualKeyCode::from_u32(v),
3274                None,
3275                "{v} must not decode to a keycode"
3276            );
3277        }
3278    }
3279
3280    #[test]
3281    fn virtual_keycode_get_lowercase_never_panics_and_maps_letters_and_digits() {
3282        // Exhaustive: no keycode may panic, and any produced char is ASCII.
3283        for v in 0..=LAST_VK {
3284            let vk = VirtualKeyCode::from_u32(v).expect("discriminant in range");
3285            if let Some(c) = vk.get_lowercase() {
3286                assert!(c.is_ascii(), "{vk:?} produced a non-ASCII char {c:?}");
3287                assert!(!c.is_ascii_uppercase(), "{vk:?} must yield lowercase");
3288            }
3289        }
3290
3291        // Letters A..Z are discriminants 10..=35 and map to 'a'..='z'.
3292        for (i, expected) in ('a'..='z').enumerate() {
3293            let vk = VirtualKeyCode::from_u32(10 + i as u32).expect("letter range");
3294            assert_eq!(vk.get_lowercase(), Some(expected));
3295        }
3296
3297        // Digits: both the top row and the numpad map to the same char.
3298        for (top, pad, c) in [
3299            (VirtualKeyCode::Key0, VirtualKeyCode::Numpad0, '0'),
3300            (VirtualKeyCode::Key1, VirtualKeyCode::Numpad1, '1'),
3301            (VirtualKeyCode::Key5, VirtualKeyCode::Numpad5, '5'),
3302            (VirtualKeyCode::Key9, VirtualKeyCode::Numpad9, '9'),
3303        ] {
3304            assert_eq!(top.get_lowercase(), Some(c));
3305            assert_eq!(pad.get_lowercase(), Some(c));
3306        }
3307
3308        // Punctuation that IS mapped.
3309        assert_eq!(VirtualKeyCode::Minus.get_lowercase(), Some('-'));
3310        assert_eq!(VirtualKeyCode::Period.get_lowercase(), Some('.'));
3311        assert_eq!(VirtualKeyCode::Slash.get_lowercase(), Some('/'));
3312        assert_eq!(VirtualKeyCode::Caret.get_lowercase(), Some('^'));
3313
3314        // Non-character keys have no lowercase form.
3315        for vk in [
3316            VirtualKeyCode::LShift,
3317            VirtualKeyCode::RControl,
3318            VirtualKeyCode::Escape,
3319            VirtualKeyCode::F12,
3320            VirtualKeyCode::Space,
3321            VirtualKeyCode::Return,
3322            VirtualKeyCode::Back,
3323        ] {
3324            assert_eq!(vk.get_lowercase(), None, "{vk:?}");
3325        }
3326    }
3327
3328    // ---------------------------------------------------------------------
3329    // WindowIcon::get_key + key-only Eq/Ord/Hash
3330    // ---------------------------------------------------------------------
3331
3332    #[test]
3333    fn window_icon_get_key_returns_the_stored_key() {
3334        let small_key = IconKey::new();
3335        let large_key = IconKey::new();
3336
3337        let small = WindowIcon::Small(SmallWindowIconBytes {
3338            key: small_key,
3339            rgba_bytes: vec![0u8; 16 * 16 * 4].into(),
3340        });
3341        let large = WindowIcon::Large(LargeWindowIconBytes {
3342            key: large_key,
3343            rgba_bytes: vec![255u8; 32 * 32 * 4].into(),
3344        });
3345
3346        assert_eq!(small.get_key(), small_key);
3347        assert_eq!(large.get_key(), large_key);
3348
3349        // Empty payloads are legal and must not panic.
3350        let empty = WindowIcon::Small(SmallWindowIconBytes {
3351            key: small_key,
3352            rgba_bytes: vec![].into(),
3353        });
3354        assert_eq!(empty.get_key(), small_key);
3355    }
3356
3357    #[test]
3358    fn window_icon_identity_is_the_key_alone() {
3359        // The whole point of IconKey: diff the key, not the bytes. Two icons with
3360        // the same key compare equal even though their pixels differ.
3361        let key = IconKey::new();
3362        let a = WindowIcon::Small(SmallWindowIconBytes {
3363            key,
3364            rgba_bytes: vec![0u8; 4].into(),
3365        });
3366        let b = WindowIcon::Small(SmallWindowIconBytes {
3367            key,
3368            rgba_bytes: vec![7u8; 1024].into(),
3369        });
3370        // ...even across the Small/Large variants.
3371        let c = WindowIcon::Large(LargeWindowIconBytes {
3372            key,
3373            rgba_bytes: vec![9u8; 32 * 32 * 4].into(),
3374        });
3375
3376        assert_eq!(a, b);
3377        assert_eq!(a, c);
3378        assert_eq!(a.cmp(&c), Ordering::Equal);
3379        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3380        // Hash must agree with Eq, or icons break as BTreeMap/HashMap keys.
3381        assert_eq!(hash_of(&a), hash_of(&b));
3382        assert_eq!(hash_of(&a), hash_of(&c));
3383
3384        // Different keys: unequal, and ordered by key.
3385        let older = WindowIcon::Small(SmallWindowIconBytes {
3386            key,
3387            rgba_bytes: vec![0u8; 4].into(),
3388        });
3389        let newer = WindowIcon::Small(SmallWindowIconBytes {
3390            key: IconKey::new(),
3391            rgba_bytes: vec![0u8; 4].into(),
3392        });
3393        assert_ne!(older, newer);
3394        assert_eq!(older.cmp(&newer), Ordering::Less);
3395    }
3396
3397    // ---------------------------------------------------------------------
3398    // UpdateFocusWarning: Display
3399    // ---------------------------------------------------------------------
3400
3401    #[test]
3402    fn update_focus_warning_display_is_non_empty_for_every_variant() {
3403        let dom = format!("{}", UpdateFocusWarning::FocusInvalidDomId(DomId::ROOT_ID));
3404        assert!(dom.contains("invalid ID"), "{dom}");
3405        assert!(!dom.is_empty());
3406
3407        let node = format!(
3408            "{}",
3409            UpdateFocusWarning::FocusInvalidNodeId(NodeHierarchyItemId::NONE)
3410        );
3411        assert!(node.contains("invalid ID"), "{node}");
3412
3413        // Edge values: a zero DomId, a raw-encoded huge node id, an empty CssPath.
3414        let huge = format!(
3415            "{}",
3416            UpdateFocusWarning::FocusInvalidNodeId(NodeHierarchyItemId::from_raw(usize::MAX))
3417        );
3418        assert!(!huge.is_empty());
3419
3420        let path = format!(
3421            "{}",
3422            UpdateFocusWarning::CouldNotFindFocusNode(CssPath::default())
3423        );
3424        assert!(
3425            path.starts_with("Could not find focus node for path:"),
3426            "{path}"
3427        );
3428    }
3429}