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
    resources::UpdateImageType,
20
    callbacks::{CoreCallback, FocusTarget, FocusTargetPath, HidpiAdjustedBounds, Update},
21
    dom::{AccessibilityAction, DomId, DomIdVec, DomNodeId, IdOrClass, NodeId, NodeType},
22
    geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, OptionLogicalRect, OptionLogicalSize, 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
    corety::{OptionString, OptionUsize},
45
    AzString, OptionU8Vec, StringVec, U8Vec,
46
};
47
use rust_fontconfig::FcFontCache;
48

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

            
55
use crate::{
56
    hit_test::FullHitTest,
57
    managers::{
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, FullWindowStateVec, 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
5
    fn from((x, y): (f32, f32)) -> Self {
94
5
        Self {
95
5
            x_tilt: x,
96
5
            y_tilt: y,
97
5
        }
98
5
    }
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, Eq)]
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<(String, SelectionRange)> for SelectAllResult {
118
3
    fn from((text, range): (String, SelectionRange)) -> Self {
119
3
        Self {
120
3
            full_text: text.into(),
121
3
            selection_range: range,
122
3
        }
123
3
    }
124
}
125

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

            
133
/// FFI-safe wrapper for delete inspection result (`range_to_delete`, `deleted_text`)
134
#[derive(Debug, Clone, PartialEq, Eq)]
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, String)> for DeleteResult {
144
2
    fn from((range, text): (SelectionRange, String)) -> Self {
145
2
        Self {
146
2
            range_to_delete: range,
147
2
            deleted_text: text.into(),
148
2
        }
149
2
    }
150
}
151

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

            
159
/// Handle to a running E2E script, so it can be cancelled.
160
///
161
/// Minted by `execute_e2e_json` BEFORE the script runs — that is what makes it
162
/// returnable from a call that may not have executed anything yet, and it is
163
/// also what makes cancelling a queued-but-not-started script possible.
164
///
165
/// A 128-bit random UUID is what this wants to be; there is no `uuid`
166
/// dependency in the tree and adding one to azul-core for a process-local
167
/// handle is not worth it. A monotonic counter gives the property actually
168
/// needed — no two live scripts in this process share a handle — and cannot
169
/// collide. It is NOT stable across runs and must not be persisted.
170
/// Whether an E2E script blocks its caller or runs alongside the UI.
171
///
172
/// A bool carries the same information; the enum is used because
173
/// `execute_e2e_json(script, Sync)` says at the call site what
174
/// `execute_e2e_json(script, true)` does not.
175
///
176
/// The debug HTTP server is SYNCHRONOUS — it waits on `rx.recv_timeout(..)`
177
/// (`e2e/full.rs:3837`) until the UI thread replies, which works because the
178
/// waiting thread is not the UI thread. A UI callback runs ON the UI thread;
179
/// `Sync` there is still SUPPORTED and is usually what an AGENT wants, but it
180
/// constrains the implementation: the executor must DRIVE FRAMES INLINE to
181
/// completion. Written as "wait on a channel the UI thread feeds" it
182
/// deadlocks, because the waiting thread is the one that has to do the
183
/// feeding.
184
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185
#[repr(C)]
186
pub enum E2eExecutionMode {
187
    /// Queue and return immediately, so a person can keep using the UI.
188
    Async,
189
    /// Block until the script finishes.
190
    Sync,
191
}
192

            
193
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
194
#[repr(C)]
195
pub struct E2eScriptHandle {
196
    pub id: u64,
197
}
198

            
199
impl E2eScriptHandle {
200
    /// Mint a fresh handle. Never returns the same value twice in a process.
201
    #[must_use]
202
    pub fn new() -> Self {
203
        use core::sync::atomic::{AtomicU64, Ordering};
204
        static NEXT: AtomicU64 = AtomicU64::new(1);
205
        Self { id: NEXT.fetch_add(1, Ordering::Relaxed) }
206
    }
207
}
208

            
209
impl Default for E2eScriptHandle {
210
    fn default() -> Self {
211
        Self::new()
212
    }
213
}
214

            
215
/// Represents a change made by a callback that will be applied after the callback returns
216
///
217
/// This transaction-based system provides:
218
/// - Clear separation between read-only queries and modifications
219
/// - Atomic application of all changes
220
/// - Easy debugging and logging of callback actions
221
/// - Future extensibility for new change types
222
#[derive(Debug, Clone)]
223
pub enum CallbackChange {
224
    /// Run an E2E script in this session, as JSON.
225
    ///
226
    /// The plugin / macro path: a user opens an `.json` scenario and it drives
227
    /// the app, with no debug HTTP server anywhere in the picture (build with
228
    /// the dll's `e2e-scripting` feature).
229
    ///
230
    /// Deferred BY CONSTRUCTION — a `CallbackChange` is applied after the
231
    /// callback returns, which is what makes this safe at all: a callback runs
232
    /// mid-frame and a scenario DRIVES frames, so executing inline would
233
    /// re-enter the frame loop. `E2eSession::running` guards the remaining
234
    /// case, a script whose own step starts another script.
235
    ExecuteE2eJson {
236
        script: azul_core::json::Json,
237
        /// Identifies this run, for `StopE2eJson`.
238
        handle: E2eScriptHandle,
239
        /// `true` blocks the UI until the script finishes — which an AGENT
240
        /// generally wants, since it needs the result before deciding what to
241
        /// do next. `false` runs it alongside the UI, so a person can keep
242
        /// interacting while it works.
243
        ///
244
        /// Blocking requires the executor to DRIVE FRAMES INLINE to
245
        /// completion. Implementing it as "wait on a channel the UI thread
246
        /// feeds" deadlocks, because the waiting thread is the one that must
247
        /// do the feeding.
248
        /// Blocking or not — see `E2eExecutionMode`. No default.
249
        mode: E2eExecutionMode,
250
    },
251
    /// Cancel a script started by `ExecuteE2eJson`, running or still queued.
252
    StopE2eJson {
253
        handle: E2eScriptHandle,
254
    },
255

            
256
    // Window State Changes
257
    /// Modify the window state (size, position, title, etc.)
258
    ModifyWindowState { state: FullWindowState },
259
    /// Inject a platform-native gesture-recognizer result into the
260
    /// in-process `GestureAndDragManager`. Read by the next
261
    /// `detect_long_press` / `detect_swipe_direction` / `detect_pinch` /
262
    /// `detect_rotation` / `detect_double_click` call, then cleared.
263
    InjectNativeGesture {
264
        gesture: crate::managers::gesture::NativeGestureEvent,
265
    },
266
    /// Apply an accessibility action (what a screen reader asks for) to a node
267
    /// and dispatch whatever callbacks it maps to.
268
    ///
269
    /// The PRIMARY ingress for an a11y action is the per-backend
270
    /// `process_accessibility_actions()` frame pump, which reads its own OS
271
    /// adapter (AT-SPI / UIA / `NSAccessibility` / `UIKit` / Android). This variant
272
    /// is the second door, for a caller that only holds a `CallbackInfo`: the
273
    /// E2E `accessibility_action` op. Both doors end in the same
274
    /// `LayoutWindow::process_accessibility_action` + synthetic-event dispatch,
275
    /// so what a test drives is what a screen reader drives.
276
    PerformAccessibilityAction {
277
        dom_id: DomId,
278
        node_id: NodeId,
279
        action: AccessibilityAction,
280
    },
281
    /// Queue multiple window state changes to be applied in sequence across frames.
282
    /// This is needed for simulating clicks (mouse down -> wait -> mouse up) where each
283
    /// state change needs to trigger separate event processing.
284
    QueueWindowStateSequence { states: Vec<FullWindowState> },
285
    /// Create a new window
286
    CreateNewWindow { options: WindowCreateOptions },
287
    /// Close the current window (via `Update::CloseWindow` return value, tracked here for logging)
288
    CloseWindow,
289

            
290
    // Focus Management
291
    /// Change keyboard focus to a specific node or clear focus
292
    SetFocusTarget { target: FocusTarget },
293

            
294
    // Event Propagation Control
295
    /// Stop event from propagating to parent nodes (W3C stopPropagation).
296
    /// Remaining handlers on the *current* node still fire, but no handlers
297
    /// on ancestor / descendant nodes in subsequent phases.
298
    StopPropagation,
299
    /// Stop event propagation immediately (W3C stopImmediatePropagation).
300
    /// No further handlers fire - not even remaining handlers on the same node.
301
    StopImmediatePropagation,
302
    /// Prevent default browser behavior (e.g., block text input from being applied)
303
    PreventDefault,
304

            
305
    // Timer Management
306
    /// Add a new timer to the window
307
    AddTimer { timer_id: TimerId, timer: Timer },
308
    /// Remove an existing timer
309
    /// Advance layout animations by an EXACT step, bypassing the wall clock.
310
    ///
311
    /// Emitted only by the E2E `tick_animations` op. A headless scenario cannot
312
    /// sample real time — the same test would land on a different point of the
313
    /// curve on a fast machine than a slow one — so stepping by a fixed `dt`
314
    /// makes the trajectory a pure function of how many steps ran.
315
    ///
316
    /// Integer microseconds, not `f32` seconds: an exact integer step is what
317
    /// lets a replayed scenario reproduce bit-for-bit.
318
    TickAnimations { dt_micros: u32, steps: u32 },
319
    /// Create/overwrite animation MOMENTUM on a node: kick its in-flight
320
    /// presence/move animation with the given velocity (logical px/s), or
321
    /// start an identity-anchored spring carrying that velocity when nothing
322
    /// is animating. Reversing a direction is `set(-get())` — see
323
    /// [`CallbackInfo::get_animation_momentum`].
324
    SetAnimationMomentum {
325
        node: DomNodeId,
326
        velocity_x: f32,
327
        velocity_y: f32,
328
    },
329
    RemoveTimer { timer_id: TimerId },
330

            
331
    // Thread Management
332
    /// Add a new background thread
333
    AddThread { thread_id: ThreadId, thread: Thread },
334
    /// Remove an existing thread
335
    RemoveThread { thread_id: ThreadId },
336

            
337
    // Content Modifications
338
    /// Change the text content of a node
339
    ChangeNodeText { node_id: DomNodeId, text: AzString },
340
    /// Change the image of a node
341
    ChangeNodeImage {
342
        dom_id: DomId,
343
        node_id: NodeId,
344
        image: ImageRef,
345
        update_type: UpdateImageType,
346
    },
347
    /// Record a STRUCTURAL document edit (Enter split / merge / wrap…) for
348
    /// the app to apply to ITS model — azul never mutates the `StyledDom`.
349
    RecordDocumentEdit {
350
        changeset: crate::managers::changeset::DocumentChangeset,
351
    },
352
    /// The commit handshake: the app confirms it applied structural edit `id`.
353
    MarkDocumentEditApplied { id: u64 },
354
    /// The handshake WITH the applier's inverse operation — the edit becomes
355
    /// structurally undoable.
356
    MarkDocumentEditAppliedWithInverse {
357
        id: u64,
358
        inverse: crate::managers::changeset::DocumentOperation,
359
    },
360
    /// Undo the newest structural edit (re-records its inverse for the app).
361
    UndoStructuralEdit,
362
    /// Redo the newest undone structural edit.
363
    RedoStructuralEdit,
364
    /// Re-render an image callback (for resize/animation)
365
    /// This triggers re-invocation of the `RenderImageCallback`
366
    UpdateImageCallback { dom_id: DomId, node_id: NodeId },
367
    /// Re-render ALL image callbacks across all DOMs.
368
    ///
369
    /// This is the most efficient way to update animated GL textures:
370
    /// it triggers only texture re-rendering without DOM rebuild or
371
    /// display list resubmission. Used by timer callbacks that need
372
    /// to update OpenGL textures every frame.
373
    UpdateAllImageCallbacks,
374
    /// Trigger re-rendering of a `VirtualView` with a new DOM
375
    /// This forces the `VirtualView` to call its callback and update the display list
376
    UpdateVirtualView { dom_id: DomId, node_id: NodeId },
377
    /// Re-render EVERY `VirtualView` on the existing DOM (no node id needed).
378
    /// For shared-dataset changes that arrive out-of-band (e.g. a background
379
    /// tile-fetch writeback): the views re-read their cloned dataset in place.
380
    UpdateAllVirtualViews,
381
    /// Change the image mask of a node
382
    ChangeNodeImageMask {
383
        dom_id: DomId,
384
        node_id: NodeId,
385
        mask: ImageMask,
386
    },
387
    /// Change CSS properties of a node
388
    ChangeNodeCssProperties {
389
        dom_id: DomId,
390
        node_id: NodeId,
391
        properties: CssPropertyVec,
392
    },
393
    /// Override CSS properties on a node via the user-override channel
394
    /// (`CssPropertyCache::user_overridden_properties`). Unlike
395
    /// `ChangeNodeCssProperties`, this does not mutate the node's static
396
    /// `css_props` - the override layer is read at higher priority by the
397
    /// property resolution pipeline, so animating a handful of properties
398
    /// per frame stays cheap. Passing `CssProperty::Initial` for a property
399
    /// removes any prior override for that type on the same node.
400
    OverrideNodeCssProperties {
401
        dom_id: DomId,
402
        node_id: NodeId,
403
        properties: CssPropertyVec,
404
    },
405

            
406
    // Scroll Management
407
    /// Scroll a node to a specific position
408
    ScrollTo {
409
        dom_id: DomId,
410
        node_id: NodeHierarchyItemId,
411
        position: LogicalPosition,
412
        /// When true, skip clamping to [0, `max_scroll`] bounds.
413
        /// Used by the scroll physics timer for rubber-banding/overscroll.
414
        unclamped: bool,
415
    },
416
    /// #28 (a): update a `VirtualView`'s VIRTUAL geometry (scrollbar math)
417
    /// WITHOUT re-invoking its callback or re-laying-out its child DOM.
418
    ///
419
    /// The streaming-pagination flow: a background thread computes the exact
420
    /// page count while the UI shows an estimate; its writeback pushes this
421
    /// change to correct the scrollbar live. Applied post-callback as the two
422
    /// stores a normal invoke writes (`VirtualViewManager::
423
    /// update_virtual_view_info` + `ScrollManager::update_virtual_scroll_
424
    /// bounds`) — the rendered window (`scroll_size`) is deliberately NOT
425
    /// touched, only the virtual extent.
426
    SetVirtualViewGeometry {
427
        dom_id: DomId,
428
        /// The `VirtualView` node in its parent DOM.
429
        node_id: NodeHierarchyItemId,
430
        /// The geometry is reconfigurable as the same two rects the callback
431
        /// returns (USER design; see doc/guide/en/dom/virtual-views.md):
432
        /// `materialized` is the rendered window and where it sits,
433
        /// `virtual_rect` is what the scrollbar represents. Each: `Some` =
434
        /// set, `None` = keep the current value.
435
        ///
436
        /// The streaming case this exists for — a background exact-pagination
437
        /// pass correcting the document extent — sets `virtual_rect` only, so
438
        /// the scrollbar re-scales while the materialized window, and every
439
        /// pixel on screen, stays exactly where it is.
440
        materialized: OptionLogicalRect,
441
        virtual_rect: OptionLogicalRect,
442
    },
443
    /// Scroll a node into view (W3C scrollIntoView API)
444
    /// The scroll adjustments are calculated and applied when the change is processed
445
    ScrollIntoView {
446
        node_id: DomNodeId,
447
        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
448
    },
449

            
450
    // Image Cache Management
451
    /// Add an image to the image cache
452
    AddImageToCache { id: AzString, image: ImageRef },
453
    /// Remove an image from the image cache
454
    RemoveImageFromCache { id: AzString },
455

            
456
    // Font Cache Management
457
    /// Reload system fonts (expensive operation)
458
    ReloadSystemFonts,
459

            
460
    // Menu Management
461
    /// Open a context menu or dropdown menu
462
    /// Whether it's native or fallback depends on `window.state.flags.use_native_context_menus`
463
    OpenMenu {
464
        menu: Menu,
465
        /// Optional position override (if None, uses menu.position)
466
        position: Option<LogicalPosition>,
467
    },
468

            
469
    // Tooltip Management
470
    /// Show a tooltip at a specific position
471
    ///
472
    /// Platform-specific implementation:
473
    /// - Windows: Uses native tooltip window (`TOOLTIPS_CLASS`)
474
    /// - macOS: Uses `NSPopover` or custom `NSWindow` with tooltip styling
475
    /// - X11: Creates transient window with _`NET_WM_WINDOW_TYPE_TOOLTIP`
476
    /// - Wayland: Creates surface with `zwlr_layer_shell_v1` (overlay layer)
477
    ShowTooltip {
478
        text: AzString,
479
        position: LogicalPosition,
480
    },
481
    /// Hide the currently displayed tooltip
482
    HideTooltip,
483

            
484
    // Text Editing
485
    /// Insert text at the current cursor position or replace selection
486
    InsertText {
487
        dom_id: DomId,
488
        node_id: NodeId,
489
        text: AzString,
490
    },
491
    /// Delete text backward (backspace) at cursor
492
    DeleteBackward { dom_id: DomId, node_id: NodeId },
493
    /// Delete text forward (delete key) at cursor
494
    DeleteForward { dom_id: DomId, node_id: NodeId },
495
    /// Move cursor to a specific position
496
    MoveCursor {
497
        dom_id: DomId,
498
        node_id: NodeId,
499
        cursor: TextCursor,
500
    },
501
    /// Set text selection range
502
    SetSelection {
503
        dom_id: DomId,
504
        node_id: NodeId,
505
        selection: Selection,
506
    },
507
    /// Set/override the text changeset for the current text input operation
508
    /// This allows callbacks to modify what text will be inserted during text input events
509
    SetTextChangeset { changeset: PendingTextEdit },
510

            
511
    // Cursor Movement Operations
512
    /// Move cursor left (arrow left)
513
    MoveCursorLeft {
514
        dom_id: DomId,
515
        node_id: NodeId,
516
        extend_selection: bool,
517
    },
518
    /// Move cursor right (arrow right)
519
    MoveCursorRight {
520
        dom_id: DomId,
521
        node_id: NodeId,
522
        extend_selection: bool,
523
    },
524
    /// Move cursor up (arrow up)
525
    MoveCursorUp {
526
        dom_id: DomId,
527
        node_id: NodeId,
528
        extend_selection: bool,
529
    },
530
    /// Move cursor down (arrow down)
531
    MoveCursorDown {
532
        dom_id: DomId,
533
        node_id: NodeId,
534
        extend_selection: bool,
535
    },
536
    /// Move cursor to line start (Home key)
537
    MoveCursorToLineStart {
538
        dom_id: DomId,
539
        node_id: NodeId,
540
        extend_selection: bool,
541
    },
542
    /// Move cursor to line end (End key)
543
    MoveCursorToLineEnd {
544
        dom_id: DomId,
545
        node_id: NodeId,
546
        extend_selection: bool,
547
    },
548
    /// Move cursor to document start (Ctrl+Home)
549
    MoveCursorToDocumentStart {
550
        dom_id: DomId,
551
        node_id: NodeId,
552
        extend_selection: bool,
553
    },
554
    /// Move cursor to document end (Ctrl+End)
555
    MoveCursorToDocumentEnd {
556
        dom_id: DomId,
557
        node_id: NodeId,
558
        extend_selection: bool,
559
    },
560

            
561
    // Multi-Cursor Operations
562
    /// Add an additional cursor at the specified position (Ctrl+Click from C API)
563
    AddCursor {
564
        dom_id: DomId,
565
        node_id: NodeId,
566
        cursor: TextCursor,
567
    },
568
    /// Add an additional selection range (for multi-cursor)
569
    AddSelectionRange {
570
        dom_id: DomId,
571
        node_id: NodeId,
572
        range: SelectionRange,
573
    },
574
    /// Remove a specific selection by its stable ID
575
    RemoveSelectionById {
576
        selection_id: azul_core::selection::SelectionId,
577
    },
578

            
579
    // Clipboard Operations (Override)
580
    /// Override clipboard content for copy operation
581
    SetCopyContent {
582
        target: DomNodeId,
583
        content: ClipboardContent,
584
    },
585
    /// Override clipboard content for cut operation
586
    SetCutContent {
587
        target: DomNodeId,
588
        content: ClipboardContent,
589
    },
590
    /// Override selection range for select-all operation
591
    SetSelectAllRange {
592
        target: DomNodeId,
593
        range: SelectionRange,
594
    },
595

            
596
    // Hit Test Request (for Debug API)
597
    /// Request a hit test update at a specific position
598
    ///
599
    /// This is used by the Debug API to update the hover manager's hit test
600
    /// data after modifying the mouse position, ensuring that callbacks
601
    /// can find the correct nodes under the cursor.
602
    RequestHitTestUpdate { position: LogicalPosition },
603

            
604
    // Text Selection (for Debug API)
605
    /// Process a text selection click at a specific position
606
    ///
607
    /// This is used by the Debug API to trigger text selection directly,
608
    /// bypassing the normal event pipeline. The handler will:
609
    /// 1. Hit-test IFC roots to find selectable text at the position
610
    /// 2. Create a text cursor at the clicked position
611
    /// 3. Update the selection manager with the new selection
612
    ProcessTextSelectionClick {
613
        position: LogicalPosition,
614
        time_ms: u64,
615
    },
616

            
617
    // Cursor Blinking (System Timer Control)
618
    /// Set the cursor visibility state (called by blink timer)
619
    SetCursorVisibility { visible: bool },
620
    /// Toggle cursor visibility based on blink timing
621
    ToggleCursorVisibility,
622
    /// Reset cursor blink state on user input (makes cursor visible, records time)
623
    ResetCursorBlink,
624
    /// Start the cursor blink timer for the focused contenteditable element
625
    StartCursorBlinkTimer,
626
    /// Stop the cursor blink timer (when focus leaves contenteditable)
627
    StopCursorBlinkTimer,
628
    
629
    // Scroll cursor/selection into view
630
    /// Scroll the active text cursor into view within its scrollable container
631
    /// This is automatically triggered after text input or cursor movement
632
    ScrollActiveCursorIntoView,
633
    
634
    // Create Text Input Event (for Debug API / Programmatic Text Input)
635
    /// Create a synthetic text input event
636
    ///
637
    /// This simulates receiving text input from the OS. The text input flow will:
638
    /// 1. Record the text in `TextInputManager` (creating a `PendingTextEdit`)
639
    /// 2. Generate synthetic `TextInput` events
640
    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
641
    /// 4. Apply the changeset if not rejected
642
    /// 5. Mark dirty nodes for re-render
643
    CreateTextInput {
644
        /// The text to insert
645
        text: AzString,
646
    },
647

            
648
    // Window Move (Compositor-Managed)
649
    /// Request the compositor to begin an interactive window move.
650
    /// On Wayland: calls `xdg_toplevel_move(toplevel`, seat, serial).
651
    /// On other platforms: this is a no-op (use `set_window_position` instead).
652
    BeginInteractiveMove,
653

            
654
    // Drag-and-Drop Data Transfer
655
    /// Set drag data for a MIME type (W3C: dataTransfer.setData)
656
    /// Should be called in a `DragStart` callback to populate the drag data.
657
    SetDragData {
658
        mime_type: AzString,
659
        data: Vec<u8>,
660
    },
661
    /// Accept the current drop on this target (W3C: `event.preventDefault()` in `DragOver`)
662
    /// Must be called from a `DragOver` or `DragEnter` callback for the Drop event to fire.
663
    AcceptDrop,
664
    /// Set the drop effect (W3C: dataTransfer.dropEffect)
665
    SetDropEffect {
666
        effect: azul_core::drag::DropEffect,
667
    },
668

            
669
    // DOM Mutation (for Debug API)
670
    /// Insert a new child node into the DOM tree.
671
    /// Creates a minimal `StyledDom` from the given `node_type` and appends it
672
    /// as a child of `parent_node_id`. If position is Some, inserts at that
673
    /// child index; otherwise appends at the end.
674
    InsertChildNode {
675
        dom_id: DomId,
676
        parent_node_id: NodeId,
677
        /// The tag/type of the new node (e.g. "div", "p", "text:Hello")
678
        node_type_str: AzString,
679
        /// Optional child index to insert at (None = append at end)
680
        position: Option<usize>,
681
        /// Optional CSS classes for the new node
682
        classes: Vec<AzString>,
683
        /// Optional ID for the new node
684
        id: Option<AzString>,
685
    },
686
    /// Delete a node from the DOM tree (and all its children).
687
    /// The node is "tombstoned" (set to an empty anonymous Div) rather than
688
    /// physically removed, to preserve node ID stability.
689
    DeleteNode {
690
        dom_id: DomId,
691
        node_id: NodeId,
692
    },
693
    /// Set the IDs and classes on an existing node.
694
    SetNodeIdsAndClasses {
695
        dom_id: DomId,
696
        node_id: NodeId,
697
        ids_and_classes: azul_core::dom::IdOrClassVec,
698
    },
699
    /// Replace the window's whole DOM with the debug `mount` op's inline
700
    /// XML+CSS document (`Some`), or drop the override again (`None`, the
701
    /// `unmount` op).
702
    ///
703
    /// The mounted document is an INPUT to the next layout, not ambient state:
704
    /// the shell applies it to [`LayoutWindow::e2e_mount`] like every other
705
    /// change and `regenerate_layout` reads it back from there. It used to
706
    /// travel through a process-global sink instead (an `e2e::hooks` function
707
    /// pointer into a `static` in the DLL), which meant a second window
708
    /// silently rendered the first window's mounted document.
709
    RemountDom { xml: Option<AzString> },
710

            
711
    // Routing
712
    /// Switch to a different route.
713
    ///
714
    /// On desktop: swaps `FullWindowState.layout_callback` to the matched
715
    /// route's callback, stores the `RouteMatch`, and triggers `RefreshDom`.
716
    /// On web: additionally calls `history.pushState()`.
717
    SwitchRoute {
718
        /// Route pattern to switch to (e.g. `"/user/:id"`)
719
        pattern: AzString,
720
        /// Route parameters (e.g. `[("id", "42")]`)
721
        params: azul_core::window::StringPairVec,
722
    },
723

            
724
    // App-global Undo / Redo
725
    /// Commit a snapshot of the current app state into the undo history.
726
    CommitUndoSnapshot,
727
    /// Undo the last committed app-state change (restores previous snapshot).
728
    UndoAppState,
729
    /// Redo a previously undone app-state change.
730
    RedoAppState,
731
}
732

            
733
/// Whether a batch of CSS property overrides can move geometry, i.e. whether
734
/// applying it has to re-run layout or only has to repaint.
735
///
736
/// [`CssPropertyType::can_trigger_relayout`] is the engine's existing authority
737
/// on the question (`background-color`, `color`, `opacity`, `transform`,
738
/// `box-shadow`, the border colours/styles and the scrollbar paint properties
739
/// are all paint-only), and the property cache already consults it in
740
/// `check_layout_properties_changed`. The two `apply_user_change`
741
/// implementations — the headless E2E host and
742
/// `dll/src/desktop/shell2/common/event.rs` — did not: both answered
743
/// `ChangeNodeCssProperties` and `OverrideNodeCssProperties` with an
744
/// unconditional `ShouldIncrementalRelayout`, so animating a colour, a
745
/// `:hover` background or an `opacity` re-laid-out the whole DOM every frame.
746
///
747
/// An EMPTY batch changes nothing and therefore needs no layout.
748
///
749
/// This lives here, next to `CallbackChange`, so the two hosts cannot drift:
750
/// the decision is one function, not two copies of a match arm.
751
#[must_use]
752
16
pub fn css_properties_need_relayout(properties: &CssPropertyVec) -> bool {
753
16
    properties
754
16
        .as_ref()
755
16
        .iter()
756
16
        .any(|p| p.get_type().can_trigger_relayout())
757
16
}
758

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

            
762
/// The TYPED signature of a component-attached presence-animation function
763
/// (`-azul-animation-in` / `-azul-animation-out: myFn 1s` next to a
764
/// `NodeData::add_animation_callback("myFn", ..)`). Stored type-erased as
765
/// `usize` in `azul_core::resources::ZombieAnimCallback` (the `CoreCallback`
766
/// pattern — `TimerCallbackInfo` lives in this crate, above azul-core).
767
///
768
/// Receives the registered data, a FULL `TimerCallbackInfo` (the live dom,
769
/// change queue, momentum API, node measurement — everything a timer can
770
/// do), and the zombie-specific `ZombieAnimInfo` (the retained tree, rect,
771
/// RAW linear `t`, the DECLARED timing, entry velocity). The callback owns
772
/// the easing math: apply the requested timing via
773
/// `AnimationTiming::evaluate(info.t)` or substitute its own.
774
pub type ZombieAnimFnType = extern "C" fn(
775
    &mut RefAny,
776
    &mut crate::timer::TimerCallbackInfo,
777
    &azul_core::resources::ZombieAnimInfo,
778
) -> azul_core::resources::ZombieFrame;
779

            
780
/// Stores a function pointer that is executed when the given UI element is hit
781
///
782
/// Must return an `Update` that denotes if the screen should be redrawn.
783
#[repr(C)]
784
pub struct Callback {
785
    pub cb: CallbackType,
786
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
787
    /// Native Rust code sets this to None
788
    pub ctx: OptionRefAny,
789
}
790

            
791
impl_callback!(Callback, CallbackType);
792

            
793
// Host-invoker plumbing for managed-FFI bindings (Lua, Ruby, Perl, ...).
794
// See `azul_core::host_invoker` for the design. This expands to a static
795
// `az_callback_thunk` that the framework dispatches by-value args to, an
796
// `AzCallback_createFromHostHandle` C-ABI export the host calls per
797
// `set_on_click(...)` site, plus the `AzApp_setCallbackInvoker` setter the
798
// host calls once at module load to register its libffi closure.
799
azul_core::impl_managed_callback! {
800
    wrapper:        Callback,
801
    info_ty:        CallbackInfo,
802
    return_ty:      Update,
803
    default_ret:    Update::DoNothing,
804
    invoker_static: CALLBACK_INVOKER,
805
    invoker_ty:     AzCallbackInvoker,
806
    thunk_fn:       az_callback_thunk,
807
    setter_fn:      AzApp_setCallbackInvoker,
808
    from_handle_fn: AzCallback_createFromHostHandle,
809
}
810

            
811
impl Callback {
812
    /// Create a callback from a raw `CallbackType` function pointer (ctx = None).
813
    ///
814
    /// The concrete `cb: CallbackType` parameter is a coercion site, so callers
815
    /// can pass a bare `extern "C" fn` item without an `as CallbackType` cast
816
    /// (unlike `Callback::from`, where trait-impl selection happens before the
817
    /// fn-item -> fn-pointer coercion could apply).
818
    #[must_use]
819
1440
    pub fn from_ptr(cb: CallbackType) -> Self {
820
1440
        Self::from(cb)
821
1440
    }
822

            
823
    /// Create a new callback with just a function pointer (for native Rust code)
824
1
    pub fn create<C: Into<Self>>(cb: C) -> Self {
825
1
        cb.into()
826
1
    }
827

            
828
    /// Convert from `CoreCallback` (stored as usize) to Callback (actual function pointer)
829
    ///
830
    /// Preserves `ctx` so that callbacks registered via the host-invoker path
831
    /// (e.g. `Callback::create_from_host_handle`) keep their host-handle ctx
832
    /// across the dispatch cycle. Without this, `info.get_ctx()` inside the
833
    /// generated thunk would see `OptionRefAny::None` and bail out with the
834
    /// kind's default value - which makes managed-FFI click handlers
835
    /// silently no-op.
836
    ///
837
    /// # Safety
838
    /// The caller must ensure that the usize in CoreCallback.cb was originally a valid
839
    /// function pointer of type `CallbackType`. This is guaranteed when `CoreCallback`
840
    /// is created through standard APIs, but unsafe code could violate this.
841
2
    #[must_use] pub fn from_core(core: CoreCallback) -> Self {
842
2
        debug_assert!(core.cb != 0, "CoreCallback.cb is null");
843
2
        Self {
844
2
            cb: unsafe { core::mem::transmute::<usize, CallbackType>(core.cb) },
845
2
            ctx: core.ctx,
846
2
        }
847
2
    }
848

            
849
    /// Convert to `CoreCallback` (function pointer stored as usize)
850
    ///
851
    /// This is always safe - we're just casting the function pointer to usize for storage.
852
1432
    #[must_use] pub fn to_core(self) -> CoreCallback {
853
1432
        CoreCallback {
854
1432
            cb: self.cb as usize,
855
1432
            ctx: self.ctx,
856
1432
        }
857
1432
    }
858
}
859

            
860
/// Allow Callback to be passed to functions expecting `C: Into<CoreCallback>`
861
impl From<Callback> for CoreCallback {
862
1430
    fn from(callback: Callback) -> Self {
863
1430
        callback.to_core()
864
1430
    }
865
}
866

            
867
impl Callback {
868
    /// Safely invoke the callback with the given data and info
869
    ///
870
    /// This is a safe wrapper around calling the function pointer directly.
871
3
    #[must_use] pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
872
3
        (self.cb)(data, info)
873
3
    }
874
}
875
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
876
/// FFI-safe Option<Callback> type for C interop.
877
///
878
/// This enum provides an ABI-stable alternative to `Option<Callback>`
879
/// that can be safely passed across FFI boundaries.
880
#[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
881
#[repr(C, u8)]
882
pub enum OptionCallback {
883
    /// No callback is present.
884
    None,
885
    /// A callback is present.
886
    Some(Callback),
887
}
888

            
889
impl OptionCallback {
890
    /// Converts this FFI-safe option into a standard Rust `Option<Callback>`.
891
4
    #[must_use] pub fn into_option(self) -> Option<Callback> {
892
4
        match self {
893
2
            Self::None => None,
894
2
            Self::Some(c) => Some(c),
895
        }
896
4
    }
897

            
898
    /// Returns `true` if a callback is present.
899
4
    #[must_use] pub const fn is_some(&self) -> bool {
900
4
        matches!(self, Self::Some(_))
901
4
    }
902

            
903
    /// Returns `true` if no callback is present.
904
4
    #[must_use] pub const fn is_none(&self) -> bool {
905
4
        matches!(self, Self::None)
906
4
    }
907
}
908

            
909
impl From<Option<Callback>> for OptionCallback {
910
4
    fn from(o: Option<Callback>) -> Self {
911
4
        o.map_or_else(|| Self::None, Self::Some)
912
4
    }
913
}
914

            
915
impl From<OptionCallback> for Option<Callback> {
916
2
    fn from(o: OptionCallback) -> Self {
917
2
        o.into_option()
918
2
    }
919
}
920

            
921
/// Reference data container for `CallbackInfo` (all read-only fields)
922
///
923
/// This struct consolidates all readonly references that callbacks need to query window state.
924
/// By grouping these into a single struct, we reduce the number of parameters to
925
/// `CallbackInfo::new()` from 13 to 3, making the API more maintainable and easier to extend.
926
///
927
/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
928
/// reference.
929
#[derive(Debug)]
930
pub struct CallbackInfoRefData<'a> {
931
    /// Pointer to the `LayoutWindow` containing all layout results (READ-ONLY for queries)
932
    pub layout_window: &'a LayoutWindow,
933
    /// Necessary to query `FontRefs` from callbacks
934
    pub renderer_resources: &'a RendererResources,
935
    /// Previous window state (for detecting changes)
936
    pub previous_window_state: &'a Option<FullWindowState>,
937
    /// State of the current window that the callback was called on (read only!)
938
    pub current_window_state: &'a FullWindowState,
939
    /// An Rc to the OpenGL context, in order to be able to render to OpenGL textures
940
    pub gl_context: &'a OptionGlContextPtr,
941
    /// Immutable reference to where the nodes are currently scrolled (current position)
942
    pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
943
    /// Handle of the current window
944
    pub current_window_handle: &'a RawWindowHandle,
945
    /// Callbacks for creating threads and getting the system time (since this crate uses `no_std`)
946
    pub system_callbacks: &'a ExternalSystemCallbacks,
947
    /// Platform-specific system style (colors, spacing, etc.)
948
    /// Arc allows safe cloning in callbacks without unsafe pointer manipulation
949
    pub system_style: Arc<SystemStyle>,
950
    /// Shared monitor list - initialized once at app start, updated by the platform
951
    /// layer on monitor topology changes (e.g. `WM_DISPLAYCHANGE`, `NSScreenParametersChanged`).
952
    /// Callbacks lock the mutex to read; platform locks to write.
953
    pub monitors: Arc<Mutex<MonitorVec>>,
954
    /// ICU4X localizer cache for internationalized formatting (numbers, dates, lists, plurals)
955
    /// Caches localizers for multiple locales. Only available when the "icu" feature is enabled.
956
    #[cfg(feature = "icu")]
957
    pub icu_localizer: IcuLocalizerHandle,
958
    /// The callable for FFI language bindings (Python, etc.)
959
    /// Cloned from the Callback struct before invocation. Native Rust callbacks have this as None.
960
    pub ctx: OptionRefAny,
961
}
962

            
963
/// `CallbackInfo` is a lightweight wrapper around pointers to stack-local data.
964
///
965
/// It can be safely copied because it only contains pointers - the underlying
966
/// data lives on the stack and outlives the callback invocation.
967
/// This allows callbacks to "consume" `CallbackInfo` by value while the caller
968
/// retains access to the same underlying data.
969
///
970
/// Information about the callback that is passed to the callback whenever a callback is invoked
971
///
972
/// # Architecture
973
///
974
/// `CallbackInfo` uses a transaction-based system:
975
/// - **Read-only pointers**: Access to layout data, window state, managers for queries
976
/// - **Change vector**: All modifications are recorded as `CallbackChange` items
977
/// - **Processing**: Changes are applied atomically after callback returns
978
///
979
/// This design provides clear separation between queries and modifications, makes debugging
980
/// easier, and allows for future extensibility.
981
///
982
/// The `changes` field uses a pointer to Arc<Mutex<...>> so that cloned `CallbackInfo` instances
983
/// (e.g., passed to timer callbacks) still push changes to the original collection,
984
/// while keeping `CallbackInfo` as Copy.
985
#[derive(Debug, Clone, Copy)]
986
#[repr(C)]
987
pub struct CallbackInfo {
988
    // Read-only Data (Query Access)
989
    /// Single reference to all readonly reference data
990
    /// This consolidates 8 individual parameters into 1, improving API ergonomics
991
    ref_data: *const CallbackInfoRefData<'static>,
992
    // Context Info (Immutable Event Data)
993
    /// The ID of the DOM + the node that was hit
994
    hit_dom_node: DomNodeId,
995
    /// The (x, y) position of the mouse cursor, **relative to top left of the element that was
996
    /// hit**
997
    cursor_relative_to_item: OptionLogicalPosition,
998
    /// The (x, y) position of the mouse cursor, **relative to top left of the window**
999
    cursor_in_viewport: OptionLogicalPosition,
    // Transaction Container (New System) - Uses pointer to Arc<Mutex> for shared access across clones
    /// All changes made by the callback, applied atomically after callback returns
    /// Stored as raw pointer so `CallbackInfo` remains Copy
    #[cfg(feature = "std")]
    changes: *const Arc<Mutex<Vec<CallbackChange>>>,
    #[cfg(not(feature = "std"))]
    changes: *mut Vec<CallbackChange>,
}
impl CallbackInfo {
    #[cfg(feature = "std")]
3060
    pub const fn new<'a>(
3060
        ref_data: &'a CallbackInfoRefData<'a>,
3060
        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
3060
        hit_dom_node: DomNodeId,
3060
        cursor_relative_to_item: OptionLogicalPosition,
3060
        cursor_in_viewport: OptionLogicalPosition,
3060
    ) -> Self {
3060
        Self {
3060
            // Read-only data (single reference to consolidated refs)
3060
            // SAFETY: We cast away the lifetime 'a to 'static because CallbackInfo
3060
            // only lives for the duration of the callback, which is shorter than 'a
3060
            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
3060
            // CallbackInfo only lives for the duration of the callback, which is shorter than 'a.
3060
            ref_data: std::ptr::from_ref::<CallbackInfoRefData<'a>>(ref_data).cast::<CallbackInfoRefData<'static>>(),
3060

            
3060
            // Context info (immutable event data)
3060
            hit_dom_node,
3060
            cursor_relative_to_item,
3060
            cursor_in_viewport,
3060

            
3060
            // Transaction container - store pointer to Arc<Mutex> for shared access
3060
            changes: std::ptr::from_ref::<Arc<Mutex<Vec<CallbackChange>>>>(changes),
3060
        }
3060
    }
    #[cfg(not(feature = "std"))]
    pub fn new<'a>(
        ref_data: &'a CallbackInfoRefData<'a>,
        changes: &'a mut Vec<CallbackChange>,
        hit_dom_node: DomNodeId,
        cursor_relative_to_item: OptionLogicalPosition,
        cursor_in_viewport: OptionLogicalPosition,
    ) -> Self {
        Self {
            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
            ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
            hit_dom_node,
            cursor_relative_to_item,
            cursor_in_viewport,
            changes: changes as *mut Vec<CallbackChange>,
        }
    }
    /// Get the callable for FFI language bindings (Python, etc.)
    ///
    /// Returns the cloned `OptionRefAny` if a callable was set, or None if this
    /// is a native Rust callback.
3
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
3
        unsafe { (*self.ref_data).ctx.clone() }
3
    }
    /// Returns the OpenGL context if available
1
    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
1
        unsafe { (*self.ref_data).gl_context.clone() }
1
    }
    // Helper methods for transaction system
    /// Push a change to be applied after the callback returns
    /// This is the primary method for modifying window state from callbacks
    #[cfg(feature = "std")]
8643
    pub fn push_change(&mut self, change: CallbackChange) {
        // SAFETY: The pointer is valid for the lifetime of the callback
        unsafe {
8643
            if let Ok(mut changes) = (*self.changes).lock() {
8643
                changes.push(change);
8643
            }
        }
8643
    }
    #[cfg(not(feature = "std"))]
    pub fn push_change(&mut self, change: CallbackChange) {
        unsafe { (*self.changes).push(change) }
    }
    /// Queue an E2E script (JSON) to run in this session after the callback
    /// returns.
    ///
    /// **NOT YET EXECUTED BY THE DESKTOP SHELL.** The variant is delivered and
    /// the shell logs an ERROR and drops it — the continuation slot it needs
    /// (`E2eSession`) is not reachable from the change-application site yet.
    /// Documented here rather than left to be discovered, because an API that
    /// looks like it works is worse than one that is absent.
    ///
    /// Returns nothing on purpose. Execution is DEFERRED, so there is no
    /// result to hand back yet — and a function named `execute_*` that
    /// returned a value here would be handing the caller something that reads
    /// as the script's outcome and is not. Poll the app's own state, or the
    /// op results, for that.
    /// Returns the handle immediately, before the script has necessarily
    /// started — pass it to `stop_e2e_json` to cancel. The handle is NOT the
    /// script's result; under `Async` there is no result yet.
    pub fn execute_e2e_json(
        &mut self,
        script: azul_core::json::Json,
        mode: E2eExecutionMode,
    ) -> E2eScriptHandle {
        let handle = E2eScriptHandle::new();
        self.push_change(CallbackChange::ExecuteE2eJson { script, handle, mode });
        handle
    }
    /// Cancel a script by handle. A handle that is unknown, already finished
    /// or already cancelled is a no-op, not an error: the caller cannot
    /// observe the race between a script ending and this arriving.
    pub fn stop_e2e_json(&mut self, handle: E2eScriptHandle) {
        self.push_change(CallbackChange::StopE2eJson { handle });
    }
    /// #28 (a): update a `VirtualView`'s VIRTUAL geometry (scrollbar math)
    /// WITHOUT re-invoking its callback or re-laying-out its child DOM —
    /// see [`CallbackChange::UpdateVirtualView`]. `node_id` addresses the
    /// `VirtualView` node in its parent DOM. Intended for streaming
    /// corrections: a background exact-pagination writeback fixes the
    /// scrollbar's total extent live while the UI keeps its materialized
    /// window untouched.
    pub fn update_virtual_view(
        &mut self,
        node_id: DomNodeId,
        materialized: OptionLogicalRect,
        virtual_rect: OptionLogicalRect,
    ) {
        self.push_change(CallbackChange::SetVirtualViewGeometry {
            dom_id: node_id.dom,
            node_id: node_id.node,
            materialized,
            virtual_rect,
        });
    }
    /// Snapshot the current app state into the undo history (mini-git commit).
1
    pub fn commit_undo_snapshot(&mut self) {
1
        self.push_change(CallbackChange::CommitUndoSnapshot);
1
    }
    /// Undo the last committed app-state change; relayouts all windows.
1
    pub fn undo_app_state(&mut self) {
1
        self.push_change(CallbackChange::UndoAppState);
1
    }
    /// Redo a previously undone app-state change; relayouts all windows.
1
    pub fn redo_app_state(&mut self) {
1
        self.push_change(CallbackChange::RedoAppState);
1
    }
    /// Debug helper to get the changes pointer for debugging
    #[cfg(feature = "std")]
3
    #[must_use] pub const fn get_changes_ptr(&self) -> *const () {
3
        self.changes.cast::<()>()
3
    }
    /// Get the collected changes (consumes them from the Arc<Mutex>)
    #[cfg(feature = "std")]
806
    #[must_use] pub fn take_changes(&self) -> Vec<CallbackChange> {
        // SAFETY: The pointer is valid for the lifetime of the callback
        unsafe {
806
            (*self.changes).lock().map_or_else(
                |_| Vec::new(),
806
                |mut changes| core::mem::take(&mut *changes),
            )
        }
806
    }
    #[cfg(not(feature = "std"))]
    pub fn take_changes(&self) -> Vec<CallbackChange> {
        unsafe { core::mem::take(&mut *self.changes) }
    }
    /// Check if pending changes require relayout before the next step.
    ///
    /// Returns true for `ModifyWindowState` (resize) and `ScrollTo` (scroll),
    /// which both need the event loop to re-run layout so that subsequent
    /// operations (like `take_screenshot`) see updated content.
    ///
    /// Used by the E2E test runner to detect when it needs to yield.
    #[cfg(feature = "std")]
629
    #[must_use] pub fn has_pending_relayout_change(&self) -> bool {
        unsafe {
636
            (*self.changes).lock().is_ok_and(|changes| changes.iter().any(|c| matches!(c,
                    CallbackChange::ModifyWindowState { .. } |
                    CallbackChange::ScrollTo { .. } |
                    // Synthetic input (E2E `click` = move/down/up applied one
                    // state per frame): the runner MUST yield here, or every
                    // post-click step executes against the pre-click DOM and
                    // the queued states only apply after the test finishes.
                    CallbackChange::QueueWindowStateSequence { .. }
                )))
        }
629
    }
    // Modern Api (using CallbackChange transactions)
    /// Add a timer to this window (applied after callback returns)
6
    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
6
        self.push_change(CallbackChange::AddTimer { timer_id, timer });
6
    }
    /// Remove a timer from this window (applied after callback returns)
3
    pub fn remove_timer(&mut self, timer_id: TimerId) {
3
        self.push_change(CallbackChange::RemoveTimer { timer_id });
3
    }
    /// Add a thread to this window (applied after callback returns)
26
    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
26
        self.push_change(CallbackChange::AddThread { thread_id, thread });
26
    }
    /// Checks for updates ASYNCHRONOUSLY: spawns a background thread that
    /// reads `AppConfig.updates` (manifest URL, current version, mode),
    /// applies the install-kind backstops (package-managed binaries never
    /// self-update) and the anti-downgrade/suspend policy, optionally STAGES
    /// the artifact (`options.download_automatically` — staging is not
    /// installing), and invokes `callback(data, info, check)` on the main
    /// thread with the result. Returns the thread's id.
    #[cfg(feature = "updater")]
    pub fn check_for_updates(
        &mut self,
        data: RefAny,
        callback: crate::updater::UpdateCheckCallback,
        options: crate::updater::UpdateOptions,
    ) -> ThreadId {
        let (thread_id, thread) = crate::updater::spawn_update_check(data, callback, options);
        self.add_thread(thread_id, thread);
        thread_id
    }
    /// Records an app-defined COUNTER metric with free-form labels (pass an
    /// empty vec for none). Labels are sanitized and capped (6 keys, 64-char
    /// values); every distinct combination counts against the global series
    /// ceiling. No-op unless the `telemetry` feature is on and the user's
    /// consent tier collects metrics.
    #[cfg(feature = "std")]
    pub fn record_counter(
        &mut self,
        name: AzString,
        value: u64,
        labels: azul_core::window::StringPairVec,
    ) {
        #[cfg(feature = "telemetry")]
        {
            let pairs: Vec<(&str, &str)> = labels
                .as_ref()
                .iter()
                .map(|p| (p.key.as_str(), p.value.as_str()))
                .collect();
            crate::telemetry::count_with(name.as_str(), value, &pairs);
        }
        #[cfg(not(feature = "telemetry"))]
        {
            drop((name, value, labels));
        }
    }
    /// Records an app-defined HISTOGRAM observation with free-form labels
    /// (same sanitization/caps as [`Self::record_counter`]).
    #[cfg(feature = "std")]
    pub fn record_histogram(
        &mut self,
        name: AzString,
        value: f64,
        labels: azul_core::window::StringPairVec,
    ) {
        #[cfg(feature = "telemetry")]
        {
            let pairs: Vec<(&str, &str)> = labels
                .as_ref()
                .iter()
                .map(|p| (p.key.as_str(), p.value.as_str()))
                .collect();
            crate::telemetry::observe_with(name.as_str(), value, &pairs);
        }
        #[cfg(not(feature = "telemetry"))]
        {
            drop((name, value, labels));
        }
    }
    /// Sets an app-defined GAUGE with free-form labels (same
    /// sanitization/caps as [`Self::record_counter`]).
    #[cfg(feature = "std")]
    pub fn record_gauge(
        &mut self,
        name: AzString,
        value: f64,
        labels: azul_core::window::StringPairVec,
    ) {
        #[cfg(feature = "telemetry")]
        {
            let pairs: Vec<(&str, &str)> = labels
                .as_ref()
                .iter()
                .map(|p| (p.key.as_str(), p.value.as_str()))
                .collect();
            crate::telemetry::gauge_with(name.as_str(), value, &pairs);
        }
        #[cfg(not(feature = "telemetry"))]
        {
            drop((name, value, labels));
        }
    }
    /// Opens one of the built-in system dialogs (always CPU-rendered — a
    /// dialog reporting a problem must not depend on the GPU working):
    ///
    /// * `ReportProblem`: captures a screenshot of THIS window, then opens
    ///   the report dialog (message + optional screenshot/system info →
    ///   `AppConfig.report_problem` mailbox, or disk without one).
    /// * `UpdateVersion`: opens the update dialog and starts the async
    ///   check (manifest + changelog; install only after consent, and only
    ///   where the install kind permits self-update).
    #[cfg(all(feature = "std", feature = "widgets", feature = "text_layout"))]
    pub fn invoke_system_dialog(&mut self, dialog: azul_core::window::SysDialogType) {
        match dialog {
            azul_core::window::SysDialogType::ReportProblem => {
                // Capture BEFORE the dialog exists so it can never be in
                // its own screenshot. Best-effort: a failed capture still
                // opens the dialog, just without the attachment.
                let screenshot = self
                    .take_screenshot(DomId::ROOT_ID)
                    .ok();
                crate::dialogs::report_problem::open(self, screenshot);
            }
            azul_core::window::SysDialogType::UpdateVersion => {
                #[cfg(feature = "updater")]
                crate::dialogs::update_version::open(self);
                #[cfg(not(feature = "updater"))]
                eprintln!(
                    "[azul] invoke_system_dialog(UpdateVersion): azul-layout was built \
                     without the `updater` feature; the dialog is unavailable"
                );
            }
            azul_core::window::SysDialogType::TelemetryConsent => {
                #[cfg(feature = "telemetry")]
                crate::dialogs::telemetry_consent::open(self);
                #[cfg(not(feature = "telemetry"))]
                eprintln!(
                    "[azul] invoke_system_dialog(TelemetryConsent): azul-layout was built \
                     without the `telemetry` feature; the dialog is unavailable"
                );
            }
            azul_core::window::SysDialogType::GpuCheck => {
                crate::dialogs::gpu_check::open(self);
            }
        }
    }
    /// Remove a thread from this window (applied after callback returns)
2
    pub fn remove_thread(&mut self, thread_id: ThreadId) {
2
        self.push_change(CallbackChange::RemoveThread { thread_id });
2
    }
    /// Stop event propagation (applied after callback returns)
    ///
    /// W3C `stopPropagation()`: remaining handlers on the *current* node
    /// still fire, but no handlers on ancestor/descendant nodes are called.
5
    pub fn stop_propagation(&mut self) {
5
        self.push_change(CallbackChange::StopPropagation);
5
    }
    /// Stop event propagation immediately (applied after callback returns)
    ///
    /// W3C `stopImmediatePropagation()`: no further handlers fire,
    /// not even remaining handlers registered on the same node.
2
    pub fn stop_immediate_propagation(&mut self) {
2
        self.push_change(CallbackChange::StopImmediatePropagation);
2
    }
    /// Set keyboard focus target (applied after callback returns)
11
    pub fn set_focus(&mut self, target: FocusTarget) {
11
        self.push_change(CallbackChange::SetFocusTarget { target });
11
    }
    /// Create a new window (applied after callback returns)
2
    pub fn create_window(&mut self, options: WindowCreateOptions) {
2
        self.push_change(CallbackChange::CreateNewWindow { options });
2
    }
    /// Close the current window (applied after callback returns)
3
    pub fn close_window(&mut self) {
3
        self.push_change(CallbackChange::CloseWindow);
3
    }
    /// Switch to a different route (applied after callback returns).
    ///
    /// On desktop: swaps the layout callback and triggers `RefreshDom`.
    /// On web: also calls `history.pushState()`.
    ///
    /// # C API
    /// ```c
    /// AzCallbackInfo_switchRoute(&info, AzString_fromConstStr("/user/:id"),
    ///     AzStringPairVec_fromConstSlice(&[AzStringPair { key: "id", value: "42" }]));
    /// ```
1
    pub fn switch_route(&mut self, pattern: AzString, params: azul_core::window::StringPairVec) {
1
        self.push_change(CallbackChange::SwitchRoute { pattern, params });
1
    }
    /// Get the current active route pattern (e.g. `"/user/:id"`).
    ///
    /// `"/"` when the app configured no routes - an app without routing is on
    /// the default route. Matches `LayoutCallbackInfo::get_route_pattern`, so
    /// the same branch works in both callbacks.
    ///
    /// # C API
    /// ```c
    /// AzString pattern = AzCallbackInfo_getRoutePattern(&info);
    /// ```
1
    #[must_use] pub fn get_route_pattern(&self) -> AzString {
1
        match &self.get_current_window_state().active_route {
            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1
            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str("/"),
        }
1
    }
    /// Get a route parameter by key (e.g. `"id"` from `/user/:id`).
    ///
    /// Returns empty string if the parameter doesn't exist or no route is active.
    ///
    /// # C API
    /// ```c
    /// AzString id = AzCallbackInfo_getRouteParam(&info, AzString_fromConstStr("id"));
    /// ```
    // FFI-exported (AzCallbackInfo_getRouteParam): the owned AzString key is the api.json signature.
    #[allow(clippy::needless_pass_by_value)]
4
    #[must_use] pub fn get_route_param(&self, key: AzString) -> AzString {
4
        match &self.get_current_window_state().active_route {
            azul_core::resources::OptionRouteMatch::Some(rm) => {
                rm.get_param(key.as_str())
                    .cloned()
                    .unwrap_or_else(|| AzString::from_const_str(""))
            }
4
            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
        }
4
    }
    /// Set a route parameter value and trigger re-render.
    ///
    /// This modifies the active route's params in-place and triggers a DOM refresh.
    /// On web, this also updates the URL via `history.replaceState()`.
    ///
    /// # C API
    /// ```c
    /// AzCallbackInfo_setRouteParam(&info, AzString_fromConstStr("id"), AzString_fromConstStr("99"));
    /// ```
1
    pub fn set_route_param(&mut self, key: AzString, value: AzString) {
1
        let ws = self.get_current_window_state();
1
        let pattern = match &ws.active_route {
            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1
            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),
        });
1
    }
    /// Modify the window state (applied after callback returns)
348
    pub fn modify_window_state(&mut self, state: FullWindowState) {
348
        self.push_change(CallbackChange::ModifyWindowState { state });
348
    }
    /// 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.
2
    pub fn begin_interactive_move(&mut self) {
2
        self.push_change(CallbackChange::BeginInteractiveMove);
2
    }
    /// 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.
5
    pub fn queue_window_state_sequence(&mut self, states: FullWindowStateVec) {
5
        self.push_change(CallbackChange::QueueWindowStateSequence {
5
            states: states.into_library_owned_vec(),
5
        });
5
    }
    /// 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
522
    pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
522
        self.push_change(CallbackChange::ChangeNodeText { node_id, text });
522
    }
    /// Change the image of a node (applied after callback returns)
15
    pub fn change_node_image(
15
        &mut self,
15
        dom_id: DomId,
15
        node_id: NodeId,
15
        image: ImageRef,
15
        update_type: UpdateImageType,
15
    ) {
15
        self.push_change(CallbackChange::ChangeNodeImage {
15
            dom_id,
15
            node_id,
15
            image,
15
            update_type,
15
        });
15
    }
    /// 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)
1
    pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
1
        self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
1
    }
    /// 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.
3
    pub fn update_all_image_callbacks(&mut self) {
3
        self.push_change(CallbackChange::UpdateAllImageCallbacks);
3
    }
    /// 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)
2
    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
2
        self.push_change(CallbackChange::UpdateVirtualView { dom_id, node_id });
2
    }
    /// Re-render EVERY `VirtualView` on the existing DOM - no node id required.
    ///
    /// Use from a callback that mutated a dataset shared with a `VirtualView`'s
    /// `refany` (the two are clones of one `RefAny`, so they point at the same
    /// underlying data). The canonical case is a background thread writeback:
    /// e.g. the `MapWidget`'s tile-fetch worker decodes a tile, writes it into
    /// the shared `MapTileCache`, then calls this so the pure `VirtualView`
    /// content callback re-reads the cache and rebuilds its child DOM in place -
    /// WITHOUT a `RefreshDom` (which would rebuild the DOM and orphan the
    /// worker's clone of the cache).
29
    pub fn trigger_all_virtual_view_rerender(&mut self) {
29
        self.push_change(CallbackChange::UpdateAllVirtualViews);
29
    }
    // 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.
18
    #[must_use] pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
18
        let layout_window = self.get_layout_window();
18
        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
18
    }
    /// Get the parent node of the given node
    ///
    /// Returns None if the node has no parent (i.e., it's the root node)
4
    #[must_use] pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
4
        let layout_window = self.get_layout_window();
4
        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()
4
    }
    /// Get the next sibling of the given node
    ///
    /// Returns None if the node has no next sibling
4
    #[must_use] pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
4
        let layout_window = self.get_layout_window();
4
        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()
4
    }
    /// Get the previous sibling of the given node
    ///
    /// Returns None if the node has no previous sibling
4
    #[must_use] pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
4
        let layout_window = self.get_layout_window();
4
        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()
4
    }
    /// Get the first child of the given node
    ///
    /// Returns None if the node has no children
4
    #[must_use] pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
4
        let layout_window = self.get_layout_window();
4
        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)
4
    }
    /// Get the last child of the given node
    ///
    /// Returns None if the node has no children
4
    #[must_use] pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
4
        let layout_window = self.get_layout_window();
4
        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()
4
    }
    /// 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.
4
    #[must_use] pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeHierarchyItemIdVec {
4
        let layout_window = self.get_layout_window();
4
        let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
4
            return NodeHierarchyItemIdVec::from_const_slice(&[]);
        };
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let Some(hier_item) = node_hierarchy.get(node_id) else {
            return NodeHierarchyItemIdVec::from_const_slice(&[]);
        };
        // Get first child - if none, return empty
        let Some(first_child) = hier_item.first_child_id(node_id) else {
            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(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
        {
            children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
            current = next_sibling;
        }
        NodeHierarchyItemIdVec::from(children)
4
    }
    /// Get the number of direct children of the given node
    ///
    /// Uses the contiguous node layout for efficient counting.
4
    #[must_use] pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
4
        let layout_window = self.get_layout_window();
4
        let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
4
            return 0;
        };
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
        let Some(hier_item) = node_hierarchy.get(node_id) else {
            return 0;
        };
        // Get first child - if none, return 0
        let Some(first_child) = hier_item.first_child_id(node_id) else {
            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(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
        {
            count += 1;
            current = next_sibling;
        }
        count
4
    }
    /// 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)
7172
    pub fn change_node_css_properties(
7172
        &mut self,
7172
        dom_id: DomId,
7172
        node_id: NodeId,
7172
        properties: CssPropertyVec,
7172
    ) {
7172
        self.push_change(CallbackChange::ChangeNodeCssProperties {
7172
            dom_id,
7172
            node_id,
7172
            properties,
7172
        });
7172
    }
    /// 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
    /// # Panics
    ///
    /// Panics if `node_id.node` is None; the target must reference a concrete node.
7165
    pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
7165
        let dom_id = node_id.dom;
7165
        let internal_node_id = node_id
7165
            .node
7165
            .into_crate_internal()
7165
            .expect("DomNodeId node should not be None");
7165
        self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
7165
    }
    /// 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.
1
    pub fn override_node_css_properties(
1
        &mut self,
1
        dom_id: DomId,
1
        node_id: NodeId,
1
        properties: CssPropertyVec,
1
    ) {
1
        self.push_change(CallbackChange::OverrideNodeCssProperties {
1
            dom_id,
1
            node_id,
1
            properties,
1
        });
1
    }
    /// Convenience wrapper for `override_node_css_properties` that targets a
    /// single property on the hit node's DOM (typical for animation callbacks).
    /// # Panics
    ///
    /// Panics if `node_id.node` is None; the target must reference a concrete node.
2
    pub fn override_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
2
        let dom_id = node_id.dom;
2
        let internal_node_id = node_id
2
            .node
2
            .into_crate_internal()
2
            .expect("DomNodeId node should not be None");
2
        self.override_node_css_properties(dom_id, internal_node_id, vec![property].into());
2
    }
    /// Scroll a node to a specific position (applied after callback returns)
126
    pub fn scroll_to(
126
        &mut self,
126
        dom_id: DomId,
126
        node_id: NodeHierarchyItemId,
126
        position: LogicalPosition,
126
    ) {
126
        self.push_change(CallbackChange::ScrollTo {
126
            dom_id,
126
            node_id,
126
            position,
126
            unclamped: false,
126
        });
126
    }
    /// Scroll a node toward a target offset with the critically-damped
    /// spring the scroll physics already uses for momentum and rubber-band
    /// (AZUL-STILL-TODO B8/I27): the position GLIDES to the target instead
    /// of jumping, and a retarget mid-flight keeps the current velocity.
    /// One mechanism shared by scroll-to-caret, scroll-to-page and
    /// find-result navigation.
    ///
    /// The input lands in the shared scroll queue; the platform shell
    /// starts the physics timer when it sees pending input (the same path
    /// wheel momentum takes).
    pub fn scroll_to_animated(
        &mut self,
        dom_id: DomId,
        node_id: NodeId,
        target: LogicalPosition,
    ) {
        use crate::managers::scroll_state::{ScrollInput, ScrollInputDevice, ScrollInputSource};
        let now = self.get_current_time();
        self.get_scroll_manager().scroll_input_queue.push(ScrollInput {
            dom_id,
            node_id,
            delta: target,
            timestamp: now,
            source: ScrollInputSource::AnimateTo,
            device: ScrollInputDevice::Programmatic,
        });
    }
    /// Scroll a node to a specific position without clamping.
    /// Used by the scroll physics timer for rubber-banding/overscroll.
128
    pub fn scroll_to_unclamped(
128
        &mut self,
128
        dom_id: DomId,
128
        node_id: NodeHierarchyItemId,
128
        position: LogicalPosition,
128
    ) {
128
        self.push_change(CallbackChange::ScrollTo {
128
            dom_id,
128
            node_id,
128
            position,
128
            unclamped: true,
128
        });
128
    }
    /// 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.
3
    pub fn scroll_node_into_view(
3
        &mut self,
3
        node_id: DomNodeId,
3
        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
3
    ) {
3
        self.push_change(CallbackChange::ScrollIntoView {
3
            node_id,
3
            options,
3
        });
3
    }
    /// Record a structural document edit programmatically (the bold/italic
    /// toolbar path — keyboard defaults record through the default-action
    /// layer instead). Applied after the callback returns; azul never applies
    /// it to the `StyledDom`.
    pub fn record_document_edit(
        &mut self,
        changeset: crate::managers::changeset::DocumentChangeset,
    ) {
        self.push_change(CallbackChange::RecordDocumentEdit { changeset });
    }
    /// The commit handshake: confirm structural edit `id` was applied to the
    /// app's model (usually called for you by
    /// `document_edit::apply_document_operation` users).
    pub fn mark_document_edit_applied(&mut self, id: u64) {
        self.push_change(CallbackChange::MarkDocumentEditApplied { id });
    }
    /// The handshake carrying the applier's INVERSE operation (from
    /// `document_edit::AppliedEdit::inverse`) — makes the edit structurally
    /// undoable (Ctrl+Z re-records the inverse through the same loop).
    pub fn mark_document_edit_applied_with_inverse(
        &mut self,
        id: u64,
        inverse: crate::managers::changeset::DocumentOperation,
    ) {
        self.push_change(CallbackChange::MarkDocumentEditAppliedWithInverse { id, inverse });
    }
    /// Undo the newest structural edit: a NEW changeset (the inverse) is
    /// recorded for the app to apply — nothing mutates here.
    pub fn undo_structural_edit(&mut self) {
        self.push_change(CallbackChange::UndoStructuralEdit);
    }
    /// Redo the newest undone structural edit.
    pub fn redo_structural_edit(&mut self) {
        self.push_change(CallbackChange::RedoStructuralEdit);
    }
    /// The pending structural edit, if any (inspect in a callback before
    /// deciding to apply or `prevent_default`).
    #[must_use]
    pub fn get_document_edit_clone(
        &self,
    ) -> crate::managers::changeset::OptionDocumentChangeset {
        self.get_layout_window()
            .get_pending_document_edit()
            .cloned()
            .into()
    }
    /// Add an image to the image cache (applied after callback returns)
3
    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
3
        self.push_change(CallbackChange::AddImageToCache { id, image });
3
    }
    /// Remove an image from the image cache (applied after callback returns)
3
    pub fn remove_image_from_cache(&mut self, id: AzString) {
3
        self.push_change(CallbackChange::RemoveImageFromCache { id });
3
    }
    /// Reload system fonts (applied after callback returns)
    ///
    /// Note: This is an expensive operation that rebuilds the entire font cache
2
    pub fn reload_system_fonts(&mut self) {
2
        self.push_change(CallbackChange::ReloadSystemFonts);
2
    }
    // 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.
71
    #[must_use] pub const fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
71
        self.get_layout_window()
71
            .text_input_manager
71
            .get_pending_changeset()
71
    }
    /// 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
1
    pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
1
        self.push_change(CallbackChange::SetTextChangeset { changeset });
1
    }
    /// 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
1
    pub fn create_text_input(&mut self, text: AzString) {
1
        self.push_change(CallbackChange::CreateTextInput { text });
1
    }
    // 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
4
    pub fn insert_child_node(
4
        &mut self,
4
        dom_id: DomId,
4
        parent_node_id: NodeId,
4
        node_type_str: AzString,
4
        position: OptionUsize,
4
        classes: StringVec,
4
        id: OptionString,
4
    ) {
4
        self.push_change(CallbackChange::InsertChildNode {
4
            dom_id,
4
            parent_node_id,
4
            node_type_str,
4
            position: position.into(),
4
            classes: classes.into_library_owned_vec(),
4
            id: id.into(),
4
        });
4
    }
    /// 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
2
    pub fn delete_node(&mut self, dom_id: DomId, node_id: NodeId) {
2
        self.push_change(CallbackChange::DeleteNode { dom_id, node_id });
2
    }
    /// 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
1
    pub fn set_node_ids_and_classes(
1
        &mut self,
1
        dom_id: DomId,
1
        node_id: NodeId,
1
        ids_and_classes: azul_core::dom::IdOrClassVec,
1
    ) {
1
        self.push_change(CallbackChange::SetNodeIdsAndClasses {
1
            dom_id,
1
            node_id,
1
            ids_and_classes,
1
        });
1
    }
    /// 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.
11
    pub fn prevent_default(&mut self) {
11
        self.push_change(CallbackChange::PreventDefault);
11
    }
    // 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.
2
    pub fn set_cursor_visibility(&mut self, visible: bool) {
2
        self.push_change(CallbackChange::SetCursorVisibility { visible });
2
    }
    /// 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.
1
    pub fn reset_cursor_blink(&mut self) {
1
        self.push_change(CallbackChange::ResetCursorBlink);
1
    }
    /// 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
2
    pub fn open_menu(&mut self, menu: Menu) {
2
        self.push_change(CallbackChange::OpenMenu {
2
            menu,
2
            position: None,
2
        });
2
    }
    /// 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)
6
    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
6
        self.push_change(CallbackChange::OpenMenu {
6
            menu,
6
            position: Some(position),
6
        });
6
    }
    // 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
3
    pub fn show_tooltip(&mut self, text: AzString) {
3
        let position = self
3
            .get_cursor_relative_to_viewport()
3
            .into_option()
3
            .unwrap_or_else(LogicalPosition::zero);
3
        self.push_change(CallbackChange::ShowTooltip { text, position });
3
    }
    /// 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)
3
    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
3
        self.push_change(CallbackChange::ShowTooltip { text, position });
3
    }
    /// Hide the currently displayed tooltip
3
    pub fn hide_tooltip(&mut self) {
3
        self.push_change(CallbackChange::HideTooltip);
3
    }
    // 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
1
    pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
1
        self.push_change(CallbackChange::InsertText {
1
            dom_id,
1
            node_id,
1
            text,
1
        });
1
    }
    /// 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
1
    pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
1
        self.push_change(CallbackChange::MoveCursor {
1
            dom_id,
1
            node_id,
1
            cursor,
1
        });
1
    }
    /// 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)
1
    pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
1
        self.push_change(CallbackChange::SetSelection {
1
            dom_id,
1
            node_id,
1
            selection,
1
        });
1
    }
    // === 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.
    #[must_use] pub fn get_multi_cursor_selections(&self, dom_id: &DomId) -> azul_core::selection::IdentifiedSelectionVec {
        let lw = self.get_layout_window();
        lw.text_edit_manager.multi_cursor.as_ref()
            .map(|mc| mc.selections.clone())
            .unwrap_or_default()
            .into()
    }
    /// Get the primary (last-added) selection from the `MultiCursorState`.
1
    #[must_use] pub fn get_primary_selection(&self, dom_id: &DomId) -> Option<azul_core::selection::IdentifiedSelection> {
1
        let lw = self.get_layout_window();
1
        lw.text_edit_manager.multi_cursor.as_ref()
1
            .and_then(|mc| mc.get_primary().copied())
1
    }
    /// Get the number of active cursors/selections.
1
    #[must_use] pub fn get_selection_count(&self, dom_id: &DomId) -> usize {
1
        let lw = self.get_layout_window();
1
        lw.text_edit_manager.multi_cursor.as_ref()
1
            .map_or(0, azul_core::selection::MultiCursorState::len)
1
    }
    /// 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
35
    pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
        // Position the menu at the hit node's bottom-left. Prefer the display-list
        // hit-test bounds: they always carry the node's final rendered rect for an
        // interactive (tagged) node, whereas get_node_rect (position + used_size)
        // can be None for nodes whose used_size isn't recorded on the layout node.
35
        let rect = self
35
            .get_node_hit_test_bounds(node_id)
35
            .or_else(|| self.get_node_rect(node_id));
35
        rect.is_some_and(|rect| {
            // Position menu at bottom-left of the node
24
            let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
24
            self.push_change(CallbackChange::OpenMenu {
24
                menu,
24
                position: Some(position),
24
            });
24
            true
24
        })
35
    }
    /// 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
35
    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
35
        let hit_node = self.get_hit_node();
35
        self.open_menu_for_node(menu, hit_node)
35
    }
    // 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()`.
24137
    #[must_use] pub const fn get_layout_window(&self) -> &LayoutWindow {
24137
        unsafe { (*self.ref_data).layout_window }
24137
    }
    /// #28: "what WOULD the page breaks be for this content at this page
    /// size?" — a SPECULATIVE pagination query answered from the window's
    /// live caches (shaping shared read-only, nothing committed, nothing
    /// polluted). The result is bbox-level only: break positions, page
    /// count, total content height. See [`LayoutWindow::query_pagination`]
    /// for the cache-fork mechanics. Intended for app-side lazy pagination:
    /// lay out a monitor-height prefix eagerly, estimate the page count,
    /// then correct asynchronously with this query.
    #[must_use] pub fn query_pagination(
        &self,
        styled_dom: &StyledDom,
        page_size: LogicalSize,
        page_config: crate::solver3::pagination::FakePageConfig,
    ) -> Option<crate::solver3::page_breaks::PaginationInfo> {
        // No ImageCache travels with CallbackInfo; an empty one matches what
        // app-side pagination (miniword) passes today. Callers that need
        // image-sized pagination can use `LayoutWindow::query_pagination`
        // directly with a real cache.
        self.get_layout_window().query_pagination(
            styled_dom,
            page_size,
            page_config,
            &ImageCache::default(),
        )
    }
    /// 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(|b| b.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
140
    #[must_use] pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
140
        self.get_layout_window().get_node_size(node_id)
140
    }
    /// Get the logical position of a node, or `None` if the node doesn't exist
249
    #[must_use] pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
249
        self.get_layout_window().get_node_position(node_id)
249
    }
    /// Current animation MOMENTUM of a node: the velocity (logical px/s) of
    /// its in-flight presence/move animation. `None` while nothing animates
    /// the node. Springs carry velocity across retargets by design, so this
    /// is the value a custom animation function reads to hand motion off
    /// smoothly (or to reverse it: push the negation back via
    /// [`Self::set_animation_momentum`]).
    #[must_use]
    pub fn get_animation_momentum(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
        let lw = self.get_layout_window();
        let node = node_id.node.into_crate_internal()?;
        let key = lw
            .anim_key_to_node
            .iter()
            .find_map(|(k, n)| (*n == node).then_some(*k))?;
        let anim = lw.animations.get(key)?;
        Some(LogicalPosition::new(
            anim.translate_x.velocity,
            anim.translate_y.velocity,
        ))
    }
    /// Queue a momentum write for after this callback returns: kick the
    /// node's in-flight animation with `velocity` (logical px/s), or start an
    /// identity-anchored spring carrying it when nothing is animating.
    pub fn set_animation_momentum(&mut self, node_id: DomNodeId, velocity: LogicalPosition) {
        self.push_change(CallbackChange::SetAnimationMomentum {
            node: node_id,
            velocity_x: velocity.x,
            velocity_y: velocity.y,
        });
    }
    /// 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.
36
    #[must_use] pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
36
        self.get_layout_window().get_node_hit_test_bounds(node_id)
36
    }
    /// 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.
235
    #[must_use] pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
235
        let position = self.get_node_position(node_id)?;
123
        let size = self.get_node_size(node_id)?;
123
        Some(LogicalRect::new(position, size))
235
    }
    /// 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.
199
    #[must_use] pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
199
        let hit_node = self.get_hit_node();
199
        self.get_node_rect(hit_node)
199
    }
    // Timer Management (Query APIs)
    /// Get a reference to a timer
1
    #[must_use] pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1
        self.get_layout_window().get_timer(timer_id)
1
    }
    /// Get all timer IDs
1
    #[must_use] pub fn get_timer_ids(&self) -> TimerIdVec {
1
        self.get_layout_window().get_timer_ids()
1
    }
    // Thread Management (Query APIs)
    /// Get a reference to a thread
1
    #[must_use] pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1
        self.get_layout_window().get_thread(thread_id)
1
    }
    /// Get all thread IDs
1
    #[must_use] pub fn get_thread_ids(&self) -> ThreadIdVec {
1
        self.get_layout_window().get_thread_ids()
1
    }
    // Gpu Value Cache Management (Query APIs)
    /// Get the GPU value cache for a specific DOM
1
    #[must_use] pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
1
        self.get_layout_window().get_gpu_cache(dom_id)
1
    }
    // Layout Result Access (Query APIs)
    /// Get a layout result for a specific DOM
1
    #[must_use] pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
1
        self.get_layout_window().get_layout_result(dom_id)
1
    }
    /// Get all DOM IDs that have layout results
30
    #[must_use] pub fn get_dom_ids(&self) -> DomIdVec {
30
        self.get_layout_window().get_dom_ids()
30
    }
    // Node Hierarchy Navigation
    /// Get the DOM node that was hit by the event that triggered this callback
1842
    #[must_use] pub const fn get_hit_node(&self) -> DomNodeId {
1842
        self.hit_dom_node
1842
    }
    /// Check if a node is anonymous (generated for table layout)
    #[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
9826
    fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
9826
        let layout_window = self.get_layout_window();
9826
        let Some(layout_result) = layout_window.get_layout_result(dom_id) else {
            return false;
        };
9826
        let node_data_cont = layout_result.styled_dom.node_data.as_container();
9826
        let Some(node_data) = node_data_cont.get(node_id) else {
            return false;
        };
9826
        node_data.is_anonymous()
9826
    }
    /// Get the parent of a node, skipping anonymous (table-generated) nodes
913
    #[must_use] pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
913
        let layout_window = self.get_layout_window();
913
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
900
        let node_id_internal = node_id.node.into_crate_internal()?;
896
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
896
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous parent nodes - walk up the tree until we find a non-anonymous node
881
        let mut current_parent_id = hier_item.parent_id()?;
        loop {
868
            if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
868
                return Some(DomNodeId {
868
                    dom: node_id.dom,
868
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
868
                });
            }
            // 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()?;
        }
913
    }
    /// Get the previous sibling of a node, skipping anonymous nodes
227
    #[must_use] pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
227
        let layout_window = self.get_layout_window();
227
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
225
        let node_id_internal = node_id.node.into_crate_internal()?;
225
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
225
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous siblings - walk backwards until we find a non-anonymous node
225
        let mut current_sibling_id = hier_item.previous_sibling_id()?;
        loop {
211
            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
211
                return Some(DomNodeId {
211
                    dom: node_id.dom,
211
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
211
                });
            }
            // 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()?;
        }
227
    }
    /// Get the next sibling of a node, skipping anonymous nodes
5398
    #[must_use] pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
5398
        let layout_window = self.get_layout_window();
5398
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
5394
        let node_id_internal = node_id.node.into_crate_internal()?;
5394
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
5394
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Skip anonymous siblings - walk forwards until we find a non-anonymous node
5391
        let mut current_sibling_id = hier_item.next_sibling_id()?;
        loop {
4911
            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
4911
                return Some(DomNodeId {
4911
                    dom: node_id.dom,
4911
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
4911
                });
            }
            // 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()?;
        }
5398
    }
    /// Get the first child of a node, skipping anonymous nodes
3897
    #[must_use] pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
3897
        let layout_window = self.get_layout_window();
3897
        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
3888
        let node_id_internal = node_id.node.into_crate_internal()?;
3876
        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
3876
        let hier_item = node_hierarchy.get(node_id_internal)?;
        // Get first child, then skip anonymous nodes
3864
        let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
        loop {
3836
            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
3836
                return Some(DomNodeId {
3836
                    dom: node_id.dom,
3836
                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
3836
                });
            }
            // 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()?;
        }
3897
    }
    /// Get the last child of a node, skipping anonymous nodes
1
    #[must_use] pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1
        let layout_window = self.get_layout_window();
1
        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()?;
        }
1
    }
    // 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`
    // owned RefAny passed by value per the azul FFI / api.json convention.
    #[allow(clippy::needless_pass_by_value)]
29
    pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
29
        let mut found: Option<(u64, DomNodeId)> = None;
29
        let search_type_id = search_key.get_type_id();
29
        for dom_id in self.get_dom_ids().as_ref().iter().copied() {
17
            let layout_window = self.get_layout_window();
17
            let Some(layout_result) = layout_window.get_layout_result(&dom_id) else {
                continue;
            };
17
            let node_data_cont = layout_result.styled_dom.node_data.as_container();
227
            for (node_idx, node_data) in node_data_cont.iter().enumerate() {
227
                if let Some(dataset) = node_data.get_dataset().cloned() {
27
                    if dataset.get_type_id() == search_type_id {
15
                        let node_id = DomNodeId {
15
                            dom: dom_id,
15
                            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
15
                                node_idx,
15
                            ))),
15
                        };
15
                        let instance_id = dataset.instance_id;
15
                        match found {
14
                            None => found = Some((instance_id, node_id)),
1
                            Some((prev_instance, _)) => {
1
                                if instance_id < prev_instance {
                                    found = Some((instance_id, node_id));
1
                                }
                            }
                        }
12
                    }
200
                }
            }
        }
29
        found.map(|s| s.1)
29
    }
    /// Get the text content of a text node, or `None` if the node is not a text node
    #[must_use] 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".
    #[must_use] 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.
    // Cross-type AttributeType payload dispatch: each `(attr_name, AttributeType::X(v))`
    // arm binds a differently-typed `v`, so the same-bodied arms can't be merged into
    // one or-pattern (won't type-check) — they are intentionally one-per-attribute.
    #[allow(clippy::match_same_arms)]
    #[must_use] 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());
                }
                _ => {}
            }
        }
        None
    }
    /// Get all classes of a node as a vector of strings
2
    #[must_use] pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
2
        let Some(layout_window) = self.get_layout_window().get_layout_result(&node_id.dom) else {
2
            return StringVec::from_const_slice(&[]);
        };
        let Some(node_id_internal) = node_id.node.into_crate_internal() else {
            return StringVec::from_const_slice(&[]);
        };
        let node_data_cont = layout_window.styled_dom.node_data.as_container();
        let Some(node_data) = node_data_cont.get(node_id_internal) else {
            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)
2
    }
    /// Get the ID attribute of a node (if it has one)
    #[must_use] 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() {
            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`)
    #[must_use] pub const 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`)
    #[must_use] pub fn has_selection(&self, _dom_id: &DomId) -> bool {
        self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
    }
    /// Get the primary cursor for a DOM (via `multi_cursor`)
    #[must_use] pub fn get_primary_cursor(&self, _dom_id: &DomId) -> Option<TextCursor> {
        self.get_layout_window()
            .text_edit_manager.multi_cursor.as_ref()
            .and_then(azul_core::selection::MultiCursorState::get_primary_cursor)
    }
    /// Get all selection ranges (excludes plain cursors, via `multi_cursor`)
    #[must_use] 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),
                Selection::Cursor(_) => 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
    #[must_use] pub const fn get_text_cache(&self) -> &TextLayoutCache {
        &self.get_layout_window().text_cache
    }
    // Window State Access
    /// Get full current window state (immutable reference)
626
    #[must_use] pub const fn get_current_window_state(&self) -> &FullWindowState {
        // SAFETY: current_window_state is a valid pointer for the lifetime of CallbackInfo
626
        unsafe { (*self.ref_data).current_window_state }
626
    }
    /// Get current window flags
1
    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
1
        self.get_current_window_state().flags
1
    }
    /// Get current keyboard state
65
    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
65
        self.get_current_window_state().keyboard_state.clone()
65
    }
    /// Get current mouse state
11
    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
11
        self.get_current_window_state().mouse_state
11
    }
    /// Get full previous window state (immutable reference)
6
    #[must_use] pub const fn get_previous_window_state(&self) -> &Option<FullWindowState> {
6
        unsafe { (*self.ref_data).previous_window_state }
6
    }
    /// Get previous window flags
1
    #[must_use] pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
1
        Some(self.get_previous_window_state().as_ref()?.flags)
1
    }
    /// Get previous keyboard state
1
    #[must_use] pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
        Some(
1
            self.get_previous_window_state()
1
                .as_ref()?
                .keyboard_state
                .clone(),
        )
1
    }
    /// Get previous mouse state
3
    #[must_use] pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
        Some(
3
            self.get_previous_window_state()
3
                .as_ref()?
                .mouse_state,
        )
3
    }
    // Cursor and Input
216
    #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
        use azul_core::geom::{CursorNodePosition, OptionCursorNodePosition};
216
        match self.cursor_relative_to_item {
206
            OptionLogicalPosition::Some(p) => OptionCursorNodePosition::Some(CursorNodePosition::from_logical(p)),
10
            OptionLogicalPosition::None => OptionCursorNodePosition::None,
        }
216
    }
5
    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
5
        self.cursor_in_viewport
5
    }
    /// 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) |
    #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/counter/fixed-point cast
    #[must_use] 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 Some(cursor_local) = ws.mouse_state.cursor_position.get_position() else {
            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 / relative-to-parent: absolute screen position unknown here
            // (relative needs the parent's screen pos), fall back to window-local.
            WindowPosition::Uninitialized | WindowPosition::RelativeToParentWindow(_) => {
                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.
1
    #[must_use] pub fn get_drag_delta(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
1
        let gm = self.get_gesture_drag_manager();
1
        match gm.get_drag_delta() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
1
            None => OptionDragDelta::None,
        }
1
    }
    /// 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.
1
    #[must_use] pub fn get_drag_delta_screen(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
1
        let gm = self.get_gesture_drag_manager();
1
        match gm.get_drag_delta_screen() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
1
            None => OptionDragDelta::None,
        }
1
    }
    /// 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.
7
    #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> azul_core::geom::OptionDragDelta {
        use azul_core::geom::{DragDelta, OptionDragDelta};
7
        let gm = self.get_gesture_drag_manager();
7
        match gm.get_drag_delta_screen_incremental() {
            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
7
            None => OptionDragDelta::None,
        }
7
    }
1
    #[must_use] pub const fn get_current_window_handle(&self) -> RawWindowHandle {
1
        unsafe { *(*self.ref_data).current_window_handle }
1
    }
    /// Get the system style (for menu rendering, CSD, etc.)
    /// This is useful for creating custom menus or other system-styled UI.
    #[must_use] 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.
1
    #[must_use] pub fn get_monitors(&self) -> MonitorVec {
1
        let monitors_arc = unsafe { &(*self.ref_data).monitors };
1
        monitors_arc.lock().map_or_else(|_| MonitorVec::from_const_slice(&[]), |g| g.clone())
1
    }
    /// 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.
1
    #[must_use] pub fn get_current_monitor(&self) -> OptionMonitor {
1
        let ws = self.get_current_window_state();
1
        let monitor_index = match ws.monitor_id {
            azul_css::corety::OptionU32::Some(idx) => idx as usize,
1
            azul_css::corety::OptionU32::None => return OptionMonitor::None,
        };
        let monitors_arc = unsafe { &(*self.ref_data).monitors };
        let Ok(guard) = monitors_arc.lock() else {
            return OptionMonitor::None;
        };
        for m in guard.as_ref() {
            if m.monitor_id.index == monitor_index {
                return OptionMonitor::Some(m.clone());
            }
        }
        OptionMonitor::None
1
    }
    // ==================== 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: StringVec, list_type: ListType) -> AzString {
        self.get_icu_localizer()
            .format_list(locale, items.as_ref(), 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: StringVec) -> IcuStringVec {
        self.get_icu_localizer()
            .sort_strings(locale, strings.as_ref())
    }
    /// 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
2
    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
2
        self.cursor_in_viewport.into_option()
2
    }
    /// Get the layout rectangle of the currently hit node (in logical coordinates)
    #[must_use] 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)
3
    #[must_use] pub fn get_computed_css_property(
3
        &self,
3
        node_id: DomNodeId,
3
        property_type: CssPropertyType,
3
    ) -> Option<CssProperty> {
3
        let layout_window = self.get_layout_window();
        // Get the layout result for this DOM
3
        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()
3
    }
    /// Get the computed width of a node from CSS
    ///
    /// Convenience method for getting the CSS width property.
1
    #[must_use] pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
1
        self.get_computed_css_property(node_id, CssPropertyType::Width)
1
    }
    /// Get the computed height of a node from CSS
    ///
    /// Convenience method for getting the CSS height property.
1
    #[must_use] pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
1
        self.get_computed_css_property(node_id, CssPropertyType::Height)
1
    }
    // System Callbacks
3
    #[must_use] pub const fn get_system_time_fn(&self) -> GetSystemTimeCallback {
3
        unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
3
    }
1
    #[must_use] pub fn get_current_time(&self) -> task::Instant {
1
        let cb = self.get_system_time_fn();
1
        (cb.cb)()
1
    }
    /// Get immutable reference to the renderer resources
    ///
    /// This provides access to fonts, images, and other rendering resources.
    /// Useful for custom rendering or screenshot functionality.
    #[must_use] pub const 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).
    //
    // IMAGES: the windowing shell owns the live image cache
    // (`common.image_cache`), but its two mutation points
    // (`CallbackChange::AddImageToCache` / `RemoveImageFromCache`, handled in
    // `dll shell2/common/event.rs`) MIRROR every change into
    // `LayoutWindow.image_cache`, so the cache reachable from here always
    // holds "all registered images right now" — `ImageRef` is refcounted, the
    // mirror shares decoded pixels rather than copying them. That is what
    // makes `get_image_cache_clone()` below possible without threading
    // `&ImageCache` through every `CallbackInfoRefData` construction site.
    /// Snapshot of the app's registered images (`css id -> ImageRef`) as an
    /// ABI handle ([`crate::resource_handles::ImageCacheSnapshot`]).
    ///
    /// `ImageRef`s are refcounted: decoded pixel data is SHARED with the
    /// running app, never copied or re-decoded. The handle stays valid after
    /// the callback returns, so an export job (e.g.
    /// `Pdf::from_styled_dom_with_resources`) can run off-thread.
    ///
    /// Images embedded DIRECTLY in the DOM (an `ImageRef` on an image node)
    /// do NOT need this snapshot — they travel with the DOM clone itself
    /// (`get_styled_dom_clone` / `get_dom_subtree` clone the refcounted
    /// handles). This snapshot covers the INDIRECT case: images registered
    /// by css id and resolved through the cache at layout time.
    #[must_use] pub fn get_image_cache_clone(&self) -> crate::resource_handles::ImageCacheSnapshot {
        // Core's ImageCache has no Clone (derive(Clone)+Drop double-free
        // audit); clone the refcounted-handle map explicitly.
        crate::resource_handles::ImageCacheSnapshot::from_image_cache(
            ImageCache {
                image_id_map: self
                    .get_layout_window()
                    .image_cache
                    .image_id_map
                    .clone(),
            },
        )
    }
    /// Snapshot of the window's font resolution state (shared parsed-font
    /// pool, resolved fallback chains, embedded/in-memory fonts) as an ABI
    /// handle ([`crate::resource_handles::FontCacheSnapshot`]).
    ///
    /// Consumers lay text out with EXACTLY the fonts the screen resolved —
    /// same fallback chains, no re-discovery, no re-parse from disk.
    ///
    /// Fonts embedded DIRECTLY in the DOM (`FontStack::Ref`, e.g. icon
    /// fonts) are covered twice over: the window's layout already interned
    /// them into the manager's `embedded_fonts` (shared by this snapshot),
    /// and the DOM clone carries the `FontRef` handles anyway. The snapshot
    /// is what preserves the INDIRECT state: family-name resolution,
    /// fallback chains and the parsed system faces backing them.
    #[cfg(feature = "text_layout")]
    #[must_use] pub fn get_font_cache_clone(&self) -> crate::resource_handles::FontCacheSnapshot {
        crate::resource_handles::FontCacheSnapshot::from_font_manager(
            self.get_layout_window().font_manager.clone_shared(),
        )
    }
    /// Clone of the CURRENT fully-styled root DOM — CSS cascade already
    /// applied, exactly what is on screen right now.
    ///
    /// This is the parity input for typed exporters: hand it to
    /// `Pdf::from_styled_dom_with_resources` together with
    /// [`get_font_cache_clone`](Self::get_font_cache_clone) /
    /// [`get_image_cache_clone`](Self::get_image_cache_clone) and the PDF
    /// lays out the same styled content with the same resources.
    #[must_use] pub fn get_styled_dom_clone(&self) -> StyledDom {
        // Overlay-merged (A5.3): un-committed text edits are spliced in, so an
        // export taken mid-typing contains what the user SEES, not the
        // pre-edit DOM.
        self.get_layout_window()
            .styled_dom_with_edits(DomId::ROOT_ID)
            .unwrap_or_else(|| StyledDom::create_from_dom(azul_core::dom::Dom::create_div()))
    }
    /// Reconstruct a plain [`Dom`](azul_core::dom::Dom) from a subtree of the
    /// running UI, cloning each node's `NodeData` (refcounted handles inside,
    /// so images/callbacks are shared, not copied). The cascade's retained
    /// author CSS is re-attached to the returned root, so re-styling the
    /// reconstruction reproduces the on-screen cascade.
    ///
    /// Returns `None` if the node does not exist. For a NON-root subtree the
    /// attached stylesheets are an approximation (selectors that depended on
    /// ancestors outside the subtree may match differently); when exact
    /// parity matters, use
    /// [`get_styled_dom_clone`](Self::get_styled_dom_clone) instead. This
    /// getter is for re-composing a subtree into a new document (e.g.
    /// wrapping it in a print layout).
    #[must_use] pub fn get_dom_subtree(&self, node_id: DomNodeId) -> azul_core::dom::OptionDom {
        let lw = self.get_layout_window();
        let Some(nid) = node_id.node.into_crate_internal() else {
            return azul_core::dom::OptionDom::None;
        };
        // Overlay-merged (A5.3): reconstruct from the DOM WITH un-committed
        // text edits spliced in, so a subtree export reflects what the user
        // sees. Costs a DOM clone; export paths are not per-frame.
        let Some(merged) = lw.styled_dom_with_edits(node_id.dom) else {
            return azul_core::dom::OptionDom::None;
        };
        if merged.node_data.as_container().get(nid).is_none() {
            return azul_core::dom::OptionDom::None;
        }
        azul_core::dom::OptionDom::Some(merged.reconstruct_dom_subtree(Some(nid)))
    }
    /// 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")]
1
    #[must_use] pub fn get_loaded_fonts(&self) -> LoadedFontVec {
1
        let font_manager = &self.get_layout_window().font_manager;
1
        let Ok(guard) = font_manager.parsed_fonts.lock() else {
            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.
1
        let mut out: Vec<LoadedFont> = guard
1
            .values()
1
            .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: u32::from(parsed.num_glyphs),
                    has_bytes: parsed.source_bytes_for_subset().is_some(),
                }
            })
1
            .collect();
1
        out.sort_by(|a, b| a.font_hash.cmp(&b.font_hash));
1
        out.into()
1
    }
    /// 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")]
4
    #[must_use] pub fn get_loaded_font_bytes(&self, font_hash: u64) -> OptionU8Vec {
4
        let font_manager = &self.get_layout_window().font_manager;
        // Resolve through the ONE lookup: an embedded (`StyleFontFamily::Ref`)
        // face can carry a glyph run just as a loaded one can, so a callback asking
        // for "the bytes behind this glyph run" must find either.
4
        let Some(font_ref) = font_manager.resolve_font_by_hash(font_hash) else {
4
            return OptionU8Vec::None;
        };
        let parsed = crate::font_ref_to_parsed_font(&font_ref);
        parsed.source_bytes_for_subset().map_or_else(|| OptionU8Vec::None, |bytes| OptionU8Vec::Some(U8Vec::from_vec(bytes.as_slice().to_vec())))
4
    }
    // 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")]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
90
    pub fn take_screenshot(&self, dom_id: DomId) -> Result<Vec<u8>, AzString> {
        use crate::cpurender::CpuRenderState;
90
        let layout_window = self.get_layout_window();
90
        let renderer_resources = &layout_window.renderer_resources;
        // Get the layout result for this DOM
90
        let layout_result = layout_window
90
            .layout_results
90
            .get(&dom_id)
90
            .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
        // Use the current window state dimensions
87
        let ws = self.get_current_window_state();
87
        let width = ws.size.dimensions.width;
87
        let height = ws.size.dimensions.height;
87
        if width <= 0.0 || height <= 0.0 {
            return Err(AzString::from("Invalid viewport dimensions"));
87
        }
87
        let display_list = &layout_result.display_list;
87
        let dpi_factor = ws.size.get_hidpi_factor().inner.get();
        // Build scroll offset map from the current ScrollManager state
87
        let scroll_offsets = layout_window.scroll_manager
87
            .build_scroll_offset_map(dom_id, &layout_result.scroll_id_to_node_id);
        // 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.
87
        let gpu_cache = layout_window.gpu_state_manager
87
            .get_cache(dom_id);
        // Virtual-view child DOMs (map tiles, embedded views) render through
        // their OWN display lists composited at the placeholder's position —
        // without them a screenshot showed grey placeholders where the live
        // window showed tiles (the long-standing e2e-screenshot gap).
87
        let vview_dls: BTreeMap<DomId, Arc<crate::solver3::display_list::DisplayList>> =
87
            layout_window
87
                .layout_results
87
                .iter()
87
                .filter(|(id, _)| id.inner != dom_id.inner)
87
                .map(|(id, r)| (*id, r.display_list.clone()))
87
                .collect();
87
        let render_state = CpuRenderState::from_gpu_cache(
87
            gpu_cache,
87
            dom_id,
87
            &scroll_offsets,
        )
87
        .with_system_style(layout_window.system_style.clone())
87
        .with_virtual_view_display_lists(vview_dls);
        #[cfg(feature = "std")]
87
        if std::env::var_os("AZ_ANIM_DEBUG").is_some() {
            let refs = display_list.items.iter().filter(|i| matches!(i,
                crate::solver3::display_list::DisplayListItem::PushReferenceFrame { .. })).count();
            let vals: Vec<String> = render_state.transforms.iter()
                .map(|(k, t)| alloc::format!("{k}=>tx{}", t.m[3][0])).collect();
            eprintln!("[shot] dl_items={} refframes={refs} transforms={vals:?}",
                display_list.items.len());
87
        }
        // COMPOSITED render, not the flat item walk. The flat rasteriser's
        // per-item pass maintains a transform stack that nothing consumes —
        // transforms (drag, CSS `transform`, scrollbar thumbs, diff-driven
        // animation) are realised exclusively by the compositor's layer
        // promotion. Rendering a screenshot through the flat walk therefore
        // produced pixels where every transformed node sat at its LAYOUT
        // position: a mid-animation screenshot was byte-identical to the
        // settled one, which is exactly how the transition e2e captured six
        // identical "mid-flight" frames while the engine state was provably
        // animating. The present path (headless CpuBackend and the shells)
        // composites; a screenshot must go through the same door.
87
        let pixel_w = (width * dpi_factor).ceil().max(1.0) as u32;
87
        let pixel_h = (height * dpi_factor).ceil().max(1.0) as u32;
87
        let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
87
        let mut compositor = crate::cpurender::CompositorState::new(pixel_w, pixel_h);
87
        compositor.allocate_layers_from_display_list(
87
            display_list,
87
            dpi_factor,
87
            &render_state.transforms,
87
            &render_state.opacities,
        );
87
        compositor
87
            .render_layers(
87
                display_list,
87
                dpi_factor,
87
                renderer_resources,
87
                &layout_window.font_manager,
87
                &mut glyph_cache,
87
                &render_state,
            )
87
            .map_err(AzString::from)?;
87
        let mut pixmap = crate::cpurender::AzulPixmap::new(pixel_w, pixel_h)
87
            .ok_or_else(|| AzString::from("pixmap alloc failed"))?;
87
        pixmap.fill(255, 255, 255, 255);
87
        compositor.composite_frame(&mut pixmap, dpi_factor);
        // B ∪ retained-A-zombies: exits are part of the frame a screenshot
        // must show, same as the present path.
87
        layout_window.composite_zombies_cpu(
87
            &mut pixmap,
87
            dpi_factor,
87
            renderer_resources,
87
            &mut glyph_cache,
        );
        // Encode to PNG
87
        let png_data = pixmap
87
            .encode_png()
87
            .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {e}")))?;
87
        Ok(png_data)
90
    }
    /// Renders ONE NODE to a PNG, using the same fonts, images and layout
    /// the live window is drawing from.
    ///
    /// The window is rendered exactly as [`take_screenshot`](Self::take_screenshot)
    /// does — same compositor, same caches, so the result is what the user
    /// sees — and the node's box is then cut out of it. That keeps clipping,
    /// overlap and effects from ancestors correct: a node re-rendered in
    /// isolation would show none of them.
    ///
    /// The problem-report dialog uses this to show the user the exact image
    /// it is about to attach.
    ///
    /// # Errors
    ///
    /// Returns a description when the DOM has no layout, the node has no
    /// bounds, or encoding fails.
    // `widgets` + `text_layout` are in the gate because the crop goes through
    // `crate::dialogs::report::crop_png`, and the whole `dialogs` module is
    // `#[cfg(all(std, widgets, text_layout))]` (lib.rs). Without them this body
    // referenced a module that was configured out, so the combination
    // `std,font_loading,text_layout,cpurender` — which is exactly what
    // `wr_azul_glyph_rasterizer` asks for — did not compile at all. Nothing
    // noticed because no CI job ever built azul-layout on that feature set on
    // its own; every job that touches webrender also pulls azul-dll, whose
    // default features unify `widgets` back in.
    #[cfg(all(
        feature = "std",
        feature = "cpurender",
        feature = "widgets",
        feature = "text_layout"
    ))]
    pub fn take_screenshot_of_node(&self, node_id: DomNodeId) -> Result<Vec<u8>, AzString> {
        let full = self.take_screenshot(node_id.dom)?;
        let Some(position) = self.get_node_position(node_id) else {
            return Err(AzString::from("node has no position in the current layout"));
        };
        let Some(size) = self.get_node_size(node_id) else {
            return Err(AzString::from("node has no size in the current layout"));
        };
        // The screenshot is in PHYSICAL pixels; node bounds are logical.
        let dpi = self.get_current_window_state().size.get_hidpi_factor().inner.get();
        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
        let (x, y) = (
            (position.x * dpi).max(0.0) as u32,
            (position.y * dpi).max(0.0) as u32,
        );
        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
        let (w, h) = (
            (size.width * dpi).max(0.0) as u32,
            (size.height * dpi).max(0.0) as u32,
        );
        crate::dialogs::report::crop_png(&full, x, y, w, h).map_err(AzString::from)
    }
    /// 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"))]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
    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")]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
    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")]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
    pub fn take_native_screenshot_bytes(&self) -> Result<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}")))?;
        drop(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")]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
    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")]
    /// # Errors
    ///
    /// Returns an error message if the screenshot cannot be captured or encoded.
21
    pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
21
        let png_bytes = self.take_screenshot(dom_id)?;
20
        let base64_str = base64_encode(&png_bytes);
20
        Ok(AzString::from(alloc::format!(
20
            "data:image/png;base64,{base64_str}"
20
        )))
21
    }
    // 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`.
374
    #[must_use] pub const fn get_scroll_manager(&self) -> &ScrollManager {
374
        unsafe { &(*self.ref_data).layout_window.scroll_manager }
374
    }
    /// 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.
24
    #[must_use] pub const fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
24
        unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
24
    }
    /// 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 });
    }
    /// Perform an accessibility action on a node, exactly as if assistive
    /// technology had requested it.
    ///
    /// Applied after the callback returns, via
    /// `CallbackChange::PerformAccessibilityAction` →
    /// `LayoutWindow::process_accessibility_action` → dispatch of the synthetic
    /// events the action maps to (e.g. `AccessibilityAction::Default` on a
    /// button becomes a `MouseUp` on that button, so its `on_click` runs).
    ///
    /// This is the door the E2E `accessibility_action` op uses. Before it,
    /// nothing outside a real screen reader could reach that code path, so
    /// "activation invokes no callback" was unobservable from any test.
3
    pub fn perform_accessibility_action(
3
        &mut self,
3
        dom_id: DomId,
3
        node_id: NodeId,
3
        action: AccessibilityAction,
3
    ) {
3
        self.push_change(CallbackChange::PerformAccessibilityAction {
3
            dom_id,
3
            node_id,
3
            action,
3
        });
3
    }
    /// 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.
1
    #[must_use] pub const fn get_focus_manager(&self) -> &FocusManager {
1
        &self.get_layout_window().focus_manager
1
    }
    /// 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()`.
12
    #[must_use] pub const fn get_undo_redo_manager(&self) -> &UndoRedoManager {
12
        &self.get_layout_window().undo_redo_manager
12
    }
    /// Get immutable reference to the hover manager
    ///
    /// Use this to query which nodes are currently hovered at various input points
    /// (mouse, touch points, pen).
1
    #[must_use] pub const fn get_hover_manager(&self) -> &HoverManager {
1
        &self.get_layout_window().hover_manager
1
    }
    /// Get immutable reference to the text input manager
    ///
    /// Use this to query text selection state, cursor positions, and IME composition.
    #[must_use] pub const 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()`.
1
    #[must_use] pub fn has_any_selection(&self) -> bool {
1
        self.get_layout_window()
1
            .text_edit_manager.multi_cursor.as_ref()
1
            .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
1
    }
    /// Check if a specific node is currently focused
1
    #[must_use] pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
1
        self.get_focus_manager().has_focus(&node_id)
1
    }
    /// Check if any node in a specific DOM is focused
2
    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
2
        self.get_focused_node()
2
            .is_some_and(|n| n.dom == dom_id)
2
    }
    // Pen/Stylus Query Methods
    /// Get current pen/stylus state if a pen is active
8
    #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
8
        self.get_gesture_drag_manager().get_pen_state()
8
    }
    /// 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).
    #[must_use] pub const 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.
    #[must_use] pub const 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.
    #[must_use] pub const 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`).
    #[must_use] pub const 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.
    #[must_use] 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.
    #[must_use] 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.
    #[must_use] pub const 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).
    #[must_use] pub const 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`).
    #[must_use] 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.
    #[must_use] 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
1
    #[must_use] pub fn get_pen_pressure(&self) -> Option<f32> {
1
        self.get_pen_state().map(|pen| pen.pressure)
1
    }
    /// Get current pen tilt angles (`x_tilt`, `y_tilt`) in degrees
    /// Returns None if no pen is active
1
    #[must_use] pub fn get_pen_tilt(&self) -> Option<PenTilt> {
1
        self.get_pen_state().map(|pen| pen.tilt)
1
    }
    /// Check if pen is currently in contact with surface
2
    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
2
        self.get_pen_state()
2
            .is_some_and(|pen| pen.in_contact)
2
    }
    /// Check if pen is in eraser mode
2
    #[must_use] pub fn is_pen_eraser(&self) -> bool {
2
        self.get_pen_state()
2
            .is_some_and(|pen| pen.is_eraser)
2
    }
    /// Check if pen barrel button is pressed
2
    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
2
        self.get_pen_state()
2
            .is_some_and(|pen| pen.barrel_button_pressed)
2
    }
    /// Get the last recorded input sample (for `event_id` and detailed input data)
    #[must_use] 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
    #[must_use] 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.
    #[must_use] 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.
5
    #[must_use] pub fn get_pinch(&self) -> crate::managers::gesture::OptionDetectedPinch {
5
        self.get_gesture_drag_manager().detect_pinch().into()
5
    }
    /// Returns the active rotation gesture (radians + center), if any.
    #[must_use] 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.
    #[must_use] 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.
1
    #[must_use] pub fn was_double_clicked(&self) -> bool {
1
        self.get_gesture_drag_manager().detect_double_click()
1
    }
    // Focus Management Methods
    /// Set focus to a specific DOM node by ID
1
    pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
1
        self.set_focus(FocusTarget::Id(DomNodeId {
1
            dom: dom_id,
1
            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1
        }));
1
    }
    /// 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
1
    pub fn focus_next(&mut self) {
1
        self.set_focus(FocusTarget::Next);
1
    }
    /// Move focus to previous focusable element in tab order
1
    pub fn focus_previous(&mut self) {
1
        self.set_focus(FocusTarget::Previous);
1
    }
    /// Move focus to first focusable element
1
    pub fn focus_first(&mut self) {
1
        self.set_focus(FocusTarget::First);
1
    }
    /// Move focus to last focusable element
1
    pub fn focus_last(&mut self) {
1
        self.set_focus(FocusTarget::Last);
1
    }
    /// Remove focus from all elements
1
    pub fn clear_focus(&mut self) {
1
        self.set_focus(FocusTarget::NoFocus);
1
    }
    // Manager Access Methods
    /// Check if a drag gesture is currently active
    ///
    /// Convenience method that queries the gesture manager.
1
    #[must_use] pub const fn is_dragging(&self) -> bool {
1
        self.get_gesture_drag_manager().is_dragging()
1
    }
    /// Get the currently focused node (if any)
    ///
    /// Returns None if no node has focus.
3
    #[must_use] pub const fn get_focused_node(&self) -> Option<DomNodeId> {
3
        self.get_layout_window()
3
            .focus_manager
3
            .get_focused_node()
3
            .copied()
3
    }
    /// Check if a specific node has focus
1
    #[must_use] pub fn has_focus(&self, node_id: DomNodeId) -> bool {
1
        self.get_layout_window().focus_manager.has_focus(&node_id)
1
    }
    /// Get the currently hovered file (if drag-drop is in progress)
    ///
    /// Returns None if no file is being hovered over the window.
    /// (First file only - use [`get_hovered_files`](Self::get_hovered_files)
    /// for multi-file drags; no longer `const` since MWA-B7 made the manager
    /// store a Vec.)
    #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
        self.get_layout_window()
            .file_drop_manager
            .get_hovered_file()
    }
    /// ALL files of the current drag hover (MWA-B7 - multi-file drags were
    /// previously truncated to the first path before they reached callbacks).
    #[must_use] pub fn get_hovered_files(&self) -> StringVec {
        self.get_layout_window()
            .file_drop_manager
            .get_hovered_files()
            .to_vec()
            .into()
    }
    /// 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. (First file only -
    /// use [`get_dropped_files`](Self::get_dropped_files) for the full list.)
    #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
        self.get_layout_window()
            .file_drop_manager
            .get_dropped_file()
    }
    /// ALL files of this frame's drop (MWA-B7; one-shot).
    #[must_use] pub fn get_dropped_files(&self) -> StringVec {
        self.get_layout_window()
            .file_drop_manager
            .get_dropped_files()
            .to_vec()
            .into()
    }
    /// Measure a DOM headlessly: style + lay it out against `available`
    /// constraints (this window's fonts / system style) without touching the
    /// live layout. Returns the union of all node bounds - use a very tall
    /// `available.height` (e.g. `1_000_000.0`) to get a DOM's natural height
    /// at a given width. Primary use: sizing `VirtualView` items to compute
    /// the virtual scroll extent. A full cold layout pass per call - cache
    /// per item template.
    #[cfg(feature = "std")]
    #[must_use] pub fn measure_dom(
        &self,
        dom: azul_core::dom::Dom,
        available: LogicalSize,
    ) -> LogicalSize {
        self.get_layout_window().measure_dom(dom, available)
    }
    /// Deepest node currently under the mouse pointer (MWA-B8). Anchor for
    /// drag auto-scroll when there is no focused node - node drags and OS
    /// file hovers scroll the container under the pointer, not the focused
    /// text field.
    #[must_use] pub fn get_deepest_hovered_node(&self) -> Option<DomNodeId> {
        let hit = self
            .get_layout_window()
            .hover_manager
            .get_current(&InputPointId::Mouse)?;
        hit.hovered_nodes.iter().next().and_then(|(dom_id, entry)| {
            entry.regular_hit_test_nodes.keys().next_back().map(|nid| DomNodeId {
                dom: *dom_id,
                node: NodeHierarchyItemId::from_crate_internal(Some(*nid)),
            })
        })
    }
    /// Check if a node or file drag is currently active
    ///
    /// Returns true if either a node drag or file drag is in progress.
    /// `gesture_drag_manager` is the single source of truth (the old
    /// `drag_drop_manager` mirror has been deleted — see `managers/drag_drop.rs`).
1
    #[must_use] pub const fn is_drag_active(&self) -> bool {
1
        self.get_layout_window().gesture_drag_manager.is_dragging()
1
    }
    /// Check if a node drag is specifically active
1
    #[must_use] pub fn is_node_drag_active(&self) -> bool {
1
        self.get_layout_window().gesture_drag_manager.is_node_drag_active()
1
    }
    /// Check if a file drag is specifically active
2
    #[must_use] pub fn is_file_drag_active(&self) -> bool {
2
        let lw = self.get_layout_window();
        // MWA-C-file_drop: an EXTERNAL OS drag (Finder/Explorer hovering
        // files over the window) lives in file_drop_manager, not in the
        // intra-app drag managers — without this arm the query answered
        // false during exactly the drag it is most often asked about.
2
        lw.gesture_drag_manager.is_file_dropping()
2
            || !lw.file_drop_manager.get_hovered_files().is_empty()
2
    }
    /// Get the current drag/drop state (if any)
    ///
    /// Returns None if no drag is active, or Some with drag state.
    #[must_use] pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
        let ctx = self.get_layout_window().gesture_drag_manager.get_drag_context()?;
        crate::managers::drag_drop::DragState::from_context(ctx)
    }
    /// 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.
    #[must_use] pub const fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
        // The gesture manager holds the LIVE context and is the ONLY source of
        // truth. (The `drag_drop_manager` mirror was a frozen clone taken at
        // drag start whose drop-target/position went stale for the whole drag;
        // it has been deleted.)
        self.get_layout_window().gesture_drag_manager.get_drag_context()
    }
    // Hover Manager Access
    /// Get the current mouse cursor hit test result (most recent frame)
    #[must_use] 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.)
    #[must_use] 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
    #[must_use] 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)
1
    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
1
        self.get_hover_manager()
1
            .has_sufficient_history_for_gestures(&InputPointId::Mouse)
1
    }
    // File Drop Manager Access
    /// Get immutable reference to the file drop manager
    #[must_use] pub const fn get_file_drop_manager(&self) -> &FileDropManager {
        &self.get_layout_window().file_drop_manager
    }
    // Drag-Drop Manager Access
    /// Get the node being dragged (if any)
    #[must_use] pub fn get_dragged_node(&self) -> Option<DomNodeId> {
        self.get_drag_context()
            .and_then(|ctx| {
                ctx.as_node_drag().map(|node_drag| {
                    DomNodeId {
                        dom: node_drag.dom_id,
                        node: NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
                    }
                })
            })
    }
    /// Get the file path being dragged (if any)
    #[must_use] pub fn get_dragged_file(&self) -> Option<&AzString> {
        // Gesture context first (intra-app file drags), then the
        // FileDropManager's hovered/dropped state (external OS drags).
        self.get_drag_context()
            .and_then(|ctx| {
                ctx.as_file_drop().and_then(|file_drop| {
                    file_drop.files.as_ref().first()
                })
            })
            .or_else(|| {
                let lw = self.get_layout_window();
                lw.file_drop_manager
                    .get_hovered_files()
                    .first()
                    .or_else(|| lw.file_drop_manager.get_dropped_files().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.
1
    #[must_use] pub fn get_drag_types(&self) -> StringVec {
1
        let lw = self.get_layout_window();
        // Try gesture manager first
1
        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();
            }
1
        }
1
        StringVec::from_const_slice(&[])
1
    }
    /// 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.
2
    #[must_use] pub fn get_drag_data(&self, mime_type: &str) -> OptionU8Vec {
2
        let lw = self.get_layout_window();
2
        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(|d| U8Vec::from(d.to_vec())).into();
            }
2
        }
2
        OptionU8Vec::None
2
    }
    /// 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.
    #[must_use] 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)
9
    #[must_use] pub fn get_scroll_offset_for_node(
9
        &self,
9
        dom_id: DomId,
9
        node_id: NodeId,
9
    ) -> Option<LogicalPosition> {
9
        self.get_scroll_manager()
9
            .get_current_offset(dom_id, node_id)
9
    }
    /// Get the scroll state (container rect, content rect, current offset) for a node
    #[must_use] 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.
364
    #[must_use] pub fn get_scroll_node_info(
364
        &self,
364
        dom_id: DomId,
364
        node_id: NodeId,
364
    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
364
        self.get_scroll_manager()
364
            .get_scroll_node_info(dom_id, node_id)
364
    }
    /// Deprecated: Returns None. Scroll deltas are no longer tracked per-frame.
    /// Kept for FFI backward compatibility.
    /// The raw wheel / trackpad delta that triggered the current `Scroll`
    /// callback, or `None` outside a scroll dispatch. The value is the per-pass
    /// delta recorded by the platform scroll handler (see
    /// `ScrollManager::pending_wheel_event`); it is global to the pass, so the
    /// `dom_id` / `node_id` arguments are advisory - a `Scroll` callback only
    /// fires on the hovered node, which is what they identify. Wheel-as-zoom
    /// widgets (the map) read `.y` here instead of consuming the scroll-physics
    /// input queue (which only carries deltas for actual scroll containers).
    #[must_use] pub const fn get_scroll_delta(
        &self,
        _dom_id: DomId,
        _node_id: NodeId,
    ) -> Option<LogicalPosition> {
        self.get_scroll_manager().pending_wheel_event
    }
    /// Deprecated: Returns false. Scroll activity flags were removed.
    /// Kept for FFI backward compatibility.
    #[must_use] pub const 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.
3
    #[must_use] pub fn find_scroll_parent(
3
        &self,
3
        dom_id: DomId,
3
        node_id: NodeId,
3
    ) -> Option<NodeId> {
3
        let layout_window = self.get_layout_window();
3
        let layout_results = &layout_window.layout_results;
3
        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)
3
    }
    /// 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")]
1
    #[must_use] pub fn get_scroll_input_queue(
1
        &self,
1
    ) -> crate::managers::scroll_state::ScrollInputQueue {
1
        self.get_scroll_manager().scroll_input_queue.clone()
1
    }
    // Gpu State Manager Access
    /// Get immutable reference to the GPU state manager
    #[must_use] pub const fn get_gpu_state_manager(&self) -> &GpuStateManager {
        &self.get_layout_window().gpu_state_manager
    }
    // VirtualView Manager Access
    /// Get immutable reference to the `VirtualView` manager
    #[must_use] pub const 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.
    #[must_use] 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.
    #[must_use] 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).
    #[must_use] 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),
                Selection::Cursor(_) => None,
            }))
    }
    /// Inspect what text would be selected by Select All operation
    ///
    /// Returns the full text content and the range that would be selected.
    #[must_use] 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: u32::try_from(text.len()).unwrap_or(u32::MAX),
            },
            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)
3
    #[must_use] pub fn inspect_delete_changeset(
3
        &self,
3
        target: DomNodeId,
3
        forward: bool,
3
    ) -> Option<DeleteResult> {
3
        let layout_window = self.get_layout_window();
3
        let dom_id = &target.dom;
3
        let node_id = target.node.into_crate_internal()?;
        // Get the inline content for this node
2
        let content = layout_window.get_text_before_textinput(target.dom, node_id);
        // Get current selection state from multi_cursor
2
        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),
                Selection::Cursor(_) => None,
            }) {
                Selection::Range(range)
            } else if let Some(cursor) = mc.get_primary_cursor() {
                Selection::Cursor(cursor)
            } else {
                return None;
            }
        } else {
2
            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(),
            }
        })
3
    }
    /// Inspect a pending undo operation
    ///
    /// Returns the operation that would be undone, allowing inspection
    /// of what state will be restored.
2
    #[must_use] pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
2
        self.get_undo_redo_manager().peek_undo(node_id)
2
    }
    /// Inspect a pending redo operation
    ///
    /// Returns the operation that would be reapplied.
2
    #[must_use] pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
2
        self.get_undo_redo_manager().peek_redo(node_id)
2
    }
    /// Check if undo is available for a specific node
    ///
    /// Returns true if there is at least one undoable operation in the stack.
2
    #[must_use] pub fn can_undo(&self, node_id: NodeId) -> bool {
2
        self.get_undo_redo_manager()
2
            .get_stack(node_id)
2
            .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_undo)
2
    }
    /// Check if redo is available for a specific node
    ///
    /// Returns true if there is at least one redoable operation in the stack.
2
    #[must_use] pub fn can_redo(&self, node_id: NodeId) -> bool {
2
        self.get_undo_redo_manager()
2
            .get_stack(node_id)
2
            .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_redo)
2
    }
    /// 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.
2
    #[must_use] pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
2
        self.get_undo_redo_manager()
2
            .peek_undo(node_id)
2
            .map(|op| op.pre_state.text_content.clone())
2
    }
    /// 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.
2
    #[must_use] pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
2
        self.get_undo_redo_manager()
2
            .peek_redo(node_id)
2
            .map(|op| op.pre_state.text_content.clone())
2
    }
    // 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
    #[must_use] pub const 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.
117
    #[must_use] pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
117
        let layout_window = self.get_layout_window();
117
        let node_id = target.node.into_crate_internal()?;
        // Some("") must mean "the node exists and its text is empty" — an empty string is
        // valid text content. get_text_before_textinput returns an empty Vec for BOTH a
        // missing node and an existing-but-empty one, so verify the node actually exists
        // (committed layout or a pending edit) before returning Some; otherwise None,
        // like the sibling selection/undo queries.
117
        let exists = layout_window
117
            .content_overlay
117
            .text_for_node(target.dom, node_id)
117
            .is_some()
117
            || layout_window
117
                .layout_results
117
                .get(&target.dom)
117
                .is_some_and(|lr| node_id.index() < lr.styled_dom.node_data.as_ref().len());
117
        if !exists {
2
            return None;
115
        }
115
        let content = layout_window.get_text_before_textinput(target.dom, node_id);
115
        Some(layout_window.extract_text_from_inline_content(&content))
117
    }
    /// Get the current cursor position in a node
    ///
    /// Returns the text cursor position if the node is focused.
57
    #[must_use] pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
57
        let layout_window = self.get_layout_window();
        // Check if this node is focused
57
        if !layout_window.focus_manager.has_focus(&target) {
57
            return None;
        }
        layout_window.text_edit_manager.get_primary_cursor()
57
    }
    /// Get the current selection ranges in a node
    ///
    /// Returns all active selection ranges for the specified DOM.
48
    #[must_use] pub fn get_node_selection_ranges(&self, _target: DomNodeId) -> SelectionRangeVec {
48
        let layout_window = self.get_layout_window();
48
        let ranges: Vec<SelectionRange> = layout_window
48
            .text_edit_manager.multi_cursor.as_ref()
48
            .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
                Selection::Range(r) => Some(*r),
                Selection::Cursor(_) => None,
48
            }).collect()).unwrap_or_default();
48
        ranges.into()
48
    }
    /// 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.
1
    #[must_use] pub fn node_has_selection(&self, target: DomNodeId) -> bool {
1
        !self.get_node_selection_ranges(target).as_ref().is_empty()
1
    }
    /// Get the length of text in a node
    ///
    /// Useful for bounds checking in custom operations.
1
    #[must_use] pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
1
        self.get_node_text_content(target).map(|text| text.len())
1
    }
    // 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
2
    pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
2
        let layout_window = self.get_layout_window();
2
        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 {
            None
        } else {
            Some(new_cursor)
        }
2
    }
    /// 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.
1
    pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
1
        let layout_window = self.get_layout_window();
1
        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 {
            None
        } else {
            Some(new_cursor)
        }
1
    }
    /// 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.
1
    pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
1
        let layout_window = self.get_layout_window();
1
        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 {
            None
        } else {
            Some(new_cursor)
        }
1
    }
    /// 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.
1
    pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
1
        let layout_window = self.get_layout_window();
1
        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 {
            None
        } else {
            Some(new_cursor)
        }
1
    }
    /// Inspect where the cursor would move when pressing Home key
    ///
    /// Returns the cursor position at the start of the current line.
1
    pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
1
        let layout_window = self.get_layout_window();
1
        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)
1
    }
    /// Inspect where the cursor would move when pressing End key
    ///
    /// Returns the cursor position at the end of the current line.
1
    pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
1
        let layout_window = self.get_layout_window();
1
        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)
1
    }
    /// Inspect where the cursor would move when pressing Ctrl+Home
    ///
    /// Returns the cursor position at the start of the document.
    #[must_use] pub const 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.
    #[must_use] 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: u32::try_from(text_len).unwrap_or(u32::MAX),
            },
            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).
2
    #[must_use] pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
2
        self.inspect_delete_changeset(target, false)
2
    }
    /// 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).
1
    #[must_use] pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
1
        self.inspect_delete_changeset(target, true)
1
    }
    // 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 {
25533
    #[must_use] pub fn rust_internal() -> Self {
        use crate::thread::create_thread_libstd;
25533
        Self {
25533
            create_thread_fn: CreateThreadCallback {
25533
                cb: create_thread_libstd,
25533
            },
25533
            get_system_time_fn: GetSystemTimeCallback {
25533
                cb: task::get_system_time_libstd,
25533
            },
25533
        }
25533
    }
}
/// Request to change focus, returned from callbacks
#[derive(Copy, 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
13
    #[must_use] pub const fn is_change(&self) -> bool {
13
        !matches!(self, Self::NoChange)
13
    }
    /// Convert to the new focused node (Some(node) or None for clear)
16
    #[must_use] pub const fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
16
        match self {
6
            Self::FocusNode(node) => Some(Some(*node)),
5
            Self::ClearFocus => Some(None),
5
            Self::NoChange => None,
        }
16
    }
    /// Create from Option<Option<DomNodeId>> (legacy format)
12
    #[must_use] pub const fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
8
        match opt {
4
            Some(Some(node)) => Self::FocusNode(node),
4
            Some(None) => Self::ClearFocus,
4
            None => Self::NoChange,
        }
12
    }
}
/// 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,
}
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
/// Optional `MenuCallback`
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
#[repr(C, u8)]
pub enum OptionMenuCallback {
    None,
    Some(MenuCallback),
}
impl OptionMenuCallback {
3
    #[must_use] pub fn into_option(self) -> Option<MenuCallback> {
3
        match self {
2
            Self::None => None,
1
            Self::Some(c) => Some(c),
        }
3
    }
2
    #[must_use] pub const fn is_some(&self) -> bool {
2
        matches!(self, Self::Some(_))
2
    }
2
    #[must_use] pub const fn is_none(&self) -> bool {
2
        matches!(self, Self::None)
2
    }
}
impl From<Option<MenuCallback>> for OptionMenuCallback {
1
    fn from(o: Option<MenuCallback>) -> Self {
1
        o.map_or_else(|| Self::None, Self::Some)
1
    }
}
impl From<OptionMenuCallback> for Option<MenuCallback> {
1
    fn from(o: OptionMenuCallback) -> Self {
1
        o.into_option()
1
    }
}
// -- 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)
1
    pub fn create(cb: RenderImageCallbackType) -> Self {
1
        Self {
1
            cb,
1
            ctx: OptionRefAny::None,
1
        }
1
    }
    /// 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.
1
    #[must_use] pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
1
        debug_assert!(core_callback.cb != 0, "CoreRenderImageCallback.cb is null");
1
        Self {
1
            cb: unsafe { core::mem::transmute::<usize, RenderImageCallbackType>(core_callback.cb) },
1
            ctx: core_callback.ctx.clone(),
1
        }
1
    }
    /// Convert to `CoreRenderImageCallback` (function pointer stored as usize)
    ///
    /// This is always safe - we're just casting the function pointer to usize for storage.
1
    #[must_use] pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
1
        azul_core::callbacks::CoreRenderImageCallback {
1
            cb: self.cb as usize,
1
            ctx: self.ctx,
1
        }
1
    }
}
/// 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 {
    // `_abi_mut` is an intentional FFI/api.json ABI-stability placeholder field.
    #[allow(clippy::used_underscore_binding)]
1
    fn clone(&self) -> Self {
1
        Self {
1
            callback_node_id: self.callback_node_id,
1
            bounds: self.bounds,
1
            gl_context: self.gl_context,
1
            image_cache: self.image_cache,
1
            system_fonts: self.system_fonts,
1
            callable_ptr: self.callable_ptr,
1
            _abi_mut: self._abi_mut,
1
        }
1
    }
}
impl RenderImageCallbackInfo {
6
    #[must_use] pub const fn new<'a>(
6
        callback_node_id: DomNodeId,
6
        bounds: HidpiAdjustedBounds,
6
        gl_context: &'a OptionGlContextPtr,
6
        image_cache: &'a ImageCache,
6
        system_fonts: &'a FcFontCache,
6
    ) -> Self {
6
        Self {
6
            callback_node_id,
6
            bounds,
6
            gl_context: std::ptr::from_ref::<OptionGlContextPtr>(gl_context),
6
            image_cache: std::ptr::from_ref::<ImageCache>(image_cache),
6
            system_fonts: std::ptr::from_ref::<FcFontCache>(system_fonts),
6
            callable_ptr: core::ptr::null(),
6
            _abi_mut: core::ptr::null_mut(),
6
        }
6
    }
    /// Get the callable for FFI language bindings (Python, etc.)
4
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
4
        if self.callable_ptr.is_null() {
3
            OptionRefAny::None
        } else {
1
            unsafe { (*self.callable_ptr).clone() }
        }
4
    }
    /// Set the callable pointer (called before invoking callback)
    ///
    /// # Safety
    ///
    /// `ptr` must either be null or point to an `OptionRefAny` that stays valid
    /// for as long as this `CallbackInfo` may read it (i.e. until the pointer is
    /// replaced or the callback returns). The pointee is read by [`get_ctx`]; a
    /// dangling or misaligned `ptr` is undefined behavior.
    ///
    /// [`get_ctx`]: Self::get_ctx
2
    pub const unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
2
        self.callable_ptr = ptr;
2
    }
2
    #[must_use] pub const fn get_callback_node_id(&self) -> DomNodeId {
2
        self.callback_node_id
2
    }
5
    #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
5
        self.bounds
5
    }
1
    const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
1
        unsafe { &*self.gl_context }
1
    }
    const fn internal_get_image_cache(&self) -> &ImageCache {
        unsafe { &*self.image_cache }
    }
    const fn internal_get_system_fonts(&self) -> &FcFontCache {
        unsafe { &*self.system_fonts }
    }
1
    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
1
        self.internal_get_gl_context().clone()
1
    }
}
// ============================================================================
// Result types for FFI
// ============================================================================
/// Result type for functions returning `U8Vec` or a String error
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum ResultU8VecString {
    Ok(U8Vec),
    Err(AzString),
}
impl From<Result<Vec<u8>, AzString>> for ResultU8VecString {
3
    fn from(result: Result<Vec<u8>, AzString>) -> Self {
3
        match result {
2
            Ok(v) => Self::Ok(v.into()),
1
            Err(e) => Self::Err(e),
        }
3
    }
}
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
/// 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 {
2
    fn from(result: Result<(), AzString>) -> Self {
2
        match result {
1
            Ok(()) => Self::Ok,
1
            Err(e) => Self::Err(e),
        }
2
    }
}
/// 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 {
2
    fn from(result: Result<AzString, AzString>) -> Self {
2
        match result {
1
            Ok(s) => Self::Ok(s),
1
            Err(e) => Self::Err(e),
        }
2
    }
}
// ============================================================================
// Base64 encoding helper
// ============================================================================
const BASE64_ALPHABET: &[u8; 64] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Encode bytes to Base64 string
235
#[must_use] pub fn base64_encode(input: &[u8]) -> String {
235
    let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
428105
    for chunk in input.chunks(3) {
428105
        let b0 = chunk[0] as usize;
428105
        let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
428105
        let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
428105
        let n = (b0 << 16) | (b1 << 8) | b2;
428105
        output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
428105
        output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
428105
        if chunk.len() > 1 {
428026
            output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
428026
        } else {
79
            output.push('=');
79
        }
428105
        if chunk.len() > 2 {
427952
            output.push(BASE64_ALPHABET[n & 0x3F] as char);
427952
        } else {
153
            output.push('=');
153
        }
    }
235
    output
235
}
#[cfg(all(test, feature = "std"))]
#[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
mod autotest_generated {
    use super::*;
    // ------------------------------------------------------------------
    // Harness
    // ------------------------------------------------------------------
    /// Runs `f` with a fully-constructed `CallbackInfo` backed by an *empty*
    /// `LayoutWindow` (no layout results, no timers, no threads, no routes).
    /// Every query API therefore hits its "nothing there" path — which is
    /// exactly the path adversarial tests need to exercise.
    fn with_info<R>(hit: DomNodeId, f: impl FnOnce(&mut CallbackInfo) -> R) -> R {
        let layout_window =
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
        let renderer_resources = RendererResources::default();
        let previous_window_state: Option<FullWindowState> = None;
        let current_window_state = FullWindowState::default();
        let gl_context = OptionGlContextPtr::None;
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
            BTreeMap::new();
        let window_handle = RawWindowHandle::Unsupported;
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
        let ref_data = CallbackInfoRefData {
            layout_window: &layout_window,
            renderer_resources: &renderer_resources,
            previous_window_state: &previous_window_state,
            current_window_state: &current_window_state,
            gl_context: &gl_context,
            current_scroll_manager: &scroll_states,
            current_window_handle: &window_handle,
            system_callbacks: &system_callbacks,
            system_style: Arc::new(SystemStyle::default()),
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
            #[cfg(feature = "icu")]
            icu_localizer: IcuLocalizerHandle::default(),
            ctx: OptionRefAny::None,
        };
        let changes: Arc<Mutex<Vec<CallbackChange>>> =
            Arc::new(Mutex::new(Vec::new()));
        let mut info = CallbackInfo::new(
            &ref_data,
            &changes,
            hit,
            OptionLogicalPosition::None,
            OptionLogicalPosition::None,
        );
        f(&mut info)
    }
    /// `DomNodeId` pointing at node 0 of the root DOM.
    fn node0() -> DomNodeId {
        DomNodeId {
            dom: DomId::ROOT_ID,
            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
        }
    }
    /// `DomNodeId` whose node component is `None` (the "no concrete node" case).
    fn node_none() -> DomNodeId {
        DomNodeId {
            dom: DomId::ROOT_ID,
            node: NodeHierarchyItemId::NONE,
        }
    }
    extern "C" fn cb_do_nothing(_: RefAny, _: CallbackInfo) -> Update {
        Update::DoNothing
    }
    extern "C" fn cb_refresh_dom(_: RefAny, _: CallbackInfo) -> Update {
        Update::RefreshDom
    }
    /// Pushes a change, so we can prove `invoke` really reaches the transaction log.
    extern "C" fn cb_pushes_change(_: RefAny, mut info: CallbackInfo) -> Update {
        info.stop_propagation();
        Update::RefreshDomAllWindows
    }
    extern "C" fn img_cb(_: RefAny, _: RenderImageCallbackInfo) -> ImageRef {
        ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
    }
    fn a_css_property() -> CssProperty {
        use azul_css::props::{basic::PixelValue, layout::dimensions::LayoutWidth};
        CssProperty::const_width(LayoutWidth::Px(PixelValue::px(123.0)))
    }
    fn a_cursor() -> TextCursor {
        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
        TextCursor {
            cluster_id: GraphemeClusterId {
                source_run: 0,
                start_byte_in_run: 0,
            },
            affinity: CursorAffinity::Leading,
        }
    }
    // ------------------------------------------------------------------
    // base64_encode - round-trip, boundary, huge, unicode
    // ------------------------------------------------------------------
    /// Strict RFC-4648 decoder, written independently of the encoder so that
    /// `decode(encode(x)) == x` is a real round-trip and not a tautology.
    fn base64_decode(s: &str) -> Option<Vec<u8>> {
        fn val(c: u8) -> Option<u32> {
            match c {
                b'A'..=b'Z' => Some((c - b'A') as u32),
                b'a'..=b'z' => Some((c - b'a') as u32 + 26),
                b'0'..=b'9' => Some((c - b'0') as u32 + 52),
                b'+' => Some(62),
                b'/' => Some(63),
                _ => None,
            }
        }
        let bytes = s.as_bytes();
        if bytes.len() % 4 != 0 {
            return None;
        }
        let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
        for chunk in bytes.chunks(4) {
            let pad = chunk.iter().filter(|&&c| c == b'=').count();
            if pad > 2 {
                return None;
            }
            let mut n: u32 = 0;
            for (i, &c) in chunk.iter().enumerate() {
                let v = if c == b'=' { 0 } else { val(c)? };
                n |= v << (18 - 6 * i as u32);
            }
            out.push(((n >> 16) & 0xFF) as u8);
            if pad < 2 {
                out.push(((n >> 8) & 0xFF) as u8);
            }
            if pad < 1 {
                out.push((n & 0xFF) as u8);
            }
        }
        Some(out)
    }
    #[test]
    fn base64_encode_rfc4648_test_vectors() {
        assert_eq!(base64_encode(b""), "");
        assert_eq!(base64_encode(b"f"), "Zg==");
        assert_eq!(base64_encode(b"fo"), "Zm8=");
        assert_eq!(base64_encode(b"foo"), "Zm9v");
        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
    }
    #[test]
    fn base64_encode_extreme_bytes() {
        // All-zero and all-ones map to the first / last alphabet entries.
        assert_eq!(base64_encode(&[0x00, 0x00, 0x00]), "AAAA");
        assert_eq!(base64_encode(&[0xFF, 0xFF, 0xFF]), "////");
        // Single 0xFF byte: two significant chars + two pad chars.
        assert_eq!(base64_encode(&[0xFF]), "/w==");
        assert_eq!(base64_encode(&[0xFF, 0xFF]), "//8=");
        // Every 6-bit value 0..63 appears exactly once in the alphabet.
        let all: Vec<u8> = (0u8..=255).collect();
        let enc = base64_encode(&all);
        assert_eq!(base64_decode(&enc).as_deref(), Some(all.as_slice()));
    }
    #[test]
    fn base64_encode_output_length_is_ceil_div_3_times_4() {
        for n in 0usize..=64 {
            let input = vec![0xABu8; n];
            let enc = base64_encode(&input);
            assert_eq!(
                enc.len(),
                n.div_ceil(3) * 4,
                "unexpected encoded length for {n} input bytes"
            );
            // Padding is only ever at the very end, and never more than 2 chars.
            let pad = enc.bytes().filter(|&c| c == b'=').count();
            assert!(pad <= 2, "too much padding for n = {n}");
            assert_eq!(pad, (3 - n % 3) % 3, "wrong padding count for n = {n}");
            if pad > 0 {
                assert!(enc.ends_with(&"=".repeat(pad)));
            }
        }
    }
    #[test]
    fn base64_encode_emits_only_alphabet_characters() {
        let input: Vec<u8> = (0u8..=255).chain(0u8..=255).collect();
        let enc = base64_encode(&input);
        for c in enc.bytes() {
            assert!(
                c == b'=' || BASE64_ALPHABET.contains(&c),
                "non-base64 char {c:?} in output"
            );
        }
    }
    #[test]
    fn base64_encode_round_trips_for_every_length_remainder() {
        // 0, 1, 2 mod 3 all exercise a different padding branch.
        for n in 0usize..=130 {
            let input: Vec<u8> = (0..n).map(|i| (i * 7 + 13) as u8).collect();
            let enc = base64_encode(&input);
            let dec = base64_decode(&enc).unwrap_or_else(|| panic!("failed to decode {enc:?}"));
            assert_eq!(dec, input, "round-trip failed at length {n}");
        }
    }
    #[test]
    fn base64_encode_unicode_bytes_round_trip() {
        for s in [
            "\u{1F600}",                 // emoji (4-byte UTF-8)
            "e\u{301}",                  // combining acute accent
            "\u{0}\u{7F}\u{80}\u{FFFF}", // control + boundary code points
            "тест 日本語 🌍",
        ] {
            let enc = base64_encode(s.as_bytes());
            assert_eq!(base64_decode(&enc).as_deref(), Some(s.as_bytes()));
        }
        // Known-good positive control: base64("😀") == "8J+YgA=="
        assert_eq!(base64_encode("\u{1F600}".as_bytes()), "8J+YgA==");
    }
    #[test]
    fn base64_encode_one_megabyte_does_not_panic_or_hang() {
        let input = vec![0x5Au8; 1_000_000];
        let enc = base64_encode(&input);
        assert_eq!(enc.len(), 1_000_000usize.div_ceil(3) * 4);
        // 1_000_000 % 3 == 1 -> exactly two padding chars.
        assert!(enc.ends_with("=="));
        assert_eq!(base64_decode(&enc).map(|v| v.len()), Some(1_000_000));
    }
    // ------------------------------------------------------------------
    // PenTilt / SelectAllResult / DeleteResult (From conversions)
    // ------------------------------------------------------------------
    #[test]
    fn pen_tilt_from_tuple_preserves_extreme_floats() {
        let t = PenTilt::from((0.0, -0.0));
        assert_eq!(t.x_tilt, 0.0);
        assert!(t.y_tilt.is_sign_negative());
        let t = PenTilt::from((f32::MAX, f32::MIN));
        assert_eq!(t.x_tilt, f32::MAX);
        assert_eq!(t.y_tilt, f32::MIN);
        let t = PenTilt::from((f32::INFINITY, f32::NEG_INFINITY));
        assert!(t.x_tilt.is_infinite() && t.x_tilt.is_sign_positive());
        assert!(t.y_tilt.is_infinite() && t.y_tilt.is_sign_negative());
        // NaN is passed through unchanged (no sanitisation) - and, being NaN,
        // makes the derived PartialEq report "not equal to itself".
        let t = PenTilt::from((f32::NAN, 90.0));
        assert!(t.x_tilt.is_nan());
        assert_eq!(t.y_tilt, 90.0);
        assert_ne!(t, t);
    }
    #[test]
    fn option_pen_tilt_is_some_is_none_are_exclusive() {
        let some = OptionPenTilt::Some(PenTilt::from((1.0, 2.0)));
        let none = OptionPenTilt::None;
        assert!(some.is_some() && !some.is_none());
        assert!(none.is_none() && !none.is_some());
    }
    #[test]
    fn select_all_result_from_tuple_keeps_fields_including_empty_and_huge() {
        let range = SelectionRange {
            start: a_cursor(),
            end: a_cursor(),
        };
        let empty = SelectAllResult::from((String::new(), range));
        assert_eq!(empty.full_text.as_str(), "");
        assert_eq!(empty.selection_range, range);
        let huge = SelectAllResult::from(("x".repeat(100_000), range));
        assert_eq!(huge.full_text.as_str().len(), 100_000);
        let unicode = SelectAllResult::from(("🌍\u{0}é".to_string(), range));
        assert_eq!(unicode.full_text.as_str(), "🌍\u{0}é");
    }
    #[test]
    fn delete_result_from_tuple_keeps_fields() {
        let range = SelectionRange {
            start: a_cursor(),
            end: a_cursor(),
        };
        let d = DeleteResult::from((range, String::new()));
        assert_eq!(d.range_to_delete, range);
        assert_eq!(d.deleted_text.as_str(), "");
        let d = DeleteResult::from((range, "\u{1F600}".to_string()));
        assert_eq!(d.deleted_text.as_str(), "\u{1F600}");
    }
    // ------------------------------------------------------------------
    // Callback: constructors, core round-trip, invoke, eq/hash invariants
    // ------------------------------------------------------------------
    #[test]
    fn callback_from_ptr_and_create_and_from_agree() {
        let a = Callback::from_ptr(cb_do_nothing);
        let b = Callback::create(cb_do_nothing as CallbackType);
        let c = Callback::from(cb_do_nothing as CallbackType);
        assert_eq!(a, b);
        assert_eq!(b, c);
        // Constructed from a bare fn pointer => no FFI ctx attached.
        assert!(a.ctx.is_none());
        assert!(b.ctx.is_none());
        assert!(c.ctx.is_none());
        assert_ne!(a.cb as usize, 0);
    }
    #[test]
    fn callback_to_core_from_core_round_trips_pointer_and_ctx() {
        let original = Callback {
            cb: cb_refresh_dom,
            ctx: OptionRefAny::Some(RefAny::new(0xDEAD_BEEFu32)),
        };
        let ptr = original.cb as usize;
        let core = original.to_core();
        assert_eq!(core.cb, ptr);
        assert!(core.ctx.is_some(), "to_core must not drop the FFI ctx");
        let back = Callback::from_core(core);
        assert_eq!(back.cb as usize, ptr, "encode == decode for the fn pointer");
        assert!(
            back.ctx.is_some(),
            "from_core must preserve ctx (managed-FFI handlers rely on it)"
        );
    }
    #[test]
    fn callback_to_core_of_ctxless_callback_keeps_ctx_none() {
        let core = Callback::from_ptr(cb_do_nothing).to_core();
        assert!(core.ctx.is_none());
        assert_eq!(Callback::from_core(core).cb as usize, cb_do_nothing as usize);
    }
    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "CoreCallback.cb is null")]
    fn callback_from_core_null_pointer_trips_debug_assert() {
        // A null fn pointer would be UB to call; from_core must not silently
        // hand one back in a debug build.
        let _ = Callback::from_core(CoreCallback {
            cb: 0,
            ctx: OptionRefAny::None,
        });
    }
    #[test]
    fn callback_invoke_returns_the_functions_update() {
        let update = with_info(node_none(), |info| {
            Callback::from_ptr(cb_refresh_dom).invoke(RefAny::new(1u8), *info)
        });
        assert!(matches!(update, Update::RefreshDom));
        let update = with_info(node_none(), |info| {
            Callback::from_ptr(cb_do_nothing).invoke(RefAny::new(1u8), *info)
        });
        assert!(matches!(update, Update::DoNothing));
    }
    #[test]
    fn callback_invoke_changes_reach_the_callers_transaction_log() {
        // CallbackInfo is Copy; a change pushed through the *copy* handed to the
        // callback must still land in the original's change vector.
        let changes = with_info(node_none(), |info| {
            let update = Callback::from_ptr(cb_pushes_change).invoke(RefAny::new(0u8), *info);
            assert!(matches!(update, Update::RefreshDomAllWindows));
            info.take_changes()
        });
        assert_eq!(changes.len(), 1);
        assert!(matches!(changes[0], CallbackChange::StopPropagation));
    }
    #[test]
    fn callback_eq_and_hash_ignore_ctx_but_stay_consistent() {
        use std::{
            collections::hash_map::DefaultHasher,
            hash::{Hash, Hasher},
        };
        let plain = Callback::from_ptr(cb_do_nothing);
        let with_ctx = Callback {
            cb: cb_do_nothing,
            ctx: OptionRefAny::Some(RefAny::new(7u64)),
        };
        let other_fn = Callback::from_ptr(cb_refresh_dom);
        // Documented macro behaviour: identity is the fn pointer alone.
        assert_eq!(plain, with_ctx);
        assert_ne!(plain, other_fn);
        // Eq/Hash must agree, or these end up as duplicate keys in a HashMap.
        let hash = |c: &Callback| {
            let mut h = DefaultHasher::new();
            c.hash(&mut h);
            h.finish()
        };
        assert_eq!(hash(&plain), hash(&with_ctx));
    }
    // ------------------------------------------------------------------
    // OptionCallback / OptionMenuCallback predicates + round-trips
    // ------------------------------------------------------------------
    #[test]
    fn option_callback_predicates_are_exclusive_and_total() {
        let none = OptionCallback::None;
        let some = OptionCallback::Some(Callback::from_ptr(cb_do_nothing));
        assert!(none.is_none() && !none.is_some());
        assert!(some.is_some() && !some.is_none());
        // Exactly one of the two predicates holds, for every value.
        for v in [&none, &some] {
            assert!(v.is_some() ^ v.is_none());
        }
    }
    #[test]
    fn option_callback_round_trips_through_std_option() {
        let cb = Callback::from_ptr(cb_do_nothing);
        let round = OptionCallback::from(Some(cb.clone())).into_option();
        assert_eq!(round, Some(cb.clone()));
        let round = OptionCallback::from(None).into_option();
        assert_eq!(round, None);
        // and the other direction of the From impls
        let ffi: OptionCallback = Some(cb.clone()).into();
        let back: Option<Callback> = ffi.into();
        assert_eq!(back, Some(cb));
        let ffi: OptionCallback = None.into();
        let back: Option<Callback> = ffi.into();
        assert_eq!(back, None);
    }
    #[test]
    fn option_menu_callback_predicates_and_round_trip() {
        let mc = MenuCallback {
            callback: Callback::from_ptr(cb_do_nothing),
            refany: RefAny::new(5i32),
        };
        let none = OptionMenuCallback::None;
        assert!(none.is_none() && !none.is_some());
        assert_eq!(none.into_option(), None);
        let some = OptionMenuCallback::from(Some(mc.clone()));
        assert!(some.is_some() && !some.is_none());
        assert_eq!(some.into_option(), Some(mc));
        let back: Option<MenuCallback> = OptionMenuCallback::None.into();
        assert!(back.is_none());
    }
    // ------------------------------------------------------------------
    // RenderImageCallback + RenderImageCallbackInfo
    // ------------------------------------------------------------------
    #[test]
    fn render_image_callback_core_round_trip() {
        let cb = RenderImageCallback::create(img_cb);
        assert!(cb.ctx.is_none());
        let ptr = cb.cb as usize;
        let core = cb.to_core();
        assert_eq!(core.cb, ptr);
        let back = RenderImageCallback::from_core(&core);
        assert_eq!(back.cb as usize, ptr);
        assert!(back.ctx.is_none());
    }
    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "CoreRenderImageCallback.cb is null")]
    fn render_image_callback_from_core_null_pointer_trips_debug_assert() {
        let core = azul_core::callbacks::CoreRenderImageCallback {
            cb: 0,
            ctx: OptionRefAny::None,
        };
        let _ = RenderImageCallback::from_core(&core);
    }
    #[test]
    fn render_image_callback_info_getters_and_null_ctx() {
        let gl = OptionGlContextPtr::None;
        let image_cache = ImageCache::default();
        let fonts = FcFontCache::default();
        let bounds = HidpiAdjustedBounds {
            logical_size: LogicalSize::new(640.0, 480.0),
            hidpi_factor: azul_core::resources::DpiScaleFactor::new(2.0),
        };
        let info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
        assert_eq!(info.get_callback_node_id(), node0());
        assert_eq!(info.get_bounds().logical_size, LogicalSize::new(640.0, 480.0));
        // callable_ptr is null for native Rust callbacks: get_ctx must return
        // None rather than dereferencing the null pointer.
        assert!(info.get_ctx().is_none());
        assert!(info.get_gl_context().is_none());
        // Clone is a field-wise pointer copy - the getters must still work.
        let cloned = info.clone();
        assert_eq!(cloned.get_callback_node_id(), node0());
        assert!(cloned.get_ctx().is_none());
    }
    #[test]
    fn render_image_callback_info_accepts_degenerate_and_nan_bounds() {
        let gl = OptionGlContextPtr::None;
        let image_cache = ImageCache::default();
        let fonts = FcFontCache::default();
        for (w, h, dpi) in [
            (0.0f32, 0.0f32, 0.0f32),
            (-1.0, -1.0, 1.0),
            (f32::MAX, f32::MAX, f32::MAX),
            (f32::INFINITY, f32::NAN, 1.0),
        ] {
            let bounds = HidpiAdjustedBounds {
                logical_size: LogicalSize::new(w, h),
                hidpi_factor: azul_core::resources::DpiScaleFactor::new(dpi),
            };
            let info = RenderImageCallbackInfo::new(node_none(), bounds, &gl, &image_cache, &fonts);
            let got = info.get_bounds().logical_size;
            assert_eq!(got.width.is_nan(), w.is_nan());
            assert_eq!(got.height.is_nan(), h.is_nan());
        }
    }
    #[test]
    fn render_image_callback_info_set_callable_ptr_makes_ctx_visible() {
        let gl = OptionGlContextPtr::None;
        let image_cache = ImageCache::default();
        let fonts = FcFontCache::default();
        let bounds = HidpiAdjustedBounds {
            logical_size: LogicalSize::new(1.0, 1.0),
            hidpi_factor: azul_core::resources::DpiScaleFactor::new(1.0),
        };
        let mut info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
        let ctx = OptionRefAny::Some(RefAny::new(99u32));
        // SAFETY: `ctx` outlives `info` (both are dropped at the end of this fn).
        unsafe { info.set_callable_ptr(core::ptr::from_ref(&ctx)) };
        assert!(info.get_ctx().is_some());
        // Resetting to null must go back to the safe "no ctx" answer.
        unsafe { info.set_callable_ptr(core::ptr::null()) };
        assert!(info.get_ctx().is_none());
    }
    // ------------------------------------------------------------------
    // FocusUpdateRequest - predicate + round-trip laws
    // ------------------------------------------------------------------
    #[test]
    fn focus_update_request_is_change_matches_variant() {
        assert!(FocusUpdateRequest::FocusNode(node0()).is_change());
        assert!(FocusUpdateRequest::ClearFocus.is_change());
        assert!(!FocusUpdateRequest::NoChange.is_change());
    }
    #[test]
    fn focus_update_request_optional_round_trip_is_lossless() {
        for req in [
            FocusUpdateRequest::FocusNode(node0()),
            FocusUpdateRequest::FocusNode(node_none()),
            FocusUpdateRequest::ClearFocus,
            FocusUpdateRequest::NoChange,
        ] {
            assert_eq!(
                FocusUpdateRequest::from_optional(req.to_focused_node()),
                req,
                "from_optional . to_focused_node must be the identity"
            );
        }
        // ... and in the other direction, for the legacy Option<Option<_>> form.
        for opt in [Some(Some(node0())), Some(None), None] {
            assert_eq!(FocusUpdateRequest::from_optional(opt).to_focused_node(), opt);
        }
        // is_change agrees with "to_focused_node produced something"
        for req in [
            FocusUpdateRequest::FocusNode(node0()),
            FocusUpdateRequest::ClearFocus,
            FocusUpdateRequest::NoChange,
        ] {
            assert_eq!(req.is_change(), req.to_focused_node().is_some());
        }
    }
    // ------------------------------------------------------------------
    // FFI Result enums
    // ------------------------------------------------------------------
    #[test]
    fn result_u8vec_string_from_maps_ok_and_err() {
        let ok = ResultU8VecString::from(Ok(Vec::new()));
        assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.is_empty()));
        let ok = ResultU8VecString::from(Ok(vec![0u8; 100_000]));
        assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.len() == 100_000));
        let err = ResultU8VecString::from(Err(AzString::from("boom")));
        assert!(matches!(&err, ResultU8VecString::Err(e) if e.as_str() == "boom"));
    }
    #[test]
    fn result_void_string_from_maps_ok_and_err() {
        assert!(matches!(ResultVoidString::from(Ok(())), ResultVoidString::Ok));
        let err = ResultVoidString::from(Err(AzString::from("")));
        assert!(matches!(&err, ResultVoidString::Err(e) if e.as_str().is_empty()));
    }
    #[test]
    fn result_string_string_from_keeps_both_sides_distinct() {
        let ok = ResultStringString::from(Ok(AzString::from("x")));
        assert!(matches!(&ok, ResultStringString::Ok(s) if s.as_str() == "x"));
        // Same payload type on both sides - the discriminant is what carries meaning.
        let err = ResultStringString::from(Err(AzString::from("x")));
        assert!(matches!(&err, ResultStringString::Err(s) if s.as_str() == "x"));
    }
    // ------------------------------------------------------------------
    // ExternalSystemCallbacks
    // ------------------------------------------------------------------
    #[test]
    fn external_system_callbacks_time_fn_is_callable_and_monotonic() {
        let cbs = ExternalSystemCallbacks::rust_internal();
        let t0 = (cbs.get_system_time_fn.cb)();
        let t1 = (cbs.get_system_time_fn.cb)();
        // Both calls must succeed; we only assert they produce a value (the
        // clock resolution makes strict ordering flaky).
        let _ = (t0, t1);
    }
    // ------------------------------------------------------------------
    // CallbackInfo: transaction log (push / take / relayout predicate)
    // ------------------------------------------------------------------
    #[test]
    fn callback_info_starts_with_an_empty_change_log() {
        with_info(node_none(), |info| {
            assert!(info.take_changes().is_empty());
            assert!(!info.has_pending_relayout_change());
            assert!(!info.get_changes_ptr().is_null());
        });
    }
    #[test]
    fn callback_info_take_changes_drains_the_log() {
        with_info(node_none(), |info| {
            info.stop_propagation();
            info.prevent_default();
            let first = info.take_changes();
            assert_eq!(first.len(), 2);
            // Second take must not hand out the same changes again.
            assert!(
                info.take_changes().is_empty(),
                "take_changes must consume the log"
            );
        });
    }
    #[test]
    fn callback_info_is_copy_and_copies_share_one_change_log() {
        with_info(node_none(), |info| {
            let mut copy = *info;
            copy.stop_immediate_propagation();
            assert_eq!(
                info.get_changes_ptr(),
                copy.get_changes_ptr(),
                "a Copy of CallbackInfo must alias the same Arc<Mutex<..>>"
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(matches!(changes[0], CallbackChange::StopImmediatePropagation));
        });
    }
    #[test]
    fn has_pending_relayout_change_is_true_only_for_relayout_changes() {
        // Known-false: a propagation change needs no relayout.
        with_info(node_none(), |info| {
            info.stop_propagation();
            assert!(!info.has_pending_relayout_change());
        });
        // Known-true: window resize.
        with_info(node_none(), |info| {
            info.modify_window_state(FullWindowState::default());
            assert!(info.has_pending_relayout_change());
        });
        // Known-true: scroll.
        with_info(node_none(), |info| {
            info.scroll_to(
                DomId::ROOT_ID,
                NodeHierarchyItemId::NONE,
                LogicalPosition::new(0.0, 0.0),
            );
            assert!(info.has_pending_relayout_change());
        });
        // Known-true: queued synthetic input sequence.
        with_info(node_none(), |info| {
            info.queue_window_state_sequence(FullWindowStateVec::from_vec(vec![
                FullWindowState::default(),
            ]));
            assert!(info.has_pending_relayout_change());
        });
        // A relayout change anywhere in the log counts, not just at the head.
        with_info(node_none(), |info| {
            info.prevent_default();
            info.hide_tooltip();
            info.close_window();
            assert!(!info.has_pending_relayout_change());
            info.modify_window_state(FullWindowState::default());
            assert!(info.has_pending_relayout_change());
            // Querying must not consume the log.
            assert!(info.has_pending_relayout_change());
            assert_eq!(info.take_changes().len(), 4);
        });
    }
    #[test]
    fn callback_info_flag_mutators_queue_exactly_one_matching_change() {
        macro_rules! assert_queues {
            ($call:expr, $pat:pat) => {{
                with_info(node_none(), |info| {
                    let f: &dyn Fn(&mut CallbackInfo) = &$call;
                    f(info);
                    let changes = info.take_changes();
                    assert_eq!(changes.len(), 1, "expected exactly one queued change");
                    assert!(
                        matches!(changes[0], $pat),
                        "queued the wrong CallbackChange: {:?}",
                        changes[0]
                    );
                });
            }};
        }
        assert_queues!(
            |i: &mut CallbackInfo| i.stop_propagation(),
            CallbackChange::StopPropagation
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.stop_immediate_propagation(),
            CallbackChange::StopImmediatePropagation
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.prevent_default(),
            CallbackChange::PreventDefault
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.close_window(),
            CallbackChange::CloseWindow
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.begin_interactive_move(),
            CallbackChange::BeginInteractiveMove
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.commit_undo_snapshot(),
            CallbackChange::CommitUndoSnapshot
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.undo_app_state(),
            CallbackChange::UndoAppState
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.redo_app_state(),
            CallbackChange::RedoAppState
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.update_all_image_callbacks(),
            CallbackChange::UpdateAllImageCallbacks
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.trigger_all_virtual_view_rerender(),
            CallbackChange::UpdateAllVirtualViews
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.reload_system_fonts(),
            CallbackChange::ReloadSystemFonts
        );
        assert_queues!(
            |i: &mut CallbackInfo| i.hide_tooltip(),
            CallbackChange::HideTooltip
        );
    }
    #[test]
    fn callback_info_timer_and_thread_ids_survive_boundary_values() {
        with_info(node_none(), |info| {
            info.add_timer(TimerId { id: 0 }, Timer::default());
            info.add_timer(TimerId { id: usize::MAX }, Timer::default());
            info.remove_timer(TimerId { id: usize::MAX });
            info.remove_thread(ThreadId::unique());
            let changes = info.take_changes();
            assert_eq!(changes.len(), 4);
            assert!(
                matches!(&changes[1], CallbackChange::AddTimer { timer_id, .. } if timer_id.id == usize::MAX)
            );
            assert!(
                matches!(&changes[2], CallbackChange::RemoveTimer { timer_id } if timer_id.id == usize::MAX)
            );
            assert!(matches!(&changes[3], CallbackChange::RemoveThread { .. }));
        });
    }
    // ------------------------------------------------------------------
    // CallbackInfo: numeric edges (scroll / menu / tooltip positions)
    // ------------------------------------------------------------------
    #[test]
    fn scroll_to_records_position_verbatim_at_numeric_extremes() {
        let positions = [
            LogicalPosition::new(0.0, 0.0),
            LogicalPosition::new(-0.0, -1_000_000.0),
            LogicalPosition::new(f32::MIN, f32::MAX),
            LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
            LogicalPosition::new(f32::NAN, f32::NAN),
        ];
        with_info(node_none(), |info| {
            for p in positions {
                info.scroll_to(DomId::ROOT_ID, NodeHierarchyItemId::NONE, p);
            }
            let changes = info.take_changes();
            assert_eq!(changes.len(), positions.len());
            for (change, expected) in changes.iter().zip(positions) {
                let CallbackChange::ScrollTo {
                    position, unclamped, ..
                } = change
                else {
                    panic!("expected ScrollTo, got {change:?}");
                };
                assert!(!*unclamped, "scroll_to must request clamping");
                // No sanitisation happens here - NaN/inf reach the change log
                // unchanged, and clamping is the change-processor's job.
                assert_eq!(position.x.is_nan(), expected.x.is_nan());
                if !expected.x.is_nan() {
                    assert_eq!(position.x, expected.x);
                    assert_eq!(position.y, expected.y);
                }
            }
        });
    }
    #[test]
    fn scroll_to_unclamped_sets_the_unclamped_flag() {
        with_info(node_none(), |info| {
            info.scroll_to_unclamped(
                DomId { inner: usize::MAX },
                NodeHierarchyItemId::from_raw(usize::MAX),
                LogicalPosition::new(-99999.0, 99999.0),
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            let CallbackChange::ScrollTo {
                unclamped,
                dom_id,
                position,
                ..
            } = &changes[0]
            else {
                panic!("expected ScrollTo");
            };
            assert!(*unclamped, "scroll_to_unclamped must skip clamping");
            assert_eq!(dom_id.inner, usize::MAX, "an unknown DomId is not rejected here");
            assert_eq!(position.x, -99999.0);
        });
    }
    #[test]
    fn scroll_node_into_view_queues_the_options_verbatim() {
        use crate::managers::scroll_into_view::ScrollIntoViewOptions;
        with_info(node_none(), |info| {
            info.scroll_node_into_view(node_none(), ScrollIntoViewOptions::nearest());
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(matches!(changes[0], CallbackChange::ScrollIntoView { .. }));
        });
    }
    #[test]
    fn open_menu_at_and_show_tooltip_at_accept_extreme_positions() {
        let menu = || Menu::create(azul_core::menu::MenuItemVec::from_const_slice(&[]));
        with_info(node_none(), |info| {
            info.open_menu(menu());
            info.open_menu_at(menu(), LogicalPosition::new(0.0, 0.0));
            info.open_menu_at(menu(), LogicalPosition::new(f32::MIN, f32::MAX));
            info.open_menu_at(menu(), LogicalPosition::new(f32::NAN, f32::INFINITY));
            let changes = info.take_changes();
            assert_eq!(changes.len(), 4);
            // open_menu keeps the menu's own position (None override) ...
            assert!(matches!(
                &changes[0],
                CallbackChange::OpenMenu { position: None, .. }
            ));
            // ... open_menu_at always overrides it.
            for change in &changes[1..] {
                assert!(matches!(
                    change,
                    CallbackChange::OpenMenu {
                        position: Some(_),
                        ..
                    }
                ));
            }
        });
        with_info(node_none(), |info| {
            info.show_tooltip(AzString::from(""));
            info.show_tooltip_at(AzString::from("🌍"), LogicalPosition::new(f32::NAN, -0.0));
            info.show_tooltip_at(
                AzString::from("x".repeat(100_000)),
                LogicalPosition::new(f32::MAX, f32::MIN),
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 3);
            assert!(matches!(&changes[0], CallbackChange::ShowTooltip { text, .. } if text.as_str().is_empty()));
            assert!(matches!(&changes[1], CallbackChange::ShowTooltip { text, position } if text.as_str() == "🌍" && position.x.is_nan()));
            assert!(matches!(&changes[2], CallbackChange::ShowTooltip { text, .. } if text.as_str().len() == 100_000));
        });
    }
    // ------------------------------------------------------------------
    // CallbackInfo: CSS property helpers (documented panics)
    // ------------------------------------------------------------------
    #[test]
    fn set_css_property_wraps_a_single_property() {
        with_info(node_none(), |info| {
            info.set_css_property(node0(), a_css_property());
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            let CallbackChange::ChangeNodeCssProperties {
                dom_id,
                node_id,
                properties,
            } = &changes[0]
            else {
                panic!("expected ChangeNodeCssProperties");
            };
            assert_eq!(*dom_id, DomId::ROOT_ID);
            assert_eq!(node_id.index(), 0);
            assert_eq!(properties.len(), 1);
        });
    }
    #[test]
    fn override_css_property_uses_the_override_channel_not_the_cascade() {
        with_info(node_none(), |info| {
            info.override_css_property(node0(), a_css_property());
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(
                matches!(changes[0], CallbackChange::OverrideNodeCssProperties { .. }),
                "must not fall back to the invalidating ChangeNodeCssProperties path"
            );
        });
    }
    #[test]
    #[should_panic(expected = "DomNodeId node should not be None")]
    fn set_css_property_panics_on_a_none_node_as_documented() {
        with_info(node_none(), |info| {
            info.set_css_property(node_none(), a_css_property());
        });
    }
    #[test]
    #[should_panic(expected = "DomNodeId node should not be None")]
    fn override_css_property_panics_on_a_none_node_as_documented() {
        with_info(node_none(), |info| {
            info.override_css_property(node_none(), a_css_property());
        });
    }
    #[test]
    fn change_node_css_properties_accepts_an_empty_property_vec() {
        with_info(node_none(), |info| {
            info.change_node_css_properties(
                DomId::ROOT_ID,
                NodeId::new(usize::MAX),
                CssPropertyVec::from_const_slice(&[]),
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(
                matches!(&changes[0], CallbackChange::ChangeNodeCssProperties { properties, .. } if properties.is_empty())
            );
        });
    }
    // ------------------------------------------------------------------
    // CallbackInfo: text / DOM mutation payloads (malformed + unicode + huge)
    // ------------------------------------------------------------------
    #[test]
    fn change_node_text_passes_hostile_strings_through_unchanged() {
        let inputs = [
            String::new(),
            "   \t\n  ".to_string(),
            "\u{0}embedded nul".to_string(),
            "🌍é\u{301}\u{200B}".to_string(),
            "x".repeat(1_000_000),
        ];
        with_info(node_none(), |info| {
            for s in &inputs {
                info.change_node_text(node0(), AzString::from(s.clone()));
            }
            let changes = info.take_changes();
            assert_eq!(changes.len(), inputs.len());
            for (change, expected) in changes.iter().zip(&inputs) {
                let CallbackChange::ChangeNodeText { text, .. } = change else {
                    panic!("expected ChangeNodeText");
                };
                assert_eq!(text.as_str(), expected.as_str());
            }
        });
    }
    #[test]
    fn insert_child_node_accepts_empty_and_garbage_type_strings() {
        with_info(node_none(), |info| {
            // Neither an empty tag nor a garbage tag is validated at queue time.
            info.insert_child_node(
                DomId::ROOT_ID,
                NodeId::new(0),
                AzString::from(""),
                OptionUsize::None,
                StringVec::from_const_slice(&[]),
                OptionString::None,
            );
            info.insert_child_node(
                DomId { inner: usize::MAX },
                NodeId::new(usize::MAX),
                AzString::from("\u{0}<<not a tag>>"),
                OptionUsize::Some(usize::MAX),
                StringVec::from_const_slice(&[]),
                OptionString::None,
            );
            assert_eq!(info.take_changes().len(), 2);
        });
    }
    #[test]
    fn text_editing_mutators_queue_their_changes() {
        with_info(node_none(), |info| {
            info.insert_text(DomId::ROOT_ID, NodeId::new(0), AzString::from("🌍"));
            info.move_cursor(DomId::ROOT_ID, NodeId::new(0), a_cursor());
            info.set_selection(
                DomId::ROOT_ID,
                NodeId::new(0),
                Selection::Cursor(a_cursor()),
            );
            info.set_text_changeset(PendingTextEdit {
                node: node0(),
                inserted_text: AzString::from(""),
                old_text: AzString::from(""),
            });
            info.create_text_input(AzString::from("\u{0}"));
            info.delete_node(DomId::ROOT_ID, NodeId::new(usize::MAX));
            info.set_node_ids_and_classes(
                DomId::ROOT_ID,
                NodeId::new(0),
                azul_core::dom::IdOrClassVec::from_const_slice(&[]),
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 7);
            assert!(matches!(&changes[0], CallbackChange::InsertText { text, .. } if text.as_str() == "🌍"));
            assert!(matches!(changes[1], CallbackChange::MoveCursor { .. }));
            assert!(matches!(changes[2], CallbackChange::SetSelection { .. }));
            assert!(matches!(changes[3], CallbackChange::SetTextChangeset { .. }));
            assert!(matches!(changes[5], CallbackChange::DeleteNode { .. }));
        });
    }
    #[test]
    fn image_cache_mutators_accept_empty_ids_and_null_images() {
        with_info(node_none(), |info| {
            let img = || {
                ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
            };
            info.add_image_to_cache(AzString::from(""), img());
            info.remove_image_from_cache(AzString::from(""));
            info.change_node_image(
                DomId::ROOT_ID,
                NodeId::new(0),
                img(),
                UpdateImageType::Content,
            );
            info.update_image_callback(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
            info.trigger_virtual_view_rerender(DomId::ROOT_ID, NodeId::new(usize::MAX));
            assert_eq!(info.take_changes().len(), 5);
        });
    }
    #[test]
    fn focus_mutators_queue_set_focus_target() {
        with_info(node_none(), |info| {
            info.set_focus(FocusTarget::NoFocus);
            // usize::MAX is the ONE index NodeId's 1-based encoding cannot represent
            // (into_raw does `inner + 1`); the repo pins usize::MAX - 1 as
            // MAX_ENCODABLE_NODE for exactly this. Still an out-of-range node.
            info.set_focus_to_node(DomId::ROOT_ID, NodeId::new(usize::MAX - 1));
            info.focus_next();
            info.focus_previous();
            info.focus_first();
            info.focus_last();
            info.clear_focus();
            let changes = info.take_changes();
            assert_eq!(changes.len(), 7);
            for change in &changes {
                assert!(matches!(change, CallbackChange::SetFocusTarget { .. }));
            }
            assert!(matches!(
                &changes[2],
                CallbackChange::SetFocusTarget {
                    target: FocusTarget::Next
                }
            ));
            assert!(matches!(
                &changes[6],
                CallbackChange::SetFocusTarget {
                    target: FocusTarget::NoFocus
                }
            ));
        });
    }
    #[test]
    fn create_window_queues_window_creation() {
        with_info(node_none(), |info| {
            info.create_window(WindowCreateOptions::default());
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(matches!(changes[0], CallbackChange::CreateNewWindow { .. }));
        });
    }
    // ------------------------------------------------------------------
    // CallbackInfo: routing
    // ------------------------------------------------------------------
    /// No routing configured is not "no route": the app is on `/`, and a
    /// callback branching on the pattern gets one string to branch on. Params
    /// still read empty - there is no pattern to take them from.
    #[test]
    fn route_getters_report_the_default_route_when_none_is_active() {
        with_info(node_none(), |info| {
            assert_eq!(info.get_route_pattern().as_str(), "/");
            assert_eq!(info.get_route_param(AzString::from("id")).as_str(), "");
            // Malformed / hostile keys must not panic either.
            assert_eq!(info.get_route_param(AzString::from("")).as_str(), "");
            assert_eq!(info.get_route_param(AzString::from("\u{0}🌍")).as_str(), "");
            assert_eq!(
                info.get_route_param(AzString::from("k".repeat(100_000)))
                    .as_str(),
                ""
            );
        });
    }
    #[test]
    fn set_route_param_without_an_active_route_queues_nothing() {
        with_info(node_none(), |info| {
            info.set_route_param(AzString::from("id"), AzString::from("42"));
            assert!(
                info.take_changes().is_empty(),
                "no active route => no SwitchRoute change may be queued"
            );
        });
    }
    #[test]
    fn switch_route_queues_the_pattern_verbatim() {
        with_info(node_none(), |info| {
            info.switch_route(
                AzString::from("/user/:id"),
                azul_core::window::StringPairVec::from_vec(vec![azul_core::window::AzStringPair {
                    key: AzString::from("id"),
                    value: AzString::from("42"),
                }]),
            );
            let changes = info.take_changes();
            assert_eq!(changes.len(), 1);
            assert!(
                matches!(&changes[0], CallbackChange::SwitchRoute { pattern, params } if pattern.as_str() == "/user/:id" && params.len() == 1)
            );
        });
    }
    // ------------------------------------------------------------------
    // CallbackInfo: query APIs against an EMPTY layout window
    // ------------------------------------------------------------------
    #[test]
    fn get_node_id_by_id_attribute_returns_none_for_hostile_ids() {
        let long = "a".repeat(1_000_000);
        let nested = "[".repeat(10_000);
        let ids: [&str; 12] = [
            "",
            "   ",
            "\t\n",
            "\u{0}",
            "!@#$%^&*()",
            "0",
            "-0",
            "9223372036854775807",
            "NaN",
            "inf",
            "  valid  ",
            "valid;garbage",
        ];
        with_info(node_none(), |info| {
            for id in ids {
                assert_eq!(
                    info.get_node_id_by_id_attribute(DomId::ROOT_ID, id),
                    None,
                    "id {id:?} must not resolve in an empty layout tree"
                );
            }
            // Unicode / combining marks / emoji.
            for id in ["\u{1F600}", "e\u{301}", "🌍🌍🌍"] {
                assert_eq!(info.get_node_id_by_id_attribute(DomId::ROOT_ID, id), None);
            }
            // Extremely long + deeply "nested" input must not hang or overflow.
            assert_eq!(
                info.get_node_id_by_id_attribute(DomId::ROOT_ID, &long),
                None
            );
            assert_eq!(
                info.get_node_id_by_id_attribute(DomId::ROOT_ID, &nested),
                None
            );
            // An out-of-range DomId is a miss, not a panic.
            assert_eq!(
                info.get_node_id_by_id_attribute(DomId { inner: usize::MAX }, "x"),
                None
            );
        });
    }
    #[test]
    fn hierarchy_navigation_is_none_and_zero_on_an_empty_layout_tree() {
        with_info(node_none(), |info| {
            for dom in [DomId::ROOT_ID, DomId { inner: usize::MAX }] {
                for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
                    assert_eq!(info.get_parent_node(dom, node), None);
                    assert_eq!(info.get_next_sibling_node(dom, node), None);
                    assert_eq!(info.get_previous_sibling_node(dom, node), None);
                    assert_eq!(info.get_first_child_node(dom, node), None);
                    assert_eq!(info.get_last_child_node(dom, node), None);
                    assert_eq!(info.get_children_count(dom, node), 0);
                    assert_eq!(info.get_all_children_nodes(dom, node).len(), 0);
                }
            }
            // The DomNodeId-flavoured navigation must agree.
            assert_eq!(info.get_parent(node0()), None);
            assert_eq!(info.get_first_child(node0()), None);
            assert_eq!(info.get_last_child(node0()), None);
            assert_eq!(info.get_next_sibling(node_none()), None);
            assert_eq!(info.get_previous_sibling(node_none()), None);
        });
    }
    #[test]
    fn geometry_and_css_queries_are_none_on_an_empty_layout_tree() {
        with_info(node0(), |info| {
            assert_eq!(info.get_node_size(node0()), None);
            assert_eq!(info.get_node_position(node0()), None);
            assert_eq!(info.get_node_rect(node0()), None);
            assert_eq!(info.get_node_hit_test_bounds(node0()), None);
            assert_eq!(info.get_hit_node_rect(), None);
            assert!(info.get_computed_width(node0()).is_none());
            assert!(info.get_computed_height(node0()).is_none());
            assert!(info
                .get_computed_css_property(node_none(), CssPropertyType::Width)
                .is_none());
            assert!(info.get_layout_result(&DomId::ROOT_ID).is_none());
            assert!(info.get_gpu_cache(&DomId::ROOT_ID).is_none());
            assert_eq!(info.get_dom_ids().len(), 0);
        });
    }
    #[test]
    fn state_getters_reflect_the_construction_arguments() {
        let hit = node0();
        with_info(hit, |info| {
            assert_eq!(info.get_hit_node(), hit);
            // No cursor was supplied at construction.
            assert!(info.get_cursor_relative_to_viewport().is_none());
            assert!(info.get_cursor_relative_to_node().is_none());
            // Native Rust callback => no FFI ctx, no GL context.
            assert!(info.get_ctx().is_none());
            assert!(info.get_gl_context().is_none());
            // No previous frame yet.
            assert!(info.get_previous_window_state().is_none());
            assert!(info.get_previous_window_flags().is_none());
            assert!(info.get_previous_mouse_state().is_none());
            assert!(info.get_previous_keyboard_state().is_none());
            assert!(matches!(
                info.get_current_window_handle(),
                RawWindowHandle::Unsupported
            ));
            assert_eq!(info.get_monitors().len(), 0);
            assert!(info.get_current_monitor().is_none());
            assert_eq!(info.get_timer_ids().len(), 0);
            assert_eq!(info.get_thread_ids().len(), 0);
            assert!(info.get_timer(&TimerId { id: 0 }).is_none());
            assert!(info.get_thread(&ThreadId::unique()).is_none());
            // The system-time callback must be wired up and callable.
            let _now = info.get_current_time();
        });
    }
    #[test]
    fn selection_and_undo_queries_are_empty_for_unknown_nodes() {
        with_info(node_none(), |info| {
            assert!(!info.has_any_selection());
            assert_eq!(info.get_selection_count(&DomId::ROOT_ID), 0);
            assert!(info.get_primary_selection(&DomId::ROOT_ID).is_none());
            assert!(!info.node_has_selection(node0()));
            for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
                assert!(!info.can_undo(node));
                assert!(!info.can_redo(node));
                assert!(info.get_undo_text(node).is_none());
                assert!(info.get_redo_text(node).is_none());
                assert!(info.inspect_undo_operation(node).is_none());
                assert!(info.inspect_redo_operation(node).is_none());
            }
            assert!(info.get_node_text_content(node0()).is_none());
            assert_eq!(info.get_node_text_length(node0()), None);
            assert!(info.get_text_changeset().is_none());
            assert!(!info.is_node_focused(node0()));
            assert!(!info.has_focus(node0()));
            assert!(info.get_focused_node().is_none());
        });
    }
    #[test]
    fn cursor_inspection_is_none_without_a_text_layout() {
        with_info(node_none(), |info| {
            assert!(info.inspect_move_cursor_left(node0()).is_none());
            assert!(info.inspect_move_cursor_right(node0()).is_none());
            assert!(info.inspect_move_cursor_up(node0()).is_none());
            assert!(info.inspect_move_cursor_down(node0()).is_none());
            assert!(info.inspect_move_cursor_to_line_start(node0()).is_none());
            assert!(info.inspect_move_cursor_to_line_end(node0()).is_none());
            assert!(info.inspect_backspace(node0()).is_none());
            assert!(info.inspect_delete(node0()).is_none());
            // ... and the same for a node id that does not decode at all.
            assert!(info.inspect_move_cursor_left(node_none()).is_none());
            assert!(info.inspect_backspace(node_none()).is_none());
        });
    }
    #[test]
    fn drag_and_gesture_queries_are_inactive_by_default() {
        with_info(node_none(), |info| {
            assert!(!info.is_dragging());
            assert!(!info.is_drag_active());
            assert!(!info.is_node_drag_active());
            assert!(!info.is_file_drag_active());
            assert!(info.get_drag_delta().is_none());
            assert!(info.get_drag_delta_screen().is_none());
            assert!(info.get_drag_delta_screen_incremental().is_none());
            assert!(!info.was_double_clicked());
            assert!(info.get_pen_pressure().is_none());
            assert!(info.get_pen_tilt().is_none());
            assert!(!info.is_pen_in_contact());
            assert!(!info.is_pen_eraser());
            assert!(!info.is_pen_barrel_button_pressed());
            assert_eq!(info.get_drag_types().len(), 0);
            assert!(info.get_drag_data("text/plain").is_none());
            assert!(info.get_drag_data("").is_none());
        });
    }
    #[test]
    #[cfg(feature = "text_layout")]
    fn get_loaded_font_bytes_returns_none_for_boundary_hashes() {
        with_info(node_none(), |info| {
            // No fonts are loaded, so every hash - including the numeric
            // boundaries - must miss rather than index out of bounds.
            for hash in [0u64, 1, u64::MAX, u64::MAX / 2] {
                assert!(info.get_loaded_font_bytes(hash).is_none());
            }
            assert_eq!(info.get_loaded_fonts().len(), 0);
        });
    }
    #[test]
    #[cfg(feature = "cpurender")]
    fn take_screenshot_of_a_missing_dom_is_an_error_not_a_panic() {
        with_info(node_none(), |info| {
            let err = info
                .take_screenshot(DomId::ROOT_ID)
                .expect_err("an empty layout window has no DOM to screenshot");
            assert_eq!(err.as_str(), "DOM not found in layout results");
            let err = info
                .take_screenshot(DomId { inner: usize::MAX })
                .expect_err("an out-of-range DomId must be rejected");
            assert_eq!(err.as_str(), "DOM not found in layout results");
            assert!(info.take_screenshot_base64(DomId::ROOT_ID).is_err());
        });
    }
    // ------------------------------------------------------------------
    // CallbackChange payload smoke test
    // ------------------------------------------------------------------
    #[test]
    fn callback_change_is_debug_and_clone() {
        let change = CallbackChange::ScrollTo {
            dom_id: DomId::ROOT_ID,
            node_id: NodeHierarchyItemId::NONE,
            position: LogicalPosition::new(f32::NAN, 0.0),
            unclamped: true,
        };
        let cloned = change.clone();
        assert!(matches!(
            cloned,
            CallbackChange::ScrollTo { unclamped: true, .. }
        ));
        assert!(!format!("{change:?}").is_empty());
    }
}