Skip to main content

azul_layout/
callbacks.rs

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
8use alloc::{
9    boxed::Box,
10    collections::{btree_map::BTreeMap, VecDeque},
11    sync::Arc,
12    vec::Vec,
13};
14
15#[cfg(feature = "std")]
16use std::sync::Mutex;
17
18use azul_core::{
19    resources::UpdateImageType,
20    callbacks::{CoreCallback, FocusTarget, FocusTargetPath, HidpiAdjustedBounds, Update},
21    dom::{DomId, DomIdVec, DomNodeId, IdOrClass, NodeId, NodeType},
22    geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, OptionCursorNodePosition, OptionScreenPosition, OptionDragDelta, CursorNodePosition, ScreenPosition, DragDelta},
23    gl::OptionGlContextPtr,
24    gpu::GpuValueCache,
25    hit_test::ScrollPosition,
26    id::NodeId as CoreNodeId,
27    impl_callback,
28    menu::Menu,
29    refany::{OptionRefAny, RefAny},
30    resources::{ImageCache, ImageMask, ImageRef, LoadedFont, LoadedFontVec, RendererResources},
31    selection::{Selection, SelectionRange, SelectionRangeVec, SelectionState, TextCursor},
32    styled_dom::{NodeHierarchyItemId, NodeHierarchyItemIdVec, StyledDom},
33    task::{self, GetSystemTimeCallback, Instant, ThreadId, ThreadIdVec, TimerId, TimerIdVec},
34    window::{KeyboardState, Monitor, MonitorVec, MouseState, OptionMonitor, RawWindowHandle, WindowFlags, WindowSize},
35    FastBTreeSet, OrderedMap,
36};
37use 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};
47use rust_fontconfig::FcFontCache;
48
49#[cfg(feature = "icu")]
50use crate::icu::{
51    FormatLength, IcuDate, IcuDateTime, IcuLocalizerHandle, IcuResult,
52    IcuStringVec, IcuTime, ListType, PluralCategory,
53};
54
55use 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
76use 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)]
85pub 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
92impl From<(f32, f32)> for PenTilt {
93    fn from((x, y): (f32, f32)) -> Self {
94        Self {
95            x_tilt: x,
96            y_tilt: y,
97        }
98    }
99}
100
101impl_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)]
110pub 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
117impl From<(String, SelectionRange)> for SelectAllResult {
118    fn from((text, range): (String, SelectionRange)) -> Self {
119        Self {
120            full_text: text.into(),
121            selection_range: range,
122        }
123    }
124}
125
126impl_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)]
136pub 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
143impl From<(SelectionRange, String)> for DeleteResult {
144    fn from((range, text): (SelectionRange, String)) -> Self {
145        Self {
146            range_to_delete: range,
147            deleted_text: text.into(),
148        }
149    }
150}
151
152impl_option!(
153    DeleteResult,
154    OptionDeleteResult,
155    copy = false,
156    [Debug, Clone, PartialEq, Eq]
157);
158
159/// Represents a change made by a callback that will be applied after the callback returns
160///
161/// This transaction-based system provides:
162/// - Clear separation between read-only queries and modifications
163/// - Atomic application of all changes
164/// - Easy debugging and logging of callback actions
165/// - Future extensibility for new change types
166#[derive(Debug, Clone)]
167pub enum CallbackChange {
168    // Window State Changes
169    /// Modify the window state (size, position, title, etc.)
170    ModifyWindowState { state: FullWindowState },
171    /// Inject a platform-native gesture-recognizer result into the
172    /// in-process `GestureAndDragManager`. Read by the next
173    /// `detect_long_press` / `detect_swipe_direction` / `detect_pinch` /
174    /// `detect_rotation` / `detect_double_click` call, then cleared.
175    InjectNativeGesture {
176        gesture: crate::managers::gesture::NativeGestureEvent,
177    },
178    /// Queue multiple window state changes to be applied in sequence across frames.
179    /// This is needed for simulating clicks (mouse down -> wait -> mouse up) where each
180    /// state change needs to trigger separate event processing.
181    QueueWindowStateSequence { states: Vec<FullWindowState> },
182    /// Create a new window
183    CreateNewWindow { options: WindowCreateOptions },
184    /// Close the current window (via `Update::CloseWindow` return value, tracked here for logging)
185    CloseWindow,
186
187    // Focus Management
188    /// Change keyboard focus to a specific node or clear focus
189    SetFocusTarget { target: FocusTarget },
190
191    // Event Propagation Control
192    /// Stop event from propagating to parent nodes (W3C stopPropagation).
193    /// Remaining handlers on the *current* node still fire, but no handlers
194    /// on ancestor / descendant nodes in subsequent phases.
195    StopPropagation,
196    /// Stop event propagation immediately (W3C stopImmediatePropagation).
197    /// No further handlers fire - not even remaining handlers on the same node.
198    StopImmediatePropagation,
199    /// Prevent default browser behavior (e.g., block text input from being applied)
200    PreventDefault,
201
202    // Timer Management
203    /// Add a new timer to the window
204    AddTimer { timer_id: TimerId, timer: Timer },
205    /// Remove an existing timer
206    RemoveTimer { timer_id: TimerId },
207
208    // Thread Management
209    /// Add a new background thread
210    AddThread { thread_id: ThreadId, thread: Thread },
211    /// Remove an existing thread
212    RemoveThread { thread_id: ThreadId },
213
214    // Content Modifications
215    /// Change the text content of a node
216    ChangeNodeText { node_id: DomNodeId, text: AzString },
217    /// Change the image of a node
218    ChangeNodeImage {
219        dom_id: DomId,
220        node_id: NodeId,
221        image: ImageRef,
222        update_type: UpdateImageType,
223    },
224    /// Re-render an image callback (for resize/animation)
225    /// This triggers re-invocation of the `RenderImageCallback`
226    UpdateImageCallback { dom_id: DomId, node_id: NodeId },
227    /// Re-render ALL image callbacks across all DOMs.
228    ///
229    /// This is the most efficient way to update animated GL textures:
230    /// it triggers only texture re-rendering without DOM rebuild or
231    /// display list resubmission. Used by timer callbacks that need
232    /// to update OpenGL textures every frame.
233    UpdateAllImageCallbacks,
234    /// Trigger re-rendering of a `VirtualView` with a new DOM
235    /// This forces the `VirtualView` to call its callback and update the display list
236    UpdateVirtualView { dom_id: DomId, node_id: NodeId },
237    /// Re-render EVERY `VirtualView` on the existing DOM (no node id needed).
238    /// For shared-dataset changes that arrive out-of-band (e.g. a background
239    /// tile-fetch writeback): the views re-read their cloned dataset in place.
240    UpdateAllVirtualViews,
241    /// Change the image mask of a node
242    ChangeNodeImageMask {
243        dom_id: DomId,
244        node_id: NodeId,
245        mask: ImageMask,
246    },
247    /// Change CSS properties of a node
248    ChangeNodeCssProperties {
249        dom_id: DomId,
250        node_id: NodeId,
251        properties: CssPropertyVec,
252    },
253    /// Override CSS properties on a node via the user-override channel
254    /// (`CssPropertyCache::user_overridden_properties`). Unlike
255    /// `ChangeNodeCssProperties`, this does not mutate the node's static
256    /// `css_props` - the override layer is read at higher priority by the
257    /// property resolution pipeline, so animating a handful of properties
258    /// per frame stays cheap. Passing `CssProperty::Initial` for a property
259    /// removes any prior override for that type on the same node.
260    OverrideNodeCssProperties {
261        dom_id: DomId,
262        node_id: NodeId,
263        properties: CssPropertyVec,
264    },
265
266    // Scroll Management
267    /// Scroll a node to a specific position
268    ScrollTo {
269        dom_id: DomId,
270        node_id: NodeHierarchyItemId,
271        position: LogicalPosition,
272        /// When true, skip clamping to [0, `max_scroll`] bounds.
273        /// Used by the scroll physics timer for rubber-banding/overscroll.
274        unclamped: bool,
275    },
276    /// Scroll a node into view (W3C scrollIntoView API)
277    /// The scroll adjustments are calculated and applied when the change is processed
278    ScrollIntoView {
279        node_id: DomNodeId,
280        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
281    },
282
283    // Image Cache Management
284    /// Add an image to the image cache
285    AddImageToCache { id: AzString, image: ImageRef },
286    /// Remove an image from the image cache
287    RemoveImageFromCache { id: AzString },
288
289    // Font Cache Management
290    /// Reload system fonts (expensive operation)
291    ReloadSystemFonts,
292
293    // Menu Management
294    /// Open a context menu or dropdown menu
295    /// Whether it's native or fallback depends on `window.state.flags.use_native_context_menus`
296    OpenMenu {
297        menu: Menu,
298        /// Optional position override (if None, uses menu.position)
299        position: Option<LogicalPosition>,
300    },
301
302    // Tooltip Management
303    /// Show a tooltip at a specific position
304    ///
305    /// Platform-specific implementation:
306    /// - Windows: Uses native tooltip window (`TOOLTIPS_CLASS`)
307    /// - macOS: Uses `NSPopover` or custom `NSWindow` with tooltip styling
308    /// - X11: Creates transient window with _`NET_WM_WINDOW_TYPE_TOOLTIP`
309    /// - Wayland: Creates surface with `zwlr_layer_shell_v1` (overlay layer)
310    ShowTooltip {
311        text: AzString,
312        position: LogicalPosition,
313    },
314    /// Hide the currently displayed tooltip
315    HideTooltip,
316
317    // Text Editing
318    /// Insert text at the current cursor position or replace selection
319    InsertText {
320        dom_id: DomId,
321        node_id: NodeId,
322        text: AzString,
323    },
324    /// Delete text backward (backspace) at cursor
325    DeleteBackward { dom_id: DomId, node_id: NodeId },
326    /// Delete text forward (delete key) at cursor
327    DeleteForward { dom_id: DomId, node_id: NodeId },
328    /// Move cursor to a specific position
329    MoveCursor {
330        dom_id: DomId,
331        node_id: NodeId,
332        cursor: TextCursor,
333    },
334    /// Set text selection range
335    SetSelection {
336        dom_id: DomId,
337        node_id: NodeId,
338        selection: Selection,
339    },
340    /// Set/override the text changeset for the current text input operation
341    /// This allows callbacks to modify what text will be inserted during text input events
342    SetTextChangeset { changeset: PendingTextEdit },
343
344    // Cursor Movement Operations
345    /// Move cursor left (arrow left)
346    MoveCursorLeft {
347        dom_id: DomId,
348        node_id: NodeId,
349        extend_selection: bool,
350    },
351    /// Move cursor right (arrow right)
352    MoveCursorRight {
353        dom_id: DomId,
354        node_id: NodeId,
355        extend_selection: bool,
356    },
357    /// Move cursor up (arrow up)
358    MoveCursorUp {
359        dom_id: DomId,
360        node_id: NodeId,
361        extend_selection: bool,
362    },
363    /// Move cursor down (arrow down)
364    MoveCursorDown {
365        dom_id: DomId,
366        node_id: NodeId,
367        extend_selection: bool,
368    },
369    /// Move cursor to line start (Home key)
370    MoveCursorToLineStart {
371        dom_id: DomId,
372        node_id: NodeId,
373        extend_selection: bool,
374    },
375    /// Move cursor to line end (End key)
376    MoveCursorToLineEnd {
377        dom_id: DomId,
378        node_id: NodeId,
379        extend_selection: bool,
380    },
381    /// Move cursor to document start (Ctrl+Home)
382    MoveCursorToDocumentStart {
383        dom_id: DomId,
384        node_id: NodeId,
385        extend_selection: bool,
386    },
387    /// Move cursor to document end (Ctrl+End)
388    MoveCursorToDocumentEnd {
389        dom_id: DomId,
390        node_id: NodeId,
391        extend_selection: bool,
392    },
393
394    // Multi-Cursor Operations
395    /// Add an additional cursor at the specified position (Ctrl+Click from C API)
396    AddCursor {
397        dom_id: DomId,
398        node_id: NodeId,
399        cursor: TextCursor,
400    },
401    /// Add an additional selection range (for multi-cursor)
402    AddSelectionRange {
403        dom_id: DomId,
404        node_id: NodeId,
405        range: SelectionRange,
406    },
407    /// Remove a specific selection by its stable ID
408    RemoveSelectionById {
409        selection_id: azul_core::selection::SelectionId,
410    },
411
412    // Clipboard Operations (Override)
413    /// Override clipboard content for copy operation
414    SetCopyContent {
415        target: DomNodeId,
416        content: ClipboardContent,
417    },
418    /// Override clipboard content for cut operation
419    SetCutContent {
420        target: DomNodeId,
421        content: ClipboardContent,
422    },
423    /// Override selection range for select-all operation
424    SetSelectAllRange {
425        target: DomNodeId,
426        range: SelectionRange,
427    },
428
429    // Hit Test Request (for Debug API)
430    /// Request a hit test update at a specific position
431    ///
432    /// This is used by the Debug API to update the hover manager's hit test
433    /// data after modifying the mouse position, ensuring that callbacks
434    /// can find the correct nodes under the cursor.
435    RequestHitTestUpdate { position: LogicalPosition },
436
437    // Text Selection (for Debug API)
438    /// Process a text selection click at a specific position
439    ///
440    /// This is used by the Debug API to trigger text selection directly,
441    /// bypassing the normal event pipeline. The handler will:
442    /// 1. Hit-test IFC roots to find selectable text at the position
443    /// 2. Create a text cursor at the clicked position
444    /// 3. Update the selection manager with the new selection
445    ProcessTextSelectionClick {
446        position: LogicalPosition,
447        time_ms: u64,
448    },
449
450    // Cursor Blinking (System Timer Control)
451    /// Set the cursor visibility state (called by blink timer)
452    SetCursorVisibility { visible: bool },
453    /// Toggle cursor visibility based on blink timing
454    ToggleCursorVisibility,
455    /// Reset cursor blink state on user input (makes cursor visible, records time)
456    ResetCursorBlink,
457    /// Start the cursor blink timer for the focused contenteditable element
458    StartCursorBlinkTimer,
459    /// Stop the cursor blink timer (when focus leaves contenteditable)
460    StopCursorBlinkTimer,
461    
462    // Scroll cursor/selection into view
463    /// Scroll the active text cursor into view within its scrollable container
464    /// This is automatically triggered after text input or cursor movement
465    ScrollActiveCursorIntoView,
466    
467    // Create Text Input Event (for Debug API / Programmatic Text Input)
468    /// Create a synthetic text input event
469    ///
470    /// This simulates receiving text input from the OS. The text input flow will:
471    /// 1. Record the text in `TextInputManager` (creating a `PendingTextEdit`)
472    /// 2. Generate synthetic `TextInput` events
473    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
474    /// 4. Apply the changeset if not rejected
475    /// 5. Mark dirty nodes for re-render
476    CreateTextInput {
477        /// The text to insert
478        text: AzString,
479    },
480
481    // Window Move (Compositor-Managed)
482    /// Request the compositor to begin an interactive window move.
483    /// On Wayland: calls `xdg_toplevel_move(toplevel`, seat, serial).
484    /// On other platforms: this is a no-op (use `set_window_position` instead).
485    BeginInteractiveMove,
486
487    // Drag-and-Drop Data Transfer
488    /// Set drag data for a MIME type (W3C: dataTransfer.setData)
489    /// Should be called in a `DragStart` callback to populate the drag data.
490    SetDragData {
491        mime_type: AzString,
492        data: Vec<u8>,
493    },
494    /// Accept the current drop on this target (W3C: `event.preventDefault()` in `DragOver`)
495    /// Must be called from a `DragOver` or `DragEnter` callback for the Drop event to fire.
496    AcceptDrop,
497    /// Set the drop effect (W3C: dataTransfer.dropEffect)
498    SetDropEffect {
499        effect: azul_core::drag::DropEffect,
500    },
501
502    // DOM Mutation (for Debug API)
503    /// Insert a new child node into the DOM tree.
504    /// Creates a minimal `StyledDom` from the given `node_type` and appends it
505    /// as a child of `parent_node_id`. If position is Some, inserts at that
506    /// child index; otherwise appends at the end.
507    InsertChildNode {
508        dom_id: DomId,
509        parent_node_id: NodeId,
510        /// The tag/type of the new node (e.g. "div", "p", "text:Hello")
511        node_type_str: AzString,
512        /// Optional child index to insert at (None = append at end)
513        position: Option<usize>,
514        /// Optional CSS classes for the new node
515        classes: Vec<AzString>,
516        /// Optional ID for the new node
517        id: Option<AzString>,
518    },
519    /// Delete a node from the DOM tree (and all its children).
520    /// The node is "tombstoned" (set to an empty anonymous Div) rather than
521    /// physically removed, to preserve node ID stability.
522    DeleteNode {
523        dom_id: DomId,
524        node_id: NodeId,
525    },
526    /// Set the IDs and classes on an existing node.
527    SetNodeIdsAndClasses {
528        dom_id: DomId,
529        node_id: NodeId,
530        ids_and_classes: azul_core::dom::IdOrClassVec,
531    },
532
533    // Routing
534    /// Switch to a different route.
535    ///
536    /// On desktop: swaps `FullWindowState.layout_callback` to the matched
537    /// route's callback, stores the `RouteMatch`, and triggers `RefreshDom`.
538    /// On web: additionally calls `history.pushState()`.
539    SwitchRoute {
540        /// Route pattern to switch to (e.g. `"/user/:id"`)
541        pattern: AzString,
542        /// Route parameters (e.g. `[("id", "42")]`)
543        params: azul_core::window::StringPairVec,
544    },
545
546    // App-global Undo / Redo
547    /// Commit a snapshot of the current app state into the undo history.
548    CommitUndoSnapshot,
549    /// Undo the last committed app-state change (restores previous snapshot).
550    UndoAppState,
551    /// Redo a previously undone app-state change.
552    RedoAppState,
553}
554
555/// Main callback type for UI event handling
556pub type CallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
557
558/// Stores a function pointer that is executed when the given UI element is hit
559///
560/// Must return an `Update` that denotes if the screen should be redrawn.
561#[repr(C)]
562pub struct Callback {
563    pub cb: CallbackType,
564    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
565    /// Native Rust code sets this to None
566    pub ctx: OptionRefAny,
567}
568
569impl_callback!(Callback, CallbackType);
570
571// Host-invoker plumbing for managed-FFI bindings (Lua, Ruby, Perl, ...).
572// See `azul_core::host_invoker` for the design. This expands to a static
573// `az_callback_thunk` that the framework dispatches by-value args to, an
574// `AzCallback_createFromHostHandle` C-ABI export the host calls per
575// `set_on_click(...)` site, plus the `AzApp_setCallbackInvoker` setter the
576// host calls once at module load to register its libffi closure.
577azul_core::impl_managed_callback! {
578    wrapper:        Callback,
579    info_ty:        CallbackInfo,
580    return_ty:      Update,
581    default_ret:    Update::DoNothing,
582    invoker_static: CALLBACK_INVOKER,
583    invoker_ty:     AzCallbackInvoker,
584    thunk_fn:       az_callback_thunk,
585    setter_fn:      AzApp_setCallbackInvoker,
586    from_handle_fn: AzCallback_createFromHostHandle,
587}
588
589impl Callback {
590    /// Create a callback from a raw `CallbackType` function pointer (ctx = None).
591    ///
592    /// The concrete `cb: CallbackType` parameter is a coercion site, so callers
593    /// can pass a bare `extern "C" fn` item without an `as CallbackType` cast
594    /// (unlike `Callback::from`, where trait-impl selection happens before the
595    /// fn-item -> fn-pointer coercion could apply).
596    #[must_use]
597    pub fn from_ptr(cb: CallbackType) -> Self {
598        Self::from(cb)
599    }
600
601    /// Create a new callback with just a function pointer (for native Rust code)
602    pub fn create<C: Into<Self>>(cb: C) -> Self {
603        cb.into()
604    }
605
606    /// Convert from `CoreCallback` (stored as usize) to Callback (actual function pointer)
607    ///
608    /// Preserves `ctx` so that callbacks registered via the host-invoker path
609    /// (e.g. `Callback::create_from_host_handle`) keep their host-handle ctx
610    /// across the dispatch cycle. Without this, `info.get_ctx()` inside the
611    /// generated thunk would see `OptionRefAny::None` and bail out with the
612    /// kind's default value - which makes managed-FFI click handlers
613    /// silently no-op.
614    ///
615    /// # Safety
616    /// The caller must ensure that the usize in CoreCallback.cb was originally a valid
617    /// function pointer of type `CallbackType`. This is guaranteed when `CoreCallback`
618    /// is created through standard APIs, but unsafe code could violate this.
619    #[must_use] pub fn from_core(core: CoreCallback) -> Self {
620        debug_assert!(core.cb != 0, "CoreCallback.cb is null");
621        Self {
622            cb: unsafe { core::mem::transmute::<usize, CallbackType>(core.cb) },
623            ctx: core.ctx,
624        }
625    }
626
627    /// Convert to `CoreCallback` (function pointer stored as usize)
628    ///
629    /// This is always safe - we're just casting the function pointer to usize for storage.
630    #[must_use] pub fn to_core(self) -> CoreCallback {
631        CoreCallback {
632            cb: self.cb as usize,
633            ctx: self.ctx,
634        }
635    }
636}
637
638/// Allow Callback to be passed to functions expecting `C: Into<CoreCallback>`
639impl From<Callback> for CoreCallback {
640    fn from(callback: Callback) -> Self {
641        callback.to_core()
642    }
643}
644
645impl Callback {
646    /// Safely invoke the callback with the given data and info
647    ///
648    /// This is a safe wrapper around calling the function pointer directly.
649    #[must_use] pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
650        (self.cb)(data, info)
651    }
652}
653#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
654/// FFI-safe Option<Callback> type for C interop.
655///
656/// This enum provides an ABI-stable alternative to `Option<Callback>`
657/// that can be safely passed across FFI boundaries.
658#[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
659#[repr(C, u8)]
660pub enum OptionCallback {
661    /// No callback is present.
662    None,
663    /// A callback is present.
664    Some(Callback),
665}
666
667impl OptionCallback {
668    /// Converts this FFI-safe option into a standard Rust `Option<Callback>`.
669    #[must_use] pub fn into_option(self) -> Option<Callback> {
670        match self {
671            Self::None => None,
672            Self::Some(c) => Some(c),
673        }
674    }
675
676    /// Returns `true` if a callback is present.
677    #[must_use] pub const fn is_some(&self) -> bool {
678        matches!(self, Self::Some(_))
679    }
680
681    /// Returns `true` if no callback is present.
682    #[must_use] pub const fn is_none(&self) -> bool {
683        matches!(self, Self::None)
684    }
685}
686
687impl From<Option<Callback>> for OptionCallback {
688    fn from(o: Option<Callback>) -> Self {
689        o.map_or_else(|| Self::None, Self::Some)
690    }
691}
692
693impl From<OptionCallback> for Option<Callback> {
694    fn from(o: OptionCallback) -> Self {
695        o.into_option()
696    }
697}
698
699/// Reference data container for `CallbackInfo` (all read-only fields)
700///
701/// This struct consolidates all readonly references that callbacks need to query window state.
702/// By grouping these into a single struct, we reduce the number of parameters to
703/// `CallbackInfo::new()` from 13 to 3, making the API more maintainable and easier to extend.
704///
705/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
706/// reference.
707#[derive(Debug)]
708pub struct CallbackInfoRefData<'a> {
709    /// Pointer to the `LayoutWindow` containing all layout results (READ-ONLY for queries)
710    pub layout_window: &'a LayoutWindow,
711    /// Necessary to query `FontRefs` from callbacks
712    pub renderer_resources: &'a RendererResources,
713    /// Previous window state (for detecting changes)
714    pub previous_window_state: &'a Option<FullWindowState>,
715    /// State of the current window that the callback was called on (read only!)
716    pub current_window_state: &'a FullWindowState,
717    /// An Rc to the OpenGL context, in order to be able to render to OpenGL textures
718    pub gl_context: &'a OptionGlContextPtr,
719    /// Immutable reference to where the nodes are currently scrolled (current position)
720    pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
721    /// Handle of the current window
722    pub current_window_handle: &'a RawWindowHandle,
723    /// Callbacks for creating threads and getting the system time (since this crate uses `no_std`)
724    pub system_callbacks: &'a ExternalSystemCallbacks,
725    /// Platform-specific system style (colors, spacing, etc.)
726    /// Arc allows safe cloning in callbacks without unsafe pointer manipulation
727    pub system_style: Arc<SystemStyle>,
728    /// Shared monitor list - initialized once at app start, updated by the platform
729    /// layer on monitor topology changes (e.g. `WM_DISPLAYCHANGE`, `NSScreenParametersChanged`).
730    /// Callbacks lock the mutex to read; platform locks to write.
731    pub monitors: Arc<Mutex<MonitorVec>>,
732    /// ICU4X localizer cache for internationalized formatting (numbers, dates, lists, plurals)
733    /// Caches localizers for multiple locales. Only available when the "icu" feature is enabled.
734    #[cfg(feature = "icu")]
735    pub icu_localizer: IcuLocalizerHandle,
736    /// The callable for FFI language bindings (Python, etc.)
737    /// Cloned from the Callback struct before invocation. Native Rust callbacks have this as None.
738    pub ctx: OptionRefAny,
739}
740
741/// `CallbackInfo` is a lightweight wrapper around pointers to stack-local data.
742///
743/// It can be safely copied because it only contains pointers - the underlying
744/// data lives on the stack and outlives the callback invocation.
745/// This allows callbacks to "consume" `CallbackInfo` by value while the caller
746/// retains access to the same underlying data.
747///
748/// Information about the callback that is passed to the callback whenever a callback is invoked
749///
750/// # Architecture
751///
752/// `CallbackInfo` uses a transaction-based system:
753/// - **Read-only pointers**: Access to layout data, window state, managers for queries
754/// - **Change vector**: All modifications are recorded as `CallbackChange` items
755/// - **Processing**: Changes are applied atomically after callback returns
756///
757/// This design provides clear separation between queries and modifications, makes debugging
758/// easier, and allows for future extensibility.
759///
760/// The `changes` field uses a pointer to Arc<Mutex<...>> so that cloned `CallbackInfo` instances
761/// (e.g., passed to timer callbacks) still push changes to the original collection,
762/// while keeping `CallbackInfo` as Copy.
763#[derive(Debug, Clone, Copy)]
764#[repr(C)]
765pub struct CallbackInfo {
766    // Read-only Data (Query Access)
767    /// Single reference to all readonly reference data
768    /// This consolidates 8 individual parameters into 1, improving API ergonomics
769    ref_data: *const CallbackInfoRefData<'static>,
770    // Context Info (Immutable Event Data)
771    /// The ID of the DOM + the node that was hit
772    hit_dom_node: DomNodeId,
773    /// The (x, y) position of the mouse cursor, **relative to top left of the element that was
774    /// hit**
775    cursor_relative_to_item: OptionLogicalPosition,
776    /// The (x, y) position of the mouse cursor, **relative to top left of the window**
777    cursor_in_viewport: OptionLogicalPosition,
778    // Transaction Container (New System) - Uses pointer to Arc<Mutex> for shared access across clones
779    /// All changes made by the callback, applied atomically after callback returns
780    /// Stored as raw pointer so `CallbackInfo` remains Copy
781    #[cfg(feature = "std")]
782    changes: *const Arc<Mutex<Vec<CallbackChange>>>,
783    #[cfg(not(feature = "std"))]
784    changes: *mut Vec<CallbackChange>,
785}
786
787impl CallbackInfo {
788    #[cfg(feature = "std")]
789    pub const fn new<'a>(
790        ref_data: &'a CallbackInfoRefData<'a>,
791        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
792        hit_dom_node: DomNodeId,
793        cursor_relative_to_item: OptionLogicalPosition,
794        cursor_in_viewport: OptionLogicalPosition,
795    ) -> Self {
796        Self {
797            // Read-only data (single reference to consolidated refs)
798            // SAFETY: We cast away the lifetime 'a to 'static because CallbackInfo
799            // only lives for the duration of the callback, which is shorter than 'a
800            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
801            // CallbackInfo only lives for the duration of the callback, which is shorter than 'a.
802            ref_data: std::ptr::from_ref::<CallbackInfoRefData<'a>>(ref_data).cast::<CallbackInfoRefData<'static>>(),
803
804            // Context info (immutable event data)
805            hit_dom_node,
806            cursor_relative_to_item,
807            cursor_in_viewport,
808
809            // Transaction container - store pointer to Arc<Mutex> for shared access
810            changes: std::ptr::from_ref::<Arc<Mutex<Vec<CallbackChange>>>>(changes),
811        }
812    }
813
814    #[cfg(not(feature = "std"))]
815    pub fn new<'a>(
816        ref_data: &'a CallbackInfoRefData<'a>,
817        changes: &'a mut Vec<CallbackChange>,
818        hit_dom_node: DomNodeId,
819        cursor_relative_to_item: OptionLogicalPosition,
820        cursor_in_viewport: OptionLogicalPosition,
821    ) -> Self {
822        Self {
823            // SAFETY: pointer cast only - erases lifetime 'a to 'static.
824            ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
825            hit_dom_node,
826            cursor_relative_to_item,
827            cursor_in_viewport,
828            changes: changes as *mut Vec<CallbackChange>,
829        }
830    }
831
832    /// Get the callable for FFI language bindings (Python, etc.)
833    ///
834    /// Returns the cloned `OptionRefAny` if a callable was set, or None if this
835    /// is a native Rust callback.
836    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
837        unsafe { (*self.ref_data).ctx.clone() }
838    }
839
840    /// Returns the OpenGL context if available
841    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
842        unsafe { (*self.ref_data).gl_context.clone() }
843    }
844
845    // Helper methods for transaction system
846
847    /// Push a change to be applied after the callback returns
848    /// This is the primary method for modifying window state from callbacks
849    #[cfg(feature = "std")]
850    pub fn push_change(&mut self, change: CallbackChange) {
851        // SAFETY: The pointer is valid for the lifetime of the callback
852        unsafe {
853            if let Ok(mut changes) = (*self.changes).lock() {
854                changes.push(change);
855            }
856        }
857    }
858
859    #[cfg(not(feature = "std"))]
860    pub fn push_change(&mut self, change: CallbackChange) {
861        unsafe { (*self.changes).push(change) }
862    }
863
864    /// Snapshot the current app state into the undo history (mini-git commit).
865    pub fn commit_undo_snapshot(&mut self) {
866        self.push_change(CallbackChange::CommitUndoSnapshot);
867    }
868
869    /// Undo the last committed app-state change; relayouts all windows.
870    pub fn undo_app_state(&mut self) {
871        self.push_change(CallbackChange::UndoAppState);
872    }
873
874    /// Redo a previously undone app-state change; relayouts all windows.
875    pub fn redo_app_state(&mut self) {
876        self.push_change(CallbackChange::RedoAppState);
877    }
878
879    /// Debug helper to get the changes pointer for debugging
880    #[cfg(feature = "std")]
881    #[must_use] pub const fn get_changes_ptr(&self) -> *const () {
882        self.changes.cast::<()>()
883    }
884
885    /// Get the collected changes (consumes them from the Arc<Mutex>)
886    #[cfg(feature = "std")]
887    #[must_use] pub fn take_changes(&self) -> Vec<CallbackChange> {
888        // SAFETY: The pointer is valid for the lifetime of the callback
889        unsafe {
890            (*self.changes).lock().map_or_else(
891                |_| Vec::new(),
892                |mut changes| core::mem::take(&mut *changes),
893            )
894        }
895    }
896
897    #[cfg(not(feature = "std"))]
898    pub fn take_changes(&self) -> Vec<CallbackChange> {
899        unsafe { core::mem::take(&mut *self.changes) }
900    }
901
902    /// Check if pending changes require relayout before the next step.
903    ///
904    /// Returns true for `ModifyWindowState` (resize) and `ScrollTo` (scroll),
905    /// which both need the event loop to re-run layout so that subsequent
906    /// operations (like `take_screenshot`) see updated content.
907    ///
908    /// Used by the E2E test runner to detect when it needs to yield.
909    #[cfg(feature = "std")]
910    #[must_use] pub fn has_pending_relayout_change(&self) -> bool {
911        unsafe {
912            (*self.changes).lock().is_ok_and(|changes| changes.iter().any(|c| matches!(c,
913                    CallbackChange::ModifyWindowState { .. } |
914                    CallbackChange::ScrollTo { .. } |
915                    // Synthetic input (E2E `click` = move/down/up applied one
916                    // state per frame): the runner MUST yield here, or every
917                    // post-click step executes against the pre-click DOM and
918                    // the queued states only apply after the test finishes.
919                    CallbackChange::QueueWindowStateSequence { .. }
920                )))
921        }
922    }
923
924    // Modern Api (using CallbackChange transactions)
925
926    /// Add a timer to this window (applied after callback returns)
927    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
928        self.push_change(CallbackChange::AddTimer { timer_id, timer });
929    }
930
931    /// Remove a timer from this window (applied after callback returns)
932    pub fn remove_timer(&mut self, timer_id: TimerId) {
933        self.push_change(CallbackChange::RemoveTimer { timer_id });
934    }
935
936    /// Add a thread to this window (applied after callback returns)
937    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
938        self.push_change(CallbackChange::AddThread { thread_id, thread });
939    }
940
941    /// Remove a thread from this window (applied after callback returns)
942    pub fn remove_thread(&mut self, thread_id: ThreadId) {
943        self.push_change(CallbackChange::RemoveThread { thread_id });
944    }
945
946    /// Stop event propagation (applied after callback returns)
947    ///
948    /// W3C `stopPropagation()`: remaining handlers on the *current* node
949    /// still fire, but no handlers on ancestor/descendant nodes are called.
950    pub fn stop_propagation(&mut self) {
951        self.push_change(CallbackChange::StopPropagation);
952    }
953
954    /// Stop event propagation immediately (applied after callback returns)
955    ///
956    /// W3C `stopImmediatePropagation()`: no further handlers fire,
957    /// not even remaining handlers registered on the same node.
958    pub fn stop_immediate_propagation(&mut self) {
959        self.push_change(CallbackChange::StopImmediatePropagation);
960    }
961
962    /// Set keyboard focus target (applied after callback returns)
963    pub fn set_focus(&mut self, target: FocusTarget) {
964        self.push_change(CallbackChange::SetFocusTarget { target });
965    }
966
967    /// Create a new window (applied after callback returns)
968    pub fn create_window(&mut self, options: WindowCreateOptions) {
969        self.push_change(CallbackChange::CreateNewWindow { options });
970    }
971
972    /// Close the current window (applied after callback returns)
973    pub fn close_window(&mut self) {
974        self.push_change(CallbackChange::CloseWindow);
975    }
976
977    /// Switch to a different route (applied after callback returns).
978    ///
979    /// On desktop: swaps the layout callback and triggers `RefreshDom`.
980    /// On web: also calls `history.pushState()`.
981    ///
982    /// # C API
983    /// ```c
984    /// AzCallbackInfo_switchRoute(&info, AzString_fromConstStr("/user/:id"),
985    ///     AzStringPairVec_fromConstSlice(&[AzStringPair { key: "id", value: "42" }]));
986    /// ```
987    pub fn switch_route(&mut self, pattern: AzString, params: azul_core::window::StringPairVec) {
988        self.push_change(CallbackChange::SwitchRoute { pattern, params });
989    }
990
991    /// Get the current active route pattern (e.g. `"/user/:id"`).
992    ///
993    /// Returns empty string if no route is active.
994    ///
995    /// # C API
996    /// ```c
997    /// AzString pattern = AzCallbackInfo_getRoutePattern(&info);
998    /// ```
999    #[must_use] pub fn get_route_pattern(&self) -> AzString {
1000        match &self.get_current_window_state().active_route {
1001            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1002            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1003        }
1004    }
1005
1006    /// Get a route parameter by key (e.g. `"id"` from `/user/:id`).
1007    ///
1008    /// Returns empty string if the parameter doesn't exist or no route is active.
1009    ///
1010    /// # C API
1011    /// ```c
1012    /// AzString id = AzCallbackInfo_getRouteParam(&info, AzString_fromConstStr("id"));
1013    /// ```
1014    // FFI-exported (AzCallbackInfo_getRouteParam): the owned AzString key is the api.json signature.
1015    #[allow(clippy::needless_pass_by_value)]
1016    #[must_use] pub fn get_route_param(&self, key: AzString) -> AzString {
1017        match &self.get_current_window_state().active_route {
1018            azul_core::resources::OptionRouteMatch::Some(rm) => {
1019                rm.get_param(key.as_str())
1020                    .cloned()
1021                    .unwrap_or_else(|| AzString::from_const_str(""))
1022            }
1023            azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1024        }
1025    }
1026
1027    /// Set a route parameter value and trigger re-render.
1028    ///
1029    /// This modifies the active route's params in-place and triggers a DOM refresh.
1030    /// On web, this also updates the URL via `history.replaceState()`.
1031    ///
1032    /// # C API
1033    /// ```c
1034    /// AzCallbackInfo_setRouteParam(&info, AzString_fromConstStr("id"), AzString_fromConstStr("99"));
1035    /// ```
1036    pub fn set_route_param(&mut self, key: AzString, value: AzString) {
1037        let ws = self.get_current_window_state();
1038        let pattern = match &ws.active_route {
1039            azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1040            azul_core::resources::OptionRouteMatch::None => return,
1041        };
1042        let mut params = match &ws.active_route {
1043            azul_core::resources::OptionRouteMatch::Some(rm) => {
1044                rm.params.as_ref().to_vec()
1045            }
1046            azul_core::resources::OptionRouteMatch::None => return,
1047        };
1048        // Update or insert the parameter
1049        if let Some(existing) = params.iter_mut().find(|p| p.key.as_str() == key.as_str()) {
1050            existing.value = value;
1051        } else {
1052            params.push(azul_core::window::AzStringPair { key, value });
1053        }
1054        self.push_change(CallbackChange::SwitchRoute {
1055            pattern,
1056            params: azul_core::window::StringPairVec::from_vec(params),
1057        });
1058    }
1059
1060    /// Modify the window state (applied after callback returns)
1061    pub fn modify_window_state(&mut self, state: FullWindowState) {
1062        self.push_change(CallbackChange::ModifyWindowState { state });
1063    }
1064
1065    /// Request the compositor to begin an interactive window move.
1066    ///
1067    /// On Wayland: calls `xdg_toplevel_move(toplevel, seat, serial)` which lets
1068    /// the compositor handle the move. This is the only way to move windows on Wayland.
1069    /// On other platforms: this is a no-op; use `modify_window_state()` to set position.
1070    pub fn begin_interactive_move(&mut self) {
1071        self.push_change(CallbackChange::BeginInteractiveMove);
1072    }
1073
1074    /// Queue multiple window state changes to be applied in sequence.
1075    /// Each state triggers a separate event processing cycle, which is needed
1076    /// for simulating clicks where mouse down and mouse up must be separate events.
1077    pub fn queue_window_state_sequence(&mut self, states: FullWindowStateVec) {
1078        self.push_change(CallbackChange::QueueWindowStateSequence {
1079            states: states.into_library_owned_vec(),
1080        });
1081    }
1082
1083    /// Change the text content of a node (applied after callback returns)
1084    ///
1085    /// This method was previously called `set_string_contents` in older API versions.
1086    ///
1087    /// # Arguments
1088    /// * `node_id` - The text node to modify (`DomNodeId` containing both DOM and node IDs)
1089    /// * `text` - The new text content
1090    pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
1091        self.push_change(CallbackChange::ChangeNodeText { node_id, text });
1092    }
1093
1094    /// Change the image of a node (applied after callback returns)
1095    pub fn change_node_image(
1096        &mut self,
1097        dom_id: DomId,
1098        node_id: NodeId,
1099        image: ImageRef,
1100        update_type: UpdateImageType,
1101    ) {
1102        self.push_change(CallbackChange::ChangeNodeImage {
1103            dom_id,
1104            node_id,
1105            image,
1106            update_type,
1107        });
1108    }
1109
1110    /// Re-render an image callback (for resize/animation updates)
1111    ///
1112    /// This triggers re-invocation of the `RenderImageCallback` associated with the node.
1113    /// Useful for:
1114    /// - Responding to window resize (image needs to match new size)
1115    /// - Animation frames (update OpenGL texture each frame)
1116    /// - Interactive content (user input changes rendering)
1117    pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
1118        self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
1119    }
1120
1121    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
1122    ///
1123    /// This is the most efficient way to update animated GL textures.
1124    /// Unlike returning `Update::RefreshDom`, this triggers only:
1125    /// - Re-invocation of all `RenderImageCallback` functions
1126    /// - GL texture swap in `WebRender`
1127    ///
1128    /// It does NOT trigger:
1129    /// - DOM rebuild (no `layout()` callback)
1130    /// - Display list resubmission (`WebRender` reuses existing scene)
1131    /// - Relayout
1132    ///
1133    /// Ideal for timer callbacks that animate OpenGL content at 60fps.
1134    pub fn update_all_image_callbacks(&mut self) {
1135        self.push_change(CallbackChange::UpdateAllImageCallbacks);
1136    }
1137
1138    /// Trigger re-rendering of a `VirtualView` (applied after callback returns)
1139    ///
1140    /// This forces the `VirtualView` to call its layout callback with reason `DomRecreated`
1141    /// and submit a new display list to `WebRender`. The `VirtualView`'s pipeline will be updated
1142    /// without affecting other parts of the window.
1143    ///
1144    /// Useful for:
1145    /// - Live preview panes (update when source code changes)
1146    /// - Dynamic content that needs manual refresh
1147    /// - Editor previews (re-parse and display new DOM)
1148    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
1149        self.push_change(CallbackChange::UpdateVirtualView { dom_id, node_id });
1150    }
1151
1152    /// Re-render EVERY `VirtualView` on the existing DOM - no node id required.
1153    ///
1154    /// Use from a callback that mutated a dataset shared with a `VirtualView`'s
1155    /// `refany` (the two are clones of one `RefAny`, so they point at the same
1156    /// underlying data). The canonical case is a background thread writeback:
1157    /// e.g. the `MapWidget`'s tile-fetch worker decodes a tile, writes it into
1158    /// the shared `MapTileCache`, then calls this so the pure `VirtualView`
1159    /// content callback re-reads the cache and rebuilds its child DOM in place -
1160    /// WITHOUT a `RefreshDom` (which would rebuild the DOM and orphan the
1161    /// worker's clone of the cache).
1162    pub fn trigger_all_virtual_view_rerender(&mut self) {
1163        self.push_change(CallbackChange::UpdateAllVirtualViews);
1164    }
1165
1166    // Dom Tree Navigation
1167
1168    /// Find a node by ID attribute in the layout tree
1169    ///
1170    /// Returns the `NodeId` of the first node with the given ID attribute, or None if not found.
1171    #[must_use] pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
1172        let layout_window = self.get_layout_window();
1173        let layout_result = layout_window.layout_results.get(&dom_id)?;
1174        let styled_dom = &layout_result.styled_dom;
1175
1176        // Search through all nodes to find one with matching ID attribute
1177        for (node_idx, node_data) in styled_dom.node_data.as_ref().iter().enumerate() {
1178            if node_data.has_id(id) {
1179                return Some(NodeId::new(node_idx));
1180            }
1181        }
1182
1183        None
1184    }
1185
1186    /// Get the parent node of the given node
1187    ///
1188    /// Returns None if the node has no parent (i.e., it's the root node)
1189    #[must_use] pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1190        let layout_window = self.get_layout_window();
1191        let layout_result = layout_window.layout_results.get(&dom_id)?;
1192        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1193        let node = node_hierarchy.as_ref().get(node_id.index())?;
1194        node.parent_id()
1195    }
1196
1197    /// Get the next sibling of the given node
1198    ///
1199    /// Returns None if the node has no next sibling
1200    #[must_use] pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1201        let layout_window = self.get_layout_window();
1202        let layout_result = layout_window.layout_results.get(&dom_id)?;
1203        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1204        let node = node_hierarchy.as_ref().get(node_id.index())?;
1205        node.next_sibling_id()
1206    }
1207
1208    /// Get the previous sibling of the given node
1209    ///
1210    /// Returns None if the node has no previous sibling
1211    #[must_use] pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1212        let layout_window = self.get_layout_window();
1213        let layout_result = layout_window.layout_results.get(&dom_id)?;
1214        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1215        let node = node_hierarchy.as_ref().get(node_id.index())?;
1216        node.previous_sibling_id()
1217    }
1218
1219    /// Get the first child of the given node
1220    ///
1221    /// Returns None if the node has no children
1222    #[must_use] pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1223        let layout_window = self.get_layout_window();
1224        let layout_result = layout_window.layout_results.get(&dom_id)?;
1225        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1226        let node = node_hierarchy.as_ref().get(node_id.index())?;
1227        node.first_child_id(node_id)
1228    }
1229
1230    /// Get the last child of the given node
1231    ///
1232    /// Returns None if the node has no children
1233    #[must_use] pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1234        let layout_window = self.get_layout_window();
1235        let layout_result = layout_window.layout_results.get(&dom_id)?;
1236        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1237        let node = node_hierarchy.as_ref().get(node_id.index())?;
1238        node.last_child_id()
1239    }
1240
1241    /// Get all direct children of the given node
1242    ///
1243    /// Returns an empty vector if the node has no children.
1244    /// Uses the contiguous node layout for efficient iteration.
1245    #[must_use] pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeHierarchyItemIdVec {
1246        let layout_window = self.get_layout_window();
1247        let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1248            return NodeHierarchyItemIdVec::from_const_slice(&[]);
1249        };
1250        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1251        let Some(hier_item) = node_hierarchy.get(node_id) else {
1252            return NodeHierarchyItemIdVec::from_const_slice(&[]);
1253        };
1254
1255        // Get first child - if none, return empty
1256        let Some(first_child) = hier_item.first_child_id(node_id) else {
1257            return NodeHierarchyItemIdVec::from_const_slice(&[]);
1258        };
1259
1260        // Collect children by walking the sibling chain
1261        let mut children: Vec<NodeHierarchyItemId> = Vec::new();
1262        children.push(NodeHierarchyItemId::from_crate_internal(Some(first_child)));
1263
1264        let mut current = first_child;
1265        while let Some(next_sibling) = node_hierarchy
1266            .get(current)
1267            .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1268        {
1269            children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
1270            current = next_sibling;
1271        }
1272
1273        NodeHierarchyItemIdVec::from(children)
1274    }
1275
1276    /// Get the number of direct children of the given node
1277    ///
1278    /// Uses the contiguous node layout for efficient counting.
1279    #[must_use] pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
1280        let layout_window = self.get_layout_window();
1281        let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1282            return 0;
1283        };
1284        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1285        let Some(hier_item) = node_hierarchy.get(node_id) else {
1286            return 0;
1287        };
1288
1289        // Get first child - if none, return 0
1290        let Some(first_child) = hier_item.first_child_id(node_id) else {
1291            return 0;
1292        };
1293
1294        // Count children by walking the sibling chain
1295        let mut count = 1;
1296        let mut current = first_child;
1297        while let Some(next_sibling) = node_hierarchy
1298            .get(current)
1299            .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1300        {
1301            count += 1;
1302            current = next_sibling;
1303        }
1304
1305        count
1306    }
1307
1308    /// Change the image mask of a node (applied after callback returns)
1309    pub fn change_node_image_mask(&mut self, dom_id: DomId, node_id: NodeId, mask: ImageMask) {
1310        self.push_change(CallbackChange::ChangeNodeImageMask {
1311            dom_id,
1312            node_id,
1313            mask,
1314        });
1315    }
1316
1317    /// Change CSS properties of a node (applied after callback returns)
1318    pub fn change_node_css_properties(
1319        &mut self,
1320        dom_id: DomId,
1321        node_id: NodeId,
1322        properties: CssPropertyVec,
1323    ) {
1324        self.push_change(CallbackChange::ChangeNodeCssProperties {
1325            dom_id,
1326            node_id,
1327            properties,
1328        });
1329    }
1330
1331    /// Set a single CSS property on a node (convenience method for widgets)
1332    ///
1333    /// This is a helper method that wraps `change_node_css_properties` for the common case
1334    /// of setting a single property. It uses the hit node's DOM ID automatically.
1335    ///
1336    /// # Arguments
1337    /// * `node_id` - The node to set the property on (uses hit node's DOM ID)
1338    /// * `property` - The CSS property to set
1339    /// # Panics
1340    ///
1341    /// Panics if `node_id.node` is None; the target must reference a concrete node.
1342    pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1343        let dom_id = node_id.dom;
1344        let internal_node_id = node_id
1345            .node
1346            .into_crate_internal()
1347            .expect("DomNodeId node should not be None");
1348        self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
1349    }
1350
1351    /// Quickly override CSS properties on a node for animation or other
1352    /// transient visual changes. Writes go through
1353    /// `CssPropertyCache::user_overridden_properties`, which is consulted at
1354    /// higher priority than the static cascade, so this does not invalidate
1355    /// the styled DOM's CSS rules. Pass `CssProperty::Initial` for a given
1356    /// property type to remove any prior override for that type.
1357    pub fn override_node_css_properties(
1358        &mut self,
1359        dom_id: DomId,
1360        node_id: NodeId,
1361        properties: CssPropertyVec,
1362    ) {
1363        self.push_change(CallbackChange::OverrideNodeCssProperties {
1364            dom_id,
1365            node_id,
1366            properties,
1367        });
1368    }
1369
1370    /// Convenience wrapper for `override_node_css_properties` that targets a
1371    /// single property on the hit node's DOM (typical for animation callbacks).
1372    /// # Panics
1373    ///
1374    /// Panics if `node_id.node` is None; the target must reference a concrete node.
1375    pub fn override_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1376        let dom_id = node_id.dom;
1377        let internal_node_id = node_id
1378            .node
1379            .into_crate_internal()
1380            .expect("DomNodeId node should not be None");
1381        self.override_node_css_properties(dom_id, internal_node_id, vec![property].into());
1382    }
1383
1384    /// Scroll a node to a specific position (applied after callback returns)
1385    pub fn scroll_to(
1386        &mut self,
1387        dom_id: DomId,
1388        node_id: NodeHierarchyItemId,
1389        position: LogicalPosition,
1390    ) {
1391        self.push_change(CallbackChange::ScrollTo {
1392            dom_id,
1393            node_id,
1394            position,
1395            unclamped: false,
1396        });
1397    }
1398
1399    /// Scroll a node to a specific position without clamping.
1400    /// Used by the scroll physics timer for rubber-banding/overscroll.
1401    pub fn scroll_to_unclamped(
1402        &mut self,
1403        dom_id: DomId,
1404        node_id: NodeHierarchyItemId,
1405        position: LogicalPosition,
1406    ) {
1407        self.push_change(CallbackChange::ScrollTo {
1408            dom_id,
1409            node_id,
1410            position,
1411            unclamped: true,
1412        });
1413    }
1414
1415    /// Scroll a node into view (W3C scrollIntoView API)
1416    ///
1417    /// Scrolls the element into the visible area of its scroll container.
1418    /// This is the recommended way to programmatically scroll elements into view.
1419    ///
1420    /// # Arguments
1421    ///
1422    /// * `node_id` - The node to scroll into view
1423    /// * `options` - Scroll alignment and animation options
1424    ///
1425    /// # Note
1426    ///
1427    /// This uses the transactional change system - the scroll is queued and applied
1428    /// after the callback returns. The actual scroll adjustments are calculated
1429    /// during change processing.
1430    pub fn scroll_node_into_view(
1431        &mut self,
1432        node_id: DomNodeId,
1433        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1434    ) {
1435        self.push_change(CallbackChange::ScrollIntoView {
1436            node_id,
1437            options,
1438        });
1439    }
1440
1441    /// Add an image to the image cache (applied after callback returns)
1442    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
1443        self.push_change(CallbackChange::AddImageToCache { id, image });
1444    }
1445
1446    /// Remove an image from the image cache (applied after callback returns)
1447    pub fn remove_image_from_cache(&mut self, id: AzString) {
1448        self.push_change(CallbackChange::RemoveImageFromCache { id });
1449    }
1450
1451    /// Reload system fonts (applied after callback returns)
1452    ///
1453    /// Note: This is an expensive operation that rebuilds the entire font cache
1454    pub fn reload_system_fonts(&mut self) {
1455        self.push_change(CallbackChange::ReloadSystemFonts);
1456    }
1457
1458    // Text Input / Changeset Api
1459
1460    /// Get the current text changeset being processed (if any)
1461    ///
1462    /// This allows callbacks to inspect what text input is about to be applied.
1463    /// Returns None if no text input is currently being processed.
1464    ///
1465    /// Use `set_text_changeset()` to modify the text that will be inserted,
1466    /// and `prevent_default()` to block the text input entirely.
1467    #[must_use] pub const fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
1468        self.get_layout_window()
1469            .text_input_manager
1470            .get_pending_changeset()
1471    }
1472
1473    /// Set/override the text changeset for the current text input operation
1474    ///
1475    /// This allows you to modify what text will be inserted during text input events.
1476    /// Typically used in combination with `prevent_default()` to transform user input.
1477    ///
1478    /// # Arguments
1479    /// * `changeset` - The modified text changeset to apply
1480    pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
1481        self.push_change(CallbackChange::SetTextChangeset { changeset });
1482    }
1483
1484    /// Create a synthetic text input event
1485    ///
1486    /// This simulates receiving text input from the OS. Use this to programmatically
1487    /// insert text into contenteditable elements, for example from the debug server
1488    /// or from accessibility APIs.
1489    ///
1490    /// The text input flow will:
1491    /// 1. Record the text in `TextInputManager` (creating a `PendingTextEdit`)
1492    /// 2. Generate synthetic `TextInput` events
1493    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
1494    /// 4. Apply the changeset if not rejected
1495    /// 5. Mark dirty nodes for re-render
1496    ///
1497    /// # Arguments
1498    /// * `text` - The text to insert at the current cursor position
1499    pub fn create_text_input(&mut self, text: AzString) {
1500        self.push_change(CallbackChange::CreateTextInput { text });
1501    }
1502
1503    // DOM Mutation Api (for Debug API)
1504
1505    /// Insert a new child node into the DOM tree (applied after callback returns)
1506    ///
1507    /// Creates a new node with the given type string and appends it as a child
1508    /// of the specified parent node. The `node_type_str` can be:
1509    /// - A tag name: "div", "p", "span", "button", etc.
1510    /// - Text content: "text:Hello World"
1511    ///
1512    /// # Arguments
1513    /// * `dom_id` - The DOM to modify
1514    /// * `parent_node_id` - The parent node to insert under
1515    /// * `node_type_str` - The node type (tag name or "text:content")
1516    /// * `position` - Optional child index (None = append at end)
1517    /// * `classes` - CSS classes for the new node
1518    /// * `id` - Optional ID for the new node
1519    pub fn insert_child_node(
1520        &mut self,
1521        dom_id: DomId,
1522        parent_node_id: NodeId,
1523        node_type_str: AzString,
1524        position: OptionUsize,
1525        classes: StringVec,
1526        id: OptionString,
1527    ) {
1528        self.push_change(CallbackChange::InsertChildNode {
1529            dom_id,
1530            parent_node_id,
1531            node_type_str,
1532            position: position.into(),
1533            classes: classes.into_library_owned_vec(),
1534            id: id.into(),
1535        });
1536    }
1537
1538    /// Delete a node from the DOM tree (applied after callback returns)
1539    ///
1540    /// Tombstones the node by setting it to an empty anonymous Div and
1541    /// unlinking it from the hierarchy. This preserves node ID stability
1542    /// (other node IDs don't shift).
1543    ///
1544    /// # Arguments
1545    /// * `dom_id` - The DOM containing the node
1546    /// * `node_id` - The node to delete
1547    pub fn delete_node(&mut self, dom_id: DomId, node_id: NodeId) {
1548        self.push_change(CallbackChange::DeleteNode { dom_id, node_id });
1549    }
1550
1551    /// Set the IDs and classes on an existing node (applied after callback returns)
1552    ///
1553    /// Replaces the current IDs and classes of a node with the given set.
1554    ///
1555    /// # Arguments
1556    /// * `dom_id` - The DOM containing the node
1557    /// * `node_id` - The node to modify
1558    /// * `ids_and_classes` - The new set of IDs and classes
1559    pub fn set_node_ids_and_classes(
1560        &mut self,
1561        dom_id: DomId,
1562        node_id: NodeId,
1563        ids_and_classes: azul_core::dom::IdOrClassVec,
1564    ) {
1565        self.push_change(CallbackChange::SetNodeIdsAndClasses {
1566            dom_id,
1567            node_id,
1568            ids_and_classes,
1569        });
1570    }
1571
1572    /// Prevent the default text input from being applied
1573    ///
1574    /// When called in a `TextInput` callback, prevents the typed text from being inserted.
1575    /// Useful for custom validation, filtering, or text transformation.
1576    pub fn prevent_default(&mut self) {
1577        self.push_change(CallbackChange::PreventDefault);
1578    }
1579
1580    // Cursor Blinking Api (for system timer control)
1581    
1582    /// Set cursor visibility state
1583    ///
1584    /// This is primarily used internally by the cursor blink timer callback.
1585    /// User code typically doesn't need to call this directly.
1586    pub fn set_cursor_visibility(&mut self, visible: bool) {
1587        self.push_change(CallbackChange::SetCursorVisibility { visible });
1588    }
1589    
1590    /// Reset cursor blink state on user input
1591    ///
1592    /// This makes the cursor visible and records the current time, so the blink
1593    /// timer knows to keep the cursor solid for a while before blinking.
1594    /// Called automatically on keyboard input, but can be called manually.
1595    pub fn reset_cursor_blink(&mut self) {
1596        self.push_change(CallbackChange::ResetCursorBlink);
1597    }
1598    
1599    /// Start the cursor blink timer
1600    ///
1601    /// Called automatically when focus lands on a contenteditable element.
1602    /// The timer will toggle cursor visibility at ~530ms intervals.
1603    pub fn start_cursor_blink_timer(&mut self) {
1604        self.push_change(CallbackChange::StartCursorBlinkTimer);
1605    }
1606    
1607    /// Stop the cursor blink timer
1608    ///
1609    /// Called automatically when focus leaves a contenteditable element.
1610    pub fn stop_cursor_blink_timer(&mut self) {
1611        self.push_change(CallbackChange::StopCursorBlinkTimer);
1612    }
1613    
1614    /// Scroll the active cursor into view
1615    ///
1616    /// This scrolls the focused text element's cursor into the visible area
1617    /// of any scrollable ancestor. Called automatically after text input.
1618    pub fn scroll_active_cursor_into_view(&mut self) {
1619        self.push_change(CallbackChange::ScrollActiveCursorIntoView);
1620    }
1621
1622    /// Open a menu (context menu or dropdown)
1623    ///
1624    /// The menu will be displayed either as a native menu or a fallback DOM-based menu
1625    /// depending on the window's `use_native_context_menus` flag.
1626    /// Uses the position specified in the menu itself.
1627    ///
1628    /// # Arguments
1629    /// * `menu` - The menu to display
1630    pub fn open_menu(&mut self, menu: Menu) {
1631        self.push_change(CallbackChange::OpenMenu {
1632            menu,
1633            position: None,
1634        });
1635    }
1636
1637    /// Open a menu at a specific position
1638    ///
1639    /// # Arguments
1640    /// * `menu` - The menu to display
1641    /// * `position` - The position where the menu should appear (overrides menu's position)
1642    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
1643        self.push_change(CallbackChange::OpenMenu {
1644            menu,
1645            position: Some(position),
1646        });
1647    }
1648
1649    // Tooltip Api
1650
1651    /// Show a tooltip at the current cursor position
1652    ///
1653    /// Displays a simple text tooltip near the mouse cursor.
1654    /// The tooltip will be shown using platform-specific native APIs where available.
1655    ///
1656    /// Platform implementations:
1657    /// - **Windows**: Uses `TOOLTIPS_CLASS` Win32 control
1658    /// - **macOS**: Uses `NSPopover` or custom `NSWindow` with tooltip styling
1659    /// - **X11**: Creates transient window with `_NET_WM_WINDOW_TYPE_TOOLTIP`
1660    /// - **Wayland**: Uses `zwlr_layer_shell_v1` with overlay layer
1661    ///
1662    /// # Arguments
1663    /// * `text` - The tooltip text to display
1664    pub fn show_tooltip(&mut self, text: AzString) {
1665        let position = self
1666            .get_cursor_relative_to_viewport()
1667            .into_option()
1668            .unwrap_or_else(LogicalPosition::zero);
1669        self.push_change(CallbackChange::ShowTooltip { text, position });
1670    }
1671
1672    /// Show a tooltip at a specific position
1673    ///
1674    /// # Arguments
1675    /// * `text` - The tooltip text to display
1676    /// * `position` - The position where the tooltip should appear (in window coordinates)
1677    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
1678        self.push_change(CallbackChange::ShowTooltip { text, position });
1679    }
1680
1681    /// Hide the currently displayed tooltip
1682    pub fn hide_tooltip(&mut self) {
1683        self.push_change(CallbackChange::HideTooltip);
1684    }
1685
1686    // Text Editing Api (transactional)
1687
1688    /// Insert text at the current cursor position in a text node
1689    ///
1690    /// This operation is transactional - the text will be inserted after the callback returns.
1691    /// If there's a selection, it will be replaced with the inserted text.
1692    ///
1693    /// # Arguments
1694    /// * `dom_id` - The DOM containing the text node
1695    /// * `node_id` - The node to insert text into
1696    /// * `text` - The text to insert
1697    pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
1698        self.push_change(CallbackChange::InsertText {
1699            dom_id,
1700            node_id,
1701            text,
1702        });
1703    }
1704
1705    /// Move the text cursor to a specific position
1706    ///
1707    /// # Arguments
1708    /// * `dom_id` - The DOM containing the text node
1709    /// * `node_id` - The node containing the cursor
1710    /// * `cursor` - The new cursor position
1711    pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
1712        self.push_change(CallbackChange::MoveCursor {
1713            dom_id,
1714            node_id,
1715            cursor,
1716        });
1717    }
1718
1719    /// Set the text selection range
1720    ///
1721    /// # Arguments
1722    /// * `dom_id` - The DOM containing the text node
1723    /// * `node_id` - The node containing the selection
1724    /// * `selection` - The new selection (can be a cursor or range)
1725    pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
1726        self.push_change(CallbackChange::SetSelection {
1727            dom_id,
1728            node_id,
1729            selection,
1730        });
1731    }
1732
1733    // === Multi-Cursor Operations ===
1734
1735    /// Add an additional cursor at the specified position (for multi-cursor editing).
1736    ///
1737    /// If a `MultiCursorState` already exists, the cursor is added and overlapping
1738    /// selections are merged. If not, a new `MultiCursorState` is created.
1739    ///
1740    /// Returns the `SelectionId` of the new cursor.
1741    pub fn add_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) -> azul_core::selection::SelectionId {
1742        let id = azul_core::selection::SelectionId::new();
1743        self.push_change(CallbackChange::AddCursor {
1744            dom_id,
1745            node_id,
1746            cursor,
1747        });
1748        id
1749    }
1750
1751    /// Add an additional selection range (for multi-cursor editing).
1752    ///
1753    /// Returns the `SelectionId` of the new selection.
1754    pub fn add_selection_range(&mut self, dom_id: DomId, node_id: NodeId, range: SelectionRange) -> azul_core::selection::SelectionId {
1755        let id = azul_core::selection::SelectionId::new();
1756        self.push_change(CallbackChange::AddSelectionRange {
1757            dom_id,
1758            node_id,
1759            range,
1760        });
1761        id
1762    }
1763
1764    /// Remove a specific selection/cursor by its stable ID.
1765    ///
1766    /// Returns true if a selection with that ID existed and was removed.
1767    pub fn remove_selection_by_id(&mut self, selection_id: azul_core::selection::SelectionId) -> bool {
1768        self.push_change(CallbackChange::RemoveSelectionById {
1769            selection_id,
1770        });
1771        true // Actual removal happens deferred; assume success
1772    }
1773
1774    /// Get all selections for the given DOM (read-only).
1775    ///
1776    /// Returns a Vec of `IdentifiedSelection` from the `MultiCursorState`, or empty
1777    /// if no multi-cursor state exists.
1778    #[must_use] pub fn get_multi_cursor_selections(&self, dom_id: &DomId) -> azul_core::selection::IdentifiedSelectionVec {
1779        let lw = self.get_layout_window();
1780        lw.text_edit_manager.multi_cursor.as_ref()
1781            .map(|mc| mc.selections.clone())
1782            .unwrap_or_default()
1783            .into()
1784    }
1785
1786    /// Get the primary (last-added) selection from the `MultiCursorState`.
1787    #[must_use] pub fn get_primary_selection(&self, dom_id: &DomId) -> Option<azul_core::selection::IdentifiedSelection> {
1788        let lw = self.get_layout_window();
1789        lw.text_edit_manager.multi_cursor.as_ref()
1790            .and_then(|mc| mc.get_primary().copied())
1791    }
1792
1793    /// Get the number of active cursors/selections.
1794    #[must_use] pub fn get_selection_count(&self, dom_id: &DomId) -> usize {
1795        let lw = self.get_layout_window();
1796        lw.text_edit_manager.multi_cursor.as_ref()
1797            .map_or(0, azul_core::selection::MultiCursorState::len)
1798    }
1799
1800    /// Open a menu positioned relative to a specific DOM node
1801    ///
1802    /// This is useful for dropdowns, combo boxes, and context menus that should appear
1803    /// near a specific UI element. The menu will be positioned below the node by default.
1804    ///
1805    /// # Arguments
1806    /// * `menu` - The menu to display
1807    /// * `node_id` - The DOM node to position the menu relative to
1808    ///
1809    /// # Returns
1810    /// * `true` if the menu was queued for opening
1811    /// * `false` if the node doesn't exist or has no layout information
1812    pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
1813        // Position the menu at the hit node's bottom-left. Prefer the display-list
1814        // hit-test bounds: they always carry the node's final rendered rect for an
1815        // interactive (tagged) node, whereas get_node_rect (position + used_size)
1816        // can be None for nodes whose used_size isn't recorded on the layout node.
1817        let rect = self
1818            .get_node_hit_test_bounds(node_id)
1819            .or_else(|| self.get_node_rect(node_id));
1820        rect.is_some_and(|rect| {
1821            // Position menu at bottom-left of the node
1822            let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
1823            self.push_change(CallbackChange::OpenMenu {
1824                menu,
1825                position: Some(position),
1826            });
1827            true
1828        })
1829    }
1830
1831    /// Open a menu positioned relative to the currently hit node
1832    ///
1833    /// Convenience method for opening a menu at the element that triggered the callback.
1834    /// Equivalent to `open_menu_for_node(menu, info.get_hit_node())`.
1835    ///
1836    /// # Arguments
1837    /// * `menu` - The menu to display
1838    ///
1839    /// # Returns
1840    /// * `true` if the menu was queued for opening
1841    /// * `false` if no node is currently hit or it has no layout information
1842    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
1843        let hit_node = self.get_hit_node();
1844        self.open_menu_for_node(menu, hit_node)
1845    }
1846
1847    // Internal accessors
1848
1849    /// Get reference to the underlying `LayoutWindow` for queries
1850    ///
1851    /// This provides read-only access to layout data, node hierarchies, managers, etc.
1852    /// All modifications should go through `CallbackChange` transactions via `push_change()`.
1853    #[must_use] pub const fn get_layout_window(&self) -> &LayoutWindow {
1854        unsafe { (*self.ref_data).layout_window }
1855    }
1856
1857    /// Internal helper: Get the inline text layout for a given node
1858    ///
1859    /// This efficiently looks up the text layout by following the chain:
1860    /// `LayoutWindow` -> `layout_results` -> `LayoutTree` -> `dom_to_layout` -> `LayoutNode` ->
1861    /// `inline_layout_result`
1862    ///
1863    /// Returns None if:
1864    /// - The DOM doesn't exist in `layout_results`
1865    /// - The node doesn't have a layout node mapping
1866    /// - The layout node doesn't have inline text layout
1867    fn get_inline_layout_for_node(&self, node_id: &DomNodeId) -> Option<&Arc<UnifiedLayout>> {
1868        let layout_window = self.get_layout_window();
1869
1870        // Get the layout result for this DOM
1871        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
1872
1873        // Convert NodeHierarchyItemId to NodeId
1874        let dom_node_id = node_id.node.into_crate_internal()?;
1875
1876        // Look up the layout node index(es) for this DOM node
1877        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&dom_node_id)?;
1878
1879        // Get the first layout node (a DOM node can generate multiple layout nodes,
1880        // but for text we typically only care about the first one)
1881        let layout_index = *layout_indices.first()?;
1882
1883        // Get the layout node's inline layout result (warm data)
1884        let warm_node = layout_result.layout_tree.warm(layout_index)?;
1885        warm_node
1886            .inline_layout_result
1887            .as_ref()
1888            .map(super::solver3::layout_tree::CachedInlineLayout::get_layout)
1889    }
1890
1891    // Public query Api
1892    // All methods below delegate to LayoutWindow for read-only access
1893
1894    /// Get the logical size of a node, or `None` if the node doesn't exist
1895    #[must_use] pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1896        self.get_layout_window().get_node_size(node_id)
1897    }
1898
1899    /// Get the logical position of a node, or `None` if the node doesn't exist
1900    #[must_use] pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1901        self.get_layout_window().get_node_position(node_id)
1902    }
1903
1904    /// Get the hit test bounds of a node from the display list
1905    ///
1906    /// This is more reliable than `get_node_rect` because the display list
1907    /// always contains the correct final rendered positions.
1908    #[must_use] pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1909        self.get_layout_window().get_node_hit_test_bounds(node_id)
1910    }
1911
1912    /// Get the bounding rectangle of a node (position + size)
1913    ///
1914    /// This is particularly useful for menu positioning, where you need
1915    /// to know where a UI element is to popup a menu relative to it.
1916    #[must_use] pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1917        let position = self.get_node_position(node_id)?;
1918        let size = self.get_node_size(node_id)?;
1919        Some(LogicalRect::new(position, size))
1920    }
1921
1922    /// Get the bounding rectangle of the hit node
1923    ///
1924    /// Convenience method that combines `get_hit_node()` and `get_node_rect()`.
1925    /// Useful for menu positioning based on the clicked element.
1926    #[must_use] pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
1927        let hit_node = self.get_hit_node();
1928        self.get_node_rect(hit_node)
1929    }
1930
1931    // Timer Management (Query APIs)
1932
1933    /// Get a reference to a timer
1934    #[must_use] pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1935        self.get_layout_window().get_timer(timer_id)
1936    }
1937
1938    /// Get all timer IDs
1939    #[must_use] pub fn get_timer_ids(&self) -> TimerIdVec {
1940        self.get_layout_window().get_timer_ids()
1941    }
1942
1943    // Thread Management (Query APIs)
1944
1945    /// Get a reference to a thread
1946    #[must_use] pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1947        self.get_layout_window().get_thread(thread_id)
1948    }
1949
1950    /// Get all thread IDs
1951    #[must_use] pub fn get_thread_ids(&self) -> ThreadIdVec {
1952        self.get_layout_window().get_thread_ids()
1953    }
1954
1955    // Gpu Value Cache Management (Query APIs)
1956
1957    /// Get the GPU value cache for a specific DOM
1958    #[must_use] pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
1959        self.get_layout_window().get_gpu_cache(dom_id)
1960    }
1961
1962    // Layout Result Access (Query APIs)
1963
1964    /// Get a layout result for a specific DOM
1965    #[must_use] pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
1966        self.get_layout_window().get_layout_result(dom_id)
1967    }
1968
1969    /// Get all DOM IDs that have layout results
1970    #[must_use] pub fn get_dom_ids(&self) -> DomIdVec {
1971        self.get_layout_window().get_dom_ids()
1972    }
1973
1974    // Node Hierarchy Navigation
1975
1976    /// Get the DOM node that was hit by the event that triggered this callback
1977    #[must_use] pub const fn get_hit_node(&self) -> DomNodeId {
1978        self.hit_dom_node
1979    }
1980
1981    /// Check if a node is anonymous (generated for table layout)
1982    #[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)
1983    fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
1984        let layout_window = self.get_layout_window();
1985        let Some(layout_result) = layout_window.get_layout_result(dom_id) else {
1986            return false;
1987        };
1988        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1989        let Some(node_data) = node_data_cont.get(node_id) else {
1990            return false;
1991        };
1992        node_data.is_anonymous()
1993    }
1994
1995    /// Get the parent of a node, skipping anonymous (table-generated) nodes
1996    #[must_use] pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1997        let layout_window = self.get_layout_window();
1998        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1999        let node_id_internal = node_id.node.into_crate_internal()?;
2000        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2001        let hier_item = node_hierarchy.get(node_id_internal)?;
2002
2003        // Skip anonymous parent nodes - walk up the tree until we find a non-anonymous node
2004        let mut current_parent_id = hier_item.parent_id()?;
2005        loop {
2006            if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
2007                return Some(DomNodeId {
2008                    dom: node_id.dom,
2009                    node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
2010                });
2011            }
2012
2013            // This parent is anonymous, try its parent
2014            let parent_hier_item = node_hierarchy.get(current_parent_id)?;
2015            current_parent_id = parent_hier_item.parent_id()?;
2016        }
2017    }
2018
2019    /// Get the previous sibling of a node, skipping anonymous nodes
2020    #[must_use] pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2021        let layout_window = self.get_layout_window();
2022        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2023        let node_id_internal = node_id.node.into_crate_internal()?;
2024        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2025        let hier_item = node_hierarchy.get(node_id_internal)?;
2026
2027        // Skip anonymous siblings - walk backwards until we find a non-anonymous node
2028        let mut current_sibling_id = hier_item.previous_sibling_id()?;
2029        loop {
2030            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2031                return Some(DomNodeId {
2032                    dom: node_id.dom,
2033                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2034                });
2035            }
2036
2037            // This sibling is anonymous, try the previous one
2038            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2039            current_sibling_id = sibling_hier_item.previous_sibling_id()?;
2040        }
2041    }
2042
2043    /// Get the next sibling of a node, skipping anonymous nodes
2044    #[must_use] pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2045        let layout_window = self.get_layout_window();
2046        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2047        let node_id_internal = node_id.node.into_crate_internal()?;
2048        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2049        let hier_item = node_hierarchy.get(node_id_internal)?;
2050
2051        // Skip anonymous siblings - walk forwards until we find a non-anonymous node
2052        let mut current_sibling_id = hier_item.next_sibling_id()?;
2053        loop {
2054            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2055                return Some(DomNodeId {
2056                    dom: node_id.dom,
2057                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2058                });
2059            }
2060
2061            // This sibling is anonymous, try the next one
2062            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2063            current_sibling_id = sibling_hier_item.next_sibling_id()?;
2064        }
2065    }
2066
2067    /// Get the first child of a node, skipping anonymous nodes
2068    #[must_use] pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2069        let layout_window = self.get_layout_window();
2070        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2071        let node_id_internal = node_id.node.into_crate_internal()?;
2072        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2073        let hier_item = node_hierarchy.get(node_id_internal)?;
2074
2075        // Get first child, then skip anonymous nodes
2076        let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
2077        loop {
2078            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2079                return Some(DomNodeId {
2080                    dom: node_id.dom,
2081                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2082                });
2083            }
2084
2085            // This child is anonymous, try the next sibling
2086            let child_hier_item = node_hierarchy.get(current_child_id)?;
2087            current_child_id = child_hier_item.next_sibling_id()?;
2088        }
2089    }
2090
2091    /// Get the last child of a node, skipping anonymous nodes
2092    #[must_use] pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2093        let layout_window = self.get_layout_window();
2094        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2095        let node_id_internal = node_id.node.into_crate_internal()?;
2096        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2097        let hier_item = node_hierarchy.get(node_id_internal)?;
2098
2099        // Get last child, then skip anonymous nodes by walking backwards
2100        let mut current_child_id = hier_item.last_child_id()?;
2101        loop {
2102            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2103                return Some(DomNodeId {
2104                    dom: node_id.dom,
2105                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2106                });
2107            }
2108
2109            // This child is anonymous, try the previous sibling
2110            let child_hier_item = node_hierarchy.get(current_child_id)?;
2111            current_child_id = child_hier_item.previous_sibling_id()?;
2112        }
2113    }
2114
2115    // Node Data and State
2116
2117    /// Get the dataset (user-attached `RefAny`) of a node, or `None` if unset
2118    pub fn get_dataset(&mut self, node_id: DomNodeId) -> Option<RefAny> {
2119        let layout_window = self.get_layout_window();
2120        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2121        let node_id_internal = node_id.node.into_crate_internal()?;
2122        let node_data_cont = layout_result.styled_dom.node_data.as_container();
2123        let node_data = node_data_cont.get(node_id_internal)?;
2124        node_data.get_dataset().cloned()
2125    }
2126
2127    /// Find the root-level node whose dataset matches the type of `search_key`
2128    // owned RefAny passed by value per the azul FFI / api.json convention.
2129    #[allow(clippy::needless_pass_by_value)]
2130    pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
2131        let mut found: Option<(u64, DomNodeId)> = None;
2132        let search_type_id = search_key.get_type_id();
2133
2134        for dom_id in self.get_dom_ids().as_ref().iter().copied() {
2135            let layout_window = self.get_layout_window();
2136            let Some(layout_result) = layout_window.get_layout_result(&dom_id) else {
2137                continue;
2138            };
2139
2140            let node_data_cont = layout_result.styled_dom.node_data.as_container();
2141            for (node_idx, node_data) in node_data_cont.iter().enumerate() {
2142                if let Some(dataset) = node_data.get_dataset().cloned() {
2143                    if dataset.get_type_id() == search_type_id {
2144                        let node_id = DomNodeId {
2145                            dom: dom_id,
2146                            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
2147                                node_idx,
2148                            ))),
2149                        };
2150                        let instance_id = dataset.instance_id;
2151
2152                        match found {
2153                            None => found = Some((instance_id, node_id)),
2154                            Some((prev_instance, _)) => {
2155                                if instance_id < prev_instance {
2156                                    found = Some((instance_id, node_id));
2157                                }
2158                            }
2159                        }
2160                    }
2161                }
2162            }
2163        }
2164
2165        found.map(|s| s.1)
2166    }
2167
2168    /// Get the text content of a text node, or `None` if the node is not a text node
2169    #[must_use] pub fn get_string_contents(&self, node_id: DomNodeId) -> Option<AzString> {
2170        let layout_window = self.get_layout_window();
2171        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2172        let node_id_internal = node_id.node.into_crate_internal()?;
2173        let node_data_cont = layout_result.styled_dom.node_data.as_container();
2174        let node_data = node_data_cont.get(node_id_internal)?;
2175
2176        if let NodeType::Text(text) = node_data.get_node_type() {
2177            Some(text.clone_self())
2178        } else {
2179            None
2180        }
2181    }
2182
2183    /// Get the tag name of a node (e.g., "div", "p", "span")
2184    ///
2185    /// Returns the HTML tag name as a string for the given node.
2186    /// For text nodes, returns "text". For image nodes, returns "img".
2187    #[must_use] pub fn get_node_tag_name(&self, node_id: DomNodeId) -> Option<AzString> {
2188        let layout_window = self.get_layout_window();
2189        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2190        let node_id_internal = node_id.node.into_crate_internal()?;
2191        let node_data_cont = layout_result.styled_dom.node_data.as_container();
2192        let node_data = node_data_cont.get(node_id_internal)?;
2193
2194        let tag = node_data.get_node_type().get_path();
2195        Some(tag.to_string().into())
2196    }
2197
2198    /// Get an attribute value from a node by attribute name
2199    ///
2200    /// # Arguments
2201    /// * `node_id` - The node to query
2202    /// * `attr_name` - The attribute name (e.g., "id", "class", "href", "data-custom", "aria-label")
2203    ///
2204    /// Returns the attribute value if found, None otherwise.
2205    /// This searches the strongly-typed `AttributeVec` on the node.
2206    // Cross-type AttributeType payload dispatch: each `(attr_name, AttributeType::X(v))`
2207    // arm binds a differently-typed `v`, so the same-bodied arms can't be merged into
2208    // one or-pattern (won't type-check) — they are intentionally one-per-attribute.
2209    #[allow(clippy::match_same_arms)]
2210    #[must_use] pub fn get_node_attribute(&self, node_id: DomNodeId, attr_name: &str) -> Option<AzString> {
2211        use azul_core::dom::AttributeType;
2212
2213        let layout_window = self.get_layout_window();
2214        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2215        let node_id_internal = node_id.node.into_crate_internal()?;
2216        let node_data_cont = layout_result.styled_dom.node_data.as_container();
2217        let node_data = node_data_cont.get(node_id_internal)?;
2218
2219        // Check the strongly-typed attributes vec
2220        for attr in node_data.attributes().as_ref() {
2221            match (attr_name, attr) {
2222                ("id", AttributeType::Id(v)) => return Some(v.clone()),
2223                ("class", AttributeType::Class(v)) => return Some(v.clone()),
2224                ("aria-label", AttributeType::AriaLabel(v)) => return Some(v.clone()),
2225                ("aria-labelledby", AttributeType::AriaLabelledBy(v)) => return Some(v.clone()),
2226                ("aria-describedby", AttributeType::AriaDescribedBy(v)) => return Some(v.clone()),
2227                ("role", AttributeType::AriaRole(v)) => return Some(v.clone()),
2228                ("href", AttributeType::Href(v)) => return Some(v.clone()),
2229                ("rel", AttributeType::Rel(v)) => return Some(v.clone()),
2230                ("target", AttributeType::Target(v)) => return Some(v.clone()),
2231                ("src", AttributeType::Src(v)) => return Some(v.clone()),
2232                ("alt", AttributeType::Alt(v)) => return Some(v.clone()),
2233                ("title", AttributeType::Title(v)) => return Some(v.clone()),
2234                ("name", AttributeType::Name(v)) => return Some(v.clone()),
2235                ("value", AttributeType::Value(v)) => return Some(v.clone()),
2236                ("type", AttributeType::InputType(v)) => return Some(v.clone()),
2237                ("placeholder", AttributeType::Placeholder(v)) => return Some(v.clone()),
2238                ("max", AttributeType::Max(v)) => return Some(v.clone()),
2239                ("min", AttributeType::Min(v)) => return Some(v.clone()),
2240                ("step", AttributeType::Step(v)) => return Some(v.clone()),
2241                ("pattern", AttributeType::Pattern(v)) => return Some(v.clone()),
2242                ("autocomplete", AttributeType::Autocomplete(v)) => return Some(v.clone()),
2243                ("scope", AttributeType::Scope(v)) => return Some(v.clone()),
2244                ("lang", AttributeType::Lang(v)) => return Some(v.clone()),
2245                ("dir", AttributeType::Dir(v)) => return Some(v.clone()),
2246                ("required", AttributeType::Required) => return Some("true".into()),
2247                ("disabled", AttributeType::Disabled) => return Some("true".into()),
2248                ("readonly", AttributeType::Readonly) => return Some("true".into()),
2249                ("checked", AttributeType::CheckedTrue) => return Some("true".into()),
2250                ("checked", AttributeType::CheckedFalse) => return Some("false".into()),
2251                ("selected", AttributeType::Selected) => return Some("true".into()),
2252                ("hidden", AttributeType::Hidden) => return Some("true".into()),
2253                ("focusable", AttributeType::Focusable) => return Some("true".into()),
2254                ("minlength", AttributeType::MinLength(v)) => return Some(v.to_string().into()),
2255                ("maxlength", AttributeType::MaxLength(v)) => return Some(v.to_string().into()),
2256                ("colspan", AttributeType::ColSpan(v)) => return Some(v.to_string().into()),
2257                ("rowspan", AttributeType::RowSpan(v)) => return Some(v.to_string().into()),
2258                ("tabindex", AttributeType::TabIndex(v)) => return Some(v.to_string().into()),
2259                ("contenteditable", AttributeType::ContentEditable(v)) => {
2260                    return Some(v.to_string().into())
2261                }
2262                ("draggable", AttributeType::Draggable(v)) => return Some(v.to_string().into()),
2263                // Handle data-* attributes
2264                (name, AttributeType::Data(nv))
2265                    if name.starts_with("data-") && nv.attr_name.as_str() == &name[5..] =>
2266                {
2267                    return Some(nv.value.clone());
2268                }
2269                // Handle aria-* state/property attributes
2270                (name, AttributeType::AriaState(nv))
2271                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
2272                {
2273                    return Some(nv.value.clone());
2274                }
2275                (name, AttributeType::AriaProperty(nv))
2276                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
2277                {
2278                    return Some(nv.value.clone());
2279                }
2280                // Handle custom attributes
2281                (name, AttributeType::Custom(nv)) if nv.attr_name.as_str() == name => {
2282                    return Some(nv.value.clone());
2283                }
2284                _ => {}
2285            }
2286        }
2287
2288        None
2289    }
2290
2291    /// Get all classes of a node as a vector of strings
2292    #[must_use] pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
2293        let Some(layout_window) = self.get_layout_window().get_layout_result(&node_id.dom) else {
2294            return StringVec::from_const_slice(&[]);
2295        };
2296        let Some(node_id_internal) = node_id.node.into_crate_internal() else {
2297            return StringVec::from_const_slice(&[]);
2298        };
2299        let node_data_cont = layout_window.styled_dom.node_data.as_container();
2300        let Some(node_data) = node_data_cont.get(node_id_internal) else {
2301            return StringVec::from_const_slice(&[]);
2302        };
2303
2304        let classes: Vec<AzString> = node_data
2305            .attributes()
2306            .as_ref()
2307            .iter()
2308            .filter_map(|attr| {
2309                attr.as_class().map(|c| c.to_string().into())
2310            })
2311            .collect();
2312
2313        StringVec::from(classes)
2314    }
2315
2316    /// Get the ID attribute of a node (if it has one)
2317    #[must_use] pub fn get_node_id(&self, node_id: DomNodeId) -> Option<AzString> {
2318        let layout_window = self.get_layout_window();
2319        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2320        let node_id_internal = node_id.node.into_crate_internal()?;
2321        let node_data_cont = layout_result.styled_dom.node_data.as_container();
2322        let node_data = node_data_cont.get(node_id_internal)?;
2323
2324        for attr in node_data.attributes().as_ref() {
2325            if let Some(id) = attr.as_id() {
2326                return Some(id.to_string().into());
2327            }
2328        }
2329        None
2330    }
2331
2332    // Text Selection Management
2333
2334    /// Get the current selection state for a DOM (via `multi_cursor`)
2335    #[must_use] pub const fn get_selection(&self, _dom_id: &DomId) -> Option<&SelectionState> {
2336        // SelectionManager removed; multi_cursor is the source of truth.
2337        // SelectionState is a legacy type; return None.
2338        None
2339    }
2340
2341    /// Check if a DOM has any selection (via `multi_cursor`)
2342    #[must_use] pub fn has_selection(&self, _dom_id: &DomId) -> bool {
2343        self.get_layout_window()
2344            .text_edit_manager.multi_cursor.as_ref()
2345            .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
2346    }
2347
2348    /// Get the primary cursor for a DOM (via `multi_cursor`)
2349    #[must_use] pub fn get_primary_cursor(&self, _dom_id: &DomId) -> Option<TextCursor> {
2350        self.get_layout_window()
2351            .text_edit_manager.multi_cursor.as_ref()
2352            .and_then(azul_core::selection::MultiCursorState::get_primary_cursor)
2353    }
2354
2355    /// Get all selection ranges (excludes plain cursors, via `multi_cursor`)
2356    #[must_use] pub fn get_selection_ranges(&self, _dom_id: &DomId) -> SelectionRangeVec {
2357        let ranges: Vec<SelectionRange> = self.get_layout_window()
2358            .text_edit_manager.multi_cursor.as_ref()
2359            .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
2360                Selection::Range(r) => Some(*r),
2361                Selection::Cursor(_) => None,
2362            }).collect()).unwrap_or_default();
2363        ranges.into()
2364    }
2365
2366    /// Get direct access to the text layout cache
2367    ///
2368    /// Note: This provides direct read-only access to the text layout cache, but you need
2369    /// to know the `CacheId` for the specific text node you want. Currently there's
2370    /// no direct mapping from `NodeId` to `CacheId` exposed in the public API.
2371    ///
2372    /// For text modifications, use `CallbackChange` transactions:
2373    /// - `change_node_text()` for changing text content
2374    /// - `set_selection()` for setting selections
2375    /// - `get_selection()`, `get_primary_cursor()` for reading selections
2376    ///
2377    /// Future: Add `NodeId` -> `CacheId` mapping to enable node-specific layout access
2378    #[must_use] pub const fn get_text_cache(&self) -> &TextLayoutCache {
2379        &self.get_layout_window().text_cache
2380    }
2381
2382    // Window State Access
2383
2384    /// Get full current window state (immutable reference)
2385    #[must_use] pub const fn get_current_window_state(&self) -> &FullWindowState {
2386        // SAFETY: current_window_state is a valid pointer for the lifetime of CallbackInfo
2387        unsafe { (*self.ref_data).current_window_state }
2388    }
2389
2390    /// Get current window flags
2391    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
2392        self.get_current_window_state().flags
2393    }
2394
2395    /// Get current keyboard state
2396    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
2397        self.get_current_window_state().keyboard_state.clone()
2398    }
2399
2400    /// Get current mouse state
2401    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
2402        self.get_current_window_state().mouse_state
2403    }
2404
2405    /// Get full previous window state (immutable reference)
2406    #[must_use] pub const fn get_previous_window_state(&self) -> &Option<FullWindowState> {
2407        unsafe { (*self.ref_data).previous_window_state }
2408    }
2409
2410    /// Get previous window flags
2411    #[must_use] pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
2412        Some(self.get_previous_window_state().as_ref()?.flags)
2413    }
2414
2415    /// Get previous keyboard state
2416    #[must_use] pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
2417        Some(
2418            self.get_previous_window_state()
2419                .as_ref()?
2420                .keyboard_state
2421                .clone(),
2422        )
2423    }
2424
2425    /// Get previous mouse state
2426    #[must_use] pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
2427        Some(
2428            self.get_previous_window_state()
2429                .as_ref()?
2430                .mouse_state,
2431        )
2432    }
2433
2434    // Cursor and Input
2435
2436    #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
2437        use azul_core::geom::{CursorNodePosition, OptionCursorNodePosition};
2438        match self.cursor_relative_to_item {
2439            OptionLogicalPosition::Some(p) => OptionCursorNodePosition::Some(CursorNodePosition::from_logical(p)),
2440            OptionLogicalPosition::None => OptionCursorNodePosition::None,
2441        }
2442    }
2443
2444    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
2445        self.cursor_in_viewport
2446    }
2447
2448    /// Get cursor position in virtual screen coordinates (all monitors combined).
2449    ///
2450    /// Computed as: `window_position + cursor_position_in_window`.
2451    /// All coordinates are in logical pixels (HiDPI-independent on macOS; on Win32
2452    /// this depends on DPI-awareness mode).
2453    ///
2454    /// The origin (0, 0) is at the **top-left of the primary monitor**.
2455    /// Y increases downward.  On multi-monitor setups, coordinates may be negative
2456    /// for monitors to the left of or above the primary monitor.
2457    ///
2458    /// Returns `None` if the cursor is outside the window or the window position
2459    /// is unknown.
2460    ///
2461    /// ## Platform notes
2462    ///
2463    /// | Platform | Accuracy |
2464    /// |----------|----------|
2465    /// | **macOS**   | Exact (points = logical pixels) |
2466    /// | **Win32**   | Exact when DPI-aware; approximate otherwise |
2467    /// | **X11**     | Exact (pixels) |
2468    /// | **Wayland** | Falls back to window-local (compositor hides global position) |
2469    #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/counter/fixed-point cast
2470    #[must_use] pub fn get_cursor_position_screen(&self) -> azul_core::geom::OptionScreenPosition {
2471        use azul_core::window::WindowPosition;
2472        use azul_core::geom::{LogicalPosition, ScreenPosition, OptionScreenPosition};
2473
2474        let ws = self.get_current_window_state();
2475        let Some(cursor_local) = ws.mouse_state.cursor_position.get_position() else {
2476            return OptionScreenPosition::None;
2477        };
2478        match ws.position {
2479            WindowPosition::Initialized(pos) => {
2480                OptionScreenPosition::Some(ScreenPosition::new(
2481                    pos.x as f32 + cursor_local.x,
2482                    pos.y as f32 + cursor_local.y,
2483                ))
2484            }
2485            // Wayland / relative-to-parent: absolute screen position unknown here
2486            // (relative needs the parent's screen pos), fall back to window-local.
2487            WindowPosition::Uninitialized | WindowPosition::RelativeToParentWindow(_) => {
2488                OptionScreenPosition::Some(ScreenPosition::new(cursor_local.x, cursor_local.y))
2489            }
2490        }
2491    }
2492
2493    /// Get the drag delta in window-local coordinates.
2494    ///
2495    /// Returns the offset from drag start to current cursor position in window-local
2496    /// logical pixels. Returns `None` if no drag is active.
2497    ///
2498    /// **Warning**: This is NOT stable during window moves (titlebar drag).
2499    /// Use `get_drag_delta_screen()` for titlebar dragging.
2500    #[must_use] pub fn get_drag_delta(&self) -> azul_core::geom::OptionDragDelta {
2501        use azul_core::geom::{DragDelta, OptionDragDelta};
2502        let gm = self.get_gesture_drag_manager();
2503        match gm.get_drag_delta() {
2504            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2505            None => OptionDragDelta::None,
2506        }
2507    }
2508
2509    /// Get the drag delta in screen coordinates.
2510    ///
2511    /// Unlike `get_drag_delta()`, this is stable even when the window moves
2512    /// (e.g., during titlebar drag). Returns `None` if no drag is active.
2513    /// On Wayland: falls back to window-local delta.
2514    #[must_use] pub fn get_drag_delta_screen(&self) -> azul_core::geom::OptionDragDelta {
2515        use azul_core::geom::{DragDelta, OptionDragDelta};
2516        let gm = self.get_gesture_drag_manager();
2517        match gm.get_drag_delta_screen() {
2518            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2519            None => OptionDragDelta::None,
2520        }
2521    }
2522
2523    /// Get the **incremental** (frame-to-frame) drag delta in screen coordinates.
2524    ///
2525    /// Returns the screen-space delta between the current and previous sample
2526    /// (not the total delta since drag start). Use this with the current window
2527    /// position for robust titlebar drag:
2528    ///
2529    /// ```text
2530    /// new_pos = current_window_pos + incremental_delta
2531    /// ```
2532    ///
2533    /// This handles external position changes (DPI change, OS clamping, compositor
2534    /// resize) that would make the initial position stale.
2535    /// Returns `None` if no drag is active or fewer than 2 samples exist.
2536    #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> azul_core::geom::OptionDragDelta {
2537        use azul_core::geom::{DragDelta, OptionDragDelta};
2538        let gm = self.get_gesture_drag_manager();
2539        match gm.get_drag_delta_screen_incremental() {
2540            Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2541            None => OptionDragDelta::None,
2542        }
2543    }
2544
2545    #[must_use] pub const fn get_current_window_handle(&self) -> RawWindowHandle {
2546        unsafe { *(*self.ref_data).current_window_handle }
2547    }
2548
2549    /// Get the system style (for menu rendering, CSD, etc.)
2550    /// This is useful for creating custom menus or other system-styled UI.
2551    #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
2552        unsafe { (*self.ref_data).system_style.clone() }
2553    }
2554
2555    /// Get a snapshot of all monitors available on the system.
2556    ///
2557    /// The returned `MonitorVec` is cloned from the shared monitor cache.
2558    /// The cache is initialized once at app start and updated by the platform
2559    /// layer on monitor topology changes. No OS calls are made here.
2560    #[must_use] pub fn get_monitors(&self) -> MonitorVec {
2561        let monitors_arc = unsafe { &(*self.ref_data).monitors };
2562        monitors_arc.lock().map_or_else(|_| MonitorVec::from_const_slice(&[]), |g| g.clone())
2563    }
2564
2565    /// Get the monitor that the current window is on, if known.
2566    ///
2567    /// Uses `FullWindowState::monitor_id` (set by the platform layer) to find
2568    /// the matching monitor in the cached monitor list. Returns `None` if the
2569    /// monitor ID is not set or no matching monitor is found.
2570    #[must_use] pub fn get_current_monitor(&self) -> OptionMonitor {
2571        let ws = self.get_current_window_state();
2572        let monitor_index = match ws.monitor_id {
2573            azul_css::corety::OptionU32::Some(idx) => idx as usize,
2574            azul_css::corety::OptionU32::None => return OptionMonitor::None,
2575        };
2576        let monitors_arc = unsafe { &(*self.ref_data).monitors };
2577        let Ok(guard) = monitors_arc.lock() else {
2578            return OptionMonitor::None;
2579        };
2580        for m in guard.as_ref() {
2581            if m.monitor_id.index == monitor_index {
2582                return OptionMonitor::Some(m.clone());
2583            }
2584        }
2585        OptionMonitor::None
2586    }
2587
2588    // ==================== ICU4X Internationalization API ====================
2589    //
2590    // All formatting functions take a locale string (BCP 47 format) as the first
2591    // parameter, allowing dynamic language switching per-call.
2592    //
2593    // For date/time construction, use the static methods on IcuDate, IcuTime, IcuDateTime:
2594    // - IcuDate::now(), IcuDate::now_utc(), IcuDate::new(year, month, day)
2595    // - IcuTime::now(), IcuTime::now_utc(), IcuTime::new(hour, minute, second)
2596    // - IcuDateTime::now(), IcuDateTime::now_utc(), IcuDateTime::from_timestamp(secs)
2597
2598    /// Get the ICU localizer cache for internationalized formatting.
2599    ///
2600    /// The cache stores localizers for multiple locales. Each locale's formatter
2601    /// is lazily created on first use and cached for subsequent calls.
2602    #[cfg(feature = "icu")]
2603    pub fn get_icu_localizer(&self) -> &IcuLocalizerHandle {
2604        unsafe { &(*self.ref_data).icu_localizer }
2605    }
2606
2607    /// Format an integer with locale-appropriate grouping separators.
2608    ///
2609    /// # Arguments
2610    /// * `locale` - BCP 47 locale string (e.g., "en-US", "de-DE", "ja-JP")
2611    /// * `value` - The integer to format
2612    ///
2613    /// # Example
2614    /// ```rust,ignore
2615    /// info.format_integer("en-US", 1234567) // -> "1,234,567"
2616    /// info.format_integer("de-DE", 1234567) // -> "1.234.567"
2617    /// info.format_integer("fr-FR", 1234567) // -> "1 234 567"
2618    /// ```
2619    #[cfg(feature = "icu")]
2620    pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
2621        self.get_icu_localizer().format_integer(locale, value)
2622    }
2623
2624    /// Format a decimal number with locale-appropriate separators.
2625    ///
2626    /// # Arguments
2627    /// * `locale` - BCP 47 locale string
2628    /// * `integer_part` - The full integer value (e.g., 123456 for 1234.56)
2629    /// * `decimal_places` - Number of decimal places (e.g., 2 for 1234.56)
2630    ///
2631    /// # Example
2632    /// ```rust,ignore
2633    /// info.format_decimal("en-US", 123456, 2) // -> "1,234.56"
2634    /// info.format_decimal("de-DE", 123456, 2) // -> "1.234,56"
2635    /// ```
2636    #[cfg(feature = "icu")]
2637    pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
2638        self.get_icu_localizer().format_decimal(locale, integer_part, decimal_places)
2639    }
2640
2641    /// Get the plural category for a number (cardinal: "1 item", "2 items").
2642    ///
2643    /// # Arguments
2644    /// * `locale` - BCP 47 locale string
2645    /// * `value` - The number to get the plural category for
2646    ///
2647    /// # Example
2648    /// ```rust,ignore
2649    /// info.get_plural_category("en", 1)  // -> PluralCategory::One
2650    /// info.get_plural_category("en", 2)  // -> PluralCategory::Other
2651    /// info.get_plural_category("pl", 2)  // -> PluralCategory::Few
2652    /// info.get_plural_category("pl", 5)  // -> PluralCategory::Many
2653    /// ```
2654    #[cfg(feature = "icu")]
2655    pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
2656        self.get_icu_localizer().get_plural_category(locale, value)
2657    }
2658
2659    /// Select the appropriate string based on plural rules.
2660    ///
2661    /// # Arguments
2662    /// * `locale` - BCP 47 locale string
2663    /// * `value` - The number to pluralize
2664    /// * `zero`, `one`, `two`, `few`, `many`, `other` - Strings for each category
2665    ///
2666    /// # Example
2667    /// ```rust,ignore
2668    /// info.pluralize("en", count, "no items", "1 item", "2 items", "{} items", "{} items", "{} items")
2669    /// info.pluralize("pl", count, "brak", "1 element", "2 elementy", "{} elementy", "{} elementów", "{} elementów")
2670    /// ```
2671    #[cfg(feature = "icu")]
2672    pub fn pluralize(
2673        &self,
2674        locale: &str,
2675        value: i64,
2676        zero: &str,
2677        one: &str,
2678        two: &str,
2679        few: &str,
2680        many: &str,
2681        other: &str,
2682    ) -> AzString {
2683        self.get_icu_localizer().pluralize(locale, value, zero, one, two, few, many, other)
2684    }
2685
2686    /// Format a list of items with locale-appropriate conjunctions.
2687    ///
2688    /// # Arguments
2689    /// * `locale` - BCP 47 locale string
2690    /// * `items` - The items to format as a list
2691    /// * `list_type` - And, Or, or Unit list type
2692    ///
2693    /// # Example
2694    /// ```rust,ignore
2695    /// info.format_list("en-US", &items, ListType::And) // -> "A, B, and C"
2696    /// info.format_list("es-ES", &items, ListType::And) // -> "A, B y C"
2697    /// ```
2698    #[cfg(feature = "icu")]
2699    pub fn format_list(&self, locale: &str, items: StringVec, list_type: ListType) -> AzString {
2700        self.get_icu_localizer()
2701            .format_list(locale, items.as_ref(), list_type)
2702    }
2703
2704    /// Format a date according to the specified locale.
2705    ///
2706    /// # Arguments
2707    /// * `locale` - BCP 47 locale string
2708    /// * `date` - The date to format (use IcuDate::now() or IcuDate::new())
2709    /// * `length` - Short, Medium, or Long format
2710    ///
2711    /// # Example
2712    /// ```rust,ignore
2713    /// let today = IcuDate::now();
2714    /// info.format_date("en-US", today, FormatLength::Medium) // -> "Jan 15, 2025"
2715    /// info.format_date("de-DE", today, FormatLength::Medium) // -> "15.01.2025"
2716    /// ```
2717    #[cfg(feature = "icu")]
2718    pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
2719        self.get_icu_localizer().format_date(locale, date, length)
2720    }
2721
2722    /// Format a time according to the specified locale.
2723    ///
2724    /// # Arguments
2725    /// * `locale` - BCP 47 locale string
2726    /// * `time` - The time to format (use IcuTime::now() or IcuTime::new())
2727    /// * `include_seconds` - Whether to include seconds in the output
2728    ///
2729    /// # Example
2730    /// ```rust,ignore
2731    /// let now = IcuTime::now();
2732    /// info.format_time("en-US", now, false) // -> "4:30 PM"
2733    /// info.format_time("de-DE", now, false) // -> "16:30"
2734    /// ```
2735    #[cfg(feature = "icu")]
2736    pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
2737        self.get_icu_localizer().format_time(locale, time, include_seconds)
2738    }
2739
2740    /// Format a date and time according to the specified locale.
2741    ///
2742    /// # Arguments
2743    /// * `locale` - BCP 47 locale string
2744    /// * `datetime` - The date and time to format (use IcuDateTime::now())
2745    /// * `length` - Short, Medium, or Long format
2746    #[cfg(feature = "icu")]
2747    pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
2748        self.get_icu_localizer().format_datetime(locale, datetime, length)
2749    }
2750
2751    /// Compare two strings according to locale-specific collation rules.
2752    ///
2753    /// Returns -1 if a < b, 0 if a == b, 1 if a > b.
2754    /// This is useful for locale-aware sorting where "Ä" should sort with "A" in German.
2755    ///
2756    /// # Arguments
2757    /// * `locale` - BCP 47 locale string
2758    /// * `a` - First string to compare
2759    /// * `b` - Second string to compare
2760    ///
2761    /// # Example
2762    /// ```rust,ignore
2763    /// info.compare_strings("de-DE", "Äpfel", "Banane") // -> -1 (Ä sorts with A)
2764    /// info.compare_strings("sv-SE", "Äpple", "Öl")     // -> -1 (Swedish: Ä before Ö)
2765    /// ```
2766    #[cfg(feature = "icu")]
2767    pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
2768        self.get_icu_localizer().compare_strings(locale, a, b)
2769    }
2770
2771    /// Sort a list of strings using locale-aware collation.
2772    ///
2773    /// This properly handles accented characters, case sensitivity, and
2774    /// language-specific sorting rules.
2775    ///
2776    /// # Arguments
2777    /// * `locale` - BCP 47 locale string
2778    /// * `strings` - The strings to sort
2779    ///
2780    /// # Example
2781    /// ```rust,ignore
2782    /// let sorted = info.sort_strings("de-DE", &["Österreich", "Andorra", "Ägypten"]);
2783    /// // Result: ["Ägypten", "Andorra", "Österreich"] (Ä sorts with A, Ö with O)
2784    /// ```
2785    #[cfg(feature = "icu")]
2786    pub fn sort_strings(&self, locale: &str, strings: StringVec) -> IcuStringVec {
2787        self.get_icu_localizer()
2788            .sort_strings(locale, strings.as_ref())
2789    }
2790
2791    /// Check if two strings are equal according to locale collation rules.
2792    ///
2793    /// This may return `true` for strings that differ in case or accents,
2794    /// depending on the collation strength.
2795    ///
2796    /// # Arguments
2797    /// * `locale` - BCP 47 locale string
2798    /// * `a` - First string to compare
2799    /// * `b` - Second string to compare
2800    #[cfg(feature = "icu")]
2801    pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
2802        self.get_icu_localizer().strings_equal(locale, a, b)
2803    }
2804
2805    /// Get the current cursor position in logical coordinates relative to the window
2806    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
2807        self.cursor_in_viewport.into_option()
2808    }
2809
2810    /// Get the layout rectangle of the currently hit node (in logical coordinates)
2811    #[must_use] pub fn get_hit_node_layout_rect(&self) -> Option<LogicalRect> {
2812        self.get_layout_window()
2813            .get_node_layout_rect(self.hit_dom_node)
2814    }
2815
2816    // Css Property Access
2817
2818    /// Get the computed CSS property for a specific DOM node
2819    ///
2820    /// This queries the CSS property cache and returns the resolved property value
2821    /// for the given node, taking into account:
2822    /// - User overrides (from callbacks)
2823    /// - Node state (:hover, :active, :focus)
2824    /// - CSS rules from stylesheets
2825    /// - Cascaded properties from parents
2826    /// - Inline styles
2827    ///
2828    /// # Arguments
2829    /// * `node_id` - The DOM node to query
2830    /// * `property_type` - The CSS property type to retrieve
2831    ///
2832    /// # Returns
2833    /// * `Some(CssProperty)` if the property is set on this node
2834    /// * `None` if the property is not set (will use default value)
2835    #[must_use] pub fn get_computed_css_property(
2836        &self,
2837        node_id: DomNodeId,
2838        property_type: CssPropertyType,
2839    ) -> Option<CssProperty> {
2840        let layout_window = self.get_layout_window();
2841
2842        // Get the layout result for this DOM
2843        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
2844
2845        // Get the styled DOM
2846        let styled_dom = &layout_result.styled_dom;
2847
2848        // Convert DomNodeId to NodeId using proper decoding
2849        let internal_node_id = node_id.node.into_crate_internal()?;
2850
2851        // Get the node data
2852        let node_data_container = styled_dom.node_data.as_container();
2853        let node_data = node_data_container.get(internal_node_id)?;
2854
2855        // Get the styled node state
2856        let styled_nodes_container = styled_dom.styled_nodes.as_container();
2857        let styled_node = styled_nodes_container.get(internal_node_id)?;
2858        let node_state = &styled_node.styled_node_state;
2859
2860        // Query the CSS property cache
2861        let css_property_cache = &styled_dom.css_property_cache.ptr;
2862        css_property_cache
2863            .get_property(node_data, &internal_node_id, node_state, &property_type)
2864            .cloned()
2865    }
2866
2867    /// Get the computed width of a node from CSS
2868    ///
2869    /// Convenience method for getting the CSS width property.
2870    #[must_use] pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
2871        self.get_computed_css_property(node_id, CssPropertyType::Width)
2872    }
2873
2874    /// Get the computed height of a node from CSS
2875    ///
2876    /// Convenience method for getting the CSS height property.
2877    #[must_use] pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
2878        self.get_computed_css_property(node_id, CssPropertyType::Height)
2879    }
2880
2881    // System Callbacks
2882
2883    #[must_use] pub const fn get_system_time_fn(&self) -> GetSystemTimeCallback {
2884        unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
2885    }
2886
2887    #[must_use] pub fn get_current_time(&self) -> task::Instant {
2888        let cb = self.get_system_time_fn();
2889        (cb.cb)()
2890    }
2891
2892    /// Get immutable reference to the renderer resources
2893    ///
2894    /// This provides access to fonts, images, and other rendering resources.
2895    /// Useful for custom rendering or screenshot functionality.
2896    #[must_use] pub const fn get_renderer_resources(&self) -> &RendererResources {
2897        unsafe { (*self.ref_data).renderer_resources }
2898    }
2899
2900    // Font Cache Introspection
2901    //
2902    // These let a callback discover and retrieve the fonts the layout engine
2903    // has actually loaded into its font cache, without having to pass them in
2904    // up-front. The primary use case is "embed every font the layout actually
2905    // used" from a callback (e.g. a printpdf consumer correlating
2906    // `DisplayListItem::Text.font_hash` glyph runs with the loaded font bytes).
2907    //
2908    // IMAGE GAP (deferred): there is no analogous `get_loaded_image_*` here yet.
2909    // Unlike fonts (whose cache lives in `LayoutWindow.font_manager`, reachable
2910    // via `get_layout_window()`), the live image cache with the actual
2911    // `ImageRef` bytes is owned by the windowing shell (`common.image_cache`)
2912    // and is only passed *by reference* into the layout pass - it is not stored
2913    // on the `LayoutWindow` (its `image_cache` field is initialised empty and
2914    // never populated) and is not part of `CallbackInfoRefData`. Exposing image
2915    // bytes here therefore requires threading `&ImageCache` into
2916    // `CallbackInfoRefData` and through `invoke_single_callback` /
2917    // `run_single_timer` / `run_all_threads` and all their per-platform callers
2918    // (macOS / Wayland / X11 / Windows). `RendererResources.currently_registered_images`
2919    // is reachable via `get_renderer_resources()` but only carries the WebRender
2920    // key + `ImageDescriptor`, not the pixel bytes.
2921
2922    /// Enumerate every font the layout engine currently has loaded in its font
2923    /// cache.
2924    ///
2925    /// Returns one [`LoadedFont`](azul_core::resources::LoadedFont) descriptor
2926    /// per loaded face. The `font_hash` field of each descriptor is identical
2927    /// to the `font_hash` carried by `DisplayListItem::Text` glyph runs, so a
2928    /// callback can correlate a loaded font with the text that uses it and then
2929    /// pull the raw bytes via [`get_loaded_font_bytes`](Self::get_loaded_font_bytes).
2930    ///
2931    /// The list includes fallback faces that were resolved during layout, not
2932    /// just the families named in the source CSS.
2933    #[cfg(feature = "text_layout")]
2934    #[must_use] pub fn get_loaded_fonts(&self) -> LoadedFontVec {
2935        let font_manager = &self.get_layout_window().font_manager;
2936        let Ok(guard) = font_manager.parsed_fonts.lock() else {
2937            return Vec::new().into();
2938        };
2939        // BTreeMap-style stable iteration is not guaranteed here (HashMap), so
2940        // we collect then sort by font_hash for a deterministic order.
2941        let mut out: Vec<LoadedFont> = guard
2942            .values()
2943            .map(|font_ref| {
2944                let parsed = crate::font_ref_to_parsed_font(font_ref);
2945                let family_name = parsed
2946                    .font_name
2947                    .as_ref()
2948                    .map(|s| AzString::from(s.clone()))
2949                    .unwrap_or_default();
2950                LoadedFont {
2951                    font_hash: parsed.hash,
2952                    family_name,
2953                    num_glyphs: u32::from(parsed.num_glyphs),
2954                    has_bytes: parsed.source_bytes_for_subset().is_some(),
2955                }
2956            })
2957            .collect();
2958        out.sort_by(|a, b| a.font_hash.cmp(&b.font_hash));
2959        out.into()
2960    }
2961
2962    /// Retrieve the raw source bytes (TTF / OTF / TTC, etc.) for a loaded font,
2963    /// looked up by the `font_hash` returned from
2964    /// [`get_loaded_fonts`](Self::get_loaded_fonts) (or carried on a
2965    /// `DisplayListItem::Text` glyph run).
2966    ///
2967    /// Returns `None` if no loaded font matches `font_hash`, or if the matching
2968    /// font did not retain its source bytes (e.g. a test-only font; production
2969    /// fonts loaded from disk retain an mmap-backed handle and always succeed).
2970    /// The returned bytes can be embedded directly into a generated document.
2971    #[cfg(feature = "text_layout")]
2972    #[must_use] pub fn get_loaded_font_bytes(&self, font_hash: u64) -> OptionU8Vec {
2973        let font_manager = &self.get_layout_window().font_manager;
2974        let Some(font_ref) = font_manager.get_font_by_hash(font_hash) else {
2975            return OptionU8Vec::None;
2976        };
2977        let parsed = crate::font_ref_to_parsed_font(&font_ref);
2978        parsed.source_bytes_for_subset().map_or_else(|| OptionU8Vec::None, |bytes| OptionU8Vec::Some(U8Vec::from_vec(bytes.as_slice().to_vec())))
2979    }
2980
2981    // Screenshot API
2982
2983    /// Take a CPU-rendered screenshot of the current window content
2984    ///
2985    /// This renders the current display list to a PNG image using CPU rendering.
2986    /// The screenshot captures the window content as it would appear on screen,
2987    /// without window decorations.
2988    ///
2989    /// # Arguments
2990    /// * `dom_id` - The DOM to screenshot (use the main DOM ID for the full window)
2991    ///
2992    /// # Returns
2993    /// * `Ok(Vec<u8>)` - PNG-encoded image data
2994    /// * `Err(String)` - Error message if rendering failed
2995    ///
2996    /// # Example
2997    /// ```ignore
2998    /// fn on_click(info: &mut CallbackInfo) -> Update {
2999    ///     let dom_id = info.get_hit_node().dom;
3000    ///     match info.take_screenshot(dom_id) {
3001    ///         Ok(png_data) => {
3002    ///             std::fs::write("screenshot.png", png_data).unwrap();
3003    ///         }
3004    ///         Err(e) => eprintln!("Screenshot failed: {}", e),
3005    ///     }
3006    ///     Update::DoNothing
3007    /// }
3008    /// ```
3009    #[cfg(feature = "cpurender")]
3010    /// # Errors
3011    ///
3012    /// Returns an error message if the screenshot cannot be captured or encoded.
3013    pub fn take_screenshot(&self, dom_id: DomId) -> Result<alloc::vec::Vec<u8>, AzString> {
3014        use crate::cpurender::{render_with_font_manager_and_scroll, CpuRenderState, RenderOptions, ScrollOffsetMap};
3015
3016        let layout_window = self.get_layout_window();
3017        let renderer_resources = &layout_window.renderer_resources;
3018
3019        // Get the layout result for this DOM
3020        let layout_result = layout_window
3021            .layout_results
3022            .get(&dom_id)
3023            .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
3024
3025        // Use the current window state dimensions
3026        let ws = self.get_current_window_state();
3027        let width = ws.size.dimensions.width;
3028        let height = ws.size.dimensions.height;
3029
3030        if width <= 0.0 || height <= 0.0 {
3031            return Err(AzString::from("Invalid viewport dimensions"));
3032        }
3033
3034        let display_list = &layout_result.display_list;
3035        let dpi_factor = ws.size.get_hidpi_factor().inner.get();
3036
3037        // Build scroll offset map from the current ScrollManager state
3038        let scroll_offsets = layout_window.scroll_manager
3039            .build_scroll_offset_map(dom_id, &layout_result.scroll_ids);
3040
3041        // Build CPU render state from GpuValueCache - provides current
3042        // transform values (scrollbar thumb positions) and opacity values
3043        // (scrollbar visibility fading) that the GPU path animates dynamically.
3044        let gpu_cache = layout_window.gpu_state_manager
3045            .get_cache(dom_id);
3046        let render_state = CpuRenderState::from_gpu_cache(
3047            gpu_cache,
3048            dom_id,
3049            &scroll_offsets,
3050        )
3051        .with_system_style(layout_window.system_style.clone());
3052
3053        let opts = RenderOptions {
3054            width,
3055            height,
3056            dpi_factor,
3057        };
3058
3059        let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
3060        let pixmap = render_with_font_manager_and_scroll(
3061            display_list,
3062            renderer_resources,
3063            &layout_window.font_manager,
3064            opts,
3065            &mut glyph_cache,
3066            &render_state,
3067        ).map_err(AzString::from)?;
3068
3069        // Encode to PNG
3070        let png_data = pixmap
3071            .encode_png()
3072            .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {e}")))?;
3073
3074        Ok(png_data)
3075    }
3076
3077    /// Take a screenshot and save it directly to a file
3078    ///
3079    /// Convenience method that combines `take_screenshot` with file writing.
3080    ///
3081    /// # Arguments
3082    /// * `dom_id` - The DOM to screenshot
3083    /// * `path` - The file path to save the PNG to
3084    ///
3085    /// # Returns
3086    /// * `Ok(())` - Screenshot saved successfully
3087    /// * `Err(String)` - Error message if rendering or saving failed
3088    #[cfg(all(feature = "std", feature = "cpurender"))]
3089    /// # Errors
3090    ///
3091    /// Returns an error message if the screenshot cannot be captured or encoded.
3092    pub fn take_screenshot_to_file(&self, dom_id: DomId, path: &str) -> Result<(), AzString> {
3093        let png_data = self.take_screenshot(dom_id)?;
3094        std::fs::write(path, png_data)
3095            .map_err(|e| AzString::from(alloc::format!("Failed to write file: {e}")))?;
3096        Ok(())
3097    }
3098
3099    /// Take a native OS-level screenshot of the window including window decorations
3100    ///
3101    /// **NOTE**: This is a stub implementation. For full native screenshot support,
3102    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate, which uses
3103    /// runtime dynamic loading (dlopen) to avoid static linking dependencies.
3104    ///
3105    /// # Returns
3106    /// * `Err(String)` - Always returns an error directing to use the extension trait
3107    #[cfg(feature = "std")]
3108    /// # Errors
3109    ///
3110    /// Returns an error message if the screenshot cannot be captured or encoded.
3111    pub fn take_native_screenshot(&self, _path: &str) -> Result<(), AzString> {
3112        Err(AzString::from(
3113            "Native screenshot requires the NativeScreenshotExt trait from azul-dll crate. \
3114             Import it with: use azul::desktop::NativeScreenshotExt;",
3115        ))
3116    }
3117
3118    /// Take a native OS-level screenshot and return the PNG data as bytes
3119    ///
3120    /// **NOTE**: This is a stub implementation. For full native screenshot support,
3121    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate.
3122    ///
3123    /// # Returns
3124    /// * `Ok(Vec<u8>)` - PNG-encoded image data
3125    /// * `Err(String)` - Error message if screenshot failed
3126    #[cfg(feature = "std")]
3127    /// # Errors
3128    ///
3129    /// Returns an error message if the screenshot cannot be captured or encoded.
3130    pub fn take_native_screenshot_bytes(&self) -> Result<alloc::vec::Vec<u8>, AzString> {
3131        // Create a temporary file, take screenshot, read bytes, delete file
3132        let temp_path = std::env::temp_dir().join("azul_screenshot_temp.png");
3133        let temp_path_str = temp_path.to_string_lossy().to_string();
3134
3135        self.take_native_screenshot(&temp_path_str)?;
3136
3137        let bytes = std::fs::read(&temp_path)
3138            .map_err(|e| AzString::from(alloc::format!("Failed to read screenshot: {e}")))?;
3139
3140        drop(std::fs::remove_file(&temp_path));
3141
3142        Ok(bytes)
3143    }
3144
3145    /// Take a native OS-level screenshot and return as a Base64 data URI
3146    ///
3147    /// Returns the screenshot as a "data:image/png;base64,..." string that can
3148    /// be directly used in HTML img tags or JSON responses.
3149    ///
3150    /// # Returns
3151    /// * `Ok(String)` - Base64 data URI string
3152    /// * `Err(String)` - Error message if screenshot failed
3153    ///
3154    #[cfg(feature = "std")]
3155    /// # Errors
3156    ///
3157    /// Returns an error message if the screenshot cannot be captured or encoded.
3158    pub fn take_native_screenshot_base64(&self) -> Result<AzString, AzString> {
3159        let png_bytes = self.take_native_screenshot_bytes()?;
3160        let base64_str = base64_encode(&png_bytes);
3161        Ok(AzString::from(alloc::format!(
3162            "data:image/png;base64,{base64_str}"
3163        )))
3164    }
3165
3166    /// Take a CPU-rendered screenshot and return as a Base64 data URI
3167    ///
3168    /// Returns the screenshot as a "data:image/png;base64,..." string.
3169    /// This is the software-rendered version without window decorations.
3170    ///
3171    /// # Returns
3172    /// * `Ok(String)` - Base64 data URI string
3173    /// * `Err(String)` - Error message if rendering failed
3174    #[cfg(feature = "cpurender")]
3175    /// # Errors
3176    ///
3177    /// Returns an error message if the screenshot cannot be captured or encoded.
3178    pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
3179        let png_bytes = self.take_screenshot(dom_id)?;
3180        let base64_str = base64_encode(&png_bytes);
3181        Ok(AzString::from(alloc::format!(
3182            "data:image/png;base64,{base64_str}"
3183        )))
3184    }
3185
3186    // Manager Access (Read-Only)
3187
3188    /// Get immutable reference to the scroll manager
3189    ///
3190    /// Use this to query scroll state for nodes without modifying it.
3191    /// To request programmatic scrolling, use `nodes_scrolled_in_callback`.
3192    #[must_use] pub const fn get_scroll_manager(&self) -> &ScrollManager {
3193        unsafe { &(*self.ref_data).layout_window.scroll_manager }
3194    }
3195
3196    /// Get immutable reference to the gesture and drag manager
3197    ///
3198    /// Use this to query current gesture/drag state (e.g., "is this node being dragged?",
3199    /// "what files are being dropped?", "is a long-press active?").
3200    ///
3201    /// The manager is updated by the event loop and provides read-only query access
3202    /// to callbacks for gesture-aware UI behavior.
3203    #[must_use] pub const fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
3204        unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
3205    }
3206
3207    /// Queue a platform-native gesture-recognizer result. Applied by
3208    /// the event-loop after the callback returns, via
3209    /// `CallbackChange::InjectNativeGesture` -> `GestureAndDragManager::
3210    /// inject_native_gesture`. Used by the iOS / Android / macOS
3211    /// platform backends from their gesture-recognizer callbacks and by
3212    /// the e2e debug-server harness so JSON tests can drive every event
3213    /// filter end-to-end.
3214    pub fn inject_native_gesture(
3215        &mut self,
3216        gesture: crate::managers::gesture::NativeGestureEvent,
3217    ) {
3218        self.push_change(CallbackChange::InjectNativeGesture { gesture });
3219    }
3220
3221    /// Get immutable reference to the focus manager
3222    ///
3223    /// Use this to query which node currently has focus and whether focus
3224    /// is being moved to another node.
3225    #[must_use] pub const fn get_focus_manager(&self) -> &FocusManager {
3226        &self.get_layout_window().focus_manager
3227    }
3228
3229    /// Get a reference to the undo/redo manager
3230    ///
3231    /// This allows user callbacks to query the undo/redo state and intercept
3232    /// undo/redo operations via `preventDefault()`.
3233    #[must_use] pub const fn get_undo_redo_manager(&self) -> &UndoRedoManager {
3234        &self.get_layout_window().undo_redo_manager
3235    }
3236
3237    /// Get immutable reference to the hover manager
3238    ///
3239    /// Use this to query which nodes are currently hovered at various input points
3240    /// (mouse, touch points, pen).
3241    #[must_use] pub const fn get_hover_manager(&self) -> &HoverManager {
3242        &self.get_layout_window().hover_manager
3243    }
3244
3245    /// Get immutable reference to the text input manager
3246    ///
3247    /// Use this to query text selection state, cursor positions, and IME composition.
3248    #[must_use] pub const fn get_text_input_manager(&self) -> &TextInputManager {
3249        &self.get_layout_window().text_input_manager
3250    }
3251
3252    /// Check if `multi_cursor` has any selection ranges.
3253    ///
3254    /// Replaces the removed `get_selection_manager()`.
3255    #[must_use] pub fn has_any_selection(&self) -> bool {
3256        self.get_layout_window()
3257            .text_edit_manager.multi_cursor.as_ref()
3258            .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
3259    }
3260
3261    /// Check if a specific node is currently focused
3262    #[must_use] pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
3263        self.get_focus_manager().has_focus(&node_id)
3264    }
3265
3266    /// Check if any node in a specific DOM is focused
3267    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
3268        self.get_focused_node()
3269            .is_some_and(|n| n.dom == dom_id)
3270    }
3271
3272    // Pen/Stylus Query Methods
3273
3274    /// Get current pen/stylus state if a pen is active
3275    #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
3276        self.get_gesture_drag_manager().get_pen_state()
3277    }
3278
3279    /// Get the current Wacom tablet-**pad** state (`ExpressKeys` + touch-ring),
3280    /// or `None` if no pad backend has delivered one. (The pen's own wacom
3281    /// features - eraser / barrel button / barrel roll / tilt / pressure -
3282    /// are in [`CallbackInfo::get_pen_state`].) Kept live by the platform pad
3283    /// backend (Wintab / libwacom+libinput / macOS tablet `NSEvent`s).
3284    #[must_use] pub const fn get_wacom_pad(&self) -> Option<crate::managers::gesture::WacomPadState> {
3285        self.get_gesture_drag_manager().get_pad_state().copied()
3286    }
3287
3288    /// Get the most recent geolocation fix, or `None` if no `GeolocationProbe`
3289    /// is mounted or no platform backend has delivered a fix yet. The fix is
3290    /// kept live by the platform backends (Android `FusedLocationProvider`,
3291    /// iOS/macOS `CLLocationManager`) via the async fix channel that the
3292    /// layout pass folds into the manager - so a callback can read the user's
3293    /// position to, e.g., place a "you are here" marker on a map.
3294    #[must_use] pub const fn get_location_fix(&self) -> Option<azul_core::geolocation::LocationFix> {
3295        self.get_layout_window().geolocation_manager.latest_fix()
3296    }
3297
3298    /// Get the latest motion-sensor reading for `kind` (Accelerometer /
3299    /// Gyroscope / Magnetometer), or `None` if no platform backend has
3300    /// delivered one. Kept live by the sensor backends (iOS `CoreMotion`,
3301    /// Android `SensorManager`) via the async channel the layout pass folds
3302    /// into the manager - so a callback can drive tilt / shake / compass UI.
3303    #[must_use] pub const fn get_sensor_reading(
3304        &self,
3305        kind: azul_core::sensors::SensorKind,
3306    ) -> Option<azul_core::sensors::SensorReading> {
3307        self.get_layout_window().sensor_manager.reading(kind)
3308    }
3309
3310    /// The safe-area insets (notch / system-UI margins) for this window, in
3311    /// logical px - lay out interactive content within them so it isn't hidden
3312    /// by a notch / rounded corners / status bar. Zero where the platform or
3313    /// window has no inset. Set by the platform shell (macOS `NSScreen` notch,
3314    /// iOS `UIView.safeAreaInsets`, Android `WindowInsets`).
3315    #[must_use] pub const fn get_safe_area_insets(&self) -> azul_css::system::SafeAreaInsets {
3316        self.get_layout_window().safe_area_insets
3317    }
3318
3319    /// Get the latest state of the gamepad `id` (button bitset + analog
3320    /// axes), or `None` if no pad with that id has connected. Kept live by
3321    /// the controller backend (gilrs / iOS `GCController` / Android
3322    /// `InputDevice`) via the async channel the layout pass folds into the
3323    /// manager - so a callback can drive movement / menu UI. For the common
3324    /// single-controller case, [`CallbackInfo::get_primary_gamepad`] skips
3325    /// the id bookkeeping.
3326    #[must_use] pub fn get_gamepad_state(
3327        &self,
3328        id: azul_core::gamepad::GamepadId,
3329    ) -> Option<azul_core::gamepad::GamepadState> {
3330        self.get_layout_window().gamepad_manager.state(id)
3331    }
3332
3333    /// Get the first currently-connected gamepad, or `None` if none is
3334    /// connected - the convenient single-controller accessor.
3335    #[must_use] pub fn get_primary_gamepad(&self) -> Option<azul_core::gamepad::GamepadState> {
3336        self.get_layout_window().gamepad_manager.primary()
3337    }
3338
3339    /// Get the most recent biometric-auth result, or `None` if no
3340    /// `request_biometric_auth` has completed yet. Kept live by the
3341    /// platform backends (iOS/macOS `LAContext`, Android `BiometricPrompt`,
3342    /// Windows `UserConsentVerifier`) via the async result channel the
3343    /// layout pass folds into the manager - so a callback can unlock a
3344    /// vault / settings panel once the user authenticates.
3345    #[must_use] pub const fn get_biometric_result(&self) -> Option<azul_core::biometric::BiometricResult> {
3346        self.get_layout_window().biometric_manager.last_result()
3347    }
3348
3349    /// Get the device's biometric capability (sync probe): `Face`,
3350    /// `Fingerprint`, `Iris`, or `NotAvailable`. Lets a callback decide
3351    /// whether to even offer a biometric unlock before requesting one
3352    /// (no OS prompt is shown - this just reads the cached probe).
3353    #[must_use] pub const fn get_biometric_kind(&self) -> azul_core::biometric::BiometricKind {
3354        self.get_layout_window().biometric_manager.availability()
3355    }
3356
3357    /// Request a biometric-auth prompt (Face ID / Touch ID / Android
3358    /// `BiometricPrompt` / Windows Hello). Returns immediately - the OS
3359    /// draws its own modal asynchronously; the outcome arrives on a later
3360    /// frame and is read via [`CallbackInfo::get_biometric_result`]. Call
3361    /// this from, e.g., an unlock button's `on_click`. The `prompt`
3362    /// configures the reason text, cancel label, and whether the OS
3363    /// passcode fallback is allowed. (No platform backend reports a real
3364    /// outcome yet - the request currently resolves to
3365    /// `BiometricResult::Unavailable`; the iOS/macOS/Android backends land
3366    /// in a later tick.)
3367    pub fn request_biometric_auth(&mut self, prompt: azul_core::biometric::BiometricPrompt) {
3368        crate::managers::biometric::push_biometric_request(prompt);
3369    }
3370
3371    /// Store `secret` under `key` in the OS keyring (Keychain / `KeyStore` /
3372    /// libsecret / `CredentialLocker`). When `require_biometry` is set, a
3373    /// later `keyring_get` of this key triggers the OS biometric prompt.
3374    /// Returns immediately; the outcome arrives via `get_keyring_result()`
3375    /// on a later frame.
3376    pub fn keyring_store(&mut self, key: AzString, secret: AzString, require_biometry: bool) {
3377        crate::managers::keyring::push_keyring_request(
3378            azul_core::keyring::KeyringRequest::Store {
3379                key,
3380                secret,
3381                require_biometry,
3382            },
3383        );
3384    }
3385
3386    /// Read the secret stored under `key`. A biometry-bound item shows the
3387    /// OS prompt first; the secret (or a denial) arrives via
3388    /// `get_keyring_result()` on a later frame.
3389    pub fn keyring_get(&mut self, key: AzString) {
3390        crate::managers::keyring::push_keyring_request(azul_core::keyring::KeyringRequest::Get {
3391            key,
3392        });
3393    }
3394
3395    /// Remove the item stored under `key` from the OS keyring (no-op if
3396    /// absent). The outcome arrives via `get_keyring_result()`.
3397    pub fn keyring_delete(&mut self, key: AzString) {
3398        crate::managers::keyring::push_keyring_request(
3399            azul_core::keyring::KeyringRequest::Delete { key },
3400        );
3401    }
3402
3403    /// Get the most recent keyring outcome, or `None` until the first op
3404    /// completes. Read after a `keyring_store/get/delete` to observe the
3405    /// result - e.g. the revealed secret from a `keyring_get`
3406    /// (`KeyringResult::Retrieved`).
3407    #[must_use] pub fn get_keyring_result(&self) -> Option<azul_core::keyring::KeyringResult> {
3408        self.get_layout_window().keyring_manager.last_result().cloned()
3409    }
3410
3411    /// Read the most recently observed permission state for `capability`
3412    /// (Camera / Microphone / Geolocation / Sensors / Notifications / …) - e.g.
3413    /// so a callback can check a capability is `Granted` before using it (show
3414    /// a camera preview only once granted). Kept live by the platform
3415    /// permission backend; a capability is subscribed by mounting its probe
3416    /// node (`CameraProbe` / `GeolocationProbe` / …) into the DOM.
3417    #[must_use] pub fn get_permission_status(
3418        &self,
3419        capability: crate::managers::permission::Capability,
3420    ) -> crate::managers::permission::PermissionState {
3421        self.get_layout_window()
3422            .permission_manager
3423            .get_status(capability)
3424    }
3425
3426    /// Get current pen pressure (0.0 to 1.0)
3427    /// Returns None if no pen is active, Some(0.5) for mouse
3428    #[must_use] pub fn get_pen_pressure(&self) -> Option<f32> {
3429        self.get_pen_state().map(|pen| pen.pressure)
3430    }
3431
3432    /// Get current pen tilt angles (`x_tilt`, `y_tilt`) in degrees
3433    /// Returns None if no pen is active
3434    #[must_use] pub fn get_pen_tilt(&self) -> Option<PenTilt> {
3435        self.get_pen_state().map(|pen| pen.tilt)
3436    }
3437
3438    /// Check if pen is currently in contact with surface
3439    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
3440        self.get_pen_state()
3441            .is_some_and(|pen| pen.in_contact)
3442    }
3443
3444    /// Check if pen is in eraser mode
3445    #[must_use] pub fn is_pen_eraser(&self) -> bool {
3446        self.get_pen_state()
3447            .is_some_and(|pen| pen.is_eraser)
3448    }
3449
3450    /// Check if pen barrel button is pressed
3451    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
3452        self.get_pen_state()
3453            .is_some_and(|pen| pen.barrel_button_pressed)
3454    }
3455
3456    /// Get the last recorded input sample (for `event_id` and detailed input data)
3457    #[must_use] pub fn get_last_input_sample(&self) -> Option<&InputSample> {
3458        let manager = self.get_gesture_drag_manager();
3459        manager
3460            .get_current_session()
3461            .and_then(|session| session.last_sample())
3462    }
3463
3464    /// Get the event ID of the current event
3465    #[must_use] pub fn get_current_event_id(&self) -> Option<u64> {
3466        self.get_last_input_sample().map(|sample| sample.event_id)
3467    }
3468
3469    // Gesture Query Methods
3470    //
3471    // These read whatever the in-process `GestureAndDragManager` has detected
3472    // from the touch / mouse stream. On platforms with native gesture
3473    // recognizers (iOS UIKit, Android `GestureDetector`), the platform
3474    // backend may inject pre-detected gestures via
3475    // `GestureAndDragManager::inject_native_gesture(...)` - accessors below
3476    // see the same data regardless of source, fulfilling Azul's
3477    // "superset of every platform" guarantee for gesture handlers.
3478
3479    /// Returns the dominant direction of the current swipe gesture, if any.
3480    /// Detection uses the touch / pointer trajectory and a velocity
3481    /// threshold; on iOS / Android the platform backend may override the
3482    /// in-process detector with a native gesture-recognizer result.
3483    #[must_use] pub fn get_swipe_direction(&self) -> crate::managers::gesture::OptionGestureDirection {
3484        self.get_gesture_drag_manager().detect_swipe_direction().into()
3485    }
3486
3487    /// Returns the active pinch gesture (scale + center + distances), if any.
3488    #[must_use] pub fn get_pinch(&self) -> crate::managers::gesture::OptionDetectedPinch {
3489        self.get_gesture_drag_manager().detect_pinch().into()
3490    }
3491
3492    /// Returns the active rotation gesture (radians + center), if any.
3493    #[must_use] pub fn get_rotation(&self) -> crate::managers::gesture::OptionDetectedRotation {
3494        self.get_gesture_drag_manager().detect_rotation().into()
3495    }
3496
3497    /// Returns the active long-press, if the user is currently holding a
3498    /// pointer in place beyond the configured threshold.
3499    #[must_use] pub fn get_long_press(&self) -> crate::managers::gesture::OptionDetectedLongPress {
3500        self.get_gesture_drag_manager().detect_long_press().into()
3501    }
3502
3503    /// True iff the gesture manager classified the current event sequence
3504    /// as a double-click / double-tap.
3505    #[must_use] pub fn was_double_clicked(&self) -> bool {
3506        self.get_gesture_drag_manager().detect_double_click()
3507    }
3508
3509    // Focus Management Methods
3510
3511    /// Set focus to a specific DOM node by ID
3512    pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
3513        self.set_focus(FocusTarget::Id(DomNodeId {
3514            dom: dom_id,
3515            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
3516        }));
3517    }
3518
3519    /// Set focus to a node matching a CSS path
3520    pub fn set_focus_to_path(&mut self, dom_id: DomId, css_path: CssPath) {
3521        self.set_focus(FocusTarget::Path(FocusTargetPath {
3522            dom: dom_id,
3523            css_path,
3524        }));
3525    }
3526
3527    /// Move focus to next focusable element in tab order
3528    pub fn focus_next(&mut self) {
3529        self.set_focus(FocusTarget::Next);
3530    }
3531
3532    /// Move focus to previous focusable element in tab order
3533    pub fn focus_previous(&mut self) {
3534        self.set_focus(FocusTarget::Previous);
3535    }
3536
3537    /// Move focus to first focusable element
3538    pub fn focus_first(&mut self) {
3539        self.set_focus(FocusTarget::First);
3540    }
3541
3542    /// Move focus to last focusable element
3543    pub fn focus_last(&mut self) {
3544        self.set_focus(FocusTarget::Last);
3545    }
3546
3547    /// Remove focus from all elements
3548    pub fn clear_focus(&mut self) {
3549        self.set_focus(FocusTarget::NoFocus);
3550    }
3551
3552    // Manager Access Methods
3553
3554    /// Check if a drag gesture is currently active
3555    ///
3556    /// Convenience method that queries the gesture manager.
3557    #[must_use] pub const fn is_dragging(&self) -> bool {
3558        self.get_gesture_drag_manager().is_dragging()
3559    }
3560
3561    /// Get the currently focused node (if any)
3562    ///
3563    /// Returns None if no node has focus.
3564    #[must_use] pub const fn get_focused_node(&self) -> Option<DomNodeId> {
3565        self.get_layout_window()
3566            .focus_manager
3567            .get_focused_node()
3568            .copied()
3569    }
3570
3571    /// Check if a specific node has focus
3572    #[must_use] pub fn has_focus(&self, node_id: DomNodeId) -> bool {
3573        self.get_layout_window().focus_manager.has_focus(&node_id)
3574    }
3575
3576    /// Get the currently hovered file (if drag-drop is in progress)
3577    ///
3578    /// Returns None if no file is being hovered over the window.
3579    /// (First file only - use [`get_hovered_files`](Self::get_hovered_files)
3580    /// for multi-file drags; no longer `const` since MWA-B7 made the manager
3581    /// store a Vec.)
3582    #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
3583        self.get_layout_window()
3584            .file_drop_manager
3585            .get_hovered_file()
3586    }
3587
3588    /// ALL files of the current drag hover (MWA-B7 - multi-file drags were
3589    /// previously truncated to the first path before they reached callbacks).
3590    #[must_use] pub fn get_hovered_files(&self) -> StringVec {
3591        self.get_layout_window()
3592            .file_drop_manager
3593            .get_hovered_files()
3594            .to_vec()
3595            .into()
3596    }
3597
3598    /// Get the currently dropped file (if a file was just dropped)
3599    ///
3600    /// This is a one-shot value that is cleared after event processing.
3601    /// Returns None if no file was dropped this frame. (First file only -
3602    /// use [`get_dropped_files`](Self::get_dropped_files) for the full list.)
3603    #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
3604        self.get_layout_window()
3605            .file_drop_manager
3606            .get_dropped_file()
3607    }
3608
3609    /// ALL files of this frame's drop (MWA-B7; one-shot).
3610    #[must_use] pub fn get_dropped_files(&self) -> StringVec {
3611        self.get_layout_window()
3612            .file_drop_manager
3613            .get_dropped_files()
3614            .to_vec()
3615            .into()
3616    }
3617
3618    /// Measure a DOM headlessly: style + lay it out against `available`
3619    /// constraints (this window's fonts / system style) without touching the
3620    /// live layout. Returns the union of all node bounds - use a very tall
3621    /// `available.height` (e.g. `1_000_000.0`) to get a DOM's natural height
3622    /// at a given width. Primary use: sizing `VirtualView` items to compute
3623    /// the virtual scroll extent. A full cold layout pass per call - cache
3624    /// per item template.
3625    #[cfg(feature = "std")]
3626    #[must_use] pub fn measure_dom(
3627        &self,
3628        dom: azul_core::dom::Dom,
3629        available: LogicalSize,
3630    ) -> LogicalSize {
3631        self.get_layout_window().measure_dom(dom, available)
3632    }
3633
3634    /// Deepest node currently under the mouse pointer (MWA-B8). Anchor for
3635    /// drag auto-scroll when there is no focused node - node drags and OS
3636    /// file hovers scroll the container under the pointer, not the focused
3637    /// text field.
3638    #[must_use] pub fn get_deepest_hovered_node(&self) -> Option<DomNodeId> {
3639        let hit = self
3640            .get_layout_window()
3641            .hover_manager
3642            .get_current(&InputPointId::Mouse)?;
3643        hit.hovered_nodes.iter().next().and_then(|(dom_id, entry)| {
3644            entry.regular_hit_test_nodes.keys().next_back().map(|nid| DomNodeId {
3645                dom: *dom_id,
3646                node: NodeHierarchyItemId::from_crate_internal(Some(*nid)),
3647            })
3648        })
3649    }
3650
3651    /// Check if a node or file drag is currently active
3652    ///
3653    /// Returns true if either a node drag or file drag is in progress.
3654    /// `gesture_drag_manager` is the single source of truth (the old
3655    /// `drag_drop_manager` mirror has been deleted — see `managers/drag_drop.rs`).
3656    #[must_use] pub const fn is_drag_active(&self) -> bool {
3657        self.get_layout_window().gesture_drag_manager.is_dragging()
3658    }
3659
3660    /// Check if a node drag is specifically active
3661    #[must_use] pub fn is_node_drag_active(&self) -> bool {
3662        self.get_layout_window().gesture_drag_manager.is_node_drag_active()
3663    }
3664
3665    /// Check if a file drag is specifically active
3666    #[must_use] pub fn is_file_drag_active(&self) -> bool {
3667        let lw = self.get_layout_window();
3668        // MWA-C-file_drop: an EXTERNAL OS drag (Finder/Explorer hovering
3669        // files over the window) lives in file_drop_manager, not in the
3670        // intra-app drag managers — without this arm the query answered
3671        // false during exactly the drag it is most often asked about.
3672        lw.gesture_drag_manager.is_file_dropping()
3673            || !lw.file_drop_manager.get_hovered_files().is_empty()
3674    }
3675
3676    /// Get the current drag/drop state (if any)
3677    ///
3678    /// Returns None if no drag is active, or Some with drag state.
3679    #[must_use] pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
3680        let ctx = self.get_layout_window().gesture_drag_manager.get_drag_context()?;
3681        crate::managers::drag_drop::DragState::from_context(ctx)
3682    }
3683
3684    /// Get the current drag context (if any)
3685    ///
3686    /// Returns None if no drag is active, or Some with drag context.
3687    /// Prefer this over `get_drag_state` for new code.
3688    #[must_use] pub const fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
3689        // The gesture manager holds the LIVE context and is the ONLY source of
3690        // truth. (The `drag_drop_manager` mirror was a frozen clone taken at
3691        // drag start whose drop-target/position went stale for the whole drag;
3692        // it has been deleted.)
3693        self.get_layout_window().gesture_drag_manager.get_drag_context()
3694    }
3695
3696    // Hover Manager Access
3697
3698    /// Get the current mouse cursor hit test result (most recent frame)
3699    #[must_use] pub fn get_current_hit_test(&self) -> Option<&FullHitTest> {
3700        self.get_hover_manager().get_current(&InputPointId::Mouse)
3701    }
3702
3703    /// Get mouse cursor hit test from N frames ago (0 = current, 1 = previous, etc.)
3704    #[must_use] pub fn get_hit_test_frame(&self, frames_ago: usize) -> Option<&FullHitTest> {
3705        self.get_hover_manager()
3706            .get_frame(&InputPointId::Mouse, frames_ago)
3707    }
3708
3709    /// Get the full mouse cursor hit test history (up to 5 frames)
3710    ///
3711    /// Returns None if no mouse history exists yet
3712    #[must_use] pub fn get_hit_test_history(&self) -> Option<&VecDeque<FullHitTest>> {
3713        self.get_hover_manager().get_history(&InputPointId::Mouse)
3714    }
3715
3716    /// Check if there's sufficient mouse history for gesture detection (at least 2 frames)
3717    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
3718        self.get_hover_manager()
3719            .has_sufficient_history_for_gestures(&InputPointId::Mouse)
3720    }
3721
3722    // File Drop Manager Access
3723
3724    /// Get immutable reference to the file drop manager
3725    #[must_use] pub const fn get_file_drop_manager(&self) -> &FileDropManager {
3726        &self.get_layout_window().file_drop_manager
3727    }
3728
3729    // Drag-Drop Manager Access
3730
3731
3732    /// Get the node being dragged (if any)
3733    #[must_use] pub fn get_dragged_node(&self) -> Option<DomNodeId> {
3734        self.get_drag_context()
3735            .and_then(|ctx| {
3736                ctx.as_node_drag().map(|node_drag| {
3737                    DomNodeId {
3738                        dom: node_drag.dom_id,
3739                        node: NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
3740                    }
3741                })
3742            })
3743    }
3744
3745    /// Get the file path being dragged (if any)
3746    #[must_use] pub fn get_dragged_file(&self) -> Option<&AzString> {
3747        // Gesture context first (intra-app file drags), then the
3748        // FileDropManager's hovered/dropped state (external OS drags).
3749        self.get_drag_context()
3750            .and_then(|ctx| {
3751                ctx.as_file_drop().and_then(|file_drop| {
3752                    file_drop.files.as_ref().first()
3753                })
3754            })
3755            .or_else(|| {
3756                let lw = self.get_layout_window();
3757                lw.file_drop_manager
3758                    .get_hovered_files()
3759                    .first()
3760                    .or_else(|| lw.file_drop_manager.get_dropped_files().first())
3761            })
3762    }
3763
3764    /// Get the MIME types available in the current drag data.
3765    ///
3766    /// W3C equivalent: `dataTransfer.types`
3767    /// Returns an empty vec if no drag is active or no data is set.
3768    #[must_use] pub fn get_drag_types(&self) -> StringVec {
3769        let lw = self.get_layout_window();
3770        // Try gesture manager first
3771        if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3772            if let Some(node_drag) = ctx.as_node_drag() {
3773                return node_drag
3774                    .drag_data
3775                    .data
3776                    .as_ref()
3777                    .iter()
3778                    .map(|e| e.mime_type.clone())
3779                    .collect();
3780            }
3781        }
3782        StringVec::from_const_slice(&[])
3783    }
3784
3785    /// Get drag data for a specific MIME type.
3786    ///
3787    /// W3C equivalent: `dataTransfer.getData(type)`
3788    /// Returns None if no drag is active or the MIME type is not set.
3789    #[must_use] pub fn get_drag_data(&self, mime_type: &str) -> OptionU8Vec {
3790        let lw = self.get_layout_window();
3791        if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3792            if let Some(node_drag) = ctx.as_node_drag() {
3793                return node_drag.drag_data.get_data(mime_type).map(|d| U8Vec::from(d.to_vec())).into();
3794            }
3795        }
3796        OptionU8Vec::None
3797    }
3798
3799    /// Set drag data for a MIME type on the active drag operation.
3800    ///
3801    /// W3C equivalent: `dataTransfer.setData(type, data)`
3802    /// Should be called from a `DragStart` callback to populate the drag data.
3803    pub fn set_drag_data(&mut self, mime_type: AzString, data: Vec<u8>) {
3804        self.push_change(CallbackChange::SetDragData { mime_type, data });
3805    }
3806
3807    /// Accept the current drop operation on this node.
3808    ///
3809    /// W3C equivalent: calling `event.preventDefault()` in a `DragOver` handler.
3810    /// This signals that the current drop target can accept the dragged data.
3811    /// Must be called from a `DragOver` or `DragEnter` callback for the Drop event
3812    /// to fire on this node.
3813    pub fn accept_drop(&mut self) {
3814        self.push_change(CallbackChange::AcceptDrop);
3815    }
3816
3817    /// Set the drop effect for the current drag operation.
3818    ///
3819    /// W3C equivalent: `dataTransfer.dropEffect = "move"|"copy"|"link"`
3820    /// Should be called from a `DragOver` or `DragEnter` callback.
3821    pub fn set_drop_effect(&mut self, effect: azul_core::drag::DropEffect) {
3822        self.push_change(CallbackChange::SetDropEffect { effect });
3823    }
3824
3825    // Scroll Manager Query Methods
3826
3827    /// Get the current scroll offset for the hit node (if it's scrollable)
3828    ///
3829    /// Convenience method that uses the `hit_dom_node` from this callback.
3830    /// Use `get_scroll_offset_for_node` if you need to query a specific node.
3831    #[must_use] pub fn get_scroll_offset(&self) -> Option<LogicalPosition> {
3832        self.get_scroll_offset_for_node(
3833            self.hit_dom_node.dom,
3834            self.hit_dom_node.node.into_crate_internal()?,
3835        )
3836    }
3837
3838    /// Get the current scroll offset for a specific node (if it's scrollable)
3839    #[must_use] pub fn get_scroll_offset_for_node(
3840        &self,
3841        dom_id: DomId,
3842        node_id: NodeId,
3843    ) -> Option<LogicalPosition> {
3844        self.get_scroll_manager()
3845            .get_current_offset(dom_id, node_id)
3846    }
3847
3848    /// Get the scroll state (container rect, content rect, current offset) for a node
3849    #[must_use] pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
3850        self.get_scroll_manager().get_scroll_state(dom_id, node_id)
3851    }
3852
3853    /// Get a read-only snapshot of a scroll node's bounds and position.
3854    ///
3855    /// This is the recommended API for timer callbacks that need to compute
3856    /// scroll physics. Returns container/content rects and max scroll bounds.
3857    #[must_use] pub fn get_scroll_node_info(
3858        &self,
3859        dom_id: DomId,
3860        node_id: NodeId,
3861    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
3862        self.get_scroll_manager()
3863            .get_scroll_node_info(dom_id, node_id)
3864    }
3865
3866    /// Deprecated: Returns None. Scroll deltas are no longer tracked per-frame.
3867    /// Kept for FFI backward compatibility.
3868    /// The raw wheel / trackpad delta that triggered the current `Scroll`
3869    /// callback, or `None` outside a scroll dispatch. The value is the per-pass
3870    /// delta recorded by the platform scroll handler (see
3871    /// `ScrollManager::pending_wheel_event`); it is global to the pass, so the
3872    /// `dom_id` / `node_id` arguments are advisory - a `Scroll` callback only
3873    /// fires on the hovered node, which is what they identify. Wheel-as-zoom
3874    /// widgets (the map) read `.y` here instead of consuming the scroll-physics
3875    /// input queue (which only carries deltas for actual scroll containers).
3876    #[must_use] pub const fn get_scroll_delta(
3877        &self,
3878        _dom_id: DomId,
3879        _node_id: NodeId,
3880    ) -> Option<LogicalPosition> {
3881        self.get_scroll_manager().pending_wheel_event
3882    }
3883
3884    /// Deprecated: Returns false. Scroll activity flags were removed.
3885    /// Kept for FFI backward compatibility.
3886    #[must_use] pub const fn had_scroll_activity(
3887        &self,
3888        _dom_id: DomId,
3889        _node_id: NodeId,
3890    ) -> bool {
3891        false
3892    }
3893
3894    /// Find the closest scrollable ancestor of a node.
3895    ///
3896    /// Walks up the node hierarchy to find a node registered in the `ScrollManager`.
3897    /// Used by auto-scroll timer to find which container to scroll.
3898    #[must_use] pub fn find_scroll_parent(
3899        &self,
3900        dom_id: DomId,
3901        node_id: NodeId,
3902    ) -> Option<NodeId> {
3903        let layout_window = self.get_layout_window();
3904        let layout_results = &layout_window.layout_results;
3905        let lr = layout_results.get(&dom_id)?;
3906        let node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem] =
3907            lr.styled_dom.node_hierarchy.as_ref();
3908        self.get_scroll_manager()
3909            .find_scroll_parent(dom_id, node_id, node_hierarchy)
3910    }
3911
3912    /// Get a clone of the scroll input queue for consuming pending inputs.
3913    ///
3914    /// Timer callbacks use this to drain pending scroll inputs recorded by
3915    /// platform event handlers. The queue is thread-safe (Arc<Mutex>), so
3916    /// the timer can call `take_all()` with only `&self`.
3917    #[cfg(feature = "std")]
3918    #[must_use] pub fn get_scroll_input_queue(
3919        &self,
3920    ) -> crate::managers::scroll_state::ScrollInputQueue {
3921        self.get_scroll_manager().scroll_input_queue.clone()
3922    }
3923
3924    // Gpu State Manager Access
3925
3926    /// Get immutable reference to the GPU state manager
3927    #[must_use] pub const fn get_gpu_state_manager(&self) -> &GpuStateManager {
3928        &self.get_layout_window().gpu_state_manager
3929    }
3930
3931    // VirtualView Manager Access
3932
3933    /// Get immutable reference to the `VirtualView` manager
3934    #[must_use] pub const fn get_virtual_view_manager(&self) -> &VirtualViewManager {
3935        &self.get_layout_window().virtual_view_manager
3936    }
3937
3938    // Changeset Inspection/Modification Methods
3939    // These methods allow callbacks to inspect pending operations and modify them before execution
3940
3941    /// Inspect a pending copy operation
3942    ///
3943    /// Returns the clipboard content that would be copied if the operation proceeds.
3944    /// Use this to validate or transform clipboard content before copying.
3945    #[must_use] pub fn inspect_copy_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
3946        let layout_window = self.get_layout_window();
3947        let dom_id = &target.dom;
3948        layout_window.get_selected_content_for_clipboard(dom_id)
3949    }
3950
3951    /// Inspect a pending cut operation
3952    ///
3953    /// Returns the clipboard content that would be cut (copied + deleted).
3954    /// Use this to validate or transform content before cutting.
3955    #[must_use] pub fn inspect_cut_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
3956        // Cut uses same content extraction as copy
3957        self.inspect_copy_changeset(target)
3958    }
3959
3960    /// Inspect the current selection range that would be affected by paste
3961    ///
3962    /// Returns the selection range that will be replaced when pasting.
3963    /// Returns None if no selection exists (paste will insert at cursor).
3964    #[must_use] pub fn inspect_paste_target_range(&self, _target: DomNodeId) -> Option<SelectionRange> {
3965        let layout_window = self.get_layout_window();
3966        layout_window
3967            .text_edit_manager.multi_cursor.as_ref()
3968            .and_then(|mc| mc.selections.iter().find_map(|s| match &s.selection {
3969                Selection::Range(r) => Some(*r),
3970                Selection::Cursor(_) => None,
3971            }))
3972    }
3973
3974    /// Inspect what text would be selected by Select All operation
3975    ///
3976    /// Returns the full text content and the range that would be selected.
3977    #[must_use] pub fn inspect_select_all_changeset(&self, target: DomNodeId) -> Option<SelectAllResult> {
3978        use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
3979
3980        let layout_window = self.get_layout_window();
3981        let node_id = target.node.into_crate_internal()?;
3982
3983        // Get text content
3984        let content = layout_window.get_text_before_textinput(target.dom, node_id);
3985        let text = layout_window.extract_text_from_inline_content(&content);
3986
3987        // Create selection range from start to end
3988        let start_cursor = TextCursor {
3989            cluster_id: GraphemeClusterId {
3990                source_run: 0,
3991                start_byte_in_run: 0,
3992            },
3993            affinity: CursorAffinity::Leading,
3994        };
3995
3996        let end_cursor = TextCursor {
3997            cluster_id: GraphemeClusterId {
3998                source_run: 0,
3999                start_byte_in_run: u32::try_from(text.len()).unwrap_or(u32::MAX),
4000            },
4001            affinity: CursorAffinity::Leading,
4002        };
4003
4004        let range = SelectionRange {
4005            start: start_cursor,
4006            end: end_cursor,
4007        };
4008
4009        Some(SelectAllResult {
4010            full_text: text.into(),
4011            selection_range: range,
4012        })
4013    }
4014
4015    /// Inspect what would be deleted by a backspace/delete operation
4016    ///
4017    /// Uses the pure functions from `text3::edit::inspect_delete()` to determine
4018    /// what would be deleted without actually performing the deletion.
4019    ///
4020    /// Returns (`range_to_delete`, `deleted_text`).
4021    /// - forward=true: Delete key (delete character after cursor)
4022    /// - forward=false: Backspace key (delete character before cursor)
4023    #[must_use] pub fn inspect_delete_changeset(
4024        &self,
4025        target: DomNodeId,
4026        forward: bool,
4027    ) -> Option<DeleteResult> {
4028        let layout_window = self.get_layout_window();
4029        let dom_id = &target.dom;
4030        let node_id = target.node.into_crate_internal()?;
4031
4032        // Get the inline content for this node
4033        let content = layout_window.get_text_before_textinput(target.dom, node_id);
4034
4035        // Get current selection state from multi_cursor
4036        let selection = if let Some(mc) = layout_window.text_edit_manager.multi_cursor.as_ref() {
4037            if let Some(range) = mc.selections.iter().find_map(|s| match &s.selection {
4038                Selection::Range(r) => Some(*r),
4039                Selection::Cursor(_) => None,
4040            }) {
4041                Selection::Range(range)
4042            } else if let Some(cursor) = mc.get_primary_cursor() {
4043                Selection::Cursor(cursor)
4044            } else {
4045                return None;
4046            }
4047        } else {
4048            return None; // No multi_cursor active
4049        };
4050
4051        // Use text3::edit::inspect_delete to determine what would be deleted
4052        crate::text3::edit::inspect_delete(&content, &selection, forward).map(|(range, text)| {
4053            DeleteResult {
4054                range_to_delete: range,
4055                deleted_text: text.into(),
4056            }
4057        })
4058    }
4059
4060    /// Inspect a pending undo operation
4061    ///
4062    /// Returns the operation that would be undone, allowing inspection
4063    /// of what state will be restored.
4064    #[must_use] pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4065        self.get_undo_redo_manager().peek_undo(node_id)
4066    }
4067
4068    /// Inspect a pending redo operation
4069    ///
4070    /// Returns the operation that would be reapplied.
4071    #[must_use] pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4072        self.get_undo_redo_manager().peek_redo(node_id)
4073    }
4074
4075    /// Check if undo is available for a specific node
4076    ///
4077    /// Returns true if there is at least one undoable operation in the stack.
4078    #[must_use] pub fn can_undo(&self, node_id: NodeId) -> bool {
4079        self.get_undo_redo_manager()
4080            .get_stack(node_id)
4081            .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_undo)
4082    }
4083
4084    /// Check if redo is available for a specific node
4085    ///
4086    /// Returns true if there is at least one redoable operation in the stack.
4087    #[must_use] pub fn can_redo(&self, node_id: NodeId) -> bool {
4088        self.get_undo_redo_manager()
4089            .get_stack(node_id)
4090            .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_redo)
4091    }
4092
4093    /// Get the text that would be restored by undo for a specific node
4094    ///
4095    /// Returns the pre-state text content that would be restored if undo is performed.
4096    /// Returns None if no undo operation is available.
4097    #[must_use] pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
4098        self.get_undo_redo_manager()
4099            .peek_undo(node_id)
4100            .map(|op| op.pre_state.text_content.clone())
4101    }
4102
4103    /// Get the text that would be restored by redo for a specific node
4104    ///
4105    /// Returns the pre-state text content that would be restored if redo is performed.
4106    /// Returns None if no redo operation is available.
4107    #[must_use] pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
4108        self.get_undo_redo_manager()
4109            .peek_redo(node_id)
4110            .map(|op| op.pre_state.text_content.clone())
4111    }
4112
4113    // Clipboard Helper Methods
4114
4115    /// Get clipboard content from system clipboard (available during paste operations)
4116    ///
4117    /// This returns content that was read from the system clipboard when Ctrl+V was pressed.
4118    /// It's only available in `On::Paste` callbacks or similar clipboard-related callbacks.
4119    ///
4120    /// Use this to inspect what will be pasted before allowing or modifying the paste operation.
4121    ///
4122    /// # Returns
4123    /// * `Some(&ClipboardContent)` - If paste is in progress and clipboard has content
4124    /// * `None` - If no paste operation is active or clipboard is empty
4125    #[must_use] pub const fn get_clipboard_content(&self) -> Option<&ClipboardContent> {
4126        unsafe {
4127            (*self.ref_data)
4128                .layout_window
4129                .clipboard_manager
4130                .get_paste_content()
4131        }
4132    }
4133
4134    /// Override clipboard content for copy/cut operations
4135    ///
4136    /// This sets custom content that will be written to the system clipboard.
4137    /// Use this in `On::Copy` or `On::Cut` callbacks to modify what gets copied.
4138    ///
4139    /// # Arguments
4140    /// * `content` - The clipboard content to write to system clipboard
4141    pub fn set_clipboard_content(&mut self, content: ClipboardContent) {
4142        self.set_copy_content(self.hit_dom_node, content);
4143    }
4144
4145    /// Set/modify the clipboard content before a copy operation
4146    ///
4147    /// Use this to transform clipboard content before copying.
4148    /// The change is queued and will be applied after the callback returns,
4149    /// if `preventDefault()` was not called.
4150    pub fn set_copy_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4151        self.push_change(CallbackChange::SetCopyContent { target, content });
4152    }
4153
4154    /// Set/modify the clipboard content before a cut operation
4155    ///
4156    /// Similar to `set_copy_content` but for cut operations.
4157    /// The change is queued and will be applied after the callback returns.
4158    pub fn set_cut_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4159        self.push_change(CallbackChange::SetCutContent { target, content });
4160    }
4161
4162    /// Override the selection range for select-all operation
4163    ///
4164    /// Use this to limit what gets selected (e.g., only select visible text).
4165    /// The change is queued and will be applied after the callback returns.
4166    pub fn set_select_all_range(&mut self, target: DomNodeId, range: SelectionRange) {
4167        self.push_change(CallbackChange::SetSelectAllRange { target, range });
4168    }
4169
4170    /// Request a hit test update at a specific position
4171    ///
4172    /// This is used by the Debug API to update the hover manager's hit test
4173    /// data after modifying the mouse position. This ensures that mouse event
4174    /// callbacks can find the correct nodes under the cursor.
4175    ///
4176    /// The hit test is performed during the next frame update.
4177    pub fn request_hit_test_update(&mut self, position: LogicalPosition) {
4178        self.push_change(CallbackChange::RequestHitTestUpdate { position });
4179    }
4180
4181    /// Process a text selection click at a specific position
4182    ///
4183    /// This is used by the Debug API to trigger text selection directly,
4184    /// bypassing the normal event pipeline which generates `PreCallbackSystemEvent::TextClick`.
4185    ///
4186    /// The selection processing is deferred until the `CallbackChange` is processed,
4187    /// at which point the `LayoutWindow` can be mutably accessed.
4188    pub fn process_text_selection_click(&mut self, position: LogicalPosition, time_ms: u64) {
4189        self.push_change(CallbackChange::ProcessTextSelectionClick { position, time_ms });
4190    }
4191
4192    /// Get the current text content of a node
4193    ///
4194    /// Helper for inspecting text before operations.
4195    #[must_use] pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
4196        let layout_window = self.get_layout_window();
4197        let node_id = target.node.into_crate_internal()?;
4198        // Some("") must mean "the node exists and its text is empty" — an empty string is
4199        // valid text content. get_text_before_textinput returns an empty Vec for BOTH a
4200        // missing node and an existing-but-empty one, so verify the node actually exists
4201        // (committed layout or a pending edit) before returning Some; otherwise None,
4202        // like the sibling selection/undo queries.
4203        let exists = layout_window.dirty_text_nodes.contains_key(&(target.dom, node_id))
4204            || layout_window
4205                .layout_results
4206                .get(&target.dom)
4207                .is_some_and(|lr| node_id.index() < lr.styled_dom.node_data.as_ref().len());
4208        if !exists {
4209            return None;
4210        }
4211        let content = layout_window.get_text_before_textinput(target.dom, node_id);
4212        Some(layout_window.extract_text_from_inline_content(&content))
4213    }
4214
4215    /// Get the current cursor position in a node
4216    ///
4217    /// Returns the text cursor position if the node is focused.
4218    #[must_use] pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
4219        let layout_window = self.get_layout_window();
4220
4221        // Check if this node is focused
4222        if !layout_window.focus_manager.has_focus(&target) {
4223            return None;
4224        }
4225
4226        layout_window.text_edit_manager.get_primary_cursor()
4227    }
4228
4229    /// Get the current selection ranges in a node
4230    ///
4231    /// Returns all active selection ranges for the specified DOM.
4232    #[must_use] pub fn get_node_selection_ranges(&self, _target: DomNodeId) -> SelectionRangeVec {
4233        let layout_window = self.get_layout_window();
4234        let ranges: Vec<SelectionRange> = layout_window
4235            .text_edit_manager.multi_cursor.as_ref()
4236            .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
4237                Selection::Range(r) => Some(*r),
4238                Selection::Cursor(_) => None,
4239            }).collect()).unwrap_or_default();
4240        ranges.into()
4241    }
4242
4243    /// Check if a specific node has an active selection
4244    ///
4245    /// This checks if the specific node (identified by `DomNodeId`) has a selection,
4246    /// as opposed to `has_selection(DomId)` which checks the entire DOM.
4247    #[must_use] pub fn node_has_selection(&self, target: DomNodeId) -> bool {
4248        !self.get_node_selection_ranges(target).as_ref().is_empty()
4249    }
4250
4251    /// Get the length of text in a node
4252    ///
4253    /// Useful for bounds checking in custom operations.
4254    #[must_use] pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
4255        self.get_node_text_content(target).map(|text| text.len())
4256    }
4257
4258    // Cursor Movement Inspection/Override Methods
4259
4260    /// Inspect where the cursor would move when pressing left arrow
4261    ///
4262    /// Returns the new cursor position that would result from moving left.
4263    /// Returns None if the cursor is already at the start of the document.
4264    ///
4265    /// # Arguments
4266    /// * `target` - The node containing the cursor
4267    pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
4268        let layout_window = self.get_layout_window();
4269        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4270
4271        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4272        // inline_layout_result
4273        let layout = self.get_inline_layout_for_node(&target)?;
4274
4275        // Use the text3::cache cursor movement logic
4276        let new_cursor = layout.move_cursor_left(cursor, &mut None);
4277
4278        // Only return if cursor actually moved
4279        if new_cursor == cursor {
4280            None
4281        } else {
4282            Some(new_cursor)
4283        }
4284    }
4285
4286    /// Inspect where the cursor would move when pressing right arrow
4287    ///
4288    /// Returns the new cursor position that would result from moving right.
4289    /// Returns None if the cursor is already at the end of the document.
4290    pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
4291        let layout_window = self.get_layout_window();
4292        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4293
4294        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4295        // inline_layout_result
4296        let layout = self.get_inline_layout_for_node(&target)?;
4297
4298        // Use the text3::cache cursor movement logic
4299        let new_cursor = layout.move_cursor_right(cursor, &mut None);
4300
4301        // Only return if cursor actually moved
4302        if new_cursor == cursor {
4303            None
4304        } else {
4305            Some(new_cursor)
4306        }
4307    }
4308
4309    /// Inspect where the cursor would move when pressing up arrow
4310    ///
4311    /// Returns the new cursor position that would result from moving up one line.
4312    /// Returns None if the cursor is already on the first line.
4313    pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
4314        let layout_window = self.get_layout_window();
4315        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4316
4317        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4318        // inline_layout_result
4319        let layout = self.get_inline_layout_for_node(&target)?;
4320
4321        // Use the text3::cache cursor movement logic
4322        // goal_x maintains horizontal position when moving vertically
4323        let new_cursor = layout.move_cursor_up(cursor, &mut None, &mut None);
4324
4325        // Only return if cursor actually moved
4326        if new_cursor == cursor {
4327            None
4328        } else {
4329            Some(new_cursor)
4330        }
4331    }
4332
4333    /// Inspect where the cursor would move when pressing down arrow
4334    ///
4335    /// Returns the new cursor position that would result from moving down one line.
4336    /// Returns None if the cursor is already on the last line.
4337    pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
4338        let layout_window = self.get_layout_window();
4339        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4340
4341        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4342        // inline_layout_result
4343        let layout = self.get_inline_layout_for_node(&target)?;
4344
4345        // Use the text3::cache cursor movement logic
4346        // goal_x maintains horizontal position when moving vertically
4347        let new_cursor = layout.move_cursor_down(cursor, &mut None, &mut None);
4348
4349        // Only return if cursor actually moved
4350        if new_cursor == cursor {
4351            None
4352        } else {
4353            Some(new_cursor)
4354        }
4355    }
4356
4357    /// Inspect where the cursor would move when pressing Home key
4358    ///
4359    /// Returns the cursor position at the start of the current line.
4360    pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
4361        let layout_window = self.get_layout_window();
4362        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4363
4364        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4365        // inline_layout_result
4366        let layout = self.get_inline_layout_for_node(&target)?;
4367
4368        // Use the text3::cache cursor movement logic
4369        let new_cursor = layout.move_cursor_to_line_start(cursor, &mut None);
4370
4371        // Always return the result (might be same as input if already at line start)
4372        Some(new_cursor)
4373    }
4374
4375    /// Inspect where the cursor would move when pressing End key
4376    ///
4377    /// Returns the cursor position at the end of the current line.
4378    pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
4379        let layout_window = self.get_layout_window();
4380        let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4381
4382        // Get the text layout directly via layout_results -> LayoutTree -> LayoutNode ->
4383        // inline_layout_result
4384        let layout = self.get_inline_layout_for_node(&target)?;
4385
4386        // Use the text3::cache cursor movement logic
4387        let new_cursor = layout.move_cursor_to_line_end(cursor, &mut None);
4388
4389        // Always return the result (might be same as input if already at line end)
4390        Some(new_cursor)
4391    }
4392
4393    /// Inspect where the cursor would move when pressing Ctrl+Home
4394    ///
4395    /// Returns the cursor position at the start of the document.
4396    #[must_use] pub const fn inspect_move_cursor_to_document_start(&self, target: DomNodeId) -> Option<TextCursor> {
4397        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4398
4399        Some(TextCursor {
4400            cluster_id: GraphemeClusterId {
4401                source_run: 0,
4402                start_byte_in_run: 0,
4403            },
4404            affinity: CursorAffinity::Leading,
4405        })
4406    }
4407
4408    /// Inspect where the cursor would move when pressing Ctrl+End
4409    ///
4410    /// Returns the cursor position at the end of the document.
4411    #[must_use] pub fn inspect_move_cursor_to_document_end(&self, target: DomNodeId) -> Option<TextCursor> {
4412        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4413
4414        let text_len = self.get_node_text_length(target)?;
4415
4416        Some(TextCursor {
4417            cluster_id: GraphemeClusterId {
4418                source_run: 0,
4419                start_byte_in_run: u32::try_from(text_len).unwrap_or(u32::MAX),
4420            },
4421            affinity: CursorAffinity::Leading,
4422        })
4423    }
4424
4425    /// Inspect what text would be deleted by backspace (including Shift+Backspace)
4426    ///
4427    /// Returns (`range_to_delete`, `deleted_text`).
4428    /// This is a convenience wrapper around `inspect_delete_changeset(target`, false).
4429    #[must_use] pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
4430        self.inspect_delete_changeset(target, false)
4431    }
4432
4433    /// Inspect what text would be deleted by delete key
4434    ///
4435    /// Returns (`range_to_delete`, `deleted_text`).
4436    /// This is a convenience wrapper around `inspect_delete_changeset(target`, true).
4437    #[must_use] pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
4438        self.inspect_delete_changeset(target, true)
4439    }
4440
4441    // Cursor Movement Override Methods
4442    // These methods queue cursor movement operations to be applied after the callback
4443
4444    /// Move cursor left (arrow left key)
4445    ///
4446    /// # Arguments
4447    /// * `target` - The node containing the cursor
4448    /// * `extend_selection` - If true, extends selection (Shift+Left); if false, moves cursor
4449    pub fn move_cursor_left(&mut self, target: DomNodeId, extend_selection: bool) {
4450        self.push_change(CallbackChange::MoveCursorLeft {
4451            dom_id: target.dom,
4452            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4453            extend_selection,
4454        });
4455    }
4456
4457    /// Move cursor right (arrow right key)
4458    pub fn move_cursor_right(&mut self, target: DomNodeId, extend_selection: bool) {
4459        self.push_change(CallbackChange::MoveCursorRight {
4460            dom_id: target.dom,
4461            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4462            extend_selection,
4463        });
4464    }
4465
4466    /// Move cursor up (arrow up key)
4467    pub fn move_cursor_up(&mut self, target: DomNodeId, extend_selection: bool) {
4468        self.push_change(CallbackChange::MoveCursorUp {
4469            dom_id: target.dom,
4470            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4471            extend_selection,
4472        });
4473    }
4474
4475    /// Move cursor down (arrow down key)
4476    pub fn move_cursor_down(&mut self, target: DomNodeId, extend_selection: bool) {
4477        self.push_change(CallbackChange::MoveCursorDown {
4478            dom_id: target.dom,
4479            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4480            extend_selection,
4481        });
4482    }
4483
4484    /// Move cursor to line start (Home key)
4485    pub fn move_cursor_to_line_start(&mut self, target: DomNodeId, extend_selection: bool) {
4486        self.push_change(CallbackChange::MoveCursorToLineStart {
4487            dom_id: target.dom,
4488            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4489            extend_selection,
4490        });
4491    }
4492
4493    /// Move cursor to line end (End key)
4494    pub fn move_cursor_to_line_end(&mut self, target: DomNodeId, extend_selection: bool) {
4495        self.push_change(CallbackChange::MoveCursorToLineEnd {
4496            dom_id: target.dom,
4497            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4498            extend_selection,
4499        });
4500    }
4501
4502    /// Move cursor to document start (Ctrl+Home)
4503    pub fn move_cursor_to_document_start(&mut self, target: DomNodeId, extend_selection: bool) {
4504        self.push_change(CallbackChange::MoveCursorToDocumentStart {
4505            dom_id: target.dom,
4506            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4507            extend_selection,
4508        });
4509    }
4510
4511    /// Move cursor to document end (Ctrl+End)
4512    pub fn move_cursor_to_document_end(&mut self, target: DomNodeId, extend_selection: bool) {
4513        self.push_change(CallbackChange::MoveCursorToDocumentEnd {
4514            dom_id: target.dom,
4515            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4516            extend_selection,
4517        });
4518    }
4519
4520    /// Delete text backward (backspace or Shift+Backspace)
4521    ///
4522    /// Queues a backspace operation to be applied after the callback.
4523    /// Use `inspect_backspace()` to see what would be deleted.
4524    pub fn delete_backward(&mut self, target: DomNodeId) {
4525        self.push_change(CallbackChange::DeleteBackward {
4526            dom_id: target.dom,
4527            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4528        });
4529    }
4530
4531    /// Delete text forward (delete key)
4532    ///
4533    /// Queues a delete operation to be applied after the callback.
4534    /// Use `inspect_delete()` to see what would be deleted.
4535    pub fn delete_forward(&mut self, target: DomNodeId) {
4536        self.push_change(CallbackChange::DeleteForward {
4537            dom_id: target.dom,
4538            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4539        });
4540    }
4541}
4542
4543/// Config necessary for threading + animations to work in `no_std` environments
4544#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4545#[repr(C)]
4546pub struct ExternalSystemCallbacks {
4547    pub create_thread_fn: CreateThreadCallback,
4548    pub get_system_time_fn: GetSystemTimeCallback,
4549}
4550
4551impl ExternalSystemCallbacks {
4552    #[must_use] pub fn rust_internal() -> Self {
4553        use crate::thread::create_thread_libstd;
4554
4555        Self {
4556            create_thread_fn: CreateThreadCallback {
4557                cb: create_thread_libstd,
4558            },
4559            get_system_time_fn: GetSystemTimeCallback {
4560                cb: task::get_system_time_libstd,
4561            },
4562        }
4563    }
4564}
4565
4566/// Request to change focus, returned from callbacks
4567#[derive(Copy, Debug, Clone, PartialEq, Eq)]
4568pub enum FocusUpdateRequest {
4569    /// Focus a specific node
4570    FocusNode(DomNodeId),
4571    /// Clear focus (no node has focus)
4572    ClearFocus,
4573    /// No focus change requested
4574    NoChange,
4575}
4576
4577impl FocusUpdateRequest {
4578    /// Check if this represents a focus change
4579    #[must_use] pub const fn is_change(&self) -> bool {
4580        !matches!(self, Self::NoChange)
4581    }
4582
4583    /// Convert to the new focused node (Some(node) or None for clear)
4584    #[must_use] pub const fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
4585        match self {
4586            Self::FocusNode(node) => Some(Some(*node)),
4587            Self::ClearFocus => Some(None),
4588            Self::NoChange => None,
4589        }
4590    }
4591
4592    /// Create from Option<Option<DomNodeId>> (legacy format)
4593    #[must_use] pub const fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
4594        match opt {
4595            Some(Some(node)) => Self::FocusNode(node),
4596            Some(None) => Self::ClearFocus,
4597            None => Self::NoChange,
4598        }
4599    }
4600}
4601
4602/// Menu callback: What data / function pointer should
4603/// be called when the menu item is clicked?
4604#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4605#[repr(C)]
4606pub struct MenuCallback {
4607    pub callback: Callback,
4608    pub refany: RefAny,
4609}
4610#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
4611/// Optional `MenuCallback`
4612#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4613#[repr(C, u8)]
4614pub enum OptionMenuCallback {
4615    None,
4616    Some(MenuCallback),
4617}
4618
4619impl OptionMenuCallback {
4620    #[must_use] pub fn into_option(self) -> Option<MenuCallback> {
4621        match self {
4622            Self::None => None,
4623            Self::Some(c) => Some(c),
4624        }
4625    }
4626
4627    #[must_use] pub const fn is_some(&self) -> bool {
4628        matches!(self, Self::Some(_))
4629    }
4630
4631    #[must_use] pub const fn is_none(&self) -> bool {
4632        matches!(self, Self::None)
4633    }
4634}
4635
4636impl From<Option<MenuCallback>> for OptionMenuCallback {
4637    fn from(o: Option<MenuCallback>) -> Self {
4638        o.map_or_else(|| Self::None, Self::Some)
4639    }
4640}
4641
4642impl From<OptionMenuCallback> for Option<MenuCallback> {
4643    fn from(o: OptionMenuCallback) -> Self {
4644        o.into_option()
4645    }
4646}
4647
4648// -- RenderImage callbacks
4649
4650/// Callback type that renders an OpenGL texture
4651///
4652/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallbackType = usize`
4653/// to avoid circular dependencies. The actual function pointer is cast to usize for
4654/// storage in the data model, then unsafely cast back to this type when invoked.
4655pub type RenderImageCallbackType = extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
4656
4657/// Callback that returns a rendered OpenGL texture
4658///
4659/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallback` with
4660/// a `cb: usize` field. When creating callbacks in the data model, function pointers
4661/// are cast to usize. This type is used in azul-layout where we can safely work
4662/// with the actual function pointer type.
4663#[repr(C)]
4664pub struct RenderImageCallback {
4665    pub cb: RenderImageCallbackType,
4666    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
4667    /// Native Rust code sets this to None
4668    pub ctx: OptionRefAny,
4669}
4670
4671impl_callback!(RenderImageCallback, RenderImageCallbackType);
4672
4673impl RenderImageCallback {
4674    /// Create a new callback with just a function pointer (for native Rust code)
4675    pub fn create(cb: RenderImageCallbackType) -> Self {
4676        Self {
4677            cb,
4678            ctx: OptionRefAny::None,
4679        }
4680    }
4681
4682    /// Convert from the core crate's `CoreRenderImageCallback` (which stores cb as usize)
4683    /// back to the layout crate's typed function pointer.
4684    ///
4685    /// # Safety
4686    ///
4687    /// This is safe because we ensure that the usize in `CoreRenderImageCallback`
4688    /// was originally created from a valid `RenderImageCallbackType` function pointer.
4689    #[must_use] pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
4690        debug_assert!(core_callback.cb != 0, "CoreRenderImageCallback.cb is null");
4691        Self {
4692            cb: unsafe { core::mem::transmute::<usize, RenderImageCallbackType>(core_callback.cb) },
4693            ctx: core_callback.ctx.clone(),
4694        }
4695    }
4696
4697    /// Convert to `CoreRenderImageCallback` (function pointer stored as usize)
4698    ///
4699    /// This is always safe - we're just casting the function pointer to usize for storage.
4700    #[must_use] pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
4701        azul_core::callbacks::CoreRenderImageCallback {
4702            cb: self.cb as usize,
4703            ctx: self.ctx,
4704        }
4705    }
4706}
4707
4708/// Allow `RenderImageCallback` to be passed to functions expecting `C: Into<CoreRenderImageCallback>`
4709impl From<RenderImageCallback> for azul_core::callbacks::CoreRenderImageCallback {
4710    fn from(callback: RenderImageCallback) -> Self {
4711        callback.to_core()
4712    }
4713}
4714
4715/// Information passed to image rendering callbacks
4716#[derive(Debug)]
4717#[repr(C)]
4718pub struct RenderImageCallbackInfo {
4719    /// The ID of the DOM node that the `ImageCallback` was attached to
4720    callback_node_id: DomNodeId,
4721    /// Bounds of the laid-out node
4722    bounds: HidpiAdjustedBounds,
4723    /// Optional OpenGL context pointer
4724    gl_context: *const OptionGlContextPtr,
4725    /// Image cache for looking up images
4726    image_cache: *const ImageCache,
4727    /// System font cache
4728    system_fonts: *const FcFontCache,
4729    /// Pointer to callable (Python/FFI callback function)
4730    callable_ptr: *const OptionRefAny,
4731    /// Extension for future ABI stability (mutable data)
4732    _abi_mut: *mut core::ffi::c_void,
4733}
4734
4735impl Clone for RenderImageCallbackInfo {
4736    // `_abi_mut` is an intentional FFI/api.json ABI-stability placeholder field.
4737    #[allow(clippy::used_underscore_binding)]
4738    fn clone(&self) -> Self {
4739        Self {
4740            callback_node_id: self.callback_node_id,
4741            bounds: self.bounds,
4742            gl_context: self.gl_context,
4743            image_cache: self.image_cache,
4744            system_fonts: self.system_fonts,
4745            callable_ptr: self.callable_ptr,
4746            _abi_mut: self._abi_mut,
4747        }
4748    }
4749}
4750
4751impl RenderImageCallbackInfo {
4752    #[must_use] pub const fn new<'a>(
4753        callback_node_id: DomNodeId,
4754        bounds: HidpiAdjustedBounds,
4755        gl_context: &'a OptionGlContextPtr,
4756        image_cache: &'a ImageCache,
4757        system_fonts: &'a FcFontCache,
4758    ) -> Self {
4759        Self {
4760            callback_node_id,
4761            bounds,
4762            gl_context: std::ptr::from_ref::<OptionGlContextPtr>(gl_context),
4763            image_cache: std::ptr::from_ref::<ImageCache>(image_cache),
4764            system_fonts: std::ptr::from_ref::<FcFontCache>(system_fonts),
4765            callable_ptr: core::ptr::null(),
4766            _abi_mut: core::ptr::null_mut(),
4767        }
4768    }
4769
4770    /// Get the callable for FFI language bindings (Python, etc.)
4771    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
4772        if self.callable_ptr.is_null() {
4773            OptionRefAny::None
4774        } else {
4775            unsafe { (*self.callable_ptr).clone() }
4776        }
4777    }
4778
4779    /// Set the callable pointer (called before invoking callback)
4780    ///
4781    /// # Safety
4782    ///
4783    /// `ptr` must either be null or point to an `OptionRefAny` that stays valid
4784    /// for as long as this `CallbackInfo` may read it (i.e. until the pointer is
4785    /// replaced or the callback returns). The pointee is read by [`get_ctx`]; a
4786    /// dangling or misaligned `ptr` is undefined behavior.
4787    ///
4788    /// [`get_ctx`]: Self::get_ctx
4789    pub const unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
4790        self.callable_ptr = ptr;
4791    }
4792
4793    #[must_use] pub const fn get_callback_node_id(&self) -> DomNodeId {
4794        self.callback_node_id
4795    }
4796
4797    #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
4798        self.bounds
4799    }
4800
4801    const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
4802        unsafe { &*self.gl_context }
4803    }
4804
4805    const fn internal_get_image_cache(&self) -> &ImageCache {
4806        unsafe { &*self.image_cache }
4807    }
4808
4809    const fn internal_get_system_fonts(&self) -> &FcFontCache {
4810        unsafe { &*self.system_fonts }
4811    }
4812
4813    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
4814        self.internal_get_gl_context().clone()
4815    }
4816}
4817
4818// ============================================================================
4819// Result types for FFI
4820// ============================================================================
4821
4822/// Result type for functions returning `U8Vec` or a String error
4823#[derive(Debug, Clone)]
4824#[repr(C, u8)]
4825pub enum ResultU8VecString {
4826    Ok(U8Vec),
4827    Err(AzString),
4828}
4829
4830impl From<Result<alloc::vec::Vec<u8>, AzString>> for ResultU8VecString {
4831    fn from(result: Result<alloc::vec::Vec<u8>, AzString>) -> Self {
4832        match result {
4833            Ok(v) => Self::Ok(v.into()),
4834            Err(e) => Self::Err(e),
4835        }
4836    }
4837}
4838#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
4839/// Result type for functions returning () or a String error  
4840#[derive(Debug, Clone)]
4841#[repr(C, u8)]
4842pub enum ResultVoidString {
4843    Ok,
4844    Err(AzString),
4845}
4846
4847impl From<Result<(), AzString>> for ResultVoidString {
4848    fn from(result: Result<(), AzString>) -> Self {
4849        match result {
4850            Ok(()) => Self::Ok,
4851            Err(e) => Self::Err(e),
4852        }
4853    }
4854}
4855
4856/// Result type for functions returning String or a String error  
4857#[derive(Debug, Clone)]
4858#[repr(C, u8)]
4859pub enum ResultStringString {
4860    Ok(AzString),
4861    Err(AzString),
4862}
4863
4864impl From<Result<AzString, AzString>> for ResultStringString {
4865    fn from(result: Result<AzString, AzString>) -> Self {
4866        match result {
4867            Ok(s) => Self::Ok(s),
4868            Err(e) => Self::Err(e),
4869        }
4870    }
4871}
4872
4873// ============================================================================
4874// Base64 encoding helper
4875// ============================================================================
4876
4877const BASE64_ALPHABET: &[u8; 64] =
4878    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4879
4880/// Encode bytes to Base64 string
4881#[must_use] pub fn base64_encode(input: &[u8]) -> String {
4882    let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
4883
4884    for chunk in input.chunks(3) {
4885        let b0 = chunk[0] as usize;
4886        let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
4887        let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
4888
4889        let n = (b0 << 16) | (b1 << 8) | b2;
4890
4891        output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
4892        output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
4893
4894        if chunk.len() > 1 {
4895            output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
4896        } else {
4897            output.push('=');
4898        }
4899
4900        if chunk.len() > 2 {
4901            output.push(BASE64_ALPHABET[n & 0x3F] as char);
4902        } else {
4903            output.push('=');
4904        }
4905    }
4906
4907    output
4908}
4909
4910#[cfg(all(test, feature = "std"))]
4911#[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
4912mod autotest_generated {
4913    use super::*;
4914
4915    // ------------------------------------------------------------------
4916    // Harness
4917    // ------------------------------------------------------------------
4918
4919    /// Runs `f` with a fully-constructed `CallbackInfo` backed by an *empty*
4920    /// `LayoutWindow` (no layout results, no timers, no threads, no routes).
4921    /// Every query API therefore hits its "nothing there" path — which is
4922    /// exactly the path adversarial tests need to exercise.
4923    fn with_info<R>(hit: DomNodeId, f: impl FnOnce(&mut CallbackInfo) -> R) -> R {
4924        let layout_window =
4925            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
4926        let renderer_resources = RendererResources::default();
4927        let previous_window_state: Option<FullWindowState> = None;
4928        let current_window_state = FullWindowState::default();
4929        let gl_context = OptionGlContextPtr::None;
4930        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
4931            BTreeMap::new();
4932        let window_handle = RawWindowHandle::Unsupported;
4933        let system_callbacks = ExternalSystemCallbacks::rust_internal();
4934
4935        let ref_data = CallbackInfoRefData {
4936            layout_window: &layout_window,
4937            renderer_resources: &renderer_resources,
4938            previous_window_state: &previous_window_state,
4939            current_window_state: &current_window_state,
4940            gl_context: &gl_context,
4941            current_scroll_manager: &scroll_states,
4942            current_window_handle: &window_handle,
4943            system_callbacks: &system_callbacks,
4944            system_style: Arc::new(SystemStyle::default()),
4945            monitors: Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
4946            #[cfg(feature = "icu")]
4947            icu_localizer: IcuLocalizerHandle::default(),
4948            ctx: OptionRefAny::None,
4949        };
4950
4951        let changes: Arc<std::sync::Mutex<Vec<CallbackChange>>> =
4952            Arc::new(std::sync::Mutex::new(Vec::new()));
4953
4954        let mut info = CallbackInfo::new(
4955            &ref_data,
4956            &changes,
4957            hit,
4958            OptionLogicalPosition::None,
4959            OptionLogicalPosition::None,
4960        );
4961
4962        f(&mut info)
4963    }
4964
4965    /// `DomNodeId` pointing at node 0 of the root DOM.
4966    fn node0() -> DomNodeId {
4967        DomNodeId {
4968            dom: DomId::ROOT_ID,
4969            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
4970        }
4971    }
4972
4973    /// `DomNodeId` whose node component is `None` (the "no concrete node" case).
4974    fn node_none() -> DomNodeId {
4975        DomNodeId {
4976            dom: DomId::ROOT_ID,
4977            node: NodeHierarchyItemId::NONE,
4978        }
4979    }
4980
4981    extern "C" fn cb_do_nothing(_: RefAny, _: CallbackInfo) -> Update {
4982        Update::DoNothing
4983    }
4984
4985    extern "C" fn cb_refresh_dom(_: RefAny, _: CallbackInfo) -> Update {
4986        Update::RefreshDom
4987    }
4988
4989    /// Pushes a change, so we can prove `invoke` really reaches the transaction log.
4990    extern "C" fn cb_pushes_change(_: RefAny, mut info: CallbackInfo) -> Update {
4991        info.stop_propagation();
4992        Update::RefreshDomAllWindows
4993    }
4994
4995    extern "C" fn img_cb(_: RefAny, _: RenderImageCallbackInfo) -> ImageRef {
4996        ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
4997    }
4998
4999    fn a_css_property() -> CssProperty {
5000        use azul_css::props::{basic::PixelValue, layout::dimensions::LayoutWidth};
5001        CssProperty::const_width(LayoutWidth::Px(PixelValue::px(123.0)))
5002    }
5003
5004    fn a_cursor() -> TextCursor {
5005        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
5006        TextCursor {
5007            cluster_id: GraphemeClusterId {
5008                source_run: 0,
5009                start_byte_in_run: 0,
5010            },
5011            affinity: CursorAffinity::Leading,
5012        }
5013    }
5014
5015    // ------------------------------------------------------------------
5016    // base64_encode - round-trip, boundary, huge, unicode
5017    // ------------------------------------------------------------------
5018
5019    /// Strict RFC-4648 decoder, written independently of the encoder so that
5020    /// `decode(encode(x)) == x` is a real round-trip and not a tautology.
5021    fn base64_decode(s: &str) -> Option<Vec<u8>> {
5022        fn val(c: u8) -> Option<u32> {
5023            match c {
5024                b'A'..=b'Z' => Some((c - b'A') as u32),
5025                b'a'..=b'z' => Some((c - b'a') as u32 + 26),
5026                b'0'..=b'9' => Some((c - b'0') as u32 + 52),
5027                b'+' => Some(62),
5028                b'/' => Some(63),
5029                _ => None,
5030            }
5031        }
5032
5033        let bytes = s.as_bytes();
5034        if bytes.len() % 4 != 0 {
5035            return None;
5036        }
5037        let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
5038        for chunk in bytes.chunks(4) {
5039            let pad = chunk.iter().filter(|&&c| c == b'=').count();
5040            if pad > 2 {
5041                return None;
5042            }
5043            let mut n: u32 = 0;
5044            for (i, &c) in chunk.iter().enumerate() {
5045                let v = if c == b'=' { 0 } else { val(c)? };
5046                n |= v << (18 - 6 * i as u32);
5047            }
5048            out.push(((n >> 16) & 0xFF) as u8);
5049            if pad < 2 {
5050                out.push(((n >> 8) & 0xFF) as u8);
5051            }
5052            if pad < 1 {
5053                out.push((n & 0xFF) as u8);
5054            }
5055        }
5056        Some(out)
5057    }
5058
5059    #[test]
5060    fn base64_encode_rfc4648_test_vectors() {
5061        assert_eq!(base64_encode(b""), "");
5062        assert_eq!(base64_encode(b"f"), "Zg==");
5063        assert_eq!(base64_encode(b"fo"), "Zm8=");
5064        assert_eq!(base64_encode(b"foo"), "Zm9v");
5065        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
5066        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
5067        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
5068    }
5069
5070    #[test]
5071    fn base64_encode_extreme_bytes() {
5072        // All-zero and all-ones map to the first / last alphabet entries.
5073        assert_eq!(base64_encode(&[0x00, 0x00, 0x00]), "AAAA");
5074        assert_eq!(base64_encode(&[0xFF, 0xFF, 0xFF]), "////");
5075        // Single 0xFF byte: two significant chars + two pad chars.
5076        assert_eq!(base64_encode(&[0xFF]), "/w==");
5077        assert_eq!(base64_encode(&[0xFF, 0xFF]), "//8=");
5078        // Every 6-bit value 0..63 appears exactly once in the alphabet.
5079        let all: Vec<u8> = (0u8..=255).collect();
5080        let enc = base64_encode(&all);
5081        assert_eq!(base64_decode(&enc).as_deref(), Some(all.as_slice()));
5082    }
5083
5084    #[test]
5085    fn base64_encode_output_length_is_ceil_div_3_times_4() {
5086        for n in 0usize..=64 {
5087            let input = vec![0xABu8; n];
5088            let enc = base64_encode(&input);
5089            assert_eq!(
5090                enc.len(),
5091                n.div_ceil(3) * 4,
5092                "unexpected encoded length for {n} input bytes"
5093            );
5094            // Padding is only ever at the very end, and never more than 2 chars.
5095            let pad = enc.bytes().filter(|&c| c == b'=').count();
5096            assert!(pad <= 2, "too much padding for n = {n}");
5097            assert_eq!(pad, (3 - n % 3) % 3, "wrong padding count for n = {n}");
5098            if pad > 0 {
5099                assert!(enc.ends_with(&"=".repeat(pad)));
5100            }
5101        }
5102    }
5103
5104    #[test]
5105    fn base64_encode_emits_only_alphabet_characters() {
5106        let input: Vec<u8> = (0u8..=255).chain(0u8..=255).collect();
5107        let enc = base64_encode(&input);
5108        for c in enc.bytes() {
5109            assert!(
5110                c == b'=' || BASE64_ALPHABET.contains(&c),
5111                "non-base64 char {c:?} in output"
5112            );
5113        }
5114    }
5115
5116    #[test]
5117    fn base64_encode_round_trips_for_every_length_remainder() {
5118        // 0, 1, 2 mod 3 all exercise a different padding branch.
5119        for n in 0usize..=130 {
5120            let input: Vec<u8> = (0..n).map(|i| (i * 7 + 13) as u8).collect();
5121            let enc = base64_encode(&input);
5122            let dec = base64_decode(&enc).unwrap_or_else(|| panic!("failed to decode {enc:?}"));
5123            assert_eq!(dec, input, "round-trip failed at length {n}");
5124        }
5125    }
5126
5127    #[test]
5128    fn base64_encode_unicode_bytes_round_trip() {
5129        for s in [
5130            "\u{1F600}",                 // emoji (4-byte UTF-8)
5131            "e\u{301}",                  // combining acute accent
5132            "\u{0}\u{7F}\u{80}\u{FFFF}", // control + boundary code points
5133            "тест 日本語 🌍",
5134        ] {
5135            let enc = base64_encode(s.as_bytes());
5136            assert_eq!(base64_decode(&enc).as_deref(), Some(s.as_bytes()));
5137        }
5138        // Known-good positive control: base64("😀") == "8J+YgA=="
5139        assert_eq!(base64_encode("\u{1F600}".as_bytes()), "8J+YgA==");
5140    }
5141
5142    #[test]
5143    fn base64_encode_one_megabyte_does_not_panic_or_hang() {
5144        let input = vec![0x5Au8; 1_000_000];
5145        let enc = base64_encode(&input);
5146        assert_eq!(enc.len(), 1_000_000usize.div_ceil(3) * 4);
5147        // 1_000_000 % 3 == 1 -> exactly two padding chars.
5148        assert!(enc.ends_with("=="));
5149        assert_eq!(base64_decode(&enc).map(|v| v.len()), Some(1_000_000));
5150    }
5151
5152    // ------------------------------------------------------------------
5153    // PenTilt / SelectAllResult / DeleteResult (From conversions)
5154    // ------------------------------------------------------------------
5155
5156    #[test]
5157    fn pen_tilt_from_tuple_preserves_extreme_floats() {
5158        let t = PenTilt::from((0.0, -0.0));
5159        assert_eq!(t.x_tilt, 0.0);
5160        assert!(t.y_tilt.is_sign_negative());
5161
5162        let t = PenTilt::from((f32::MAX, f32::MIN));
5163        assert_eq!(t.x_tilt, f32::MAX);
5164        assert_eq!(t.y_tilt, f32::MIN);
5165
5166        let t = PenTilt::from((f32::INFINITY, f32::NEG_INFINITY));
5167        assert!(t.x_tilt.is_infinite() && t.x_tilt.is_sign_positive());
5168        assert!(t.y_tilt.is_infinite() && t.y_tilt.is_sign_negative());
5169
5170        // NaN is passed through unchanged (no sanitisation) - and, being NaN,
5171        // makes the derived PartialEq report "not equal to itself".
5172        let t = PenTilt::from((f32::NAN, 90.0));
5173        assert!(t.x_tilt.is_nan());
5174        assert_eq!(t.y_tilt, 90.0);
5175        assert_ne!(t, t);
5176    }
5177
5178    #[test]
5179    fn option_pen_tilt_is_some_is_none_are_exclusive() {
5180        let some = OptionPenTilt::Some(PenTilt::from((1.0, 2.0)));
5181        let none = OptionPenTilt::None;
5182        assert!(some.is_some() && !some.is_none());
5183        assert!(none.is_none() && !none.is_some());
5184    }
5185
5186    #[test]
5187    fn select_all_result_from_tuple_keeps_fields_including_empty_and_huge() {
5188        let range = SelectionRange {
5189            start: a_cursor(),
5190            end: a_cursor(),
5191        };
5192
5193        let empty = SelectAllResult::from((String::new(), range));
5194        assert_eq!(empty.full_text.as_str(), "");
5195        assert_eq!(empty.selection_range, range);
5196
5197        let huge = SelectAllResult::from(("x".repeat(100_000), range));
5198        assert_eq!(huge.full_text.as_str().len(), 100_000);
5199
5200        let unicode = SelectAllResult::from(("🌍\u{0}é".to_string(), range));
5201        assert_eq!(unicode.full_text.as_str(), "🌍\u{0}é");
5202    }
5203
5204    #[test]
5205    fn delete_result_from_tuple_keeps_fields() {
5206        let range = SelectionRange {
5207            start: a_cursor(),
5208            end: a_cursor(),
5209        };
5210        let d = DeleteResult::from((range, String::new()));
5211        assert_eq!(d.range_to_delete, range);
5212        assert_eq!(d.deleted_text.as_str(), "");
5213
5214        let d = DeleteResult::from((range, "\u{1F600}".to_string()));
5215        assert_eq!(d.deleted_text.as_str(), "\u{1F600}");
5216    }
5217
5218    // ------------------------------------------------------------------
5219    // Callback: constructors, core round-trip, invoke, eq/hash invariants
5220    // ------------------------------------------------------------------
5221
5222    #[test]
5223    fn callback_from_ptr_and_create_and_from_agree() {
5224        let a = Callback::from_ptr(cb_do_nothing);
5225        let b = Callback::create(cb_do_nothing as CallbackType);
5226        let c = Callback::from(cb_do_nothing as CallbackType);
5227
5228        assert_eq!(a, b);
5229        assert_eq!(b, c);
5230        // Constructed from a bare fn pointer => no FFI ctx attached.
5231        assert!(a.ctx.is_none());
5232        assert!(b.ctx.is_none());
5233        assert!(c.ctx.is_none());
5234        assert_ne!(a.cb as usize, 0);
5235    }
5236
5237    #[test]
5238    fn callback_to_core_from_core_round_trips_pointer_and_ctx() {
5239        let original = Callback {
5240            cb: cb_refresh_dom,
5241            ctx: OptionRefAny::Some(RefAny::new(0xDEAD_BEEFu32)),
5242        };
5243        let ptr = original.cb as usize;
5244
5245        let core = original.to_core();
5246        assert_eq!(core.cb, ptr);
5247        assert!(core.ctx.is_some(), "to_core must not drop the FFI ctx");
5248
5249        let back = Callback::from_core(core);
5250        assert_eq!(back.cb as usize, ptr, "encode == decode for the fn pointer");
5251        assert!(
5252            back.ctx.is_some(),
5253            "from_core must preserve ctx (managed-FFI handlers rely on it)"
5254        );
5255    }
5256
5257    #[test]
5258    fn callback_to_core_of_ctxless_callback_keeps_ctx_none() {
5259        let core = Callback::from_ptr(cb_do_nothing).to_core();
5260        assert!(core.ctx.is_none());
5261        assert_eq!(Callback::from_core(core).cb as usize, cb_do_nothing as usize);
5262    }
5263
5264    #[test]
5265    #[cfg(debug_assertions)]
5266    #[should_panic(expected = "CoreCallback.cb is null")]
5267    fn callback_from_core_null_pointer_trips_debug_assert() {
5268        // A null fn pointer would be UB to call; from_core must not silently
5269        // hand one back in a debug build.
5270        let _ = Callback::from_core(CoreCallback {
5271            cb: 0,
5272            ctx: OptionRefAny::None,
5273        });
5274    }
5275
5276    #[test]
5277    fn callback_invoke_returns_the_functions_update() {
5278        let update = with_info(node_none(), |info| {
5279            Callback::from_ptr(cb_refresh_dom).invoke(RefAny::new(1u8), *info)
5280        });
5281        assert!(matches!(update, Update::RefreshDom));
5282
5283        let update = with_info(node_none(), |info| {
5284            Callback::from_ptr(cb_do_nothing).invoke(RefAny::new(1u8), *info)
5285        });
5286        assert!(matches!(update, Update::DoNothing));
5287    }
5288
5289    #[test]
5290    fn callback_invoke_changes_reach_the_callers_transaction_log() {
5291        // CallbackInfo is Copy; a change pushed through the *copy* handed to the
5292        // callback must still land in the original's change vector.
5293        let changes = with_info(node_none(), |info| {
5294            let update = Callback::from_ptr(cb_pushes_change).invoke(RefAny::new(0u8), *info);
5295            assert!(matches!(update, Update::RefreshDomAllWindows));
5296            info.take_changes()
5297        });
5298        assert_eq!(changes.len(), 1);
5299        assert!(matches!(changes[0], CallbackChange::StopPropagation));
5300    }
5301
5302    #[test]
5303    fn callback_eq_and_hash_ignore_ctx_but_stay_consistent() {
5304        use std::{
5305            collections::hash_map::DefaultHasher,
5306            hash::{Hash, Hasher},
5307        };
5308
5309        let plain = Callback::from_ptr(cb_do_nothing);
5310        let with_ctx = Callback {
5311            cb: cb_do_nothing,
5312            ctx: OptionRefAny::Some(RefAny::new(7u64)),
5313        };
5314        let other_fn = Callback::from_ptr(cb_refresh_dom);
5315
5316        // Documented macro behaviour: identity is the fn pointer alone.
5317        assert_eq!(plain, with_ctx);
5318        assert_ne!(plain, other_fn);
5319
5320        // Eq/Hash must agree, or these end up as duplicate keys in a HashMap.
5321        let hash = |c: &Callback| {
5322            let mut h = DefaultHasher::new();
5323            c.hash(&mut h);
5324            h.finish()
5325        };
5326        assert_eq!(hash(&plain), hash(&with_ctx));
5327    }
5328
5329    // ------------------------------------------------------------------
5330    // OptionCallback / OptionMenuCallback predicates + round-trips
5331    // ------------------------------------------------------------------
5332
5333    #[test]
5334    fn option_callback_predicates_are_exclusive_and_total() {
5335        let none = OptionCallback::None;
5336        let some = OptionCallback::Some(Callback::from_ptr(cb_do_nothing));
5337
5338        assert!(none.is_none() && !none.is_some());
5339        assert!(some.is_some() && !some.is_none());
5340        // Exactly one of the two predicates holds, for every value.
5341        for v in [&none, &some] {
5342            assert!(v.is_some() ^ v.is_none());
5343        }
5344    }
5345
5346    #[test]
5347    fn option_callback_round_trips_through_std_option() {
5348        let cb = Callback::from_ptr(cb_do_nothing);
5349
5350        let round = OptionCallback::from(Some(cb.clone())).into_option();
5351        assert_eq!(round, Some(cb.clone()));
5352
5353        let round = OptionCallback::from(None).into_option();
5354        assert_eq!(round, None);
5355
5356        // and the other direction of the From impls
5357        let ffi: OptionCallback = Some(cb.clone()).into();
5358        let back: Option<Callback> = ffi.into();
5359        assert_eq!(back, Some(cb));
5360
5361        let ffi: OptionCallback = None.into();
5362        let back: Option<Callback> = ffi.into();
5363        assert_eq!(back, None);
5364    }
5365
5366    #[test]
5367    fn option_menu_callback_predicates_and_round_trip() {
5368        let mc = MenuCallback {
5369            callback: Callback::from_ptr(cb_do_nothing),
5370            refany: RefAny::new(5i32),
5371        };
5372
5373        let none = OptionMenuCallback::None;
5374        assert!(none.is_none() && !none.is_some());
5375        assert_eq!(none.into_option(), None);
5376
5377        let some = OptionMenuCallback::from(Some(mc.clone()));
5378        assert!(some.is_some() && !some.is_none());
5379        assert_eq!(some.into_option(), Some(mc));
5380
5381        let back: Option<MenuCallback> = OptionMenuCallback::None.into();
5382        assert!(back.is_none());
5383    }
5384
5385    // ------------------------------------------------------------------
5386    // RenderImageCallback + RenderImageCallbackInfo
5387    // ------------------------------------------------------------------
5388
5389    #[test]
5390    fn render_image_callback_core_round_trip() {
5391        let cb = RenderImageCallback::create(img_cb);
5392        assert!(cb.ctx.is_none());
5393        let ptr = cb.cb as usize;
5394
5395        let core = cb.to_core();
5396        assert_eq!(core.cb, ptr);
5397
5398        let back = RenderImageCallback::from_core(&core);
5399        assert_eq!(back.cb as usize, ptr);
5400        assert!(back.ctx.is_none());
5401    }
5402
5403    #[test]
5404    #[cfg(debug_assertions)]
5405    #[should_panic(expected = "CoreRenderImageCallback.cb is null")]
5406    fn render_image_callback_from_core_null_pointer_trips_debug_assert() {
5407        let core = azul_core::callbacks::CoreRenderImageCallback {
5408            cb: 0,
5409            ctx: OptionRefAny::None,
5410        };
5411        let _ = RenderImageCallback::from_core(&core);
5412    }
5413
5414    #[test]
5415    fn render_image_callback_info_getters_and_null_ctx() {
5416        let gl = OptionGlContextPtr::None;
5417        let image_cache = ImageCache::default();
5418        let fonts = FcFontCache::default();
5419        let bounds = HidpiAdjustedBounds {
5420            logical_size: LogicalSize::new(640.0, 480.0),
5421            hidpi_factor: azul_core::resources::DpiScaleFactor::new(2.0),
5422        };
5423
5424        let info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5425
5426        assert_eq!(info.get_callback_node_id(), node0());
5427        assert_eq!(info.get_bounds().logical_size, LogicalSize::new(640.0, 480.0));
5428        // callable_ptr is null for native Rust callbacks: get_ctx must return
5429        // None rather than dereferencing the null pointer.
5430        assert!(info.get_ctx().is_none());
5431        assert!(info.get_gl_context().is_none());
5432
5433        // Clone is a field-wise pointer copy - the getters must still work.
5434        let cloned = info.clone();
5435        assert_eq!(cloned.get_callback_node_id(), node0());
5436        assert!(cloned.get_ctx().is_none());
5437    }
5438
5439    #[test]
5440    fn render_image_callback_info_accepts_degenerate_and_nan_bounds() {
5441        let gl = OptionGlContextPtr::None;
5442        let image_cache = ImageCache::default();
5443        let fonts = FcFontCache::default();
5444
5445        for (w, h, dpi) in [
5446            (0.0f32, 0.0f32, 0.0f32),
5447            (-1.0, -1.0, 1.0),
5448            (f32::MAX, f32::MAX, f32::MAX),
5449            (f32::INFINITY, f32::NAN, 1.0),
5450        ] {
5451            let bounds = HidpiAdjustedBounds {
5452                logical_size: LogicalSize::new(w, h),
5453                hidpi_factor: azul_core::resources::DpiScaleFactor::new(dpi),
5454            };
5455            let info = RenderImageCallbackInfo::new(node_none(), bounds, &gl, &image_cache, &fonts);
5456            let got = info.get_bounds().logical_size;
5457            assert_eq!(got.width.is_nan(), w.is_nan());
5458            assert_eq!(got.height.is_nan(), h.is_nan());
5459        }
5460    }
5461
5462    #[test]
5463    fn render_image_callback_info_set_callable_ptr_makes_ctx_visible() {
5464        let gl = OptionGlContextPtr::None;
5465        let image_cache = ImageCache::default();
5466        let fonts = FcFontCache::default();
5467        let bounds = HidpiAdjustedBounds {
5468            logical_size: LogicalSize::new(1.0, 1.0),
5469            hidpi_factor: azul_core::resources::DpiScaleFactor::new(1.0),
5470        };
5471        let mut info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5472
5473        let ctx = OptionRefAny::Some(RefAny::new(99u32));
5474        // SAFETY: `ctx` outlives `info` (both are dropped at the end of this fn).
5475        unsafe { info.set_callable_ptr(core::ptr::from_ref(&ctx)) };
5476        assert!(info.get_ctx().is_some());
5477
5478        // Resetting to null must go back to the safe "no ctx" answer.
5479        unsafe { info.set_callable_ptr(core::ptr::null()) };
5480        assert!(info.get_ctx().is_none());
5481    }
5482
5483    // ------------------------------------------------------------------
5484    // FocusUpdateRequest - predicate + round-trip laws
5485    // ------------------------------------------------------------------
5486
5487    #[test]
5488    fn focus_update_request_is_change_matches_variant() {
5489        assert!(FocusUpdateRequest::FocusNode(node0()).is_change());
5490        assert!(FocusUpdateRequest::ClearFocus.is_change());
5491        assert!(!FocusUpdateRequest::NoChange.is_change());
5492    }
5493
5494    #[test]
5495    fn focus_update_request_optional_round_trip_is_lossless() {
5496        for req in [
5497            FocusUpdateRequest::FocusNode(node0()),
5498            FocusUpdateRequest::FocusNode(node_none()),
5499            FocusUpdateRequest::ClearFocus,
5500            FocusUpdateRequest::NoChange,
5501        ] {
5502            assert_eq!(
5503                FocusUpdateRequest::from_optional(req.to_focused_node()),
5504                req,
5505                "from_optional . to_focused_node must be the identity"
5506            );
5507        }
5508
5509        // ... and in the other direction, for the legacy Option<Option<_>> form.
5510        for opt in [Some(Some(node0())), Some(None), None] {
5511            assert_eq!(FocusUpdateRequest::from_optional(opt).to_focused_node(), opt);
5512        }
5513
5514        // is_change agrees with "to_focused_node produced something"
5515        for req in [
5516            FocusUpdateRequest::FocusNode(node0()),
5517            FocusUpdateRequest::ClearFocus,
5518            FocusUpdateRequest::NoChange,
5519        ] {
5520            assert_eq!(req.is_change(), req.to_focused_node().is_some());
5521        }
5522    }
5523
5524    // ------------------------------------------------------------------
5525    // FFI Result enums
5526    // ------------------------------------------------------------------
5527
5528    #[test]
5529    fn result_u8vec_string_from_maps_ok_and_err() {
5530        let ok = ResultU8VecString::from(Ok(Vec::new()));
5531        assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.is_empty()));
5532
5533        let ok = ResultU8VecString::from(Ok(vec![0u8; 100_000]));
5534        assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.len() == 100_000));
5535
5536        let err = ResultU8VecString::from(Err(AzString::from("boom")));
5537        assert!(matches!(&err, ResultU8VecString::Err(e) if e.as_str() == "boom"));
5538    }
5539
5540    #[test]
5541    fn result_void_string_from_maps_ok_and_err() {
5542        assert!(matches!(ResultVoidString::from(Ok(())), ResultVoidString::Ok));
5543        let err = ResultVoidString::from(Err(AzString::from("")));
5544        assert!(matches!(&err, ResultVoidString::Err(e) if e.as_str().is_empty()));
5545    }
5546
5547    #[test]
5548    fn result_string_string_from_keeps_both_sides_distinct() {
5549        let ok = ResultStringString::from(Ok(AzString::from("x")));
5550        assert!(matches!(&ok, ResultStringString::Ok(s) if s.as_str() == "x"));
5551        // Same payload type on both sides - the discriminant is what carries meaning.
5552        let err = ResultStringString::from(Err(AzString::from("x")));
5553        assert!(matches!(&err, ResultStringString::Err(s) if s.as_str() == "x"));
5554    }
5555
5556    // ------------------------------------------------------------------
5557    // ExternalSystemCallbacks
5558    // ------------------------------------------------------------------
5559
5560    #[test]
5561    fn external_system_callbacks_time_fn_is_callable_and_monotonic() {
5562        let cbs = ExternalSystemCallbacks::rust_internal();
5563        let t0 = (cbs.get_system_time_fn.cb)();
5564        let t1 = (cbs.get_system_time_fn.cb)();
5565        // Both calls must succeed; we only assert they produce a value (the
5566        // clock resolution makes strict ordering flaky).
5567        let _ = (t0, t1);
5568    }
5569
5570    // ------------------------------------------------------------------
5571    // CallbackInfo: transaction log (push / take / relayout predicate)
5572    // ------------------------------------------------------------------
5573
5574    #[test]
5575    fn callback_info_starts_with_an_empty_change_log() {
5576        with_info(node_none(), |info| {
5577            assert!(info.take_changes().is_empty());
5578            assert!(!info.has_pending_relayout_change());
5579            assert!(!info.get_changes_ptr().is_null());
5580        });
5581    }
5582
5583    #[test]
5584    fn callback_info_take_changes_drains_the_log() {
5585        with_info(node_none(), |info| {
5586            info.stop_propagation();
5587            info.prevent_default();
5588            let first = info.take_changes();
5589            assert_eq!(first.len(), 2);
5590            // Second take must not hand out the same changes again.
5591            assert!(
5592                info.take_changes().is_empty(),
5593                "take_changes must consume the log"
5594            );
5595        });
5596    }
5597
5598    #[test]
5599    fn callback_info_is_copy_and_copies_share_one_change_log() {
5600        with_info(node_none(), |info| {
5601            let mut copy = *info;
5602            copy.stop_immediate_propagation();
5603            assert_eq!(
5604                info.get_changes_ptr(),
5605                copy.get_changes_ptr(),
5606                "a Copy of CallbackInfo must alias the same Arc<Mutex<..>>"
5607            );
5608            let changes = info.take_changes();
5609            assert_eq!(changes.len(), 1);
5610            assert!(matches!(changes[0], CallbackChange::StopImmediatePropagation));
5611        });
5612    }
5613
5614    #[test]
5615    fn has_pending_relayout_change_is_true_only_for_relayout_changes() {
5616        // Known-false: a propagation change needs no relayout.
5617        with_info(node_none(), |info| {
5618            info.stop_propagation();
5619            assert!(!info.has_pending_relayout_change());
5620        });
5621        // Known-true: window resize.
5622        with_info(node_none(), |info| {
5623            info.modify_window_state(FullWindowState::default());
5624            assert!(info.has_pending_relayout_change());
5625        });
5626        // Known-true: scroll.
5627        with_info(node_none(), |info| {
5628            info.scroll_to(
5629                DomId::ROOT_ID,
5630                NodeHierarchyItemId::NONE,
5631                LogicalPosition::new(0.0, 0.0),
5632            );
5633            assert!(info.has_pending_relayout_change());
5634        });
5635        // Known-true: queued synthetic input sequence.
5636        with_info(node_none(), |info| {
5637            info.queue_window_state_sequence(FullWindowStateVec::from_vec(vec![
5638                FullWindowState::default(),
5639            ]));
5640            assert!(info.has_pending_relayout_change());
5641        });
5642        // A relayout change anywhere in the log counts, not just at the head.
5643        with_info(node_none(), |info| {
5644            info.prevent_default();
5645            info.hide_tooltip();
5646            info.close_window();
5647            assert!(!info.has_pending_relayout_change());
5648            info.modify_window_state(FullWindowState::default());
5649            assert!(info.has_pending_relayout_change());
5650            // Querying must not consume the log.
5651            assert!(info.has_pending_relayout_change());
5652            assert_eq!(info.take_changes().len(), 4);
5653        });
5654    }
5655
5656    #[test]
5657    fn callback_info_flag_mutators_queue_exactly_one_matching_change() {
5658        macro_rules! assert_queues {
5659            ($call:expr, $pat:pat) => {{
5660                with_info(node_none(), |info| {
5661                    let f: &dyn Fn(&mut CallbackInfo) = &$call;
5662                    f(info);
5663                    let changes = info.take_changes();
5664                    assert_eq!(changes.len(), 1, "expected exactly one queued change");
5665                    assert!(
5666                        matches!(changes[0], $pat),
5667                        "queued the wrong CallbackChange: {:?}",
5668                        changes[0]
5669                    );
5670                });
5671            }};
5672        }
5673
5674        assert_queues!(
5675            |i: &mut CallbackInfo| i.stop_propagation(),
5676            CallbackChange::StopPropagation
5677        );
5678        assert_queues!(
5679            |i: &mut CallbackInfo| i.stop_immediate_propagation(),
5680            CallbackChange::StopImmediatePropagation
5681        );
5682        assert_queues!(
5683            |i: &mut CallbackInfo| i.prevent_default(),
5684            CallbackChange::PreventDefault
5685        );
5686        assert_queues!(
5687            |i: &mut CallbackInfo| i.close_window(),
5688            CallbackChange::CloseWindow
5689        );
5690        assert_queues!(
5691            |i: &mut CallbackInfo| i.begin_interactive_move(),
5692            CallbackChange::BeginInteractiveMove
5693        );
5694        assert_queues!(
5695            |i: &mut CallbackInfo| i.commit_undo_snapshot(),
5696            CallbackChange::CommitUndoSnapshot
5697        );
5698        assert_queues!(
5699            |i: &mut CallbackInfo| i.undo_app_state(),
5700            CallbackChange::UndoAppState
5701        );
5702        assert_queues!(
5703            |i: &mut CallbackInfo| i.redo_app_state(),
5704            CallbackChange::RedoAppState
5705        );
5706        assert_queues!(
5707            |i: &mut CallbackInfo| i.update_all_image_callbacks(),
5708            CallbackChange::UpdateAllImageCallbacks
5709        );
5710        assert_queues!(
5711            |i: &mut CallbackInfo| i.trigger_all_virtual_view_rerender(),
5712            CallbackChange::UpdateAllVirtualViews
5713        );
5714        assert_queues!(
5715            |i: &mut CallbackInfo| i.reload_system_fonts(),
5716            CallbackChange::ReloadSystemFonts
5717        );
5718        assert_queues!(
5719            |i: &mut CallbackInfo| i.hide_tooltip(),
5720            CallbackChange::HideTooltip
5721        );
5722    }
5723
5724    #[test]
5725    fn callback_info_timer_and_thread_ids_survive_boundary_values() {
5726        with_info(node_none(), |info| {
5727            info.add_timer(TimerId { id: 0 }, Timer::default());
5728            info.add_timer(TimerId { id: usize::MAX }, Timer::default());
5729            info.remove_timer(TimerId { id: usize::MAX });
5730            info.remove_thread(ThreadId::unique());
5731
5732            let changes = info.take_changes();
5733            assert_eq!(changes.len(), 4);
5734            assert!(
5735                matches!(&changes[1], CallbackChange::AddTimer { timer_id, .. } if timer_id.id == usize::MAX)
5736            );
5737            assert!(
5738                matches!(&changes[2], CallbackChange::RemoveTimer { timer_id } if timer_id.id == usize::MAX)
5739            );
5740            assert!(matches!(&changes[3], CallbackChange::RemoveThread { .. }));
5741        });
5742    }
5743
5744    // ------------------------------------------------------------------
5745    // CallbackInfo: numeric edges (scroll / menu / tooltip positions)
5746    // ------------------------------------------------------------------
5747
5748    #[test]
5749    fn scroll_to_records_position_verbatim_at_numeric_extremes() {
5750        let positions = [
5751            LogicalPosition::new(0.0, 0.0),
5752            LogicalPosition::new(-0.0, -1_000_000.0),
5753            LogicalPosition::new(f32::MIN, f32::MAX),
5754            LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
5755            LogicalPosition::new(f32::NAN, f32::NAN),
5756        ];
5757
5758        with_info(node_none(), |info| {
5759            for p in positions {
5760                info.scroll_to(DomId::ROOT_ID, NodeHierarchyItemId::NONE, p);
5761            }
5762            let changes = info.take_changes();
5763            assert_eq!(changes.len(), positions.len());
5764
5765            for (change, expected) in changes.iter().zip(positions) {
5766                let CallbackChange::ScrollTo {
5767                    position, unclamped, ..
5768                } = change
5769                else {
5770                    panic!("expected ScrollTo, got {change:?}");
5771                };
5772                assert!(!*unclamped, "scroll_to must request clamping");
5773                // No sanitisation happens here - NaN/inf reach the change log
5774                // unchanged, and clamping is the change-processor's job.
5775                assert_eq!(position.x.is_nan(), expected.x.is_nan());
5776                if !expected.x.is_nan() {
5777                    assert_eq!(position.x, expected.x);
5778                    assert_eq!(position.y, expected.y);
5779                }
5780            }
5781        });
5782    }
5783
5784    #[test]
5785    fn scroll_to_unclamped_sets_the_unclamped_flag() {
5786        with_info(node_none(), |info| {
5787            info.scroll_to_unclamped(
5788                DomId { inner: usize::MAX },
5789                NodeHierarchyItemId::from_raw(usize::MAX),
5790                LogicalPosition::new(-99999.0, 99999.0),
5791            );
5792            let changes = info.take_changes();
5793            assert_eq!(changes.len(), 1);
5794            let CallbackChange::ScrollTo {
5795                unclamped,
5796                dom_id,
5797                position,
5798                ..
5799            } = &changes[0]
5800            else {
5801                panic!("expected ScrollTo");
5802            };
5803            assert!(*unclamped, "scroll_to_unclamped must skip clamping");
5804            assert_eq!(dom_id.inner, usize::MAX, "an unknown DomId is not rejected here");
5805            assert_eq!(position.x, -99999.0);
5806        });
5807    }
5808
5809    #[test]
5810    fn scroll_node_into_view_queues_the_options_verbatim() {
5811        use crate::managers::scroll_into_view::ScrollIntoViewOptions;
5812        with_info(node_none(), |info| {
5813            info.scroll_node_into_view(node_none(), ScrollIntoViewOptions::nearest());
5814            let changes = info.take_changes();
5815            assert_eq!(changes.len(), 1);
5816            assert!(matches!(changes[0], CallbackChange::ScrollIntoView { .. }));
5817        });
5818    }
5819
5820    #[test]
5821    fn open_menu_at_and_show_tooltip_at_accept_extreme_positions() {
5822        let menu = || Menu::create(azul_core::menu::MenuItemVec::from_const_slice(&[]));
5823
5824        with_info(node_none(), |info| {
5825            info.open_menu(menu());
5826            info.open_menu_at(menu(), LogicalPosition::new(0.0, 0.0));
5827            info.open_menu_at(menu(), LogicalPosition::new(f32::MIN, f32::MAX));
5828            info.open_menu_at(menu(), LogicalPosition::new(f32::NAN, f32::INFINITY));
5829
5830            let changes = info.take_changes();
5831            assert_eq!(changes.len(), 4);
5832            // open_menu keeps the menu's own position (None override) ...
5833            assert!(matches!(
5834                &changes[0],
5835                CallbackChange::OpenMenu { position: None, .. }
5836            ));
5837            // ... open_menu_at always overrides it.
5838            for change in &changes[1..] {
5839                assert!(matches!(
5840                    change,
5841                    CallbackChange::OpenMenu {
5842                        position: Some(_),
5843                        ..
5844                    }
5845                ));
5846            }
5847        });
5848
5849        with_info(node_none(), |info| {
5850            info.show_tooltip(AzString::from(""));
5851            info.show_tooltip_at(AzString::from("🌍"), LogicalPosition::new(f32::NAN, -0.0));
5852            info.show_tooltip_at(
5853                AzString::from("x".repeat(100_000)),
5854                LogicalPosition::new(f32::MAX, f32::MIN),
5855            );
5856            let changes = info.take_changes();
5857            assert_eq!(changes.len(), 3);
5858            assert!(matches!(&changes[0], CallbackChange::ShowTooltip { text, .. } if text.as_str().is_empty()));
5859            assert!(matches!(&changes[1], CallbackChange::ShowTooltip { text, position } if text.as_str() == "🌍" && position.x.is_nan()));
5860            assert!(matches!(&changes[2], CallbackChange::ShowTooltip { text, .. } if text.as_str().len() == 100_000));
5861        });
5862    }
5863
5864    // ------------------------------------------------------------------
5865    // CallbackInfo: CSS property helpers (documented panics)
5866    // ------------------------------------------------------------------
5867
5868    #[test]
5869    fn set_css_property_wraps_a_single_property() {
5870        with_info(node_none(), |info| {
5871            info.set_css_property(node0(), a_css_property());
5872            let changes = info.take_changes();
5873            assert_eq!(changes.len(), 1);
5874            let CallbackChange::ChangeNodeCssProperties {
5875                dom_id,
5876                node_id,
5877                properties,
5878            } = &changes[0]
5879            else {
5880                panic!("expected ChangeNodeCssProperties");
5881            };
5882            assert_eq!(*dom_id, DomId::ROOT_ID);
5883            assert_eq!(node_id.index(), 0);
5884            assert_eq!(properties.len(), 1);
5885        });
5886    }
5887
5888    #[test]
5889    fn override_css_property_uses_the_override_channel_not_the_cascade() {
5890        with_info(node_none(), |info| {
5891            info.override_css_property(node0(), a_css_property());
5892            let changes = info.take_changes();
5893            assert_eq!(changes.len(), 1);
5894            assert!(
5895                matches!(changes[0], CallbackChange::OverrideNodeCssProperties { .. }),
5896                "must not fall back to the invalidating ChangeNodeCssProperties path"
5897            );
5898        });
5899    }
5900
5901    #[test]
5902    #[should_panic(expected = "DomNodeId node should not be None")]
5903    fn set_css_property_panics_on_a_none_node_as_documented() {
5904        with_info(node_none(), |info| {
5905            info.set_css_property(node_none(), a_css_property());
5906        });
5907    }
5908
5909    #[test]
5910    #[should_panic(expected = "DomNodeId node should not be None")]
5911    fn override_css_property_panics_on_a_none_node_as_documented() {
5912        with_info(node_none(), |info| {
5913            info.override_css_property(node_none(), a_css_property());
5914        });
5915    }
5916
5917    #[test]
5918    fn change_node_css_properties_accepts_an_empty_property_vec() {
5919        with_info(node_none(), |info| {
5920            info.change_node_css_properties(
5921                DomId::ROOT_ID,
5922                NodeId::new(usize::MAX),
5923                CssPropertyVec::from_const_slice(&[]),
5924            );
5925            let changes = info.take_changes();
5926            assert_eq!(changes.len(), 1);
5927            assert!(
5928                matches!(&changes[0], CallbackChange::ChangeNodeCssProperties { properties, .. } if properties.is_empty())
5929            );
5930        });
5931    }
5932
5933    // ------------------------------------------------------------------
5934    // CallbackInfo: text / DOM mutation payloads (malformed + unicode + huge)
5935    // ------------------------------------------------------------------
5936
5937    #[test]
5938    fn change_node_text_passes_hostile_strings_through_unchanged() {
5939        let inputs = [
5940            String::new(),
5941            "   \t\n  ".to_string(),
5942            "\u{0}embedded nul".to_string(),
5943            "🌍é\u{301}\u{200B}".to_string(),
5944            "x".repeat(1_000_000),
5945        ];
5946
5947        with_info(node_none(), |info| {
5948            for s in &inputs {
5949                info.change_node_text(node0(), AzString::from(s.clone()));
5950            }
5951            let changes = info.take_changes();
5952            assert_eq!(changes.len(), inputs.len());
5953            for (change, expected) in changes.iter().zip(&inputs) {
5954                let CallbackChange::ChangeNodeText { text, .. } = change else {
5955                    panic!("expected ChangeNodeText");
5956                };
5957                assert_eq!(text.as_str(), expected.as_str());
5958            }
5959        });
5960    }
5961
5962    #[test]
5963    fn insert_child_node_accepts_empty_and_garbage_type_strings() {
5964        with_info(node_none(), |info| {
5965            // Neither an empty tag nor a garbage tag is validated at queue time.
5966            info.insert_child_node(
5967                DomId::ROOT_ID,
5968                NodeId::new(0),
5969                AzString::from(""),
5970                OptionUsize::None,
5971                StringVec::from_const_slice(&[]),
5972                OptionString::None,
5973            );
5974            info.insert_child_node(
5975                DomId { inner: usize::MAX },
5976                NodeId::new(usize::MAX),
5977                AzString::from("\u{0}<<not a tag>>"),
5978                OptionUsize::Some(usize::MAX),
5979                StringVec::from_const_slice(&[]),
5980                OptionString::None,
5981            );
5982            assert_eq!(info.take_changes().len(), 2);
5983        });
5984    }
5985
5986    #[test]
5987    fn text_editing_mutators_queue_their_changes() {
5988        with_info(node_none(), |info| {
5989            info.insert_text(DomId::ROOT_ID, NodeId::new(0), AzString::from("🌍"));
5990            info.move_cursor(DomId::ROOT_ID, NodeId::new(0), a_cursor());
5991            info.set_selection(
5992                DomId::ROOT_ID,
5993                NodeId::new(0),
5994                Selection::Cursor(a_cursor()),
5995            );
5996            info.set_text_changeset(PendingTextEdit {
5997                node: node0(),
5998                inserted_text: AzString::from(""),
5999                old_text: AzString::from(""),
6000            });
6001            info.create_text_input(AzString::from("\u{0}"));
6002            info.delete_node(DomId::ROOT_ID, NodeId::new(usize::MAX));
6003            info.set_node_ids_and_classes(
6004                DomId::ROOT_ID,
6005                NodeId::new(0),
6006                azul_core::dom::IdOrClassVec::from_const_slice(&[]),
6007            );
6008
6009            let changes = info.take_changes();
6010            assert_eq!(changes.len(), 7);
6011            assert!(matches!(&changes[0], CallbackChange::InsertText { text, .. } if text.as_str() == "🌍"));
6012            assert!(matches!(changes[1], CallbackChange::MoveCursor { .. }));
6013            assert!(matches!(changes[2], CallbackChange::SetSelection { .. }));
6014            assert!(matches!(changes[3], CallbackChange::SetTextChangeset { .. }));
6015            assert!(matches!(changes[5], CallbackChange::DeleteNode { .. }));
6016        });
6017    }
6018
6019    #[test]
6020    fn image_cache_mutators_accept_empty_ids_and_null_images() {
6021        with_info(node_none(), |info| {
6022            let img = || {
6023                ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
6024            };
6025            info.add_image_to_cache(AzString::from(""), img());
6026            info.remove_image_from_cache(AzString::from(""));
6027            info.change_node_image(
6028                DomId::ROOT_ID,
6029                NodeId::new(0),
6030                img(),
6031                UpdateImageType::Content,
6032            );
6033            info.update_image_callback(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
6034            info.trigger_virtual_view_rerender(DomId::ROOT_ID, NodeId::new(usize::MAX));
6035            assert_eq!(info.take_changes().len(), 5);
6036        });
6037    }
6038
6039    #[test]
6040    fn focus_mutators_queue_set_focus_target() {
6041        with_info(node_none(), |info| {
6042            info.set_focus(FocusTarget::NoFocus);
6043            // usize::MAX is the ONE index NodeId's 1-based encoding cannot represent
6044            // (into_raw does `inner + 1`); the repo pins usize::MAX - 1 as
6045            // MAX_ENCODABLE_NODE for exactly this. Still an out-of-range node.
6046            info.set_focus_to_node(DomId::ROOT_ID, NodeId::new(usize::MAX - 1));
6047            info.focus_next();
6048            info.focus_previous();
6049            info.focus_first();
6050            info.focus_last();
6051            info.clear_focus();
6052
6053            let changes = info.take_changes();
6054            assert_eq!(changes.len(), 7);
6055            for change in &changes {
6056                assert!(matches!(change, CallbackChange::SetFocusTarget { .. }));
6057            }
6058            assert!(matches!(
6059                &changes[2],
6060                CallbackChange::SetFocusTarget {
6061                    target: FocusTarget::Next
6062                }
6063            ));
6064            assert!(matches!(
6065                &changes[6],
6066                CallbackChange::SetFocusTarget {
6067                    target: FocusTarget::NoFocus
6068                }
6069            ));
6070        });
6071    }
6072
6073    #[test]
6074    fn create_window_queues_window_creation() {
6075        with_info(node_none(), |info| {
6076            info.create_window(WindowCreateOptions::default());
6077            let changes = info.take_changes();
6078            assert_eq!(changes.len(), 1);
6079            assert!(matches!(changes[0], CallbackChange::CreateNewWindow { .. }));
6080        });
6081    }
6082
6083    // ------------------------------------------------------------------
6084    // CallbackInfo: routing
6085    // ------------------------------------------------------------------
6086
6087    #[test]
6088    fn route_getters_return_empty_strings_when_no_route_is_active() {
6089        with_info(node_none(), |info| {
6090            assert_eq!(info.get_route_pattern().as_str(), "");
6091            assert_eq!(info.get_route_param(AzString::from("id")).as_str(), "");
6092            // Malformed / hostile keys must not panic either.
6093            assert_eq!(info.get_route_param(AzString::from("")).as_str(), "");
6094            assert_eq!(info.get_route_param(AzString::from("\u{0}🌍")).as_str(), "");
6095            assert_eq!(
6096                info.get_route_param(AzString::from("k".repeat(100_000)))
6097                    .as_str(),
6098                ""
6099            );
6100        });
6101    }
6102
6103    #[test]
6104    fn set_route_param_without_an_active_route_queues_nothing() {
6105        with_info(node_none(), |info| {
6106            info.set_route_param(AzString::from("id"), AzString::from("42"));
6107            assert!(
6108                info.take_changes().is_empty(),
6109                "no active route => no SwitchRoute change may be queued"
6110            );
6111        });
6112    }
6113
6114    #[test]
6115    fn switch_route_queues_the_pattern_verbatim() {
6116        with_info(node_none(), |info| {
6117            info.switch_route(
6118                AzString::from("/user/:id"),
6119                azul_core::window::StringPairVec::from_vec(vec![azul_core::window::AzStringPair {
6120                    key: AzString::from("id"),
6121                    value: AzString::from("42"),
6122                }]),
6123            );
6124            let changes = info.take_changes();
6125            assert_eq!(changes.len(), 1);
6126            assert!(
6127                matches!(&changes[0], CallbackChange::SwitchRoute { pattern, params } if pattern.as_str() == "/user/:id" && params.len() == 1)
6128            );
6129        });
6130    }
6131
6132    // ------------------------------------------------------------------
6133    // CallbackInfo: query APIs against an EMPTY layout window
6134    // ------------------------------------------------------------------
6135
6136    #[test]
6137    fn get_node_id_by_id_attribute_returns_none_for_hostile_ids() {
6138        let long = "a".repeat(1_000_000);
6139        let nested = "[".repeat(10_000);
6140        let ids: [&str; 12] = [
6141            "",
6142            "   ",
6143            "\t\n",
6144            "\u{0}",
6145            "!@#$%^&*()",
6146            "0",
6147            "-0",
6148            "9223372036854775807",
6149            "NaN",
6150            "inf",
6151            "  valid  ",
6152            "valid;garbage",
6153        ];
6154
6155        with_info(node_none(), |info| {
6156            for id in ids {
6157                assert_eq!(
6158                    info.get_node_id_by_id_attribute(DomId::ROOT_ID, id),
6159                    None,
6160                    "id {id:?} must not resolve in an empty layout tree"
6161                );
6162            }
6163            // Unicode / combining marks / emoji.
6164            for id in ["\u{1F600}", "e\u{301}", "🌍🌍🌍"] {
6165                assert_eq!(info.get_node_id_by_id_attribute(DomId::ROOT_ID, id), None);
6166            }
6167            // Extremely long + deeply "nested" input must not hang or overflow.
6168            assert_eq!(
6169                info.get_node_id_by_id_attribute(DomId::ROOT_ID, &long),
6170                None
6171            );
6172            assert_eq!(
6173                info.get_node_id_by_id_attribute(DomId::ROOT_ID, &nested),
6174                None
6175            );
6176            // An out-of-range DomId is a miss, not a panic.
6177            assert_eq!(
6178                info.get_node_id_by_id_attribute(DomId { inner: usize::MAX }, "x"),
6179                None
6180            );
6181        });
6182    }
6183
6184    #[test]
6185    fn hierarchy_navigation_is_none_and_zero_on_an_empty_layout_tree() {
6186        with_info(node_none(), |info| {
6187            for dom in [DomId::ROOT_ID, DomId { inner: usize::MAX }] {
6188                for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6189                    assert_eq!(info.get_parent_node(dom, node), None);
6190                    assert_eq!(info.get_next_sibling_node(dom, node), None);
6191                    assert_eq!(info.get_previous_sibling_node(dom, node), None);
6192                    assert_eq!(info.get_first_child_node(dom, node), None);
6193                    assert_eq!(info.get_last_child_node(dom, node), None);
6194                    assert_eq!(info.get_children_count(dom, node), 0);
6195                    assert_eq!(info.get_all_children_nodes(dom, node).len(), 0);
6196                }
6197            }
6198            // The DomNodeId-flavoured navigation must agree.
6199            assert_eq!(info.get_parent(node0()), None);
6200            assert_eq!(info.get_first_child(node0()), None);
6201            assert_eq!(info.get_last_child(node0()), None);
6202            assert_eq!(info.get_next_sibling(node_none()), None);
6203            assert_eq!(info.get_previous_sibling(node_none()), None);
6204        });
6205    }
6206
6207    #[test]
6208    fn geometry_and_css_queries_are_none_on_an_empty_layout_tree() {
6209        with_info(node0(), |info| {
6210            assert_eq!(info.get_node_size(node0()), None);
6211            assert_eq!(info.get_node_position(node0()), None);
6212            assert_eq!(info.get_node_rect(node0()), None);
6213            assert_eq!(info.get_node_hit_test_bounds(node0()), None);
6214            assert_eq!(info.get_hit_node_rect(), None);
6215            assert!(info.get_computed_width(node0()).is_none());
6216            assert!(info.get_computed_height(node0()).is_none());
6217            assert!(info
6218                .get_computed_css_property(node_none(), CssPropertyType::Width)
6219                .is_none());
6220            assert!(info.get_layout_result(&DomId::ROOT_ID).is_none());
6221            assert!(info.get_gpu_cache(&DomId::ROOT_ID).is_none());
6222            assert_eq!(info.get_dom_ids().len(), 0);
6223        });
6224    }
6225
6226    #[test]
6227    fn state_getters_reflect_the_construction_arguments() {
6228        let hit = node0();
6229        with_info(hit, |info| {
6230            assert_eq!(info.get_hit_node(), hit);
6231            // No cursor was supplied at construction.
6232            assert!(info.get_cursor_relative_to_viewport().is_none());
6233            assert!(info.get_cursor_relative_to_node().is_none());
6234            // Native Rust callback => no FFI ctx, no GL context.
6235            assert!(info.get_ctx().is_none());
6236            assert!(info.get_gl_context().is_none());
6237            // No previous frame yet.
6238            assert!(info.get_previous_window_state().is_none());
6239            assert!(info.get_previous_window_flags().is_none());
6240            assert!(info.get_previous_mouse_state().is_none());
6241            assert!(info.get_previous_keyboard_state().is_none());
6242            assert!(matches!(
6243                info.get_current_window_handle(),
6244                RawWindowHandle::Unsupported
6245            ));
6246            assert_eq!(info.get_monitors().len(), 0);
6247            assert!(info.get_current_monitor().is_none());
6248            assert_eq!(info.get_timer_ids().len(), 0);
6249            assert_eq!(info.get_thread_ids().len(), 0);
6250            assert!(info.get_timer(&TimerId { id: 0 }).is_none());
6251            assert!(info.get_thread(&ThreadId::unique()).is_none());
6252            // The system-time callback must be wired up and callable.
6253            let _now = info.get_current_time();
6254        });
6255    }
6256
6257    #[test]
6258    fn selection_and_undo_queries_are_empty_for_unknown_nodes() {
6259        with_info(node_none(), |info| {
6260            assert!(!info.has_any_selection());
6261            assert_eq!(info.get_selection_count(&DomId::ROOT_ID), 0);
6262            assert!(info.get_primary_selection(&DomId::ROOT_ID).is_none());
6263            assert!(!info.node_has_selection(node0()));
6264
6265            for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6266                assert!(!info.can_undo(node));
6267                assert!(!info.can_redo(node));
6268                assert!(info.get_undo_text(node).is_none());
6269                assert!(info.get_redo_text(node).is_none());
6270                assert!(info.inspect_undo_operation(node).is_none());
6271                assert!(info.inspect_redo_operation(node).is_none());
6272            }
6273
6274            assert!(info.get_node_text_content(node0()).is_none());
6275            assert_eq!(info.get_node_text_length(node0()), None);
6276            assert!(info.get_text_changeset().is_none());
6277            assert!(!info.is_node_focused(node0()));
6278            assert!(!info.has_focus(node0()));
6279            assert!(info.get_focused_node().is_none());
6280        });
6281    }
6282
6283    #[test]
6284    fn cursor_inspection_is_none_without_a_text_layout() {
6285        with_info(node_none(), |info| {
6286            assert!(info.inspect_move_cursor_left(node0()).is_none());
6287            assert!(info.inspect_move_cursor_right(node0()).is_none());
6288            assert!(info.inspect_move_cursor_up(node0()).is_none());
6289            assert!(info.inspect_move_cursor_down(node0()).is_none());
6290            assert!(info.inspect_move_cursor_to_line_start(node0()).is_none());
6291            assert!(info.inspect_move_cursor_to_line_end(node0()).is_none());
6292            assert!(info.inspect_backspace(node0()).is_none());
6293            assert!(info.inspect_delete(node0()).is_none());
6294            // ... and the same for a node id that does not decode at all.
6295            assert!(info.inspect_move_cursor_left(node_none()).is_none());
6296            assert!(info.inspect_backspace(node_none()).is_none());
6297        });
6298    }
6299
6300    #[test]
6301    fn drag_and_gesture_queries_are_inactive_by_default() {
6302        with_info(node_none(), |info| {
6303            assert!(!info.is_dragging());
6304            assert!(!info.is_drag_active());
6305            assert!(!info.is_node_drag_active());
6306            assert!(!info.is_file_drag_active());
6307            assert!(info.get_drag_delta().is_none());
6308            assert!(info.get_drag_delta_screen().is_none());
6309            assert!(info.get_drag_delta_screen_incremental().is_none());
6310            assert!(!info.was_double_clicked());
6311            assert!(info.get_pen_pressure().is_none());
6312            assert!(info.get_pen_tilt().is_none());
6313            assert!(!info.is_pen_in_contact());
6314            assert!(!info.is_pen_eraser());
6315            assert!(!info.is_pen_barrel_button_pressed());
6316            assert_eq!(info.get_drag_types().len(), 0);
6317            assert!(info.get_drag_data("text/plain").is_none());
6318            assert!(info.get_drag_data("").is_none());
6319        });
6320    }
6321
6322    #[test]
6323    #[cfg(feature = "text_layout")]
6324    fn get_loaded_font_bytes_returns_none_for_boundary_hashes() {
6325        with_info(node_none(), |info| {
6326            // No fonts are loaded, so every hash - including the numeric
6327            // boundaries - must miss rather than index out of bounds.
6328            for hash in [0u64, 1, u64::MAX, u64::MAX / 2] {
6329                assert!(info.get_loaded_font_bytes(hash).is_none());
6330            }
6331            assert_eq!(info.get_loaded_fonts().len(), 0);
6332        });
6333    }
6334
6335    #[test]
6336    #[cfg(feature = "cpurender")]
6337    fn take_screenshot_of_a_missing_dom_is_an_error_not_a_panic() {
6338        with_info(node_none(), |info| {
6339            let err = info
6340                .take_screenshot(DomId::ROOT_ID)
6341                .expect_err("an empty layout window has no DOM to screenshot");
6342            assert_eq!(err.as_str(), "DOM not found in layout results");
6343
6344            let err = info
6345                .take_screenshot(DomId { inner: usize::MAX })
6346                .expect_err("an out-of-range DomId must be rejected");
6347            assert_eq!(err.as_str(), "DOM not found in layout results");
6348
6349            assert!(info.take_screenshot_base64(DomId::ROOT_ID).is_err());
6350        });
6351    }
6352
6353    // ------------------------------------------------------------------
6354    // CallbackChange payload smoke test
6355    // ------------------------------------------------------------------
6356
6357    #[test]
6358    fn callback_change_is_debug_and_clone() {
6359        let change = CallbackChange::ScrollTo {
6360            dom_id: DomId::ROOT_ID,
6361            node_id: NodeHierarchyItemId::NONE,
6362            position: LogicalPosition::new(f32::NAN, 0.0),
6363            unclamped: true,
6364        };
6365        let cloned = change.clone();
6366        assert!(matches!(
6367            cloned,
6368            CallbackChange::ScrollTo { unclamped: true, .. }
6369        ));
6370        assert!(!format!("{change:?}").is_empty());
6371    }
6372}