1
//! Callback handling for layout events
2
//!
3
//! This module provides the CallbackInfo struct and related types for handling
4
//! UI callbacks. Callbacks need access to layout information (node sizes, positions,
5
//! hierarchy), which is why this module lives in azul-layout instead of azul-core.
6

            
7
// Re-export callback macro from azul-core
8
use alloc::{
9
    boxed::Box,
10
    collections::{btree_map::BTreeMap, VecDeque},
11
    sync::Arc,
12
    vec::Vec,
13
};
14

            
15
#[cfg(feature = "std")]
16
use std::sync::Mutex;
17

            
18
use azul_core::{
19
    animation::UpdateImageType,
20
    callbacks::{CoreCallback, FocusTarget, FocusTargetPath, HidpiAdjustedBounds, Update},
21
    dom::{DomId, DomIdVec, DomNodeId, IdOrClass, NodeId, NodeType},
22
    geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, OptionCursorNodePosition, OptionScreenPosition, OptionDragDelta, CursorNodePosition, ScreenPosition, DragDelta},
23
    gl::OptionGlContextPtr,
24
    gpu::GpuValueCache,
25
    hit_test::ScrollPosition,
26
    id::NodeId as CoreNodeId,
27
    impl_callback,
28
    menu::Menu,
29
    refany::{OptionRefAny, RefAny},
30
    resources::{ImageCache, ImageMask, ImageRef, LoadedFont, LoadedFontVec, RendererResources},
31
    selection::{Selection, SelectionRange, SelectionRangeVec, SelectionState, TextCursor},
32
    styled_dom::{NodeHierarchyItemId, NodeHierarchyItemIdVec, StyledDom},
33
    task::{self, GetSystemTimeCallback, Instant, ThreadId, ThreadIdVec, TimerId, TimerIdVec},
34
    window::{KeyboardState, Monitor, MonitorVec, MouseState, OptionMonitor, RawWindowHandle, WindowFlags, WindowSize},
35
    FastBTreeSet, OrderedMap,
36
};
37
use azul_css::{
38
    css::CssPath,
39
    props::{
40
        basic::FontRef,
41
        property::{CssProperty, CssPropertyType, CssPropertyVec},
42
    },
43
    system::SystemStyle,
44
    AzString, OptionU8Vec, StringVec, U8Vec,
45
};
46
use rust_fontconfig::FcFontCache;
47

            
48
#[cfg(feature = "icu")]
49
use crate::icu::{
50
    FormatLength, IcuDate, IcuDateTime, IcuLocalizerHandle, IcuResult,
51
    IcuStringVec, IcuTime, ListType, PluralCategory,
52
};
53

            
54
use crate::{
55
    hit_test::FullHitTest,
56
    managers::{
57
        drag_drop::DragDropManager,
58
        file_drop::FileDropManager,
59
        focus_cursor::FocusManager,
60
        gesture::{GestureAndDragManager, InputSample, PenState},
61
        gpu_state::GpuStateManager,
62
        hover::{HoverManager, InputPointId},
63
        virtual_view::VirtualViewManager,
64
        scroll_state::{AnimatedScrollState, ScrollManager},
65
        selection::ClipboardContent,
66
        text_input::{PendingTextEdit, TextInputManager},
67
        undo_redo::{UndoRedoManager, UndoableOperation},
68
    },
69
    text3::cache::{TextShapingCache as TextLayoutCache, UnifiedLayout},
70
    thread::{CreateThreadCallback, Thread},
71
    timer::Timer,
72
    window::{DomLayoutResult, LayoutWindow},
73
    window_state::{FullWindowState, WindowCreateOptions},
74
};
75

            
76
use azul_css::{impl_option, impl_option_inner};
77

            
78
// ============================================================================
79
// FFI-safe wrapper types for tuple returns
80
// ============================================================================
81

            
82
/// FFI-safe wrapper for pen tilt angles (x_tilt, y_tilt) in degrees
83
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
84
#[repr(C)]
85
pub struct PenTilt {
86
    /// X-axis tilt angle in degrees (-90 to 90)
87
    pub x_tilt: f32,
88
    /// Y-axis tilt angle in degrees (-90 to 90)
89
    pub y_tilt: f32,
90
}
91

            
92
impl From<(f32, f32)> for PenTilt {
93
    fn from((x, y): (f32, f32)) -> Self {
94
        Self {
95
            x_tilt: x,
96
            y_tilt: y,
97
        }
98
    }
99
}
100

            
101
impl_option!(
102
    PenTilt,
103
    OptionPenTilt,
104
    [Debug, Clone, Copy, PartialEq, PartialOrd]
105
);
106

            
107
/// FFI-safe wrapper for select-all result (full_text, selected_range)
108
#[derive(Debug, Clone, PartialEq)]
109
#[repr(C)]
110
pub struct SelectAllResult {
111
    /// The full text content of the node
112
    pub full_text: AzString,
113
    /// The range that would be selected
114
    pub selection_range: SelectionRange,
115
}
116

            
117
impl From<(alloc::string::String, SelectionRange)> for SelectAllResult {
118
    fn from((text, range): (alloc::string::String, SelectionRange)) -> Self {
119
        Self {
120
            full_text: text.into(),
121
            selection_range: range,
122
        }
123
    }
124
}
125

            
126
impl_option!(
127
    SelectAllResult,
128
    OptionSelectAllResult,
129
    copy = false,
130
    [Debug, Clone, PartialEq]
131
);
132

            
133
/// FFI-safe wrapper for delete inspection result (range_to_delete, deleted_text)
134
#[derive(Debug, Clone, PartialEq)]
135
#[repr(C)]
136
pub struct DeleteResult {
137
    /// The range that would be deleted
138
    pub range_to_delete: SelectionRange,
139
    /// The text that would be deleted
140
    pub deleted_text: AzString,
141
}
142

            
143
impl From<(SelectionRange, alloc::string::String)> for DeleteResult {
144
    fn from((range, text): (SelectionRange, alloc::string::String)) -> Self {
145
        Self {
146
            range_to_delete: range,
147
            deleted_text: text.into(),
148
        }
149
    }
150
}
151

            
152
impl_option!(
153
    DeleteResult,
154
    OptionDeleteResult,
155
    copy = false,
156
    [Debug, Clone, PartialEq]
157
);
158

            
159
/// Represents a change made by a callback that will be applied after the callback returns
160
///
161
/// This transaction-based system provides:
162
/// - Clear separation between read-only queries and modifications
163
/// - Atomic application of all changes
164
/// - Easy debugging and logging of callback actions
165
/// - Future extensibility for new change types
166
#[derive(Debug, Clone)]
167
pub enum CallbackChange {
168
    // Window State Changes
169
    /// Modify the window state (size, position, title, etc.)
170
    ModifyWindowState { state: FullWindowState },
171
    /// Inject a platform-native gesture-recognizer result into the
172
    /// in-process `GestureAndDragManager`. Read by the next
173
    /// `detect_long_press` / `detect_swipe_direction` / `detect_pinch` /
174
    /// `detect_rotation` / `detect_double_click` call, then cleared.
175
    InjectNativeGesture {
176
        gesture: crate::managers::gesture::NativeGestureEvent,
177
    },
178
    /// Queue multiple window state changes to be applied in sequence across frames.
179
    /// This is needed for simulating clicks (mouse down -> wait -> mouse up) where each
180
    /// state change needs to trigger separate event processing.
181
    QueueWindowStateSequence { states: Vec<FullWindowState> },
182
    /// Create a new window
183
    CreateNewWindow { options: WindowCreateOptions },
184
    /// Close the current window (via Update::CloseWindow return value, tracked here for logging)
185
    CloseWindow,
186

            
187
    // Focus Management
188
    /// Change keyboard focus to a specific node or clear focus
189
    SetFocusTarget { target: FocusTarget },
190

            
191
    // Event Propagation Control
192
    /// Stop event from propagating to parent nodes (W3C stopPropagation).
193
    /// Remaining handlers on the *current* node still fire, but no handlers
194
    /// on ancestor / descendant nodes in subsequent phases.
195
    StopPropagation,
196
    /// Stop event propagation immediately (W3C stopImmediatePropagation).
197
    /// No further handlers fire - not even remaining handlers on the same node.
198
    StopImmediatePropagation,
199
    /// Prevent default browser behavior (e.g., block text input from being applied)
200
    PreventDefault,
201

            
202
    // Timer Management
203
    /// Add a new timer to the window
204
    AddTimer { timer_id: TimerId, timer: Timer },
205
    /// Remove an existing timer
206
    RemoveTimer { timer_id: TimerId },
207

            
208
    // Thread Management
209
    /// Add a new background thread
210
    AddThread { thread_id: ThreadId, thread: Thread },
211
    /// Remove an existing thread
212
    RemoveThread { thread_id: ThreadId },
213

            
214
    // Content Modifications
215
    /// Change the text content of a node
216
    ChangeNodeText { node_id: DomNodeId, text: AzString },
217
    /// Change the image of a node
218
    ChangeNodeImage {
219
        dom_id: DomId,
220
        node_id: NodeId,
221
        image: ImageRef,
222
        update_type: UpdateImageType,
223
    },
224
    /// Re-render an image callback (for resize/animation)
225
    /// This triggers re-invocation of the RenderImageCallback
226
    UpdateImageCallback { dom_id: DomId, node_id: NodeId },
227
    /// Re-render ALL image callbacks across all DOMs.
228
    ///
229
    /// This is the most efficient way to update animated GL textures:
230
    /// it triggers only texture re-rendering without DOM rebuild or
231
    /// display list resubmission. Used by timer callbacks that need
232
    /// to update OpenGL textures every frame.
233
    UpdateAllImageCallbacks,
234
    /// Trigger re-rendering of a VirtualView with a new DOM
235
    /// This forces the VirtualView to call its callback and update the display list
236
    UpdateVirtualView { dom_id: DomId, node_id: NodeId },
237
    /// Change the image mask of a node
238
    ChangeNodeImageMask {
239
        dom_id: DomId,
240
        node_id: NodeId,
241
        mask: ImageMask,
242
    },
243
    /// Change CSS properties of a node
244
    ChangeNodeCssProperties {
245
        dom_id: DomId,
246
        node_id: NodeId,
247
        properties: CssPropertyVec,
248
    },
249
    /// Override CSS properties on a node via the user-override channel
250
    /// (`CssPropertyCache::user_overridden_properties`). Unlike
251
    /// `ChangeNodeCssProperties`, this does not mutate the node's static
252
    /// `css_props` - the override layer is read at higher priority by the
253
    /// property resolution pipeline, so animating a handful of properties
254
    /// per frame stays cheap. Passing `CssProperty::Initial` for a property
255
    /// removes any prior override for that type on the same node.
256
    OverrideNodeCssProperties {
257
        dom_id: DomId,
258
        node_id: NodeId,
259
        properties: CssPropertyVec,
260
    },
261

            
262
    // Scroll Management
263
    /// Scroll a node to a specific position
264
    ScrollTo {
265
        dom_id: DomId,
266
        node_id: NodeHierarchyItemId,
267
        position: LogicalPosition,
268
        /// When true, skip clamping to [0, max_scroll] bounds.
269
        /// Used by the scroll physics timer for rubber-banding/overscroll.
270
        unclamped: bool,
271
    },
272
    /// Scroll a node into view (W3C scrollIntoView API)
273
    /// The scroll adjustments are calculated and applied when the change is processed
274
    ScrollIntoView {
275
        node_id: DomNodeId,
276
        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
277
    },
278

            
279
    // Image Cache Management
280
    /// Add an image to the image cache
281
    AddImageToCache { id: AzString, image: ImageRef },
282
    /// Remove an image from the image cache
283
    RemoveImageFromCache { id: AzString },
284

            
285
    // Font Cache Management
286
    /// Reload system fonts (expensive operation)
287
    ReloadSystemFonts,
288

            
289
    // Menu Management
290
    /// Open a context menu or dropdown menu
291
    /// Whether it's native or fallback depends on window.state.flags.use_native_context_menus
292
    OpenMenu {
293
        menu: Menu,
294
        /// Optional position override (if None, uses menu.position)
295
        position: Option<LogicalPosition>,
296
    },
297

            
298
    // Tooltip Management
299
    /// Show a tooltip at a specific position
300
    ///
301
    /// Platform-specific implementation:
302
    /// - Windows: Uses native tooltip window (TOOLTIPS_CLASS)
303
    /// - macOS: Uses NSPopover or custom NSWindow with tooltip styling
304
    /// - X11: Creates transient window with _NET_WM_WINDOW_TYPE_TOOLTIP
305
    /// - Wayland: Creates surface with zwlr_layer_shell_v1 (overlay layer)
306
    ShowTooltip {
307
        text: AzString,
308
        position: LogicalPosition,
309
    },
310
    /// Hide the currently displayed tooltip
311
    HideTooltip,
312

            
313
    // Text Editing
314
    /// Insert text at the current cursor position or replace selection
315
    InsertText {
316
        dom_id: DomId,
317
        node_id: NodeId,
318
        text: AzString,
319
    },
320
    /// Delete text backward (backspace) at cursor
321
    DeleteBackward { dom_id: DomId, node_id: NodeId },
322
    /// Delete text forward (delete key) at cursor
323
    DeleteForward { dom_id: DomId, node_id: NodeId },
324
    /// Move cursor to a specific position
325
    MoveCursor {
326
        dom_id: DomId,
327
        node_id: NodeId,
328
        cursor: TextCursor,
329
    },
330
    /// Set text selection range
331
    SetSelection {
332
        dom_id: DomId,
333
        node_id: NodeId,
334
        selection: Selection,
335
    },
336
    /// Set/override the text changeset for the current text input operation
337
    /// This allows callbacks to modify what text will be inserted during text input events
338
    SetTextChangeset { changeset: PendingTextEdit },
339

            
340
    // Cursor Movement Operations
341
    /// Move cursor left (arrow left)
342
    MoveCursorLeft {
343
        dom_id: DomId,
344
        node_id: NodeId,
345
        extend_selection: bool,
346
    },
347
    /// Move cursor right (arrow right)
348
    MoveCursorRight {
349
        dom_id: DomId,
350
        node_id: NodeId,
351
        extend_selection: bool,
352
    },
353
    /// Move cursor up (arrow up)
354
    MoveCursorUp {
355
        dom_id: DomId,
356
        node_id: NodeId,
357
        extend_selection: bool,
358
    },
359
    /// Move cursor down (arrow down)
360
    MoveCursorDown {
361
        dom_id: DomId,
362
        node_id: NodeId,
363
        extend_selection: bool,
364
    },
365
    /// Move cursor to line start (Home key)
366
    MoveCursorToLineStart {
367
        dom_id: DomId,
368
        node_id: NodeId,
369
        extend_selection: bool,
370
    },
371
    /// Move cursor to line end (End key)
372
    MoveCursorToLineEnd {
373
        dom_id: DomId,
374
        node_id: NodeId,
375
        extend_selection: bool,
376
    },
377
    /// Move cursor to document start (Ctrl+Home)
378
    MoveCursorToDocumentStart {
379
        dom_id: DomId,
380
        node_id: NodeId,
381
        extend_selection: bool,
382
    },
383
    /// Move cursor to document end (Ctrl+End)
384
    MoveCursorToDocumentEnd {
385
        dom_id: DomId,
386
        node_id: NodeId,
387
        extend_selection: bool,
388
    },
389

            
390
    // Multi-Cursor Operations
391
    /// Add an additional cursor at the specified position (Ctrl+Click from C API)
392
    AddCursor {
393
        dom_id: DomId,
394
        node_id: NodeId,
395
        cursor: TextCursor,
396
    },
397
    /// Add an additional selection range (for multi-cursor)
398
    AddSelectionRange {
399
        dom_id: DomId,
400
        node_id: NodeId,
401
        range: SelectionRange,
402
    },
403
    /// Remove a specific selection by its stable ID
404
    RemoveSelectionById {
405
        selection_id: azul_core::selection::SelectionId,
406
    },
407

            
408
    // Clipboard Operations (Override)
409
    /// Override clipboard content for copy operation
410
    SetCopyContent {
411
        target: DomNodeId,
412
        content: ClipboardContent,
413
    },
414
    /// Override clipboard content for cut operation
415
    SetCutContent {
416
        target: DomNodeId,
417
        content: ClipboardContent,
418
    },
419
    /// Override selection range for select-all operation
420
    SetSelectAllRange {
421
        target: DomNodeId,
422
        range: SelectionRange,
423
    },
424

            
425
    // Hit Test Request (for Debug API)
426
    /// Request a hit test update at a specific position
427
    ///
428
    /// This is used by the Debug API to update the hover manager's hit test
429
    /// data after modifying the mouse position, ensuring that callbacks
430
    /// can find the correct nodes under the cursor.
431
    RequestHitTestUpdate { position: LogicalPosition },
432

            
433
    // Text Selection (for Debug API)
434
    /// Process a text selection click at a specific position
435
    ///
436
    /// This is used by the Debug API to trigger text selection directly,
437
    /// bypassing the normal event pipeline. The handler will:
438
    /// 1. Hit-test IFC roots to find selectable text at the position
439
    /// 2. Create a text cursor at the clicked position
440
    /// 3. Update the selection manager with the new selection
441
    ProcessTextSelectionClick {
442
        position: LogicalPosition,
443
        time_ms: u64,
444
    },
445

            
446
    // Cursor Blinking (System Timer Control)
447
    /// Set the cursor visibility state (called by blink timer)
448
    SetCursorVisibility { visible: bool },
449
    /// Reset cursor blink state on user input (makes cursor visible, records time)
450
    ResetCursorBlink,
451
    /// Start the cursor blink timer for the focused contenteditable element
452
    StartCursorBlinkTimer,
453
    /// Stop the cursor blink timer (when focus leaves contenteditable)
454
    StopCursorBlinkTimer,
455
    
456
    // Scroll cursor/selection into view
457
    /// Scroll the active text cursor into view within its scrollable container
458
    /// This is automatically triggered after text input or cursor movement
459
    ScrollActiveCursorIntoView,
460
    
461
    // Create Text Input Event (for Debug API / Programmatic Text Input)
462
    /// Create a synthetic text input event
463
    ///
464
    /// This simulates receiving text input from the OS. The text input flow will:
465
    /// 1. Record the text in TextInputManager (creating a PendingTextEdit)
466
    /// 2. Generate synthetic TextInput events
467
    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
468
    /// 4. Apply the changeset if not rejected
469
    /// 5. Mark dirty nodes for re-render
470
    CreateTextInput {
471
        /// The text to insert
472
        text: AzString,
473
    },
474

            
475
    // Window Move (Compositor-Managed)
476
    /// Request the compositor to begin an interactive window move.
477
    /// On Wayland: calls xdg_toplevel_move(toplevel, seat, serial).
478
    /// On other platforms: this is a no-op (use set_window_position instead).
479
    BeginInteractiveMove,
480

            
481
    // Drag-and-Drop Data Transfer
482
    /// Set drag data for a MIME type (W3C: dataTransfer.setData)
483
    /// Should be called in a DragStart callback to populate the drag data.
484
    SetDragData {
485
        mime_type: AzString,
486
        data: Vec<u8>,
487
    },
488
    /// Accept the current drop on this target (W3C: event.preventDefault() in DragOver)
489
    /// Must be called from a DragOver or DragEnter callback for the Drop event to fire.
490
    AcceptDrop,
491
    /// Set the drop effect (W3C: dataTransfer.dropEffect)
492
    SetDropEffect {
493
        effect: azul_core::drag::DropEffect,
494
    },
495

            
496
    // DOM Mutation (for Debug API)
497
    /// Insert a new child node into the DOM tree.
498
    /// Creates a minimal StyledDom from the given node_type and appends it
499
    /// as a child of parent_node_id. If position is Some, inserts at that
500
    /// child index; otherwise appends at the end.
501
    InsertChildNode {
502
        dom_id: DomId,
503
        parent_node_id: NodeId,
504
        /// The tag/type of the new node (e.g. "div", "p", "text:Hello")
505
        node_type_str: AzString,
506
        /// Optional child index to insert at (None = append at end)
507
        position: Option<usize>,
508
        /// Optional CSS classes for the new node
509
        classes: Vec<AzString>,
510
        /// Optional ID for the new node
511
        id: Option<AzString>,
512
    },
513
    /// Delete a node from the DOM tree (and all its children).
514
    /// The node is "tombstoned" (set to an empty anonymous Div) rather than
515
    /// physically removed, to preserve node ID stability.
516
    DeleteNode {
517
        dom_id: DomId,
518
        node_id: NodeId,
519
    },
520
    /// Set the IDs and classes on an existing node.
521
    SetNodeIdsAndClasses {
522
        dom_id: DomId,
523
        node_id: NodeId,
524
        ids_and_classes: azul_core::dom::IdOrClassVec,
525
    },
526

            
527
    // Routing
528
    /// Switch to a different route.
529
    ///
530
    /// On desktop: swaps `FullWindowState.layout_callback` to the matched
531
    /// route's callback, stores the `RouteMatch`, and triggers `RefreshDom`.
532
    /// On web: additionally calls `history.pushState()`.
533
    SwitchRoute {
534
        /// Route pattern to switch to (e.g. `"/user/:id"`)
535
        pattern: AzString,
536
        /// Route parameters (e.g. `[("id", "42")]`)
537
        params: azul_core::window::StringPairVec,
538
    },
539
}
540

            
541
/// Main callback type for UI event handling
542
pub type CallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
543

            
544
/// Stores a function pointer that is executed when the given UI element is hit
545
///
546
/// Must return an `Update` that denotes if the screen should be redrawn.
547
#[repr(C)]
548
pub struct Callback {
549
    pub cb: CallbackType,
550
    /// For FFI: stores the foreign callable (e.g., PyFunction)
551
    /// Native Rust code sets this to None
552
    pub ctx: OptionRefAny,
553
}
554

            
555
impl_callback!(Callback, CallbackType);
556

            
557
// Host-invoker plumbing for managed-FFI bindings (Lua, Ruby, Perl, ...).
558
// See `azul_core::host_invoker` for the design. This expands to a static
559
// `az_callback_thunk` that the framework dispatches by-value args to, an
560
// `AzCallback_createFromHostHandle` C-ABI export the host calls per
561
// `set_on_click(...)` site, plus the `AzApp_setCallbackInvoker` setter the
562
// host calls once at module load to register its libffi closure.
563
azul_core::impl_managed_callback! {
564
    wrapper:        Callback,
565
    info_ty:        CallbackInfo,
566
    return_ty:      Update,
567
    default_ret:    Update::DoNothing,
568
    invoker_static: CALLBACK_INVOKER,
569
    invoker_ty:     AzCallbackInvoker,
570
    thunk_fn:       az_callback_thunk,
571
    setter_fn:      AzApp_setCallbackInvoker,
572
    from_handle_fn: AzCallback_createFromHostHandle,
573
}
574

            
575
impl Callback {
576
    /// Create a new callback with just a function pointer (for native Rust code)
577
    pub fn create<C: Into<Callback>>(cb: C) -> Self {
578
        cb.into()
579
    }
580

            
581
    /// Convert from CoreCallback (stored as usize) to Callback (actual function pointer)
582
    ///
583
    /// Preserves `ctx` so that callbacks registered via the host-invoker path
584
    /// (e.g. `Callback::create_from_host_handle`) keep their host-handle ctx
585
    /// across the dispatch cycle. Without this, `info.get_ctx()` inside the
586
    /// generated thunk would see `OptionRefAny::None` and bail out with the
587
    /// kind's default value - which makes managed-FFI click handlers
588
    /// silently no-op.
589
    ///
590
    /// # Safety
591
    /// The caller must ensure that the usize in CoreCallback.cb was originally a valid
592
    /// function pointer of type `CallbackType`. This is guaranteed when CoreCallback
593
    /// is created through standard APIs, but unsafe code could violate this.
594
    pub fn from_core(core: CoreCallback) -> Self {
595
        debug_assert!(core.cb != 0, "CoreCallback.cb is null");
596
        Self {
597
            cb: unsafe { core::mem::transmute(core.cb) },
598
            ctx: core.ctx,
599
        }
600
    }
601

            
602
    /// Convert to CoreCallback (function pointer stored as usize)
603
    ///
604
    /// This is always safe - we're just casting the function pointer to usize for storage.
605
    pub fn to_core(self) -> CoreCallback {
606
        CoreCallback {
607
            cb: self.cb as usize,
608
            ctx: self.ctx,
609
        }
610
    }
611
}
612

            
613
/// Allow Callback to be passed to functions expecting `C: Into<CoreCallback>`
614
impl From<Callback> for CoreCallback {
615
    fn from(callback: Callback) -> Self {
616
        callback.to_core()
617
    }
618
}
619

            
620
impl Callback {
621
    /// Safely invoke the callback with the given data and info
622
    ///
623
    /// This is a safe wrapper around calling the function pointer directly.
624
    pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
625
        (self.cb)(data, info)
626
    }
627
}
628

            
629
/// FFI-safe Option<Callback> type for C interop.
630
///
631
/// This enum provides an ABI-stable alternative to `Option<Callback>`
632
/// that can be safely passed across FFI boundaries.
633
#[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
634
#[repr(C, u8)]
635
pub enum OptionCallback {
636
    /// No callback is present.
637
    None,
638
    /// A callback is present.
639
    Some(Callback),
640
}
641

            
642
impl OptionCallback {
643
    /// Converts this FFI-safe option into a standard Rust `Option<Callback>`.
644
    pub fn into_option(self) -> Option<Callback> {
645
        match self {
646
            OptionCallback::None => None,
647
            OptionCallback::Some(c) => Some(c),
648
        }
649
    }
650

            
651
    /// Returns `true` if a callback is present.
652
    pub fn is_some(&self) -> bool {
653
        matches!(self, OptionCallback::Some(_))
654
    }
655

            
656
    /// Returns `true` if no callback is present.
657
    pub fn is_none(&self) -> bool {
658
        matches!(self, OptionCallback::None)
659
    }
660
}
661

            
662
impl From<Option<Callback>> for OptionCallback {
663
    fn from(o: Option<Callback>) -> Self {
664
        match o {
665
            None => OptionCallback::None,
666
            Some(c) => OptionCallback::Some(c),
667
        }
668
    }
669
}
670

            
671
impl From<OptionCallback> for Option<Callback> {
672
    fn from(o: OptionCallback) -> Self {
673
        o.into_option()
674
    }
675
}
676

            
677
/// Information about the callback that is passed to the callback whenever a callback is invoked
678
///
679
/// # Architecture
680
///
681
/// CallbackInfo uses a transaction-based system:
682
/// - **Read-only pointers**: Access to layout data, window state, managers for queries
683
/// - **Change vector**: All modifications are recorded as CallbackChange items
684
/// - **Processing**: Changes are applied atomically after callback returns
685
///
686
/// This design provides clear separation between queries and modifications, makes debugging
687
/// easier, and allows for future extensibility.
688

            
689
/// Reference data container for CallbackInfo (all read-only fields)
690
///
691
/// This struct consolidates all readonly references that callbacks need to query window state.
692
/// By grouping these into a single struct, we reduce the number of parameters to
693
/// CallbackInfo::new() from 13 to 3, making the API more maintainable and easier to extend.
694
///
695
/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
696
/// reference.
697
pub struct CallbackInfoRefData<'a> {
698
    /// Pointer to the LayoutWindow containing all layout results (READ-ONLY for queries)
699
    pub layout_window: &'a LayoutWindow,
700
    /// Necessary to query FontRefs from callbacks
701
    pub renderer_resources: &'a RendererResources,
702
    /// Previous window state (for detecting changes)
703
    pub previous_window_state: &'a Option<FullWindowState>,
704
    /// State of the current window that the callback was called on (read only!)
705
    pub current_window_state: &'a FullWindowState,
706
    /// An Rc to the OpenGL context, in order to be able to render to OpenGL textures
707
    pub gl_context: &'a OptionGlContextPtr,
708
    /// Immutable reference to where the nodes are currently scrolled (current position)
709
    pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
710
    /// Handle of the current window
711
    pub current_window_handle: &'a RawWindowHandle,
712
    /// Callbacks for creating threads and getting the system time (since this crate uses no_std)
713
    pub system_callbacks: &'a ExternalSystemCallbacks,
714
    /// Platform-specific system style (colors, spacing, etc.)
715
    /// Arc allows safe cloning in callbacks without unsafe pointer manipulation
716
    pub system_style: Arc<SystemStyle>,
717
    /// Shared monitor list - initialized once at app start, updated by the platform
718
    /// layer on monitor topology changes (e.g. WM_DISPLAYCHANGE, NSScreenParametersChanged).
719
    /// Callbacks lock the mutex to read; platform locks to write.
720
    pub monitors: Arc<Mutex<MonitorVec>>,
721
    /// ICU4X localizer cache for internationalized formatting (numbers, dates, lists, plurals)
722
    /// Caches localizers for multiple locales. Only available when the "icu" feature is enabled.
723
    #[cfg(feature = "icu")]
724
    pub icu_localizer: IcuLocalizerHandle,
725
    /// The callable for FFI language bindings (Python, etc.)
726
    /// Cloned from the Callback struct before invocation. Native Rust callbacks have this as None.
727
    pub ctx: OptionRefAny,
728
}
729

            
730
/// CallbackInfo is a lightweight wrapper around pointers to stack-local data.
731
/// It can be safely copied because it only contains pointers - the underlying
732
/// data lives on the stack and outlives the callback invocation.
733
/// This allows callbacks to "consume" CallbackInfo by value while the caller
734
/// retains access to the same underlying data.
735
///
736
/// The `changes` field uses a pointer to Arc<Mutex<...>> so that cloned CallbackInfo instances
737
/// (e.g., passed to timer callbacks) still push changes to the original collection,
738
/// while keeping CallbackInfo as Copy.
739
#[derive(Debug, Clone, Copy)]
740
#[repr(C)]
741
pub struct CallbackInfo {
742
    // Read-only Data (Query Access)
743
    /// Single reference to all readonly reference data
744
    /// This consolidates 8 individual parameters into 1, improving API ergonomics
745
    ref_data: *const CallbackInfoRefData<'static>,
746
    // Context Info (Immutable Event Data)
747
    /// The ID of the DOM + the node that was hit
748
    hit_dom_node: DomNodeId,
749
    /// The (x, y) position of the mouse cursor, **relative to top left of the element that was
750
    /// hit**
751
    cursor_relative_to_item: OptionLogicalPosition,
752
    /// The (x, y) position of the mouse cursor, **relative to top left of the window**
753
    cursor_in_viewport: OptionLogicalPosition,
754
    // Transaction Container (New System) - Uses pointer to Arc<Mutex> for shared access across clones
755
    /// All changes made by the callback, applied atomically after callback returns
756
    /// Stored as raw pointer so CallbackInfo remains Copy
757
    #[cfg(feature = "std")]
758
    changes: *const Arc<Mutex<Vec<CallbackChange>>>,
759
    #[cfg(not(feature = "std"))]
760
    changes: *mut Vec<CallbackChange>,
761
}
762

            
763
impl CallbackInfo {
764
    #[cfg(feature = "std")]
765
    pub fn new<'a>(
766
        ref_data: &'a CallbackInfoRefData<'a>,
767
        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
768
        hit_dom_node: DomNodeId,
769
        cursor_relative_to_item: OptionLogicalPosition,
770
        cursor_in_viewport: OptionLogicalPosition,
771
    ) -> Self {
772
        Self {
773
            // Read-only data (single reference to consolidated refs)
774
            // SAFETY: We cast away the lifetime 'a to 'static because CallbackInfo
775
            // only lives for the duration of the callback, which is shorter than 'a
776
            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
777
            // CallbackInfo only lives for the duration of the callback, which is shorter than 'a.
778
            ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
779

            
780
            // Context info (immutable event data)
781
            hit_dom_node,
782
            cursor_relative_to_item,
783
            cursor_in_viewport,
784

            
785
            // Transaction container - store pointer to Arc<Mutex> for shared access
786
            changes: changes as *const Arc<Mutex<Vec<CallbackChange>>>,
787
        }
788
    }
789

            
790
    #[cfg(not(feature = "std"))]
791
    pub fn new<'a>(
792
        ref_data: &'a CallbackInfoRefData<'a>,
793
        changes: &'a mut Vec<CallbackChange>,
794
        hit_dom_node: DomNodeId,
795
        cursor_relative_to_item: OptionLogicalPosition,
796
        cursor_in_viewport: OptionLogicalPosition,
797
    ) -> Self {
798
        Self {
799
            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
800
            ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
801
            hit_dom_node,
802
            cursor_relative_to_item,
803
            cursor_in_viewport,
804
            changes: changes as *mut Vec<CallbackChange>,
805
        }
806
    }
807

            
808
    /// Get the callable for FFI language bindings (Python, etc.)
809
    ///
810
    /// Returns the cloned OptionRefAny if a callable was set, or None if this
811
    /// is a native Rust callback.
812
    pub fn get_ctx(&self) -> OptionRefAny {
813
        unsafe { (*self.ref_data).ctx.clone() }
814
    }
815

            
816
    /// Returns the OpenGL context if available
817
    pub fn get_gl_context(&self) -> OptionGlContextPtr {
818
        unsafe { (*self.ref_data).gl_context.clone() }
819
    }
820

            
821
    // Helper methods for transaction system
822

            
823
    /// Push a change to be applied after the callback returns
824
    /// This is the primary method for modifying window state from callbacks
825
    #[cfg(feature = "std")]
826
    pub fn push_change(&mut self, change: CallbackChange) {
827
        // SAFETY: The pointer is valid for the lifetime of the callback
828
        unsafe {
829
            if let Ok(mut changes) = (*self.changes).lock() {
830
                changes.push(change);
831
            }
832
        }
833
    }
834

            
835
    #[cfg(not(feature = "std"))]
836
    pub fn push_change(&mut self, change: CallbackChange) {
837
        unsafe { (*self.changes).push(change) }
838
    }
839

            
840
    /// Debug helper to get the changes pointer for debugging
841
    #[cfg(feature = "std")]
842
    pub fn get_changes_ptr(&self) -> *const () {
843
        self.changes as *const ()
844
    }
845

            
846
    /// Get the collected changes (consumes them from the Arc<Mutex>)
847
    #[cfg(feature = "std")]
848
    pub fn take_changes(&self) -> Vec<CallbackChange> {
849
        // SAFETY: The pointer is valid for the lifetime of the callback
850
        unsafe {
851
            if let Ok(mut changes) = (*self.changes).lock() {
852
                core::mem::take(&mut *changes)
853
            } else {
854
                Vec::new()
855
            }
856
        }
857
    }
858

            
859
    #[cfg(not(feature = "std"))]
860
    pub fn take_changes(&self) -> Vec<CallbackChange> {
861
        unsafe { core::mem::take(&mut *self.changes) }
862
    }
863

            
864
    /// Check if pending changes require relayout before the next step.
865
    ///
866
    /// Returns true for `ModifyWindowState` (resize) and `ScrollTo` (scroll),
867
    /// which both need the event loop to re-run layout so that subsequent
868
    /// operations (like `take_screenshot`) see updated content.
869
    ///
870
    /// Used by the E2E test runner to detect when it needs to yield.
871
    #[cfg(feature = "std")]
872
    pub fn has_pending_relayout_change(&self) -> bool {
873
        unsafe {
874
            if let Ok(changes) = (*self.changes).lock() {
875
                changes.iter().any(|c| matches!(c,
876
                    CallbackChange::ModifyWindowState { .. } |
877
                    CallbackChange::ScrollTo { .. }
878
                ))
879
            } else {
880
                false
881
            }
882
        }
883
    }
884

            
885
    // Modern Api (using CallbackChange transactions)
886

            
887
    /// Add a timer to this window (applied after callback returns)
888
    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
889
        self.push_change(CallbackChange::AddTimer { timer_id, timer });
890
    }
891

            
892
    /// Remove a timer from this window (applied after callback returns)
893
    pub fn remove_timer(&mut self, timer_id: TimerId) {
894
        self.push_change(CallbackChange::RemoveTimer { timer_id });
895
    }
896

            
897
    /// Add a thread to this window (applied after callback returns)
898
    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
899
        self.push_change(CallbackChange::AddThread { thread_id, thread });
900
    }
901

            
902
    /// Remove a thread from this window (applied after callback returns)
903
    pub fn remove_thread(&mut self, thread_id: ThreadId) {
904
        self.push_change(CallbackChange::RemoveThread { thread_id });
905
    }
906

            
907
    /// Stop event propagation (applied after callback returns)
908
    ///
909
    /// W3C `stopPropagation()`: remaining handlers on the *current* node
910
    /// still fire, but no handlers on ancestor/descendant nodes are called.
911
    pub fn stop_propagation(&mut self) {
912
        self.push_change(CallbackChange::StopPropagation);
913
    }
914

            
915
    /// Stop event propagation immediately (applied after callback returns)
916
    ///
917
    /// W3C `stopImmediatePropagation()`: no further handlers fire,
918
    /// not even remaining handlers registered on the same node.
919
    pub fn stop_immediate_propagation(&mut self) {
920
        self.push_change(CallbackChange::StopImmediatePropagation);
921
    }
922

            
923
    /// Set keyboard focus target (applied after callback returns)
924
    pub fn set_focus(&mut self, target: FocusTarget) {
925
        self.push_change(CallbackChange::SetFocusTarget { target });
926
    }
927

            
928
    /// Create a new window (applied after callback returns)
929
    pub fn create_window(&mut self, options: WindowCreateOptions) {
930
        self.push_change(CallbackChange::CreateNewWindow { options });
931
    }
932

            
933
    /// Close the current window (applied after callback returns)
934
    pub fn close_window(&mut self) {
935
        self.push_change(CallbackChange::CloseWindow);
936
    }
937

            
938
    /// Switch to a different route (applied after callback returns).
939
    ///
940
    /// On desktop: swaps the layout callback and triggers `RefreshDom`.
941
    /// On web: also calls `history.pushState()`.
942
    ///
943
    /// # C API
944
    /// ```c
945
    /// AzCallbackInfo_switchRoute(&info, AzString_fromConstStr("/user/:id"),
946
    ///     AzStringPairVec_fromConstSlice(&[AzStringPair { key: "id", value: "42" }]));
947
    /// ```
948
    pub fn switch_route(&mut self, pattern: AzString, params: azul_core::window::StringPairVec) {
949
        self.push_change(CallbackChange::SwitchRoute { pattern, params });
950
    }
951

            
952
    /// Get the current active route pattern (e.g. `"/user/:id"`).
953
    ///
954
    /// Returns empty string if no route is active.
955
    ///
956
    /// # C API
957
    /// ```c
958
    /// AzString pattern = AzCallbackInfo_getRoutePattern(&info);
959
    /// ```
960
    pub fn get_route_pattern(&self) -> AzString {
961
        match &self.get_current_window_state().active_route {
962
            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
963
            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
964
        }
965
    }
966

            
967
    /// Get a route parameter by key (e.g. `"id"` from `/user/:id`).
968
    ///
969
    /// Returns empty string if the parameter doesn't exist or no route is active.
970
    ///
971
    /// # C API
972
    /// ```c
973
    /// AzString id = AzCallbackInfo_getRouteParam(&info, AzString_fromConstStr("id"));
974
    /// ```
975
    pub fn get_route_param(&self, key: AzString) -> AzString {
976
        match &self.get_current_window_state().active_route {
977
            azul_core::resources::OptionRouteMatch::Some(rm) => {
978
                rm.get_param(key.as_str())
979
                    .cloned()
980
                    .unwrap_or_else(|| AzString::from_const_str(""))
981
            }
982
            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
983
        }
984
    }
985

            
986
    /// Set a route parameter value and trigger re-render.
987
    ///
988
    /// This modifies the active route's params in-place and triggers a DOM refresh.
989
    /// On web, this also updates the URL via `history.replaceState()`.
990
    ///
991
    /// # C API
992
    /// ```c
993
    /// AzCallbackInfo_setRouteParam(&info, AzString_fromConstStr("id"), AzString_fromConstStr("99"));
994
    /// ```
995
    pub fn set_route_param(&mut self, key: AzString, value: AzString) {
996
        let ws = self.get_current_window_state();
997
        let pattern = match &ws.active_route {
998
            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
999
            azul_core::resources::OptionRouteMatch::None => return,
        };
        let mut params = match &ws.active_route {
            azul_core::resources::OptionRouteMatch::Some(rm) => {
                rm.params.as_ref().to_vec()
            }
            azul_core::resources::OptionRouteMatch::None => return,
        };
        // Update or insert the parameter
        if let Some(existing) = params.iter_mut().find(|p| p.key.as_str() == key.as_str()) {
            existing.value = value;
        } else {
            params.push(azul_core::window::AzStringPair { key, value });
        }
        self.push_change(CallbackChange::SwitchRoute {
            pattern,
            params: azul_core::window::StringPairVec::from_vec(params),
        });
    }
    /// Modify the window state (applied after callback returns)
    pub fn modify_window_state(&mut self, state: FullWindowState) {
        self.push_change(CallbackChange::ModifyWindowState { state });
    }
    /// Request the compositor to begin an interactive window move.
    ///
    /// On Wayland: calls `xdg_toplevel_move(toplevel, seat, serial)` which lets
    /// the compositor handle the move. This is the only way to move windows on Wayland.
    /// On other platforms: this is a no-op; use `modify_window_state()` to set position.
    pub fn begin_interactive_move(&mut self) {
        self.push_change(CallbackChange::BeginInteractiveMove);
    }
    /// Queue multiple window state changes to be applied in sequence.
    /// Each state triggers a separate event processing cycle, which is needed
    /// for simulating clicks where mouse down and mouse up must be separate events.
    pub fn queue_window_state_sequence(&mut self, states: Vec<FullWindowState>) {
        self.push_change(CallbackChange::QueueWindowStateSequence { states });
    }
    /// Change the text content of a node (applied after callback returns)
    ///
    /// This method was previously called `set_string_contents` in older API versions.
    ///
    /// # Arguments
    /// * `node_id` - The text node to modify (DomNodeId containing both DOM and node IDs)
    /// * `text` - The new text content
    pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
        self.push_change(CallbackChange::ChangeNodeText { node_id, text });
    }
    /// Change the image of a node (applied after callback returns)
    pub fn change_node_image(
        &mut self,
        dom_id: DomId,
        node_id: NodeId,
        image: ImageRef,
        update_type: UpdateImageType,
    ) {
        self.push_change(CallbackChange::ChangeNodeImage {
            dom_id,
            node_id,
            image,
            update_type,
        });
    }
    /// Re-render an image callback (for resize/animation updates)
    ///
    /// This triggers re-invocation of the RenderImageCallback associated with the node.
    /// Useful for:
    /// - Responding to window resize (image needs to match new size)
    /// - Animation frames (update OpenGL texture each frame)
    /// - Interactive content (user input changes rendering)
    pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
        self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
    }
    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
    ///
    /// This is the most efficient way to update animated GL textures.
    /// Unlike returning `Update::RefreshDom`, this triggers only:
    /// - Re-invocation of all `RenderImageCallback` functions
    /// - GL texture swap in WebRender
    ///
    /// It does NOT trigger:
    /// - DOM rebuild (no `layout()` callback)
    /// - Display list resubmission (WebRender reuses existing scene)
    /// - Relayout
    ///
    /// Ideal for timer callbacks that animate OpenGL content at 60fps.
    pub fn update_all_image_callbacks(&mut self) {
        self.push_change(CallbackChange::UpdateAllImageCallbacks);
    }
    /// Trigger re-rendering of a VirtualView (applied after callback returns)
    ///
    /// This forces the VirtualView to call its layout callback with reason `DomRecreated`
    /// and submit a new display list to WebRender. The VirtualView's pipeline will be updated
    /// without affecting other parts of the window.
    ///
    /// Useful for:
    /// - Live preview panes (update when source code changes)
    /// - Dynamic content that needs manual refresh
    /// - Editor previews (re-parse and display new DOM)
    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
        self.push_change(CallbackChange::UpdateVirtualView { dom_id, node_id });
    }
    // Dom Tree Navigation
    /// Find a node by ID attribute in the layout tree
    ///
    /// Returns the NodeId of the first node with the given ID attribute, or None if not found.
    pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let styled_dom = &layout_result.styled_dom;
        // Search through all nodes to find one with matching ID attribute
        for (node_idx, node_data) in styled_dom.node_data.as_ref().iter().enumerate() {
            if node_data.has_id(id) {
                return Some(NodeId::new(node_idx));
            }
        }
        None
    }
    /// Get the parent node of the given node
    ///
    /// Returns None if the node has no parent (i.e., it's the root node)
    pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
        let node = node_hierarchy.as_ref().get(node_id.index())?;
        node.parent_id()
    }
    /// Get the next sibling of the given node
    ///
    /// Returns None if the node has no next sibling
    pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
        let node = node_hierarchy.as_ref().get(node_id.index())?;
        node.next_sibling_id()
    }
    /// Get the previous sibling of the given node
    ///
    /// Returns None if the node has no previous sibling
    pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
        let node = node_hierarchy.as_ref().get(node_id.index())?;
        node.previous_sibling_id()
    }
    /// Get the first child of the given node
    ///
    /// Returns None if the node has no children
    pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
        let node = node_hierarchy.as_ref().get(node_id.index())?;
        node.first_child_id(node_id)
    }
    /// Get the last child of the given node
    ///
    /// Returns None if the node has no children
    pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.layout_results.get(&dom_id)?;
        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
        let node = node_hierarchy.as_ref().get(node_id.index())?;
        node.last_child_id()
    }
    /// Get all direct children of the given node
    ///
    /// Returns an empty vector if the node has no children.
    /// Uses the contiguous node layout for efficient iteration.
    pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeHierarchyItemIdVec {
        let layout_window = self.get_layout_window();
        let layout_result = match layout_window.layout_results.get(&dom_id) {
            Some(lr) => lr,
            None => return NodeHierarchyItemIdVec::from_const_slice(&[]),
        };
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = match node_hierarchy.get(node_id) {
            Some(h) => h,
            None => return NodeHierarchyItemIdVec::from_const_slice(&[]),
        };
        // Get first child - if none, return empty
        let first_child = match hier_item.first_child_id(node_id) {
            Some(fc) => fc,
            None => return NodeHierarchyItemIdVec::from_const_slice(&[]),
        };
        // Collect children by walking the sibling chain
        let mut children: Vec<NodeHierarchyItemId> = Vec::new();
        children.push(NodeHierarchyItemId::from_crate_internal(Some(first_child)));
        let mut current = first_child;
        while let Some(next_sibling) = node_hierarchy
            .get(current)
            .and_then(|h| h.next_sibling_id())
        {
            children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
            current = next_sibling;
        }
        NodeHierarchyItemIdVec::from(children)
    }
    /// Get the number of direct children of the given node
    ///
    /// Uses the contiguous node layout for efficient counting.
    pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
        let layout_window = self.get_layout_window();
        let layout_result = match layout_window.layout_results.get(&dom_id) {
            Some(lr) => lr,
            None => return 0,
        };
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = match node_hierarchy.get(node_id) {
            Some(h) => h,
            None => return 0,
        };
        // Get first child - if none, return 0
        let first_child = match hier_item.first_child_id(node_id) {
            Some(fc) => fc,
            None => return 0,
        };
        // Count children by walking the sibling chain
        let mut count = 1;
        let mut current = first_child;
        while let Some(next_sibling) = node_hierarchy
            .get(current)
            .and_then(|h| h.next_sibling_id())
        {
            count += 1;
            current = next_sibling;
        }
        count
    }
    /// Change the image mask of a node (applied after callback returns)
    pub fn change_node_image_mask(&mut self, dom_id: DomId, node_id: NodeId, mask: ImageMask) {
        self.push_change(CallbackChange::ChangeNodeImageMask {
            dom_id,
            node_id,
            mask,
        });
    }
    /// Change CSS properties of a node (applied after callback returns)
    pub fn change_node_css_properties(
        &mut self,
        dom_id: DomId,
        node_id: NodeId,
        properties: CssPropertyVec,
    ) {
        self.push_change(CallbackChange::ChangeNodeCssProperties {
            dom_id,
            node_id,
            properties,
        });
    }
    /// Set a single CSS property on a node (convenience method for widgets)
    ///
    /// This is a helper method that wraps `change_node_css_properties` for the common case
    /// of setting a single property. It uses the hit node's DOM ID automatically.
    ///
    /// # Arguments
    /// * `node_id` - The node to set the property on (uses hit node's DOM ID)
    /// * `property` - The CSS property to set
    pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
        let dom_id = node_id.dom;
        let internal_node_id = node_id
            .node
            .into_crate_internal()
            .expect("DomNodeId node should not be None");
        self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
    }
    /// Quickly override CSS properties on a node for animation or other
    /// transient visual changes. Writes go through
    /// `CssPropertyCache::user_overridden_properties`, which is consulted at
    /// higher priority than the static cascade, so this does not invalidate
    /// the styled DOM's CSS rules. Pass `CssProperty::Initial` for a given
    /// property type to remove any prior override for that type.
    pub fn override_node_css_properties(
        &mut self,
        dom_id: DomId,
        node_id: NodeId,
        properties: CssPropertyVec,
    ) {
        self.push_change(CallbackChange::OverrideNodeCssProperties {
            dom_id,
            node_id,
            properties,
        });
    }
    /// Convenience wrapper for `override_node_css_properties` that targets a
    /// single property on the hit node's DOM (typical for animation callbacks).
    pub fn override_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
        let dom_id = node_id.dom;
        let internal_node_id = node_id
            .node
            .into_crate_internal()
            .expect("DomNodeId node should not be None");
        self.override_node_css_properties(dom_id, internal_node_id, vec![property].into());
    }
    /// Scroll a node to a specific position (applied after callback returns)
    pub fn scroll_to(
        &mut self,
        dom_id: DomId,
        node_id: NodeHierarchyItemId,
        position: LogicalPosition,
    ) {
        self.push_change(CallbackChange::ScrollTo {
            dom_id,
            node_id,
            position,
            unclamped: false,
        });
    }
    /// Scroll a node to a specific position without clamping.
    /// Used by the scroll physics timer for rubber-banding/overscroll.
    pub fn scroll_to_unclamped(
        &mut self,
        dom_id: DomId,
        node_id: NodeHierarchyItemId,
        position: LogicalPosition,
    ) {
        self.push_change(CallbackChange::ScrollTo {
            dom_id,
            node_id,
            position,
            unclamped: true,
        });
    }
    /// Scroll a node into view (W3C scrollIntoView API)
    ///
    /// Scrolls the element into the visible area of its scroll container.
    /// This is the recommended way to programmatically scroll elements into view.
    ///
    /// # Arguments
    ///
    /// * `node_id` - The node to scroll into view
    /// * `options` - Scroll alignment and animation options
    ///
    /// # Note
    ///
    /// This uses the transactional change system - the scroll is queued and applied
    /// after the callback returns. The actual scroll adjustments are calculated
    /// during change processing.
    pub fn scroll_node_into_view(
        &mut self,
        node_id: DomNodeId,
        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
    ) {
        self.push_change(CallbackChange::ScrollIntoView {
            node_id,
            options,
        });
    }
    /// Add an image to the image cache (applied after callback returns)
    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
        self.push_change(CallbackChange::AddImageToCache { id, image });
    }
    /// Remove an image from the image cache (applied after callback returns)
    pub fn remove_image_from_cache(&mut self, id: AzString) {
        self.push_change(CallbackChange::RemoveImageFromCache { id });
    }
    /// Reload system fonts (applied after callback returns)
    ///
    /// Note: This is an expensive operation that rebuilds the entire font cache
    pub fn reload_system_fonts(&mut self) {
        self.push_change(CallbackChange::ReloadSystemFonts);
    }
    // Text Input / Changeset Api
    /// Get the current text changeset being processed (if any)
    ///
    /// This allows callbacks to inspect what text input is about to be applied.
    /// Returns None if no text input is currently being processed.
    ///
    /// Use `set_text_changeset()` to modify the text that will be inserted,
    /// and `prevent_default()` to block the text input entirely.
    pub fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
        self.get_layout_window()
            .text_input_manager
            .get_pending_changeset()
    }
    /// Set/override the text changeset for the current text input operation
    ///
    /// This allows you to modify what text will be inserted during text input events.
    /// Typically used in combination with `prevent_default()` to transform user input.
    ///
    /// # Arguments
    /// * `changeset` - The modified text changeset to apply
    pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
        self.push_change(CallbackChange::SetTextChangeset { changeset });
    }
    /// Create a synthetic text input event
    ///
    /// This simulates receiving text input from the OS. Use this to programmatically
    /// insert text into contenteditable elements, for example from the debug server
    /// or from accessibility APIs.
    ///
    /// The text input flow will:
    /// 1. Record the text in TextInputManager (creating a PendingTextEdit)
    /// 2. Generate synthetic TextInput events
    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
    /// 4. Apply the changeset if not rejected
    /// 5. Mark dirty nodes for re-render
    ///
    /// # Arguments
    /// * `text` - The text to insert at the current cursor position
    pub fn create_text_input(&mut self, text: AzString) {
        self.push_change(CallbackChange::CreateTextInput { text });
    }
    // DOM Mutation Api (for Debug API)
    /// Insert a new child node into the DOM tree (applied after callback returns)
    ///
    /// Creates a new node with the given type string and appends it as a child
    /// of the specified parent node. The node_type_str can be:
    /// - A tag name: "div", "p", "span", "button", etc.
    /// - Text content: "text:Hello World"
    ///
    /// # Arguments
    /// * `dom_id` - The DOM to modify
    /// * `parent_node_id` - The parent node to insert under
    /// * `node_type_str` - The node type (tag name or "text:content")
    /// * `position` - Optional child index (None = append at end)
    /// * `classes` - CSS classes for the new node
    /// * `id` - Optional ID for the new node
    pub fn insert_child_node(
        &mut self,
        dom_id: DomId,
        parent_node_id: NodeId,
        node_type_str: AzString,
        position: Option<usize>,
        classes: Vec<AzString>,
        id: Option<AzString>,
    ) {
        self.push_change(CallbackChange::InsertChildNode {
            dom_id,
            parent_node_id,
            node_type_str,
            position,
            classes,
            id,
        });
    }
    /// Delete a node from the DOM tree (applied after callback returns)
    ///
    /// Tombstones the node by setting it to an empty anonymous Div and
    /// unlinking it from the hierarchy. This preserves node ID stability
    /// (other node IDs don't shift).
    ///
    /// # Arguments
    /// * `dom_id` - The DOM containing the node
    /// * `node_id` - The node to delete
    pub fn delete_node(&mut self, dom_id: DomId, node_id: NodeId) {
        self.push_change(CallbackChange::DeleteNode { dom_id, node_id });
    }
    /// Set the IDs and classes on an existing node (applied after callback returns)
    ///
    /// Replaces the current IDs and classes of a node with the given set.
    ///
    /// # Arguments
    /// * `dom_id` - The DOM containing the node
    /// * `node_id` - The node to modify
    /// * `ids_and_classes` - The new set of IDs and classes
    pub fn set_node_ids_and_classes(
        &mut self,
        dom_id: DomId,
        node_id: NodeId,
        ids_and_classes: azul_core::dom::IdOrClassVec,
    ) {
        self.push_change(CallbackChange::SetNodeIdsAndClasses {
            dom_id,
            node_id,
            ids_and_classes,
        });
    }
    /// Prevent the default text input from being applied
    ///
    /// When called in a TextInput callback, prevents the typed text from being inserted.
    /// Useful for custom validation, filtering, or text transformation.
    pub fn prevent_default(&mut self) {
        self.push_change(CallbackChange::PreventDefault);
    }
    // Cursor Blinking Api (for system timer control)
    /// Set cursor visibility state
    ///
    /// This is primarily used internally by the cursor blink timer callback.
    /// User code typically doesn't need to call this directly.
    pub fn set_cursor_visibility(&mut self, visible: bool) {
        self.push_change(CallbackChange::SetCursorVisibility { visible });
    }
    /// Reset cursor blink state on user input
    ///
    /// This makes the cursor visible and records the current time, so the blink
    /// timer knows to keep the cursor solid for a while before blinking.
    /// Called automatically on keyboard input, but can be called manually.
    pub fn reset_cursor_blink(&mut self) {
        self.push_change(CallbackChange::ResetCursorBlink);
    }
    /// Start the cursor blink timer
    ///
    /// Called automatically when focus lands on a contenteditable element.
    /// The timer will toggle cursor visibility at ~530ms intervals.
    pub fn start_cursor_blink_timer(&mut self) {
        self.push_change(CallbackChange::StartCursorBlinkTimer);
    }
    /// Stop the cursor blink timer
    ///
    /// Called automatically when focus leaves a contenteditable element.
    pub fn stop_cursor_blink_timer(&mut self) {
        self.push_change(CallbackChange::StopCursorBlinkTimer);
    }
    /// Scroll the active cursor into view
    ///
    /// This scrolls the focused text element's cursor into the visible area
    /// of any scrollable ancestor. Called automatically after text input.
    pub fn scroll_active_cursor_into_view(&mut self) {
        self.push_change(CallbackChange::ScrollActiveCursorIntoView);
    }
    /// Open a menu (context menu or dropdown)
    ///
    /// The menu will be displayed either as a native menu or a fallback DOM-based menu
    /// depending on the window's `use_native_context_menus` flag.
    /// Uses the position specified in the menu itself.
    ///
    /// # Arguments
    /// * `menu` - The menu to display
    pub fn open_menu(&mut self, menu: Menu) {
        self.push_change(CallbackChange::OpenMenu {
            menu,
            position: None,
        });
    }
    /// Open a menu at a specific position
    ///
    /// # Arguments
    /// * `menu` - The menu to display
    /// * `position` - The position where the menu should appear (overrides menu's position)
    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
        self.push_change(CallbackChange::OpenMenu {
            menu,
            position: Some(position),
        });
    }
    // Tooltip Api
    /// Show a tooltip at the current cursor position
    ///
    /// Displays a simple text tooltip near the mouse cursor.
    /// The tooltip will be shown using platform-specific native APIs where available.
    ///
    /// Platform implementations:
    /// - **Windows**: Uses `TOOLTIPS_CLASS` Win32 control
    /// - **macOS**: Uses `NSPopover` or custom `NSWindow` with tooltip styling
    /// - **X11**: Creates transient window with `_NET_WM_WINDOW_TYPE_TOOLTIP`
    /// - **Wayland**: Uses `zwlr_layer_shell_v1` with overlay layer
    ///
    /// # Arguments
    /// * `text` - The tooltip text to display
    pub fn show_tooltip(&mut self, text: AzString) {
        let position = self
            .get_cursor_relative_to_viewport()
            .into_option()
            .unwrap_or_else(LogicalPosition::zero);
        self.push_change(CallbackChange::ShowTooltip { text, position });
    }
    /// Show a tooltip at a specific position
    ///
    /// # Arguments
    /// * `text` - The tooltip text to display
    /// * `position` - The position where the tooltip should appear (in window coordinates)
    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
        self.push_change(CallbackChange::ShowTooltip { text, position });
    }
    /// Hide the currently displayed tooltip
    pub fn hide_tooltip(&mut self) {
        self.push_change(CallbackChange::HideTooltip);
    }
    // Text Editing Api (transactional)
    /// Insert text at the current cursor position in a text node
    ///
    /// This operation is transactional - the text will be inserted after the callback returns.
    /// If there's a selection, it will be replaced with the inserted text.
    ///
    /// # Arguments
    /// * `dom_id` - The DOM containing the text node
    /// * `node_id` - The node to insert text into
    /// * `text` - The text to insert
    pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
        self.push_change(CallbackChange::InsertText {
            dom_id,
            node_id,
            text,
        });
    }
    /// Move the text cursor to a specific position
    ///
    /// # Arguments
    /// * `dom_id` - The DOM containing the text node
    /// * `node_id` - The node containing the cursor
    /// * `cursor` - The new cursor position
    pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
        self.push_change(CallbackChange::MoveCursor {
            dom_id,
            node_id,
            cursor,
        });
    }
    /// Set the text selection range
    ///
    /// # Arguments
    /// * `dom_id` - The DOM containing the text node
    /// * `node_id` - The node containing the selection
    /// * `selection` - The new selection (can be a cursor or range)
    pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
        self.push_change(CallbackChange::SetSelection {
            dom_id,
            node_id,
            selection,
        });
    }
    // === Multi-Cursor Operations ===
    /// Add an additional cursor at the specified position (for multi-cursor editing).
    ///
    /// If a MultiCursorState already exists, the cursor is added and overlapping
    /// selections are merged. If not, a new MultiCursorState is created.
    ///
    /// Returns the SelectionId of the new cursor.
    pub fn add_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) -> azul_core::selection::SelectionId {
        let id = azul_core::selection::SelectionId::new();
        self.push_change(CallbackChange::AddCursor {
            dom_id,
            node_id,
            cursor,
        });
        id
    }
    /// Add an additional selection range (for multi-cursor editing).
    ///
    /// Returns the SelectionId of the new selection.
    pub fn add_selection_range(&mut self, dom_id: DomId, node_id: NodeId, range: SelectionRange) -> azul_core::selection::SelectionId {
        let id = azul_core::selection::SelectionId::new();
        self.push_change(CallbackChange::AddSelectionRange {
            dom_id,
            node_id,
            range,
        });
        id
    }
    /// Remove a specific selection/cursor by its stable ID.
    ///
    /// Returns true if a selection with that ID existed and was removed.
    pub fn remove_selection_by_id(&mut self, selection_id: azul_core::selection::SelectionId) -> bool {
        self.push_change(CallbackChange::RemoveSelectionById {
            selection_id,
        });
        true // Actual removal happens deferred; assume success
    }
    /// Get all selections for the given DOM (read-only).
    ///
    /// Returns a Vec of IdentifiedSelection from the MultiCursorState, or empty
    /// if no multi-cursor state exists.
    pub fn get_multi_cursor_selections(&self, dom_id: &DomId) -> Vec<azul_core::selection::IdentifiedSelection> {
        let lw = self.get_layout_window();
        lw.text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.clone())
            .unwrap_or_default()
    }
    /// Get the primary (last-added) selection from the MultiCursorState.
    pub fn get_primary_selection(&self, dom_id: &DomId) -> Option<azul_core::selection::IdentifiedSelection> {
        let lw = self.get_layout_window();
        lw.text_edit_manager.multi_cursor.as_ref()
            .and_then(|mc| mc.get_primary().copied())
    }
    /// Get the number of active cursors/selections.
    pub fn get_selection_count(&self, dom_id: &DomId) -> usize {
        let lw = self.get_layout_window();
        lw.text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.len())
            .unwrap_or(0)
    }
    /// Open a menu positioned relative to a specific DOM node
    ///
    /// This is useful for dropdowns, combo boxes, and context menus that should appear
    /// near a specific UI element. The menu will be positioned below the node by default.
    ///
    /// # Arguments
    /// * `menu` - The menu to display
    /// * `node_id` - The DOM node to position the menu relative to
    ///
    /// # Returns
    /// * `true` if the menu was queued for opening
    /// * `false` if the node doesn't exist or has no layout information
    pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
        // Get the node's bounding rectangle
        if let Some(rect) = self.get_node_rect(node_id) {
            // Position menu at bottom-left of the node
            let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
            self.push_change(CallbackChange::OpenMenu {
                menu,
                position: Some(position),
            });
            true
        } else {
            false
        }
    }
    /// Open a menu positioned relative to the currently hit node
    ///
    /// Convenience method for opening a menu at the element that triggered the callback.
    /// Equivalent to `open_menu_for_node(menu, info.get_hit_node())`.
    ///
    /// # Arguments
    /// * `menu` - The menu to display
    ///
    /// # Returns
    /// * `true` if the menu was queued for opening
    /// * `false` if no node is currently hit or it has no layout information
    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
        let hit_node = self.get_hit_node();
        self.open_menu_for_node(menu, hit_node)
    }
    // Internal accessors
    /// Get reference to the underlying LayoutWindow for queries
    ///
    /// This provides read-only access to layout data, node hierarchies, managers, etc.
    /// All modifications should go through CallbackChange transactions via push_change().
    pub fn get_layout_window(&self) -> &LayoutWindow {
        unsafe { (*self.ref_data).layout_window }
    }
    /// Internal helper: Get the inline text layout for a given node
    ///
    /// This efficiently looks up the text layout by following the chain:
    /// LayoutWindow -> layout_results -> LayoutTree -> dom_to_layout -> LayoutNode ->
    /// inline_layout_result
    ///
    /// Returns None if:
    /// - The DOM doesn't exist in layout_results
    /// - The node doesn't have a layout node mapping
    /// - The layout node doesn't have inline text layout
    fn get_inline_layout_for_node(&self, node_id: &DomNodeId) -> Option<&Arc<UnifiedLayout>> {
        let layout_window = self.get_layout_window();
        // Get the layout result for this DOM
        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
        // Convert NodeHierarchyItemId to NodeId
        let dom_node_id = node_id.node.into_crate_internal()?;
        // Look up the layout node index(es) for this DOM node
        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&dom_node_id)?;
        // Get the first layout node (a DOM node can generate multiple layout nodes,
        // but for text we typically only care about the first one)
        let layout_index = *layout_indices.first()?;
        // Get the layout node's inline layout result (warm data)
        let warm_node = layout_result.layout_tree.warm(layout_index)?;
        warm_node
            .inline_layout_result
            .as_ref()
            .map(|c| c.get_layout())
    }
    // Public query Api
    // All methods below delegate to LayoutWindow for read-only access
    /// Get the logical size of a node, or `None` if the node doesn't exist
    pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
        self.get_layout_window().get_node_size(node_id)
    }
    /// Get the logical position of a node, or `None` if the node doesn't exist
    pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
        self.get_layout_window().get_node_position(node_id)
    }
    /// Get the hit test bounds of a node from the display list
    ///
    /// This is more reliable than get_node_rect because the display list
    /// always contains the correct final rendered positions.
    pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
        self.get_layout_window().get_node_hit_test_bounds(node_id)
    }
    /// Get the bounding rectangle of a node (position + size)
    ///
    /// This is particularly useful for menu positioning, where you need
    /// to know where a UI element is to popup a menu relative to it.
    pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
        let position = self.get_node_position(node_id)?;
        let size = self.get_node_size(node_id)?;
        Some(LogicalRect::new(position, size))
    }
    /// Get the bounding rectangle of the hit node
    ///
    /// Convenience method that combines get_hit_node() and get_node_rect().
    /// Useful for menu positioning based on the clicked element.
    pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
        let hit_node = self.get_hit_node();
        self.get_node_rect(hit_node)
    }
    // Timer Management (Query APIs)
    /// Get a reference to a timer
    pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
        self.get_layout_window().get_timer(timer_id)
    }
    /// Get all timer IDs
    pub fn get_timer_ids(&self) -> TimerIdVec {
        self.get_layout_window().get_timer_ids()
    }
    // Thread Management (Query APIs)
    /// Get a reference to a thread
    pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
        self.get_layout_window().get_thread(thread_id)
    }
    /// Get all thread IDs
    pub fn get_thread_ids(&self) -> ThreadIdVec {
        self.get_layout_window().get_thread_ids()
    }
    // Gpu Value Cache Management (Query APIs)
    /// Get the GPU value cache for a specific DOM
    pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
        self.get_layout_window().get_gpu_cache(dom_id)
    }
    // Layout Result Access (Query APIs)
    /// Get a layout result for a specific DOM
    pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
        self.get_layout_window().get_layout_result(dom_id)
    }
    /// Get all DOM IDs that have layout results
    pub fn get_dom_ids(&self) -> DomIdVec {
        self.get_layout_window().get_dom_ids()
    }
    // Node Hierarchy Navigation
    /// Get the DOM node that was hit by the event that triggered this callback
    pub fn get_hit_node(&self) -> DomNodeId {
        self.hit_dom_node
    }
    /// Check if a node is anonymous (generated for table layout)
    fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
        let layout_window = self.get_layout_window();
        let layout_result = match layout_window.get_layout_result(dom_id) {
            Some(lr) => lr,
            None => return false,
        };
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = match node_data_cont.get(node_id) {
            Some(nd) => nd,
            None => return false,
        };
        node_data.is_anonymous()
    }
    /// Get the parent of a node, skipping anonymous (table-generated) nodes
    pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous parent nodes - walk up the tree until we find a non-anonymous node
        let mut current_parent_id = hier_item.parent_id()?;
        loop {
            if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
                return Some(DomNodeId {
                    dom: node_id.dom,
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
                });
            }
            // This parent is anonymous, try its parent
            let parent_hier_item = node_hierarchy.get(current_parent_id)?;
            current_parent_id = parent_hier_item.parent_id()?;
        }
    }
    /// Get the previous sibling of a node, skipping anonymous nodes
    pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous siblings - walk backwards until we find a non-anonymous node
        let mut current_sibling_id = hier_item.previous_sibling_id()?;
        loop {
            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
                return Some(DomNodeId {
                    dom: node_id.dom,
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
                });
            }
            // This sibling is anonymous, try the previous one
            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
            current_sibling_id = sibling_hier_item.previous_sibling_id()?;
        }
    }
    /// Get the next sibling of a node, skipping anonymous nodes
    pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous siblings - walk forwards until we find a non-anonymous node
        let mut current_sibling_id = hier_item.next_sibling_id()?;
        loop {
            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
                return Some(DomNodeId {
                    dom: node_id.dom,
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
                });
            }
            // This sibling is anonymous, try the next one
            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
            current_sibling_id = sibling_hier_item.next_sibling_id()?;
        }
    }
    /// Get the first child of a node, skipping anonymous nodes
    pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Get first child, then skip anonymous nodes
        let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
        loop {
            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
                return Some(DomNodeId {
                    dom: node_id.dom,
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
                });
            }
            // This child is anonymous, try the next sibling
            let child_hier_item = node_hierarchy.get(current_child_id)?;
            current_child_id = child_hier_item.next_sibling_id()?;
        }
    }
    /// Get the last child of a node, skipping anonymous nodes
    pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Get last child, then skip anonymous nodes by walking backwards
        let mut current_child_id = hier_item.last_child_id()?;
        loop {
            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
                return Some(DomNodeId {
                    dom: node_id.dom,
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
                });
            }
            // This child is anonymous, try the previous sibling
            let child_hier_item = node_hierarchy.get(current_child_id)?;
            current_child_id = child_hier_item.previous_sibling_id()?;
        }
    }
    // Node Data and State
    /// Get the dataset (user-attached `RefAny`) of a node, or `None` if unset
    pub fn get_dataset(&mut self, node_id: DomNodeId) -> Option<RefAny> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = node_data_cont.get(node_id_internal)?;
        node_data.get_dataset().cloned()
    }
    /// Find the root-level node whose dataset matches the type of `search_key`
    pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
        let mut found: Option<(u64, DomNodeId)> = None;
        let search_type_id = search_key.get_type_id();
        for dom_id in self.get_dom_ids().as_ref().iter().copied() {
            let layout_window = self.get_layout_window();
            let layout_result = match layout_window.get_layout_result(&dom_id) {
                Some(lr) => lr,
                None => continue,
            };
            let node_data_cont = layout_result.styled_dom.node_data.as_container();
            for (node_idx, node_data) in node_data_cont.iter().enumerate() {
                if let Some(dataset) = node_data.get_dataset().cloned() {
                    if dataset.get_type_id() == search_type_id {
                        let node_id = DomNodeId {
                            dom: dom_id,
                            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
                                node_idx,
                            ))),
                        };
                        let instance_id = dataset.instance_id;
                        match found {
                            None => found = Some((instance_id, node_id)),
                            Some((prev_instance, _)) => {
                                if instance_id < prev_instance {
                                    found = Some((instance_id, node_id));
                                }
                            }
                        }
                    }
                }
            }
        }
        found.map(|s| s.1)
    }
    /// Get the text content of a text node, or `None` if the node is not a text node
    pub fn get_string_contents(&self, node_id: DomNodeId) -> Option<AzString> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = node_data_cont.get(node_id_internal)?;
        if let NodeType::Text(text) = node_data.get_node_type() {
            Some(text.clone_self())
        } else {
            None
        }
    }
    /// Get the tag name of a node (e.g., "div", "p", "span")
    ///
    /// Returns the HTML tag name as a string for the given node.
    /// For text nodes, returns "text". For image nodes, returns "img".
    pub fn get_node_tag_name(&self, node_id: DomNodeId) -> Option<AzString> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = node_data_cont.get(node_id_internal)?;
        let tag = node_data.get_node_type().get_path();
        Some(tag.to_string().into())
    }
    /// Get an attribute value from a node by attribute name
    ///
    /// # Arguments
    /// * `node_id` - The node to query
    /// * `attr_name` - The attribute name (e.g., "id", "class", "href", "data-custom", "aria-label")
    ///
    /// Returns the attribute value if found, None otherwise.
    /// This searches the strongly-typed AttributeVec on the node.
    pub fn get_node_attribute(&self, node_id: DomNodeId, attr_name: &str) -> Option<AzString> {
        use azul_core::dom::AttributeType;
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = node_data_cont.get(node_id_internal)?;
        // Check the strongly-typed attributes vec
        for attr in node_data.attributes().as_ref() {
            match (attr_name, attr) {
                ("id", AttributeType::Id(v)) => return Some(v.clone()),
                ("class", AttributeType::Class(v)) => return Some(v.clone()),
                ("aria-label", AttributeType::AriaLabel(v)) => return Some(v.clone()),
                ("aria-labelledby", AttributeType::AriaLabelledBy(v)) => return Some(v.clone()),
                ("aria-describedby", AttributeType::AriaDescribedBy(v)) => return Some(v.clone()),
                ("role", AttributeType::AriaRole(v)) => return Some(v.clone()),
                ("href", AttributeType::Href(v)) => return Some(v.clone()),
                ("rel", AttributeType::Rel(v)) => return Some(v.clone()),
                ("target", AttributeType::Target(v)) => return Some(v.clone()),
                ("src", AttributeType::Src(v)) => return Some(v.clone()),
                ("alt", AttributeType::Alt(v)) => return Some(v.clone()),
                ("title", AttributeType::Title(v)) => return Some(v.clone()),
                ("name", AttributeType::Name(v)) => return Some(v.clone()),
                ("value", AttributeType::Value(v)) => return Some(v.clone()),
                ("type", AttributeType::InputType(v)) => return Some(v.clone()),
                ("placeholder", AttributeType::Placeholder(v)) => return Some(v.clone()),
                ("max", AttributeType::Max(v)) => return Some(v.clone()),
                ("min", AttributeType::Min(v)) => return Some(v.clone()),
                ("step", AttributeType::Step(v)) => return Some(v.clone()),
                ("pattern", AttributeType::Pattern(v)) => return Some(v.clone()),
                ("autocomplete", AttributeType::Autocomplete(v)) => return Some(v.clone()),
                ("scope", AttributeType::Scope(v)) => return Some(v.clone()),
                ("lang", AttributeType::Lang(v)) => return Some(v.clone()),
                ("dir", AttributeType::Dir(v)) => return Some(v.clone()),
                ("required", AttributeType::Required) => return Some("true".into()),
                ("disabled", AttributeType::Disabled) => return Some("true".into()),
                ("readonly", AttributeType::Readonly) => return Some("true".into()),
                ("checked", AttributeType::CheckedTrue) => return Some("true".into()),
                ("checked", AttributeType::CheckedFalse) => return Some("false".into()),
                ("selected", AttributeType::Selected) => return Some("true".into()),
                ("hidden", AttributeType::Hidden) => return Some("true".into()),
                ("focusable", AttributeType::Focusable) => return Some("true".into()),
                ("minlength", AttributeType::MinLength(v)) => return Some(v.to_string().into()),
                ("maxlength", AttributeType::MaxLength(v)) => return Some(v.to_string().into()),
                ("colspan", AttributeType::ColSpan(v)) => return Some(v.to_string().into()),
                ("rowspan", AttributeType::RowSpan(v)) => return Some(v.to_string().into()),
                ("tabindex", AttributeType::TabIndex(v)) => return Some(v.to_string().into()),
                ("contenteditable", AttributeType::ContentEditable(v)) => {
                    return Some(v.to_string().into())
                }
                ("draggable", AttributeType::Draggable(v)) => return Some(v.to_string().into()),
                // Handle data-* attributes
                (name, AttributeType::Data(nv))
                    if name.starts_with("data-") && nv.attr_name.as_str() == &name[5..] =>
                {
                    return Some(nv.value.clone());
                }
                // Handle aria-* state/property attributes
                (name, AttributeType::AriaState(nv))
                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
                {
                    return Some(nv.value.clone());
                }
                (name, AttributeType::AriaProperty(nv))
                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
                {
                    return Some(nv.value.clone());
                }
                // Handle custom attributes
                (name, AttributeType::Custom(nv)) if nv.attr_name.as_str() == name => {
                    return Some(nv.value.clone());
                }
                _ => continue,
            }
        }
        None
    }
    /// Get all classes of a node as a vector of strings
    pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
        let layout_window = match self.get_layout_window().get_layout_result(&node_id.dom) {
            Some(lr) => lr,
            None => return StringVec::from_const_slice(&[]),
        };
        let node_id_internal = match node_id.node.into_crate_internal() {
            Some(n) => n,
            None => return StringVec::from_const_slice(&[]),
        };
        let node_data_cont = layout_window.styled_dom.node_data.as_container();
        let node_data = match node_data_cont.get(node_id_internal) {
            Some(n) => n,
            None => return StringVec::from_const_slice(&[]),
        };
        let classes: Vec<AzString> = node_data
            .attributes()
            .as_ref()
            .iter()
            .filter_map(|attr| {
                attr.as_class().map(|c| c.to_string().into())
            })
            .collect();
        StringVec::from(classes)
    }
    /// Get the ID attribute of a node (if it has one)
    pub fn get_node_id(&self, node_id: DomNodeId) -> Option<AzString> {
        let layout_window = self.get_layout_window();
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
        let node_id_internal = node_id.node.into_crate_internal()?;
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
        let node_data = node_data_cont.get(node_id_internal)?;
        for attr in node_data.attributes().as_ref().iter() {
            if let Some(id) = attr.as_id() {
                return Some(id.to_string().into());
            }
        }
        None
    }
    // Text Selection Management
    /// Get the current selection state for a DOM (via multi_cursor)
    pub fn get_selection(&self, _dom_id: &DomId) -> Option<&SelectionState> {
        // SelectionManager removed; multi_cursor is the source of truth.
        // SelectionState is a legacy type; return None.
        None
    }
    /// Check if a DOM has any selection (via multi_cursor)
    pub fn has_selection(&self, _dom_id: &DomId) -> bool {
        self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
            .unwrap_or(false)
    }
    /// Get the primary cursor for a DOM (via multi_cursor)
    pub fn get_primary_cursor(&self, _dom_id: &DomId) -> Option<TextCursor> {
        self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .and_then(|mc| mc.get_primary_cursor())
    }
    /// Get all selection ranges (excludes plain cursors, via multi_cursor)
    pub fn get_selection_ranges(&self, _dom_id: &DomId) -> SelectionRangeVec {
        let ranges: Vec<SelectionRange> = self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
                Selection::Range(r) => Some(*r),
                _ => None,
            }).collect()).unwrap_or_default();
        ranges.into()
    }
    /// Get direct access to the text layout cache
    ///
    /// Note: This provides direct read-only access to the text layout cache, but you need
    /// to know the CacheId for the specific text node you want. Currently there's
    /// no direct mapping from NodeId to CacheId exposed in the public API.
    ///
    /// For text modifications, use CallbackChange transactions:
    /// - `change_node_text()` for changing text content
    /// - `set_selection()` for setting selections
    /// - `get_selection()`, `get_primary_cursor()` for reading selections
    ///
    /// Future: Add NodeId -> CacheId mapping to enable node-specific layout access
    pub fn get_text_cache(&self) -> &TextLayoutCache {
        &self.get_layout_window().text_cache
    }
    // Window State Access
    /// Get full current window state (immutable reference)
    pub fn get_current_window_state(&self) -> &FullWindowState {
        // SAFETY: current_window_state is a valid pointer for the lifetime of CallbackInfo
        unsafe { (*self.ref_data).current_window_state }
    }
    /// Get current window flags
    pub fn get_current_window_flags(&self) -> WindowFlags {
        self.get_current_window_state().flags.clone()
    }
    /// Get current keyboard state
    pub fn get_current_keyboard_state(&self) -> KeyboardState {
        self.get_current_window_state().keyboard_state.clone()
    }
    /// Get current mouse state
    pub fn get_current_mouse_state(&self) -> MouseState {
        self.get_current_window_state().mouse_state.clone()
    }
    /// Get full previous window state (immutable reference)
    pub fn get_previous_window_state(&self) -> &Option<FullWindowState> {
        unsafe { (*self.ref_data).previous_window_state }
    }
    /// Get previous window flags
    pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
        Some(self.get_previous_window_state().as_ref()?.flags.clone())
    }
    /// Get previous keyboard state
    pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
        Some(
            self.get_previous_window_state()
                .as_ref()?
                .keyboard_state
                .clone(),
        )
    }
    /// Get previous mouse state
    pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
        Some(
            self.get_previous_window_state()
                .as_ref()?
                .mouse_state
                .clone(),
        )
    }
    // Cursor and Input
    pub fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
        use azul_core::geom::{CursorNodePosition, OptionCursorNodePosition};
        match self.cursor_relative_to_item {
            OptionLogicalPosition::Some(p) => OptionCursorNodePosition::Some(CursorNodePosition::from_logical(p)),
            OptionLogicalPosition::None => OptionCursorNodePosition::None,
        }
    }
    pub fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
        self.cursor_in_viewport
    }
    /// Get cursor position in virtual screen coordinates (all monitors combined).
    ///
    /// Computed as: `window_position + cursor_position_in_window`.
    /// All coordinates are in logical pixels (HiDPI-independent on macOS; on Win32
    /// this depends on DPI-awareness mode).
    ///
    /// The origin (0, 0) is at the **top-left of the primary monitor**.
    /// Y increases downward.  On multi-monitor setups, coordinates may be negative
    /// for monitors to the left of or above the primary monitor.
    ///
    /// Returns `None` if the cursor is outside the window or the window position
    /// is unknown.
    ///
    /// ## Platform notes
    ///
    /// | Platform | Accuracy |
    /// |----------|----------|
    /// | **macOS**   | Exact (points = logical pixels) |
    /// | **Win32**   | Exact when DPI-aware; approximate otherwise |
    /// | **X11**     | Exact (pixels) |
    /// | **Wayland** | Falls back to window-local (compositor hides global position) |
    pub fn get_cursor_position_screen(&self) -> azul_core::geom::OptionScreenPosition {
        use azul_core::window::WindowPosition;
        use azul_core::geom::{LogicalPosition, ScreenPosition, OptionScreenPosition};
        let ws = self.get_current_window_state();
        let cursor_local = match ws.mouse_state.cursor_position.get_position() {
            Some(p) => p,
            None => return OptionScreenPosition::None,
        };
        match ws.position {
            WindowPosition::Initialized(pos) => {
                OptionScreenPosition::Some(ScreenPosition::new(
                    pos.x as f32 + cursor_local.x,
                    pos.y as f32 + cursor_local.y,
                ))
            }
            // Wayland: window position unknown, fall back to window-local
            WindowPosition::Uninitialized => OptionScreenPosition::Some(
                ScreenPosition::new(cursor_local.x, cursor_local.y)
            ),
        }
    }
    /// Get the drag delta in window-local coordinates.
    ///
    /// Returns the offset from drag start to current cursor position in window-local
    /// logical pixels. Returns `None` if no drag is active.
    ///
    /// **Warning**: This is NOT stable during window moves (titlebar drag).
    /// Use `get_drag_delta_screen()` for titlebar dragging.
    pub fn get_drag_delta(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
        let gm = self.get_gesture_drag_manager();
        match gm.get_drag_delta() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
            None => OptionDragDelta::None,
        }
    }
    /// Get the drag delta in screen coordinates.
    ///
    /// Unlike `get_drag_delta()`, this is stable even when the window moves
    /// (e.g., during titlebar drag). Returns `None` if no drag is active.
    /// On Wayland: falls back to window-local delta.
    pub fn get_drag_delta_screen(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
        let gm = self.get_gesture_drag_manager();
        match gm.get_drag_delta_screen() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
            None => OptionDragDelta::None,
        }
    }
    /// Get the **incremental** (frame-to-frame) drag delta in screen coordinates.
    ///
    /// Returns the screen-space delta between the current and previous sample
    /// (not the total delta since drag start). Use this with the current window
    /// position for robust titlebar drag:
    ///
    /// ```text
    /// new_pos = current_window_pos + incremental_delta
    /// ```
    ///
    /// This handles external position changes (DPI change, OS clamping, compositor
    /// resize) that would make the initial position stale.
    /// Returns `None` if no drag is active or fewer than 2 samples exist.
    pub fn get_drag_delta_screen_incremental(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
        let gm = self.get_gesture_drag_manager();
        match gm.get_drag_delta_screen_incremental() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
            None => OptionDragDelta::None,
        }
    }
    pub fn get_current_window_handle(&self) -> RawWindowHandle {
        unsafe { (*self.ref_data).current_window_handle.clone() }
    }
    /// Get the system style (for menu rendering, CSD, etc.)
    /// This is useful for creating custom menus or other system-styled UI.
    pub fn get_system_style(&self) -> Arc<SystemStyle> {
        unsafe { (*self.ref_data).system_style.clone() }
    }
    /// Get a snapshot of all monitors available on the system.
    ///
    /// The returned `MonitorVec` is cloned from the shared monitor cache.
    /// The cache is initialized once at app start and updated by the platform
    /// layer on monitor topology changes. No OS calls are made here.
    pub fn get_monitors(&self) -> MonitorVec {
        let monitors_arc = unsafe { &(*self.ref_data).monitors };
        monitors_arc.lock().map(|g| g.clone()).unwrap_or_else(|_| MonitorVec::from_const_slice(&[]))
    }
    /// Get the monitor that the current window is on, if known.
    ///
    /// Uses `FullWindowState::monitor_id` (set by the platform layer) to find
    /// the matching monitor in the cached monitor list. Returns `None` if the
    /// monitor ID is not set or no matching monitor is found.
    pub fn get_current_monitor(&self) -> OptionMonitor {
        let ws = self.get_current_window_state();
        let monitor_index = match ws.monitor_id {
            azul_css::corety::OptionU32::Some(idx) => idx as usize,
            azul_css::corety::OptionU32::None => return OptionMonitor::None,
        };
        let monitors_arc = unsafe { &(*self.ref_data).monitors };
        let guard = match monitors_arc.lock() {
            Ok(g) => g,
            Err(_) => return OptionMonitor::None,
        };
        for m in guard.as_ref().iter() {
            if m.monitor_id.index == monitor_index {
                return OptionMonitor::Some(m.clone());
            }
        }
        OptionMonitor::None
    }
    // ==================== ICU4X Internationalization API ====================
    //
    // All formatting functions take a locale string (BCP 47 format) as the first
    // parameter, allowing dynamic language switching per-call.
    //
    // For date/time construction, use the static methods on IcuDate, IcuTime, IcuDateTime:
    // - IcuDate::now(), IcuDate::now_utc(), IcuDate::new(year, month, day)
    // - IcuTime::now(), IcuTime::now_utc(), IcuTime::new(hour, minute, second)
    // - IcuDateTime::now(), IcuDateTime::now_utc(), IcuDateTime::from_timestamp(secs)
    /// Get the ICU localizer cache for internationalized formatting.
    ///
    /// The cache stores localizers for multiple locales. Each locale's formatter
    /// is lazily created on first use and cached for subsequent calls.
    #[cfg(feature = "icu")]
    pub fn get_icu_localizer(&self) -> &IcuLocalizerHandle {
        unsafe { &(*self.ref_data).icu_localizer }
    }
    /// Format an integer with locale-appropriate grouping separators.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string (e.g., "en-US", "de-DE", "ja-JP")
    /// * `value` - The integer to format
    ///
    /// # Example
    /// ```rust,ignore
    /// info.format_integer("en-US", 1234567) // -> "1,234,567"
    /// info.format_integer("de-DE", 1234567) // -> "1.234.567"
    /// info.format_integer("fr-FR", 1234567) // -> "1 234 567"
    /// ```
    #[cfg(feature = "icu")]
    pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
        self.get_icu_localizer().format_integer(locale, value)
    }
    /// Format a decimal number with locale-appropriate separators.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `integer_part` - The full integer value (e.g., 123456 for 1234.56)
    /// * `decimal_places` - Number of decimal places (e.g., 2 for 1234.56)
    ///
    /// # Example
    /// ```rust,ignore
    /// info.format_decimal("en-US", 123456, 2) // -> "1,234.56"
    /// info.format_decimal("de-DE", 123456, 2) // -> "1.234,56"
    /// ```
    #[cfg(feature = "icu")]
    pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
        self.get_icu_localizer().format_decimal(locale, integer_part, decimal_places)
    }
    /// Get the plural category for a number (cardinal: "1 item", "2 items").
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `value` - The number to get the plural category for
    ///
    /// # Example
    /// ```rust,ignore
    /// info.get_plural_category("en", 1)  // -> PluralCategory::One
    /// info.get_plural_category("en", 2)  // -> PluralCategory::Other
    /// info.get_plural_category("pl", 2)  // -> PluralCategory::Few
    /// info.get_plural_category("pl", 5)  // -> PluralCategory::Many
    /// ```
    #[cfg(feature = "icu")]
    pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
        self.get_icu_localizer().get_plural_category(locale, value)
    }
    /// Select the appropriate string based on plural rules.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `value` - The number to pluralize
    /// * `zero`, `one`, `two`, `few`, `many`, `other` - Strings for each category
    ///
    /// # Example
    /// ```rust,ignore
    /// info.pluralize("en", count, "no items", "1 item", "2 items", "{} items", "{} items", "{} items")
    /// info.pluralize("pl", count, "brak", "1 element", "2 elementy", "{} elementy", "{} elementów", "{} elementów")
    /// ```
    #[cfg(feature = "icu")]
    pub fn pluralize(
        &self,
        locale: &str,
        value: i64,
        zero: &str,
        one: &str,
        two: &str,
        few: &str,
        many: &str,
        other: &str,
    ) -> AzString {
        self.get_icu_localizer().pluralize(locale, value, zero, one, two, few, many, other)
    }
    /// Format a list of items with locale-appropriate conjunctions.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `items` - The items to format as a list
    /// * `list_type` - And, Or, or Unit list type
    ///
    /// # Example
    /// ```rust,ignore
    /// info.format_list("en-US", &items, ListType::And) // -> "A, B, and C"
    /// info.format_list("es-ES", &items, ListType::And) // -> "A, B y C"
    /// ```
    #[cfg(feature = "icu")]
    pub fn format_list(&self, locale: &str, items: &[AzString], list_type: ListType) -> AzString {
        self.get_icu_localizer().format_list(locale, items, list_type)
    }
    /// Format a date according to the specified locale.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `date` - The date to format (use IcuDate::now() or IcuDate::new())
    /// * `length` - Short, Medium, or Long format
    ///
    /// # Example
    /// ```rust,ignore
    /// let today = IcuDate::now();
    /// info.format_date("en-US", today, FormatLength::Medium) // -> "Jan 15, 2025"
    /// info.format_date("de-DE", today, FormatLength::Medium) // -> "15.01.2025"
    /// ```
    #[cfg(feature = "icu")]
    pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
        self.get_icu_localizer().format_date(locale, date, length)
    }
    /// Format a time according to the specified locale.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `time` - The time to format (use IcuTime::now() or IcuTime::new())
    /// * `include_seconds` - Whether to include seconds in the output
    ///
    /// # Example
    /// ```rust,ignore
    /// let now = IcuTime::now();
    /// info.format_time("en-US", now, false) // -> "4:30 PM"
    /// info.format_time("de-DE", now, false) // -> "16:30"
    /// ```
    #[cfg(feature = "icu")]
    pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
        self.get_icu_localizer().format_time(locale, time, include_seconds)
    }
    /// Format a date and time according to the specified locale.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `datetime` - The date and time to format (use IcuDateTime::now())
    /// * `length` - Short, Medium, or Long format
    #[cfg(feature = "icu")]
    pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
        self.get_icu_localizer().format_datetime(locale, datetime, length)
    }
    /// Compare two strings according to locale-specific collation rules.
    ///
    /// Returns -1 if a < b, 0 if a == b, 1 if a > b.
    /// This is useful for locale-aware sorting where "Ä" should sort with "A" in German.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `a` - First string to compare
    /// * `b` - Second string to compare
    ///
    /// # Example
    /// ```rust,ignore
    /// info.compare_strings("de-DE", "Äpfel", "Banane") // -> -1 (Ä sorts with A)
    /// info.compare_strings("sv-SE", "Äpple", "Öl")     // -> -1 (Swedish: Ä before Ö)
    /// ```
    #[cfg(feature = "icu")]
    pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
        self.get_icu_localizer().compare_strings(locale, a, b)
    }
    /// Sort a list of strings using locale-aware collation.
    ///
    /// This properly handles accented characters, case sensitivity, and
    /// language-specific sorting rules.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `strings` - The strings to sort
    ///
    /// # Example
    /// ```rust,ignore
    /// let sorted = info.sort_strings("de-DE", &["Österreich", "Andorra", "Ägypten"]);
    /// // Result: ["Ägypten", "Andorra", "Österreich"] (Ä sorts with A, Ö with O)
    /// ```
    #[cfg(feature = "icu")]
    pub fn sort_strings(&self, locale: &str, strings: &[AzString]) -> IcuStringVec {
        self.get_icu_localizer().sort_strings(locale, strings)
    }
    /// Check if two strings are equal according to locale collation rules.
    ///
    /// This may return `true` for strings that differ in case or accents,
    /// depending on the collation strength.
    ///
    /// # Arguments
    /// * `locale` - BCP 47 locale string
    /// * `a` - First string to compare
    /// * `b` - Second string to compare
    #[cfg(feature = "icu")]
    pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
        self.get_icu_localizer().strings_equal(locale, a, b)
    }
    /// Get the current cursor position in logical coordinates relative to the window
    pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
        self.cursor_in_viewport.into_option()
    }
    /// Get the layout rectangle of the currently hit node (in logical coordinates)
    pub fn get_hit_node_layout_rect(&self) -> Option<LogicalRect> {
        self.get_layout_window()
            .get_node_layout_rect(self.hit_dom_node)
    }
    // Css Property Access
    /// Get the computed CSS property for a specific DOM node
    ///
    /// This queries the CSS property cache and returns the resolved property value
    /// for the given node, taking into account:
    /// - User overrides (from callbacks)
    /// - Node state (:hover, :active, :focus)
    /// - CSS rules from stylesheets
    /// - Cascaded properties from parents
    /// - Inline styles
    ///
    /// # Arguments
    /// * `node_id` - The DOM node to query
    /// * `property_type` - The CSS property type to retrieve
    ///
    /// # Returns
    /// * `Some(CssProperty)` if the property is set on this node
    /// * `None` if the property is not set (will use default value)
    pub fn get_computed_css_property(
        &self,
        node_id: DomNodeId,
        property_type: CssPropertyType,
    ) -> Option<CssProperty> {
        let layout_window = self.get_layout_window();
        // Get the layout result for this DOM
        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
        // Get the styled DOM
        let styled_dom = &layout_result.styled_dom;
        // Convert DomNodeId to NodeId using proper decoding
        let internal_node_id = node_id.node.into_crate_internal()?;
        // Get the node data
        let node_data_container = styled_dom.node_data.as_container();
        let node_data = node_data_container.get(internal_node_id)?;
        // Get the styled node state
        let styled_nodes_container = styled_dom.styled_nodes.as_container();
        let styled_node = styled_nodes_container.get(internal_node_id)?;
        let node_state = &styled_node.styled_node_state;
        // Query the CSS property cache
        let css_property_cache = &styled_dom.css_property_cache.ptr;
        css_property_cache
            .get_property(node_data, &internal_node_id, node_state, &property_type)
            .cloned()
    }
    /// Get the computed width of a node from CSS
    ///
    /// Convenience method for getting the CSS width property.
    pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
        self.get_computed_css_property(node_id, CssPropertyType::Width)
    }
    /// Get the computed height of a node from CSS
    ///
    /// Convenience method for getting the CSS height property.
    pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
        self.get_computed_css_property(node_id, CssPropertyType::Height)
    }
    // System Callbacks
    pub fn get_system_time_fn(&self) -> GetSystemTimeCallback {
        unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
    }
    pub fn get_current_time(&self) -> task::Instant {
        let cb = self.get_system_time_fn();
        (cb.cb)()
    }
    /// Get immutable reference to the renderer resources
    ///
    /// This provides access to fonts, images, and other rendering resources.
    /// Useful for custom rendering or screenshot functionality.
    pub fn get_renderer_resources(&self) -> &RendererResources {
        unsafe { (*self.ref_data).renderer_resources }
    }
    // Font Cache Introspection
    //
    // These let a callback discover and retrieve the fonts the layout engine
    // has actually loaded into its font cache, without having to pass them in
    // up-front. The primary use case is "embed every font the layout actually
    // used" from a callback (e.g. a printpdf consumer correlating
    // `DisplayListItem::Text.font_hash` glyph runs with the loaded font bytes).
    //
    // IMAGE GAP (deferred): there is no analogous `get_loaded_image_*` here yet.
    // Unlike fonts (whose cache lives in `LayoutWindow.font_manager`, reachable
    // via `get_layout_window()`), the live image cache with the actual
    // `ImageRef` bytes is owned by the windowing shell (`common.image_cache`)
    // and is only passed *by reference* into the layout pass - it is not stored
    // on the `LayoutWindow` (its `image_cache` field is initialised empty and
    // never populated) and is not part of `CallbackInfoRefData`. Exposing image
    // bytes here therefore requires threading `&ImageCache` into
    // `CallbackInfoRefData` and through `invoke_single_callback` /
    // `run_single_timer` / `run_all_threads` and all their per-platform callers
    // (macOS / Wayland / X11 / Windows). `RendererResources.currently_registered_images`
    // is reachable via `get_renderer_resources()` but only carries the WebRender
    // key + `ImageDescriptor`, not the pixel bytes.
    /// Enumerate every font the layout engine currently has loaded in its font
    /// cache.
    ///
    /// Returns one [`LoadedFont`](azul_core::resources::LoadedFont) descriptor
    /// per loaded face. The `font_hash` field of each descriptor is identical
    /// to the `font_hash` carried by `DisplayListItem::Text` glyph runs, so a
    /// callback can correlate a loaded font with the text that uses it and then
    /// pull the raw bytes via [`get_loaded_font_bytes`](Self::get_loaded_font_bytes).
    ///
    /// The list includes fallback faces that were resolved during layout, not
    /// just the families named in the source CSS.
    #[cfg(feature = "text_layout")]
    pub fn get_loaded_fonts(&self) -> LoadedFontVec {
        let font_manager = &self.get_layout_window().font_manager;
        let guard = match font_manager.parsed_fonts.lock() {
            Ok(g) => g,
            Err(_) => return Vec::new().into(),
        };
        // BTreeMap-style stable iteration is not guaranteed here (HashMap), so
        // we collect then sort by font_hash for a deterministic order.
        let mut out: Vec<LoadedFont> = guard
            .values()
            .map(|font_ref| {
                let parsed = crate::font_ref_to_parsed_font(font_ref);
                let family_name = parsed
                    .font_name
                    .as_ref()
                    .map(|s| AzString::from(s.clone()))
                    .unwrap_or_default();
                LoadedFont {
                    font_hash: parsed.hash,
                    family_name,
                    num_glyphs: parsed.num_glyphs as u32,
                    has_bytes: parsed.source_bytes_for_subset().is_some(),
                }
            })
            .collect();
        out.sort_by(|a, b| a.font_hash.cmp(&b.font_hash));
        out.into()
    }
    /// Retrieve the raw source bytes (TTF / OTF / TTC, etc.) for a loaded font,
    /// looked up by the `font_hash` returned from
    /// [`get_loaded_fonts`](Self::get_loaded_fonts) (or carried on a
    /// `DisplayListItem::Text` glyph run).
    ///
    /// Returns `None` if no loaded font matches `font_hash`, or if the matching
    /// font did not retain its source bytes (e.g. a test-only font; production
    /// fonts loaded from disk retain an mmap-backed handle and always succeed).
    /// The returned bytes can be embedded directly into a generated document.
    #[cfg(feature = "text_layout")]
    pub fn get_loaded_font_bytes(&self, font_hash: u64) -> OptionU8Vec {
        let font_manager = &self.get_layout_window().font_manager;
        let font_ref = match font_manager.get_font_by_hash(font_hash) {
            Some(f) => f,
            None => return OptionU8Vec::None,
        };
        let parsed = crate::font_ref_to_parsed_font(&font_ref);
        match parsed.source_bytes_for_subset() {
            Some(bytes) => OptionU8Vec::Some(U8Vec::from_vec(bytes.as_slice().to_vec())),
            None => OptionU8Vec::None,
        }
    }
    // Screenshot API
    /// Take a CPU-rendered screenshot of the current window content
    ///
    /// This renders the current display list to a PNG image using CPU rendering.
    /// The screenshot captures the window content as it would appear on screen,
    /// without window decorations.
    ///
    /// # Arguments
    /// * `dom_id` - The DOM to screenshot (use the main DOM ID for the full window)
    ///
    /// # Returns
    /// * `Ok(Vec<u8>)` - PNG-encoded image data
    /// * `Err(String)` - Error message if rendering failed
    ///
    /// # Example
    /// ```ignore
    /// fn on_click(info: &mut CallbackInfo) -> Update {
    ///     let dom_id = info.get_hit_node().dom;
    ///     match info.take_screenshot(dom_id) {
    ///         Ok(png_data) => {
    ///             std::fs::write("screenshot.png", png_data).unwrap();
    ///         }
    ///         Err(e) => eprintln!("Screenshot failed: {}", e),
    ///     }
    ///     Update::DoNothing
    /// }
    /// ```
    #[cfg(feature = "cpurender")]
    pub fn take_screenshot(&self, dom_id: DomId) -> Result<alloc::vec::Vec<u8>, AzString> {
        use crate::cpurender::{render_with_font_manager_and_scroll, CpuRenderState, RenderOptions, ScrollOffsetMap};
        let layout_window = self.get_layout_window();
        let renderer_resources = &layout_window.renderer_resources;
        // Get the layout result for this DOM
        let layout_result = layout_window
            .layout_results
            .get(&dom_id)
            .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
        // Use the current window state dimensions
        let ws = self.get_current_window_state();
        let width = ws.size.dimensions.width;
        let height = ws.size.dimensions.height;
        if width <= 0.0 || height <= 0.0 {
            return Err(AzString::from("Invalid viewport dimensions"));
        }
        let display_list = &layout_result.display_list;
        let dpi_factor = ws.size.get_hidpi_factor().inner.get();
        // Build scroll offset map from the current ScrollManager state
        let scroll_offsets = layout_window.scroll_manager
            .build_scroll_offset_map(dom_id, &layout_result.scroll_ids);
        // Build CPU render state from GpuValueCache - provides current
        // transform values (scrollbar thumb positions) and opacity values
        // (scrollbar visibility fading) that the GPU path animates dynamically.
        let gpu_cache = layout_window.gpu_state_manager
            .get_cache(dom_id);
        let render_state = CpuRenderState::from_gpu_cache(
            gpu_cache,
            dom_id,
            &scroll_offsets,
        )
        .with_system_style(layout_window.system_style.clone());
        let opts = RenderOptions {
            width,
            height,
            dpi_factor,
        };
        let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
        let pixmap = render_with_font_manager_and_scroll(
            display_list,
            renderer_resources,
            &layout_window.font_manager,
            opts,
            &mut glyph_cache,
            &render_state,
        ).map_err(|e| AzString::from(e))?;
        // Encode to PNG
        let png_data = pixmap
            .encode_png()
            .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {}", e)))?;
        Ok(png_data)
    }
    /// Take a screenshot and save it directly to a file
    ///
    /// Convenience method that combines `take_screenshot` with file writing.
    ///
    /// # Arguments
    /// * `dom_id` - The DOM to screenshot
    /// * `path` - The file path to save the PNG to
    ///
    /// # Returns
    /// * `Ok(())` - Screenshot saved successfully
    /// * `Err(String)` - Error message if rendering or saving failed
    #[cfg(all(feature = "std", feature = "cpurender"))]
    pub fn take_screenshot_to_file(&self, dom_id: DomId, path: &str) -> Result<(), AzString> {
        let png_data = self.take_screenshot(dom_id)?;
        std::fs::write(path, png_data)
            .map_err(|e| AzString::from(alloc::format!("Failed to write file: {}", e)))?;
        Ok(())
    }
    /// Take a native OS-level screenshot of the window including window decorations
    ///
    /// **NOTE**: This is a stub implementation. For full native screenshot support,
    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate, which uses
    /// runtime dynamic loading (dlopen) to avoid static linking dependencies.
    ///
    /// # Returns
    /// * `Err(String)` - Always returns an error directing to use the extension trait
    #[cfg(feature = "std")]
    pub fn take_native_screenshot(&self, _path: &str) -> Result<(), AzString> {
        Err(AzString::from(
            "Native screenshot requires the NativeScreenshotExt trait from azul-dll crate. \
             Import it with: use azul::desktop::NativeScreenshotExt;",
        ))
    }
    /// Take a native OS-level screenshot and return the PNG data as bytes
    ///
    /// **NOTE**: This is a stub implementation. For full native screenshot support,
    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate.
    ///
    /// # Returns
    /// * `Ok(Vec<u8>)` - PNG-encoded image data
    /// * `Err(String)` - Error message if screenshot failed
    #[cfg(feature = "std")]
    pub fn take_native_screenshot_bytes(&self) -> Result<alloc::vec::Vec<u8>, AzString> {
        // Create a temporary file, take screenshot, read bytes, delete file
        let temp_path = std::env::temp_dir().join("azul_screenshot_temp.png");
        let temp_path_str = temp_path.to_string_lossy().to_string();
        self.take_native_screenshot(&temp_path_str)?;
        let bytes = std::fs::read(&temp_path)
            .map_err(|e| AzString::from(alloc::format!("Failed to read screenshot: {}", e)))?;
        let _ = std::fs::remove_file(&temp_path);
        Ok(bytes)
    }
    /// Take a native OS-level screenshot and return as a Base64 data URI
    ///
    /// Returns the screenshot as a "data:image/png;base64,..." string that can
    /// be directly used in HTML img tags or JSON responses.
    ///
    /// # Returns
    /// * `Ok(String)` - Base64 data URI string
    /// * `Err(String)` - Error message if screenshot failed
    ///
    #[cfg(feature = "std")]
    pub fn take_native_screenshot_base64(&self) -> Result<AzString, AzString> {
        let png_bytes = self.take_native_screenshot_bytes()?;
        let base64_str = base64_encode(&png_bytes);
        Ok(AzString::from(alloc::format!(
            "data:image/png;base64,{}",
            base64_str
        )))
    }
    /// Take a CPU-rendered screenshot and return as a Base64 data URI
    ///
    /// Returns the screenshot as a "data:image/png;base64,..." string.
    /// This is the software-rendered version without window decorations.
    ///
    /// # Returns
    /// * `Ok(String)` - Base64 data URI string
    /// * `Err(String)` - Error message if rendering failed
    #[cfg(feature = "cpurender")]
    pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
        let png_bytes = self.take_screenshot(dom_id)?;
        let base64_str = base64_encode(&png_bytes);
        Ok(AzString::from(alloc::format!(
            "data:image/png;base64,{}",
            base64_str
        )))
    }
    // Manager Access (Read-Only)
    /// Get immutable reference to the scroll manager
    ///
    /// Use this to query scroll state for nodes without modifying it.
    /// To request programmatic scrolling, use `nodes_scrolled_in_callback`.
    pub fn get_scroll_manager(&self) -> &ScrollManager {
        unsafe { &(*self.ref_data).layout_window.scroll_manager }
    }
    /// Get immutable reference to the gesture and drag manager
    ///
    /// Use this to query current gesture/drag state (e.g., "is this node being dragged?",
    /// "what files are being dropped?", "is a long-press active?").
    ///
    /// The manager is updated by the event loop and provides read-only query access
    /// to callbacks for gesture-aware UI behavior.
    pub fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
        unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
    }
    /// Queue a platform-native gesture-recognizer result. Applied by
    /// the event-loop after the callback returns, via
    /// `CallbackChange::InjectNativeGesture` -> `GestureAndDragManager::
    /// inject_native_gesture`. Used by the iOS / Android / macOS
    /// platform backends from their gesture-recognizer callbacks and by
    /// the e2e debug-server harness so JSON tests can drive every event
    /// filter end-to-end.
    pub fn inject_native_gesture(
        &mut self,
        gesture: crate::managers::gesture::NativeGestureEvent,
    ) {
        self.push_change(CallbackChange::InjectNativeGesture { gesture });
    }
    /// Get immutable reference to the focus manager
    ///
    /// Use this to query which node currently has focus and whether focus
    /// is being moved to another node.
    pub fn get_focus_manager(&self) -> &FocusManager {
        &self.get_layout_window().focus_manager
    }
    /// Get a reference to the undo/redo manager
    ///
    /// This allows user callbacks to query the undo/redo state and intercept
    /// undo/redo operations via preventDefault().
    pub fn get_undo_redo_manager(&self) -> &UndoRedoManager {
        &self.get_layout_window().undo_redo_manager
    }
    /// Get immutable reference to the hover manager
    ///
    /// Use this to query which nodes are currently hovered at various input points
    /// (mouse, touch points, pen).
    pub fn get_hover_manager(&self) -> &HoverManager {
        &self.get_layout_window().hover_manager
    }
    /// Get immutable reference to the text input manager
    ///
    /// Use this to query text selection state, cursor positions, and IME composition.
    pub fn get_text_input_manager(&self) -> &TextInputManager {
        &self.get_layout_window().text_input_manager
    }
    /// Check if multi_cursor has any selection ranges.
    ///
    /// Replaces the removed `get_selection_manager()`.
    pub fn has_any_selection(&self) -> bool {
        self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
            .unwrap_or(false)
    }
    /// Check if a specific node is currently focused
    pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
        self.get_focus_manager().has_focus(&node_id)
    }
    /// Check if any node in a specific DOM is focused
    pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
        self.get_focused_node()
            .map(|n| n.dom == dom_id)
            .unwrap_or(false)
    }
    // Pen/Stylus Query Methods
    /// Get current pen/stylus state if a pen is active
    pub fn get_pen_state(&self) -> Option<&PenState> {
        self.get_gesture_drag_manager().get_pen_state()
    }
    /// Get the current Wacom tablet-**pad** state (ExpressKeys + touch-ring),
    /// or `None` if no pad backend has delivered one. (The pen's own wacom
    /// features - eraser / barrel button / barrel roll / tilt / pressure -
    /// are in [`CallbackInfo::get_pen_state`].) Kept live by the platform pad
    /// backend (Wintab / libwacom+libinput / macOS tablet `NSEvent`s).
    pub fn get_wacom_pad(&self) -> Option<crate::managers::gesture::WacomPadState> {
        self.get_gesture_drag_manager().get_pad_state().copied()
    }
    /// Get the most recent geolocation fix, or `None` if no `GeolocationProbe`
    /// is mounted or no platform backend has delivered a fix yet. The fix is
    /// kept live by the platform backends (Android `FusedLocationProvider`,
    /// iOS/macOS `CLLocationManager`) via the async fix channel that the
    /// layout pass folds into the manager - so a callback can read the user's
    /// position to, e.g., place a "you are here" marker on a map.
    pub fn get_location_fix(&self) -> Option<azul_core::geolocation::LocationFix> {
        self.get_layout_window().geolocation_manager.latest_fix()
    }
    /// Get the latest motion-sensor reading for `kind` (Accelerometer /
    /// Gyroscope / Magnetometer), or `None` if no platform backend has
    /// delivered one. Kept live by the sensor backends (iOS CoreMotion,
    /// Android `SensorManager`) via the async channel the layout pass folds
    /// into the manager - so a callback can drive tilt / shake / compass UI.
    pub fn get_sensor_reading(
        &self,
        kind: azul_core::sensors::SensorKind,
    ) -> Option<azul_core::sensors::SensorReading> {
        self.get_layout_window().sensor_manager.reading(kind)
    }
    /// The safe-area insets (notch / system-UI margins) for this window, in
    /// logical px - lay out interactive content within them so it isn't hidden
    /// by a notch / rounded corners / status bar. Zero where the platform or
    /// window has no inset. Set by the platform shell (macOS `NSScreen` notch,
    /// iOS `UIView.safeAreaInsets`, Android `WindowInsets`).
    pub fn get_safe_area_insets(&self) -> azul_css::system::SafeAreaInsets {
        self.get_layout_window().safe_area_insets
    }
    /// Get the latest state of the gamepad `id` (button bitset + analog
    /// axes), or `None` if no pad with that id has connected. Kept live by
    /// the controller backend (gilrs / iOS `GCController` / Android
    /// `InputDevice`) via the async channel the layout pass folds into the
    /// manager - so a callback can drive movement / menu UI. For the common
    /// single-controller case, [`CallbackInfo::get_primary_gamepad`] skips
    /// the id bookkeeping.
    pub fn get_gamepad_state(
        &self,
        id: azul_core::gamepad::GamepadId,
    ) -> Option<azul_core::gamepad::GamepadState> {
        self.get_layout_window().gamepad_manager.state(id)
    }
    /// Get the first currently-connected gamepad, or `None` if none is
    /// connected - the convenient single-controller accessor.
    pub fn get_primary_gamepad(&self) -> Option<azul_core::gamepad::GamepadState> {
        self.get_layout_window().gamepad_manager.primary()
    }
    /// Get the most recent biometric-auth result, or `None` if no
    /// `request_biometric_auth` has completed yet. Kept live by the
    /// platform backends (iOS/macOS `LAContext`, Android `BiometricPrompt`,
    /// Windows `UserConsentVerifier`) via the async result channel the
    /// layout pass folds into the manager - so a callback can unlock a
    /// vault / settings panel once the user authenticates.
    pub fn get_biometric_result(&self) -> Option<azul_core::biometric::BiometricResult> {
        self.get_layout_window().biometric_manager.last_result()
    }
    /// Get the device's biometric capability (sync probe): `Face`,
    /// `Fingerprint`, `Iris`, or `NotAvailable`. Lets a callback decide
    /// whether to even offer a biometric unlock before requesting one
    /// (no OS prompt is shown - this just reads the cached probe).
    pub fn get_biometric_kind(&self) -> azul_core::biometric::BiometricKind {
        self.get_layout_window().biometric_manager.availability()
    }
    /// Request a biometric-auth prompt (Face ID / Touch ID / Android
    /// `BiometricPrompt` / Windows Hello). Returns immediately - the OS
    /// draws its own modal asynchronously; the outcome arrives on a later
    /// frame and is read via [`CallbackInfo::get_biometric_result`]. Call
    /// this from, e.g., an unlock button's `on_click`. The `prompt`
    /// configures the reason text, cancel label, and whether the OS
    /// passcode fallback is allowed. (No platform backend reports a real
    /// outcome yet - the request currently resolves to
    /// `BiometricResult::Unavailable`; the iOS/macOS/Android backends land
    /// in a later tick.)
    pub fn request_biometric_auth(&mut self, prompt: azul_core::biometric::BiometricPrompt) {
        crate::managers::biometric::push_biometric_request(prompt);
    }
    /// Store `secret` under `key` in the OS keyring (Keychain / KeyStore /
    /// libsecret / CredentialLocker). When `require_biometry` is set, a
    /// later `keyring_get` of this key triggers the OS biometric prompt.
    /// Returns immediately; the outcome arrives via `get_keyring_result()`
    /// on a later frame.
    pub fn keyring_store(&mut self, key: AzString, secret: AzString, require_biometry: bool) {
        crate::managers::keyring::push_keyring_request(
            azul_core::keyring::KeyringRequest::Store {
                key,
                secret,
                require_biometry,
            },
        );
    }
    /// Read the secret stored under `key`. A biometry-bound item shows the
    /// OS prompt first; the secret (or a denial) arrives via
    /// `get_keyring_result()` on a later frame.
    pub fn keyring_get(&mut self, key: AzString) {
        crate::managers::keyring::push_keyring_request(azul_core::keyring::KeyringRequest::Get {
            key,
        });
    }
    /// Remove the item stored under `key` from the OS keyring (no-op if
    /// absent). The outcome arrives via `get_keyring_result()`.
    pub fn keyring_delete(&mut self, key: AzString) {
        crate::managers::keyring::push_keyring_request(
            azul_core::keyring::KeyringRequest::Delete { key },
        );
    }
    /// Get the most recent keyring outcome, or `None` until the first op
    /// completes. Read after a `keyring_store/get/delete` to observe the
    /// result - e.g. the revealed secret from a `keyring_get`
    /// (`KeyringResult::Retrieved`).
    pub fn get_keyring_result(&self) -> Option<azul_core::keyring::KeyringResult> {
        self.get_layout_window().keyring_manager.last_result().cloned()
    }
    /// Read the most recently observed permission state for `capability`
    /// (Camera / Microphone / Geolocation / Sensors / Notifications / …) — e.g.
    /// so a callback can check a capability is `Granted` before using it (show
    /// a camera preview only once granted). Kept live by the platform
    /// permission backend; a capability is subscribed by mounting its probe
    /// node (CameraProbe / GeolocationProbe / …) into the DOM.
    pub fn get_permission_status(
        &self,
        capability: crate::managers::permission::Capability,
    ) -> crate::managers::permission::PermissionState {
        self.get_layout_window()
            .permission_manager
            .get_status(capability)
    }
    /// Get current pen pressure (0.0 to 1.0)
    /// Returns None if no pen is active, Some(0.5) for mouse
    pub fn get_pen_pressure(&self) -> Option<f32> {
        self.get_pen_state().map(|pen| pen.pressure)
    }
    /// Get current pen tilt angles (x_tilt, y_tilt) in degrees
    /// Returns None if no pen is active
    pub fn get_pen_tilt(&self) -> Option<PenTilt> {
        self.get_pen_state().map(|pen| pen.tilt)
    }
    /// Check if pen is currently in contact with surface
    pub fn is_pen_in_contact(&self) -> bool {
        self.get_pen_state()
            .map(|pen| pen.in_contact)
            .unwrap_or(false)
    }
    /// Check if pen is in eraser mode
    pub fn is_pen_eraser(&self) -> bool {
        self.get_pen_state()
            .map(|pen| pen.is_eraser)
            .unwrap_or(false)
    }
    /// Check if pen barrel button is pressed
    pub fn is_pen_barrel_button_pressed(&self) -> bool {
        self.get_pen_state()
            .map(|pen| pen.barrel_button_pressed)
            .unwrap_or(false)
    }
    /// Get the last recorded input sample (for event_id and detailed input data)
    pub fn get_last_input_sample(&self) -> Option<&InputSample> {
        let manager = self.get_gesture_drag_manager();
        manager
            .get_current_session()
            .and_then(|session| session.last_sample())
    }
    /// Get the event ID of the current event
    pub fn get_current_event_id(&self) -> Option<u64> {
        self.get_last_input_sample().map(|sample| sample.event_id)
    }
    // Gesture Query Methods
    //
    // These read whatever the in-process `GestureAndDragManager` has detected
    // from the touch / mouse stream. On platforms with native gesture
    // recognizers (iOS UIKit, Android `GestureDetector`), the platform
    // backend may inject pre-detected gestures via
    // `GestureAndDragManager::inject_native_gesture(...)` - accessors below
    // see the same data regardless of source, fulfilling Azul's
    // "superset of every platform" guarantee for gesture handlers.
    /// Returns the dominant direction of the current swipe gesture, if any.
    /// Detection uses the touch / pointer trajectory and a velocity
    /// threshold; on iOS / Android the platform backend may override the
    /// in-process detector with a native gesture-recognizer result.
    pub fn get_swipe_direction(&self) -> crate::managers::gesture::OptionGestureDirection {
        self.get_gesture_drag_manager().detect_swipe_direction().into()
    }
    /// Returns the active pinch gesture (scale + center + distances), if any.
    pub fn get_pinch(&self) -> crate::managers::gesture::OptionDetectedPinch {
        self.get_gesture_drag_manager().detect_pinch().into()
    }
    /// Returns the active rotation gesture (radians + center), if any.
    pub fn get_rotation(&self) -> crate::managers::gesture::OptionDetectedRotation {
        self.get_gesture_drag_manager().detect_rotation().into()
    }
    /// Returns the active long-press, if the user is currently holding a
    /// pointer in place beyond the configured threshold.
    pub fn get_long_press(&self) -> crate::managers::gesture::OptionDetectedLongPress {
        self.get_gesture_drag_manager().detect_long_press().into()
    }
    /// True iff the gesture manager classified the current event sequence
    /// as a double-click / double-tap.
    pub fn was_double_clicked(&self) -> bool {
        self.get_gesture_drag_manager().detect_double_click()
    }
    // Focus Management Methods
    /// Set focus to a specific DOM node by ID
    pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
        self.set_focus(FocusTarget::Id(DomNodeId {
            dom: dom_id,
            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
        }));
    }
    /// Set focus to a node matching a CSS path
    pub fn set_focus_to_path(&mut self, dom_id: DomId, css_path: CssPath) {
        self.set_focus(FocusTarget::Path(FocusTargetPath {
            dom: dom_id,
            css_path,
        }));
    }
    /// Move focus to next focusable element in tab order
    pub fn focus_next(&mut self) {
        self.set_focus(FocusTarget::Next);
    }
    /// Move focus to previous focusable element in tab order
    pub fn focus_previous(&mut self) {
        self.set_focus(FocusTarget::Previous);
    }
    /// Move focus to first focusable element
    pub fn focus_first(&mut self) {
        self.set_focus(FocusTarget::First);
    }
    /// Move focus to last focusable element
    pub fn focus_last(&mut self) {
        self.set_focus(FocusTarget::Last);
    }
    /// Remove focus from all elements
    pub fn clear_focus(&mut self) {
        self.set_focus(FocusTarget::NoFocus);
    }
    // Manager Access Methods
    /// Check if a drag gesture is currently active
    ///
    /// Convenience method that queries the gesture manager.
    pub fn is_dragging(&self) -> bool {
        self.get_gesture_drag_manager().is_dragging()
    }
    /// Get the currently focused node (if any)
    ///
    /// Returns None if no node has focus.
    pub fn get_focused_node(&self) -> Option<DomNodeId> {
        self.get_layout_window()
            .focus_manager
            .get_focused_node()
            .copied()
    }
    /// Check if a specific node has focus
    pub fn has_focus(&self, node_id: DomNodeId) -> bool {
        self.get_layout_window().focus_manager.has_focus(&node_id)
    }
    /// Get the currently hovered file (if drag-drop is in progress)
    ///
    /// Returns None if no file is being hovered over the window.
    pub fn get_hovered_file(&self) -> Option<&azul_css::AzString> {
        self.get_layout_window()
            .file_drop_manager
            .get_hovered_file()
    }
    /// Get the currently dropped file (if a file was just dropped)
    ///
    /// This is a one-shot value that is cleared after event processing.
    /// Returns None if no file was dropped this frame.
    pub fn get_dropped_file(&self) -> Option<&azul_css::AzString> {
        self.get_layout_window()
            .file_drop_manager
            .dropped_file
            .as_ref()
    }
    /// Check if a node or file drag is currently active
    ///
    /// Returns true if either a node drag or file drag is in progress.
    /// Uses gesture_drag_manager as the primary source of truth,
    /// with drag_drop_manager as fallback.
    pub fn is_drag_active(&self) -> bool {
        let lw = self.get_layout_window();
        lw.gesture_drag_manager.is_dragging() || lw.drag_drop_manager.is_dragging()
    }
    /// Check if a node drag is specifically active
    pub fn is_node_drag_active(&self) -> bool {
        let lw = self.get_layout_window();
        lw.gesture_drag_manager.is_node_dragging_any() || lw.drag_drop_manager.is_dragging_node()
    }
    /// Check if a file drag is specifically active
    pub fn is_file_drag_active(&self) -> bool {
        let lw = self.get_layout_window();
        lw.gesture_drag_manager.is_file_dropping() || lw.drag_drop_manager.is_dragging_file()
    }
    /// Get the current drag/drop state (if any)
    ///
    /// Returns None if no drag is active, or Some with drag state.
    /// Checks gesture_drag_manager first, then falls back to drag_drop_manager.
    pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
        let lw = self.get_layout_window();
        // Try gesture manager first (primary source of truth)
        if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
            return crate::managers::drag_drop::DragState::from_context(ctx);
        }
        // Fallback to drag_drop_manager
        lw.drag_drop_manager.get_drag_state()
    }
    /// Get the current drag context (if any)
    ///
    /// Returns None if no drag is active, or Some with drag context.
    /// Prefer this over get_drag_state for new code.
    pub fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
        self.get_layout_window().drag_drop_manager.get_drag_context()
    }
    // Hover Manager Access
    /// Get the current mouse cursor hit test result (most recent frame)
    pub fn get_current_hit_test(&self) -> Option<&FullHitTest> {
        self.get_hover_manager().get_current(&InputPointId::Mouse)
    }
    /// Get mouse cursor hit test from N frames ago (0 = current, 1 = previous, etc.)
    pub fn get_hit_test_frame(&self, frames_ago: usize) -> Option<&FullHitTest> {
        self.get_hover_manager()
            .get_frame(&InputPointId::Mouse, frames_ago)
    }
    /// Get the full mouse cursor hit test history (up to 5 frames)
    ///
    /// Returns None if no mouse history exists yet
    pub fn get_hit_test_history(&self) -> Option<&VecDeque<FullHitTest>> {
        self.get_hover_manager().get_history(&InputPointId::Mouse)
    }
    /// Check if there's sufficient mouse history for gesture detection (at least 2 frames)
    pub fn has_sufficient_history_for_gestures(&self) -> bool {
        self.get_hover_manager()
            .has_sufficient_history_for_gestures(&InputPointId::Mouse)
    }
    // File Drop Manager Access
    /// Get immutable reference to the file drop manager
    pub fn get_file_drop_manager(&self) -> &FileDropManager {
        &self.get_layout_window().file_drop_manager
    }
    // Drag-Drop Manager Access
    /// Get immutable reference to the drag-drop manager
    pub fn get_drag_drop_manager(&self) -> &DragDropManager {
        &self.get_layout_window().drag_drop_manager
    }
    /// Get the node being dragged (if any)
    pub fn get_dragged_node(&self) -> Option<DomNodeId> {
        self.get_drag_drop_manager()
            .get_drag_context()
            .and_then(|ctx| {
                ctx.as_node_drag().map(|node_drag| {
                    DomNodeId {
                        dom: node_drag.dom_id,
                        node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
                    }
                })
            })
    }
    /// Get the file path being dragged (if any)
    pub fn get_dragged_file(&self) -> Option<&AzString> {
        self.get_drag_drop_manager()
            .get_drag_context()
            .and_then(|ctx| {
                ctx.as_file_drop().and_then(|file_drop| {
                    file_drop.files.as_ref().first()
                })
            })
    }
    /// Get the MIME types available in the current drag data.
    ///
    /// W3C equivalent: `dataTransfer.types`
    /// Returns an empty vec if no drag is active or no data is set.
    pub fn get_drag_types(&self) -> Vec<AzString> {
        let lw = self.get_layout_window();
        // Try gesture manager first
        if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
            if let Some(node_drag) = ctx.as_node_drag() {
                return node_drag
                    .drag_data
                    .data
                    .as_ref()
                    .iter()
                    .map(|e| e.mime_type.clone())
                    .collect();
            }
        }
        // Fallback to drag_drop_manager
        if let Some(ctx) = lw.drag_drop_manager.get_drag_context() {
            if let Some(node_drag) = ctx.as_node_drag() {
                return node_drag
                    .drag_data
                    .data
                    .as_ref()
                    .iter()
                    .map(|e| e.mime_type.clone())
                    .collect();
            }
        }
        Vec::new()
    }
    /// Get drag data for a specific MIME type.
    ///
    /// W3C equivalent: `dataTransfer.getData(type)`
    /// Returns None if no drag is active or the MIME type is not set.
    pub fn get_drag_data(&self, mime_type: &str) -> Option<Vec<u8>> {
        let lw = self.get_layout_window();
        if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
            if let Some(node_drag) = ctx.as_node_drag() {
                return node_drag.drag_data.get_data(mime_type).map(|s| s.to_vec());
            }
        }
        if let Some(ctx) = lw.drag_drop_manager.get_drag_context() {
            if let Some(node_drag) = ctx.as_node_drag() {
                return node_drag.drag_data.get_data(mime_type).map(|s| s.to_vec());
            }
        }
        None
    }
    /// Set drag data for a MIME type on the active drag operation.
    ///
    /// W3C equivalent: `dataTransfer.setData(type, data)`
    /// Should be called from a DragStart callback to populate the drag data.
    pub fn set_drag_data(&mut self, mime_type: AzString, data: Vec<u8>) {
        self.push_change(CallbackChange::SetDragData { mime_type, data });
    }
    /// Accept the current drop operation on this node.
    ///
    /// W3C equivalent: calling `event.preventDefault()` in a DragOver handler.
    /// This signals that the current drop target can accept the dragged data.
    /// Must be called from a DragOver or DragEnter callback for the Drop event
    /// to fire on this node.
    pub fn accept_drop(&mut self) {
        self.push_change(CallbackChange::AcceptDrop);
    }
    /// Set the drop effect for the current drag operation.
    ///
    /// W3C equivalent: `dataTransfer.dropEffect = "move"|"copy"|"link"`
    /// Should be called from a DragOver or DragEnter callback.
    pub fn set_drop_effect(&mut self, effect: azul_core::drag::DropEffect) {
        self.push_change(CallbackChange::SetDropEffect { effect });
    }
    // Scroll Manager Query Methods
    /// Get the current scroll offset for the hit node (if it's scrollable)
    ///
    /// Convenience method that uses the `hit_dom_node` from this callback.
    /// Use `get_scroll_offset_for_node` if you need to query a specific node.
    pub fn get_scroll_offset(&self) -> Option<LogicalPosition> {
        self.get_scroll_offset_for_node(
            self.hit_dom_node.dom,
            self.hit_dom_node.node.into_crate_internal()?,
        )
    }
    /// Get the current scroll offset for a specific node (if it's scrollable)
    pub fn get_scroll_offset_for_node(
        &self,
        dom_id: DomId,
        node_id: NodeId,
    ) -> Option<LogicalPosition> {
        self.get_scroll_manager()
            .get_current_offset(dom_id, node_id)
    }
    /// Get the scroll state (container rect, content rect, current offset) for a node
    pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
        self.get_scroll_manager().get_scroll_state(dom_id, node_id)
    }
    /// Get a read-only snapshot of a scroll node's bounds and position.
    ///
    /// This is the recommended API for timer callbacks that need to compute
    /// scroll physics. Returns container/content rects and max scroll bounds.
    pub fn get_scroll_node_info(
        &self,
        dom_id: DomId,
        node_id: NodeId,
    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
        self.get_scroll_manager()
            .get_scroll_node_info(dom_id, node_id)
    }
    /// Deprecated: Returns None. Scroll deltas are no longer tracked per-frame.
    /// Kept for FFI backward compatibility.
    pub fn get_scroll_delta(
        &self,
        _dom_id: DomId,
        _node_id: NodeId,
    ) -> Option<LogicalPosition> {
        None
    }
    /// Deprecated: Returns false. Scroll activity flags were removed.
    /// Kept for FFI backward compatibility.
    pub fn had_scroll_activity(
        &self,
        _dom_id: DomId,
        _node_id: NodeId,
    ) -> bool {
        false
    }
    /// Find the closest scrollable ancestor of a node.
    ///
    /// Walks up the node hierarchy to find a node registered in the ScrollManager.
    /// Used by auto-scroll timer to find which container to scroll.
    pub fn find_scroll_parent(
        &self,
        dom_id: DomId,
        node_id: NodeId,
    ) -> Option<NodeId> {
        let layout_window = self.get_layout_window();
        let layout_results = &layout_window.layout_results;
        let lr = layout_results.get(&dom_id)?;
        let node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem] =
            lr.styled_dom.node_hierarchy.as_ref();
        self.get_scroll_manager()
            .find_scroll_parent(dom_id, node_id, node_hierarchy)
    }
    /// Get a clone of the scroll input queue for consuming pending inputs.
    ///
    /// Timer callbacks use this to drain pending scroll inputs recorded by
    /// platform event handlers. The queue is thread-safe (Arc<Mutex>), so
    /// the timer can call `take_all()` with only `&self`.
    #[cfg(feature = "std")]
    pub fn get_scroll_input_queue(
        &self,
    ) -> crate::managers::scroll_state::ScrollInputQueue {
        self.get_scroll_manager().scroll_input_queue.clone()
    }
    // Gpu State Manager Access
    /// Get immutable reference to the GPU state manager
    pub fn get_gpu_state_manager(&self) -> &GpuStateManager {
        &self.get_layout_window().gpu_state_manager
    }
    // VirtualView Manager Access
    /// Get immutable reference to the VirtualView manager
    pub fn get_virtual_view_manager(&self) -> &VirtualViewManager {
        &self.get_layout_window().virtual_view_manager
    }
    // Changeset Inspection/Modification Methods
    // These methods allow callbacks to inspect pending operations and modify them before execution
    /// Inspect a pending copy operation
    ///
    /// Returns the clipboard content that would be copied if the operation proceeds.
    /// Use this to validate or transform clipboard content before copying.
    pub fn inspect_copy_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
        let layout_window = self.get_layout_window();
        let dom_id = &target.dom;
        layout_window.get_selected_content_for_clipboard(dom_id)
    }
    /// Inspect a pending cut operation
    ///
    /// Returns the clipboard content that would be cut (copied + deleted).
    /// Use this to validate or transform content before cutting.
    pub fn inspect_cut_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
        // Cut uses same content extraction as copy
        self.inspect_copy_changeset(target)
    }
    /// Inspect the current selection range that would be affected by paste
    ///
    /// Returns the selection range that will be replaced when pasting.
    /// Returns None if no selection exists (paste will insert at cursor).
    pub fn inspect_paste_target_range(&self, _target: DomNodeId) -> Option<SelectionRange> {
        let layout_window = self.get_layout_window();
        layout_window
            .text_edit_manager.multi_cursor.as_ref()
            .and_then(|mc| mc.selections.iter().find_map(|s| match &s.selection {
                Selection::Range(r) => Some(*r),
                _ => None,
            }))
    }
    /// Inspect what text would be selected by Select All operation
    ///
    /// Returns the full text content and the range that would be selected.
    pub fn inspect_select_all_changeset(&self, target: DomNodeId) -> Option<SelectAllResult> {
        use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
        let layout_window = self.get_layout_window();
        let node_id = target.node.into_crate_internal()?;
        // Get text content
        let content = layout_window.get_text_before_textinput(target.dom, node_id);
        let text = layout_window.extract_text_from_inline_content(&content);
        // Create selection range from start to end
        let start_cursor = TextCursor {
            cluster_id: GraphemeClusterId {
                source_run: 0,
                start_byte_in_run: 0,
            },
            affinity: CursorAffinity::Leading,
        };
        let end_cursor = TextCursor {
            cluster_id: GraphemeClusterId {
                source_run: 0,
                start_byte_in_run: text.len() as u32,
            },
            affinity: CursorAffinity::Leading,
        };
        let range = SelectionRange {
            start: start_cursor,
            end: end_cursor,
        };
        Some(SelectAllResult {
            full_text: text.into(),
            selection_range: range,
        })
    }
    /// Inspect what would be deleted by a backspace/delete operation
    ///
    /// Uses the pure functions from `text3::edit::inspect_delete()` to determine
    /// what would be deleted without actually performing the deletion.
    ///
    /// Returns (range_to_delete, deleted_text).
    /// - forward=true: Delete key (delete character after cursor)
    /// - forward=false: Backspace key (delete character before cursor)
    pub fn inspect_delete_changeset(
        &self,
        target: DomNodeId,
        forward: bool,
    ) -> Option<DeleteResult> {
        let layout_window = self.get_layout_window();
        let dom_id = &target.dom;
        let node_id = target.node.into_crate_internal()?;
        // Get the inline content for this node
        let content = layout_window.get_text_before_textinput(target.dom, node_id);
        // Get current selection state from multi_cursor
        let selection = if let Some(mc) = layout_window.text_edit_manager.multi_cursor.as_ref() {
            if let Some(range) = mc.selections.iter().find_map(|s| match &s.selection {
                Selection::Range(r) => Some(*r),
                _ => None,
            }) {
                Selection::Range(range)
            } else if let Some(cursor) = mc.get_primary_cursor() {
                Selection::Cursor(cursor)
            } else {
                return None;
            }
        } else {
            return None; // No multi_cursor active
        };
        // Use text3::edit::inspect_delete to determine what would be deleted
        crate::text3::edit::inspect_delete(&content, &selection, forward).map(|(range, text)| {
            DeleteResult {
                range_to_delete: range,
                deleted_text: text.into(),
            }
        })
    }
    /// Inspect a pending undo operation
    ///
    /// Returns the operation that would be undone, allowing inspection
    /// of what state will be restored.
    pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
        self.get_undo_redo_manager().peek_undo(node_id)
    }
    /// Inspect a pending redo operation
    ///
    /// Returns the operation that would be reapplied.
    pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
        self.get_undo_redo_manager().peek_redo(node_id)
    }
    /// Check if undo is available for a specific node
    ///
    /// Returns true if there is at least one undoable operation in the stack.
    pub fn can_undo(&self, node_id: NodeId) -> bool {
        self.get_undo_redo_manager()
            .get_stack(node_id)
            .map(|stack| stack.can_undo())
            .unwrap_or(false)
    }
    /// Check if redo is available for a specific node
    ///
    /// Returns true if there is at least one redoable operation in the stack.
    pub fn can_redo(&self, node_id: NodeId) -> bool {
        self.get_undo_redo_manager()
            .get_stack(node_id)
            .map(|stack| stack.can_redo())
            .unwrap_or(false)
    }
    /// Get the text that would be restored by undo for a specific node
    ///
    /// Returns the pre-state text content that would be restored if undo is performed.
    /// Returns None if no undo operation is available.
    pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
        self.get_undo_redo_manager()
            .peek_undo(node_id)
            .map(|op| op.pre_state.text_content.clone())
    }
    /// Get the text that would be restored by redo for a specific node
    ///
    /// Returns the pre-state text content that would be restored if redo is performed.
    /// Returns None if no redo operation is available.
    pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
        self.get_undo_redo_manager()
            .peek_redo(node_id)
            .map(|op| op.pre_state.text_content.clone())
    }
    // Clipboard Helper Methods
    /// Get clipboard content from system clipboard (available during paste operations)
    ///
    /// This returns content that was read from the system clipboard when Ctrl+V was pressed.
    /// It's only available in On::Paste callbacks or similar clipboard-related callbacks.
    ///
    /// Use this to inspect what will be pasted before allowing or modifying the paste operation.
    ///
    /// # Returns
    /// * `Some(&ClipboardContent)` - If paste is in progress and clipboard has content
    /// * `None` - If no paste operation is active or clipboard is empty
    pub fn get_clipboard_content(&self) -> Option<&ClipboardContent> {
        unsafe {
            (*self.ref_data)
                .layout_window
                .clipboard_manager
                .get_paste_content()
        }
    }
    /// Override clipboard content for copy/cut operations
    ///
    /// This sets custom content that will be written to the system clipboard.
    /// Use this in On::Copy or On::Cut callbacks to modify what gets copied.
    ///
    /// # Arguments
    /// * `content` - The clipboard content to write to system clipboard
    pub fn set_clipboard_content(&mut self, content: ClipboardContent) {
        self.set_copy_content(self.hit_dom_node, content);
    }
    /// Set/modify the clipboard content before a copy operation
    ///
    /// Use this to transform clipboard content before copying.
    /// The change is queued and will be applied after the callback returns,
    /// if preventDefault() was not called.
    pub fn set_copy_content(&mut self, target: DomNodeId, content: ClipboardContent) {
        self.push_change(CallbackChange::SetCopyContent { target, content });
    }
    /// Set/modify the clipboard content before a cut operation
    ///
    /// Similar to set_copy_content but for cut operations.
    /// The change is queued and will be applied after the callback returns.
    pub fn set_cut_content(&mut self, target: DomNodeId, content: ClipboardContent) {
        self.push_change(CallbackChange::SetCutContent { target, content });
    }
    /// Override the selection range for select-all operation
    ///
    /// Use this to limit what gets selected (e.g., only select visible text).
    /// The change is queued and will be applied after the callback returns.
    pub fn set_select_all_range(&mut self, target: DomNodeId, range: SelectionRange) {
        self.push_change(CallbackChange::SetSelectAllRange { target, range });
    }
    /// Request a hit test update at a specific position
    ///
    /// This is used by the Debug API to update the hover manager's hit test
    /// data after modifying the mouse position. This ensures that mouse event
    /// callbacks can find the correct nodes under the cursor.
    ///
    /// The hit test is performed during the next frame update.
    pub fn request_hit_test_update(&mut self, position: LogicalPosition) {
        self.push_change(CallbackChange::RequestHitTestUpdate { position });
    }
    /// Process a text selection click at a specific position
    ///
    /// This is used by the Debug API to trigger text selection directly,
    /// bypassing the normal event pipeline which generates PreCallbackSystemEvent::TextClick.
    ///
    /// The selection processing is deferred until the CallbackChange is processed,
    /// at which point the LayoutWindow can be mutably accessed.
    pub fn process_text_selection_click(&mut self, position: LogicalPosition, time_ms: u64) {
        self.push_change(CallbackChange::ProcessTextSelectionClick { position, time_ms });
    }
    /// Get the current text content of a node
    ///
    /// Helper for inspecting text before operations.
    pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
        let layout_window = self.get_layout_window();
        let node_id = target.node.into_crate_internal()?;
        let content = layout_window.get_text_before_textinput(target.dom, node_id);
        Some(layout_window.extract_text_from_inline_content(&content))
    }
    /// Get the current cursor position in a node
    ///
    /// Returns the text cursor position if the node is focused.
    pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        // Check if this node is focused
        if !layout_window.focus_manager.has_focus(&target) {
            return None;
        }
        layout_window.text_edit_manager.get_primary_cursor()
    }
    /// Get the current selection ranges in a node
    ///
    /// Returns all active selection ranges for the specified DOM.
    pub fn get_node_selection_ranges(&self, _target: DomNodeId) -> SelectionRangeVec {
        let layout_window = self.get_layout_window();
        let ranges: Vec<SelectionRange> = layout_window
            .text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
                Selection::Range(r) => Some(*r),
                _ => None,
            }).collect()).unwrap_or_default();
        ranges.into()
    }
    /// Check if a specific node has an active selection
    ///
    /// This checks if the specific node (identified by DomNodeId) has a selection,
    /// as opposed to has_selection(DomId) which checks the entire DOM.
    pub fn node_has_selection(&self, target: DomNodeId) -> bool {
        self.get_node_selection_ranges(target).as_ref().is_empty() == false
    }
    /// Get the length of text in a node
    ///
    /// Useful for bounds checking in custom operations.
    pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
        self.get_node_text_content(target).map(|text| text.len())
    }
    // Cursor Movement Inspection/Override Methods
    /// Inspect where the cursor would move when pressing left arrow
    ///
    /// Returns the new cursor position that would result from moving left.
    /// Returns None if the cursor is already at the start of the document.
    ///
    /// # Arguments
    /// * `target` - The node containing the cursor
    pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        let new_cursor = layout.move_cursor_left(cursor, &mut None);
        // Only return if cursor actually moved
        if new_cursor != cursor {
            Some(new_cursor)
        } else {
            None
        }
    }
    /// Inspect where the cursor would move when pressing right arrow
    ///
    /// Returns the new cursor position that would result from moving right.
    /// Returns None if the cursor is already at the end of the document.
    pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        let new_cursor = layout.move_cursor_right(cursor, &mut None);
        // Only return if cursor actually moved
        if new_cursor != cursor {
            Some(new_cursor)
        } else {
            None
        }
    }
    /// Inspect where the cursor would move when pressing up arrow
    ///
    /// Returns the new cursor position that would result from moving up one line.
    /// Returns None if the cursor is already on the first line.
    pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        // goal_x maintains horizontal position when moving vertically
        let new_cursor = layout.move_cursor_up(cursor, &mut None, &mut None);
        // Only return if cursor actually moved
        if new_cursor != cursor {
            Some(new_cursor)
        } else {
            None
        }
    }
    /// Inspect where the cursor would move when pressing down arrow
    ///
    /// Returns the new cursor position that would result from moving down one line.
    /// Returns None if the cursor is already on the last line.
    pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        // goal_x maintains horizontal position when moving vertically
        let new_cursor = layout.move_cursor_down(cursor, &mut None, &mut None);
        // Only return if cursor actually moved
        if new_cursor != cursor {
            Some(new_cursor)
        } else {
            None
        }
    }
    /// Inspect where the cursor would move when pressing Home key
    ///
    /// Returns the cursor position at the start of the current line.
    pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        let new_cursor = layout.move_cursor_to_line_start(cursor, &mut None);
        // Always return the result (might be same as input if already at line start)
        Some(new_cursor)
    }
    /// Inspect where the cursor would move when pressing End key
    ///
    /// Returns the cursor position at the end of the current line.
    pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
        let layout_window = self.get_layout_window();
        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
        // inline_layout_result
        let layout = self.get_inline_layout_for_node(&target)?;
        // Use the text3::cache cursor movement logic
        let new_cursor = layout.move_cursor_to_line_end(cursor, &mut None);
        // Always return the result (might be same as input if already at line end)
        Some(new_cursor)
    }
    /// Inspect where the cursor would move when pressing Ctrl+Home
    ///
    /// Returns the cursor position at the start of the document.
    pub fn inspect_move_cursor_to_document_start(&self, target: DomNodeId) -> Option<TextCursor> {
        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
        Some(TextCursor {
            cluster_id: GraphemeClusterId {
                source_run: 0,
                start_byte_in_run: 0,
            },
            affinity: CursorAffinity::Leading,
        })
    }
    /// Inspect where the cursor would move when pressing Ctrl+End
    ///
    /// Returns the cursor position at the end of the document.
    pub fn inspect_move_cursor_to_document_end(&self, target: DomNodeId) -> Option<TextCursor> {
        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
        let text_len = self.get_node_text_length(target)?;
        Some(TextCursor {
            cluster_id: GraphemeClusterId {
                source_run: 0,
                start_byte_in_run: text_len as u32,
            },
            affinity: CursorAffinity::Leading,
        })
    }
    /// Inspect what text would be deleted by backspace (including Shift+Backspace)
    ///
    /// Returns (range_to_delete, deleted_text).
    /// This is a convenience wrapper around inspect_delete_changeset(target, false).
    pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
        self.inspect_delete_changeset(target, false)
    }
    /// Inspect what text would be deleted by delete key
    ///
    /// Returns (range_to_delete, deleted_text).
    /// This is a convenience wrapper around inspect_delete_changeset(target, true).
    pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
        self.inspect_delete_changeset(target, true)
    }
    // Cursor Movement Override Methods
    // These methods queue cursor movement operations to be applied after the callback
    /// Move cursor left (arrow left key)
    ///
    /// # Arguments
    /// * `target` - The node containing the cursor
    /// * `extend_selection` - If true, extends selection (Shift+Left); if false, moves cursor
    pub fn move_cursor_left(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorLeft {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor right (arrow right key)
    pub fn move_cursor_right(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorRight {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor up (arrow up key)
    pub fn move_cursor_up(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorUp {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor down (arrow down key)
    pub fn move_cursor_down(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorDown {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor to line start (Home key)
    pub fn move_cursor_to_line_start(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorToLineStart {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor to line end (End key)
    pub fn move_cursor_to_line_end(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorToLineEnd {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor to document start (Ctrl+Home)
    pub fn move_cursor_to_document_start(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorToDocumentStart {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Move cursor to document end (Ctrl+End)
    pub fn move_cursor_to_document_end(&mut self, target: DomNodeId, extend_selection: bool) {
        self.push_change(CallbackChange::MoveCursorToDocumentEnd {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
            extend_selection,
        });
    }
    /// Delete text backward (backspace or Shift+Backspace)
    ///
    /// Queues a backspace operation to be applied after the callback.
    /// Use inspect_backspace() to see what would be deleted.
    pub fn delete_backward(&mut self, target: DomNodeId) {
        self.push_change(CallbackChange::DeleteBackward {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
        });
    }
    /// Delete text forward (delete key)
    ///
    /// Queues a delete operation to be applied after the callback.
    /// Use inspect_delete() to see what would be deleted.
    pub fn delete_forward(&mut self, target: DomNodeId) {
        self.push_change(CallbackChange::DeleteForward {
            dom_id: target.dom,
            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
        });
    }
}
/// Config necessary for threading + animations to work in no_std environments
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ExternalSystemCallbacks {
    pub create_thread_fn: CreateThreadCallback,
    pub get_system_time_fn: GetSystemTimeCallback,
}
impl ExternalSystemCallbacks {
2275
    pub fn rust_internal() -> Self {
        use crate::thread::create_thread_libstd;
2275
        Self {
2275
            create_thread_fn: CreateThreadCallback {
2275
                cb: create_thread_libstd,
2275
            },
2275
            get_system_time_fn: GetSystemTimeCallback {
2275
                cb: azul_core::task::get_system_time_libstd,
2275
            },
2275
        }
2275
    }
}
/// Request to change focus, returned from callbacks
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FocusUpdateRequest {
    /// Focus a specific node
    FocusNode(DomNodeId),
    /// Clear focus (no node has focus)
    ClearFocus,
    /// No focus change requested
    NoChange,
}
impl FocusUpdateRequest {
    /// Check if this represents a focus change
7
    pub fn is_change(&self) -> bool {
7
        !matches!(self, FocusUpdateRequest::NoChange)
7
    }
    /// Convert to the new focused node (Some(node) or None for clear)
6
    pub fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
6
        match self {
2
            FocusUpdateRequest::FocusNode(node) => Some(Some(*node)),
2
            FocusUpdateRequest::ClearFocus => Some(None),
2
            FocusUpdateRequest::NoChange => None,
        }
6
    }
    /// Create from Option<Option<DomNodeId>> (legacy format)
5
    pub fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
3
        match opt {
1
            Some(Some(node)) => FocusUpdateRequest::FocusNode(node),
2
            Some(None) => FocusUpdateRequest::ClearFocus,
2
            None => FocusUpdateRequest::NoChange,
        }
5
    }
}
/// Menu callback: What data / function pointer should
/// be called when the menu item is clicked?
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
#[repr(C)]
pub struct MenuCallback {
    pub callback: Callback,
    pub refany: RefAny,
}
/// Optional MenuCallback
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
#[repr(C, u8)]
pub enum OptionMenuCallback {
    None,
    Some(MenuCallback),
}
impl OptionMenuCallback {
    pub fn into_option(self) -> Option<MenuCallback> {
        match self {
            OptionMenuCallback::None => None,
            OptionMenuCallback::Some(c) => Some(c),
        }
    }
    pub fn is_some(&self) -> bool {
        matches!(self, OptionMenuCallback::Some(_))
    }
    pub fn is_none(&self) -> bool {
        matches!(self, OptionMenuCallback::None)
    }
}
impl From<Option<MenuCallback>> for OptionMenuCallback {
    fn from(o: Option<MenuCallback>) -> Self {
        match o {
            None => OptionMenuCallback::None,
            Some(c) => OptionMenuCallback::Some(c),
        }
    }
}
impl From<OptionMenuCallback> for Option<MenuCallback> {
    fn from(o: OptionMenuCallback) -> Self {
        o.into_option()
    }
}
// -- RenderImage callbacks
/// Callback type that renders an OpenGL texture
///
/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallbackType = usize`
/// to avoid circular dependencies. The actual function pointer is cast to usize for
/// storage in the data model, then unsafely cast back to this type when invoked.
pub type RenderImageCallbackType = extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
/// Callback that returns a rendered OpenGL texture
///
/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallback` with
/// a `cb: usize` field. When creating callbacks in the data model, function pointers
/// are cast to usize. This type is used in azul-layout where we can safely work
/// with the actual function pointer type.
#[repr(C)]
pub struct RenderImageCallback {
    pub cb: RenderImageCallbackType,
    /// For FFI: stores the foreign callable (e.g., PyFunction)
    /// Native Rust code sets this to None
    pub ctx: OptionRefAny,
}
impl_callback!(RenderImageCallback, RenderImageCallbackType);
impl RenderImageCallback {
    /// Create a new callback with just a function pointer (for native Rust code)
    pub fn create(cb: RenderImageCallbackType) -> Self {
        Self {
            cb,
            ctx: OptionRefAny::None,
        }
    }
    /// Convert from the core crate's `CoreRenderImageCallback` (which stores cb as usize)
    /// back to the layout crate's typed function pointer.
    ///
    /// # Safety
    ///
    /// This is safe because we ensure that the usize in CoreRenderImageCallback
    /// was originally created from a valid RenderImageCallbackType function pointer.
    pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
        debug_assert!(core_callback.cb != 0, "CoreRenderImageCallback.cb is null");
        Self {
            cb: unsafe { core::mem::transmute(core_callback.cb) },
            ctx: core_callback.ctx.clone(),
        }
    }
    /// Convert to CoreRenderImageCallback (function pointer stored as usize)
    ///
    /// This is always safe - we're just casting the function pointer to usize for storage.
    pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
        azul_core::callbacks::CoreRenderImageCallback {
            cb: self.cb as usize,
            ctx: self.ctx,
        }
    }
}
/// Allow RenderImageCallback to be passed to functions expecting `C: Into<CoreRenderImageCallback>`
impl From<RenderImageCallback> for azul_core::callbacks::CoreRenderImageCallback {
    fn from(callback: RenderImageCallback) -> Self {
        callback.to_core()
    }
}
/// Information passed to image rendering callbacks
#[derive(Debug)]
#[repr(C)]
pub struct RenderImageCallbackInfo {
    /// The ID of the DOM node that the ImageCallback was attached to
    callback_node_id: DomNodeId,
    /// Bounds of the laid-out node
    bounds: HidpiAdjustedBounds,
    /// Optional OpenGL context pointer
    gl_context: *const OptionGlContextPtr,
    /// Image cache for looking up images
    image_cache: *const ImageCache,
    /// System font cache
    system_fonts: *const FcFontCache,
    /// Pointer to callable (Python/FFI callback function)
    callable_ptr: *const OptionRefAny,
    /// Extension for future ABI stability (mutable data)
    _abi_mut: *mut core::ffi::c_void,
}
impl Clone for RenderImageCallbackInfo {
    fn clone(&self) -> Self {
        Self {
            callback_node_id: self.callback_node_id,
            bounds: self.bounds,
            gl_context: self.gl_context,
            image_cache: self.image_cache,
            system_fonts: self.system_fonts,
            callable_ptr: self.callable_ptr,
            _abi_mut: self._abi_mut,
        }
    }
}
impl RenderImageCallbackInfo {
    pub fn new<'a>(
        callback_node_id: DomNodeId,
        bounds: HidpiAdjustedBounds,
        gl_context: &'a OptionGlContextPtr,
        image_cache: &'a ImageCache,
        system_fonts: &'a FcFontCache,
    ) -> Self {
        Self {
            callback_node_id,
            bounds,
            gl_context: gl_context as *const OptionGlContextPtr,
            image_cache: image_cache as *const ImageCache,
            system_fonts: system_fonts as *const FcFontCache,
            callable_ptr: core::ptr::null(),
            _abi_mut: core::ptr::null_mut(),
        }
    }
    /// Get the callable for FFI language bindings (Python, etc.)
    pub fn get_ctx(&self) -> OptionRefAny {
        if self.callable_ptr.is_null() {
            OptionRefAny::None
        } else {
            unsafe { (*self.callable_ptr).clone() }
        }
    }
    /// Set the callable pointer (called before invoking callback)
    pub unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
        self.callable_ptr = ptr;
    }
    pub fn get_callback_node_id(&self) -> DomNodeId {
        self.callback_node_id
    }
    pub fn get_bounds(&self) -> HidpiAdjustedBounds {
        self.bounds
    }
    fn internal_get_gl_context<'a>(&'a self) -> &'a OptionGlContextPtr {
        unsafe { &*self.gl_context }
    }
    fn internal_get_image_cache<'a>(&'a self) -> &'a ImageCache {
        unsafe { &*self.image_cache }
    }
    fn internal_get_system_fonts<'a>(&'a self) -> &'a FcFontCache {
        unsafe { &*self.system_fonts }
    }
    pub fn get_gl_context(&self) -> OptionGlContextPtr {
        self.internal_get_gl_context().clone()
    }
}
// ============================================================================
// Result types for FFI
// ============================================================================
/// Result type for functions returning U8Vec or a String error
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum ResultU8VecString {
    Ok(azul_css::U8Vec),
    Err(AzString),
}
impl From<Result<alloc::vec::Vec<u8>, AzString>> for ResultU8VecString {
    fn from(result: Result<alloc::vec::Vec<u8>, AzString>) -> Self {
        match result {
            Ok(v) => ResultU8VecString::Ok(v.into()),
            Err(e) => ResultU8VecString::Err(e),
        }
    }
}
/// Result type for functions returning () or a String error  
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum ResultVoidString {
    Ok,
    Err(AzString),
}
impl From<Result<(), AzString>> for ResultVoidString {
    fn from(result: Result<(), AzString>) -> Self {
        match result {
            Ok(()) => ResultVoidString::Ok,
            Err(e) => ResultVoidString::Err(e),
        }
    }
}
/// Result type for functions returning String or a String error  
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum ResultStringString {
    Ok(AzString),
    Err(AzString),
}
impl From<Result<AzString, AzString>> for ResultStringString {
    fn from(result: Result<AzString, AzString>) -> Self {
        match result {
            Ok(s) => ResultStringString::Ok(s),
            Err(e) => ResultStringString::Err(e),
        }
    }
}
// ============================================================================
// Base64 encoding helper
// ============================================================================
const BASE64_ALPHABET: &[u8; 64] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Encode bytes to Base64 string
pub fn base64_encode(input: &[u8]) -> alloc::string::String {
    let mut output = alloc::string::String::with_capacity((input.len() + 2) / 3 * 4);
    for chunk in input.chunks(3) {
        let b0 = chunk[0] as usize;
        let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
        let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
        let n = (b0 << 16) | (b1 << 8) | b2;
        output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
        output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
        if chunk.len() > 1 {
            output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
        } else {
            output.push('=');
        }
        if chunk.len() > 2 {
            output.push(BASE64_ALPHABET[n & 0x3F] as char);
        } else {
            output.push('=');
        }
    }
    output
}