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