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