Skip to main content

azul_layout/
callbacks.rs

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