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