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    events::CallbackResultRef,
23    geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
24    gl::OptionGlContextPtr,
25    gpu::GpuValueCache,
26    hit_test::ScrollPosition,
27    id::NodeId as CoreNodeId,
28    impl_callback,
29    menu::Menu,
30    refany::{OptionRefAny, RefAny},
31    resources::{ImageCache, ImageMask, ImageRef, RendererResources},
32    selection::{Selection, SelectionRange, SelectionRangeVec, SelectionState, TextCursor},
33    styled_dom::{NodeHierarchyItemId, NodeIdVec, StyledDom},
34    task::{self, GetSystemTimeCallback, Instant, ThreadId, ThreadIdVec, TimerId, TimerIdVec},
35    window::{KeyboardState, MouseState, RawWindowHandle, WindowFlags, WindowSize},
36    FastBTreeSet, FastHashMap,
37};
38use azul_css::{
39    css::CssPath,
40    props::{
41        basic::FontRef,
42        property::{CssProperty, CssPropertyType, CssPropertyVec},
43    },
44    system::SystemStyle,
45    AzString, StringVec,
46};
47use rust_fontconfig::FcFontCache;
48
49#[cfg(feature = "icu")]
50use crate::icu::{
51    FormatLength, IcuDate, IcuDateTime, IcuLocalizerHandle, IcuResult,
52    IcuStringVec, IcuTime, ListType, PluralCategory,
53};
54
55use crate::{
56    hit_test::FullHitTest,
57    managers::{
58        drag_drop::DragDropManager,
59        file_drop::FileDropManager,
60        focus_cursor::FocusManager,
61        gesture::{GestureAndDragManager, InputSample, PenState},
62        gpu_state::GpuStateManager,
63        hover::{HoverManager, InputPointId},
64        iframe::IFrameManager,
65        scroll_state::{AnimatedScrollState, ScrollManager},
66        selection::{ClipboardContent, SelectionManager},
67        text_input::{PendingTextEdit, TextInputManager},
68        undo_redo::{UndoRedoManager, UndoableOperation},
69    },
70    text3::cache::{LayoutCache as TextLayoutCache, UnifiedLayout},
71    thread::{CreateThreadCallback, Thread},
72    timer::Timer,
73    window::{DomLayoutResult, LayoutWindow},
74    window_state::{FullWindowState, WindowCreateOptions},
75};
76
77use azul_css::{impl_option, impl_option_inner};
78
79// ============================================================================
80// FFI-safe wrapper types for tuple returns
81// ============================================================================
82
83/// FFI-safe wrapper for pen tilt angles (x_tilt, y_tilt) in degrees
84#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
85#[repr(C)]
86pub struct PenTilt {
87    /// X-axis tilt angle in degrees (-90 to 90)
88    pub x_tilt: f32,
89    /// Y-axis tilt angle in degrees (-90 to 90)
90    pub y_tilt: f32,
91}
92
93impl From<(f32, f32)> for PenTilt {
94    fn from((x, y): (f32, f32)) -> Self {
95        Self {
96            x_tilt: x,
97            y_tilt: y,
98        }
99    }
100}
101
102impl_option!(
103    PenTilt,
104    OptionPenTilt,
105    [Debug, Clone, Copy, PartialEq, PartialOrd]
106);
107
108/// FFI-safe wrapper for select-all result (full_text, selected_range)
109#[derive(Debug, Clone, PartialEq)]
110#[repr(C)]
111pub struct SelectAllResult {
112    /// The full text content of the node
113    pub full_text: AzString,
114    /// The range that would be selected
115    pub selection_range: SelectionRange,
116}
117
118impl From<(alloc::string::String, SelectionRange)> for SelectAllResult {
119    fn from((text, range): (alloc::string::String, SelectionRange)) -> Self {
120        Self {
121            full_text: text.into(),
122            selection_range: range,
123        }
124    }
125}
126
127impl_option!(
128    SelectAllResult,
129    OptionSelectAllResult,
130    copy = false,
131    [Debug, Clone, PartialEq]
132);
133
134/// FFI-safe wrapper for delete inspection result (range_to_delete, deleted_text)
135#[derive(Debug, Clone, PartialEq)]
136#[repr(C)]
137pub struct DeleteResult {
138    /// The range that would be deleted
139    pub range_to_delete: SelectionRange,
140    /// The text that would be deleted
141    pub deleted_text: AzString,
142}
143
144impl From<(SelectionRange, alloc::string::String)> for DeleteResult {
145    fn from((range, text): (SelectionRange, alloc::string::String)) -> Self {
146        Self {
147            range_to_delete: range,
148            deleted_text: text.into(),
149        }
150    }
151}
152
153impl_option!(
154    DeleteResult,
155    OptionDeleteResult,
156    copy = false,
157    [Debug, Clone, PartialEq]
158);
159
160/// Represents a change made by a callback that will be applied after the callback returns
161///
162/// This transaction-based system provides:
163/// - Clear separation between read-only queries and modifications
164/// - Atomic application of all changes
165/// - Easy debugging and logging of callback actions
166/// - Future extensibility for new change types
167#[derive(Debug, Clone)]
168pub enum CallbackChange {
169    // Window State Changes
170    /// Modify the window state (size, position, title, etc.)
171    ModifyWindowState { state: FullWindowState },
172    /// Queue multiple window state changes to be applied in sequence across frames.
173    /// This is needed for simulating clicks (mouse down → wait → mouse up) where each
174    /// state change needs to trigger separate event processing.
175    QueueWindowStateSequence { states: Vec<FullWindowState> },
176    /// Create a new window
177    CreateNewWindow { options: WindowCreateOptions },
178    /// Close the current window (via Update::CloseWindow return value, tracked here for logging)
179    CloseWindow,
180
181    // Focus Management
182    /// Change keyboard focus to a specific node or clear focus
183    SetFocusTarget { target: FocusTarget },
184
185    // Event Propagation Control
186    /// Stop event from propagating to parent nodes
187    StopPropagation,
188    /// Prevent default browser behavior (e.g., block text input from being applied)
189    PreventDefault,
190
191    // Timer Management
192    /// Add a new timer to the window
193    AddTimer { timer_id: TimerId, timer: Timer },
194    /// Remove an existing timer
195    RemoveTimer { timer_id: TimerId },
196
197    // Thread Management
198    /// Add a new background thread
199    AddThread { thread_id: ThreadId, thread: Thread },
200    /// Remove an existing thread
201    RemoveThread { thread_id: ThreadId },
202
203    // Content Modifications
204    /// Change the text content of a node
205    ChangeNodeText { node_id: DomNodeId, text: AzString },
206    /// Change the image of a node
207    ChangeNodeImage {
208        dom_id: DomId,
209        node_id: NodeId,
210        image: ImageRef,
211        update_type: UpdateImageType,
212    },
213    /// Re-render an image callback (for resize/animation)
214    /// This triggers re-invocation of the RenderImageCallback
215    UpdateImageCallback { dom_id: DomId, node_id: NodeId },
216    /// Trigger re-rendering of an IFrame with a new DOM
217    /// This forces the IFrame to call its callback and update the display list
218    UpdateIFrame { dom_id: DomId, node_id: NodeId },
219    /// Change the image mask of a node
220    ChangeNodeImageMask {
221        dom_id: DomId,
222        node_id: NodeId,
223        mask: ImageMask,
224    },
225    /// Change CSS properties of a node
226    ChangeNodeCssProperties {
227        dom_id: DomId,
228        node_id: NodeId,
229        properties: CssPropertyVec,
230    },
231
232    // Scroll Management
233    /// Scroll a node to a specific position
234    ScrollTo {
235        dom_id: DomId,
236        node_id: NodeHierarchyItemId,
237        position: LogicalPosition,
238    },
239    /// Scroll a node into view (W3C scrollIntoView API)
240    /// The scroll adjustments are calculated and applied when the change is processed
241    ScrollIntoView {
242        node_id: DomNodeId,
243        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
244    },
245
246    // Image Cache Management
247    /// Add an image to the image cache
248    AddImageToCache { id: AzString, image: ImageRef },
249    /// Remove an image from the image cache
250    RemoveImageFromCache { id: AzString },
251
252    // Font Cache Management
253    /// Reload system fonts (expensive operation)
254    ReloadSystemFonts,
255
256    // Menu Management
257    /// Open a context menu or dropdown menu
258    /// Whether it's native or fallback depends on window.state.flags.use_native_context_menus
259    OpenMenu {
260        menu: Menu,
261        /// Optional position override (if None, uses menu.position)
262        position: Option<LogicalPosition>,
263    },
264
265    // Tooltip Management
266    /// Show a tooltip at a specific position
267    ///
268    /// Platform-specific implementation:
269    /// - Windows: Uses native tooltip window (TOOLTIPS_CLASS)
270    /// - macOS: Uses NSPopover or custom NSWindow with tooltip styling
271    /// - X11: Creates transient window with _NET_WM_WINDOW_TYPE_TOOLTIP
272    /// - Wayland: Creates surface with zwlr_layer_shell_v1 (overlay layer)
273    ShowTooltip {
274        text: AzString,
275        position: LogicalPosition,
276    },
277    /// Hide the currently displayed tooltip
278    HideTooltip,
279
280    // Text Editing
281    /// Insert text at the current cursor position or replace selection
282    InsertText {
283        dom_id: DomId,
284        node_id: NodeId,
285        text: AzString,
286    },
287    /// Delete text backward (backspace) at cursor
288    DeleteBackward { dom_id: DomId, node_id: NodeId },
289    /// Delete text forward (delete key) at cursor
290    DeleteForward { dom_id: DomId, node_id: NodeId },
291    /// Move cursor to a specific position
292    MoveCursor {
293        dom_id: DomId,
294        node_id: NodeId,
295        cursor: TextCursor,
296    },
297    /// Set text selection range
298    SetSelection {
299        dom_id: DomId,
300        node_id: NodeId,
301        selection: Selection,
302    },
303    /// Set/override the text changeset for the current text input operation
304    /// This allows callbacks to modify what text will be inserted during text input events
305    SetTextChangeset { changeset: PendingTextEdit },
306
307    // Cursor Movement Operations
308    /// Move cursor left (arrow left)
309    MoveCursorLeft {
310        dom_id: DomId,
311        node_id: NodeId,
312        extend_selection: bool,
313    },
314    /// Move cursor right (arrow right)
315    MoveCursorRight {
316        dom_id: DomId,
317        node_id: NodeId,
318        extend_selection: bool,
319    },
320    /// Move cursor up (arrow up)
321    MoveCursorUp {
322        dom_id: DomId,
323        node_id: NodeId,
324        extend_selection: bool,
325    },
326    /// Move cursor down (arrow down)
327    MoveCursorDown {
328        dom_id: DomId,
329        node_id: NodeId,
330        extend_selection: bool,
331    },
332    /// Move cursor to line start (Home key)
333    MoveCursorToLineStart {
334        dom_id: DomId,
335        node_id: NodeId,
336        extend_selection: bool,
337    },
338    /// Move cursor to line end (End key)
339    MoveCursorToLineEnd {
340        dom_id: DomId,
341        node_id: NodeId,
342        extend_selection: bool,
343    },
344    /// Move cursor to document start (Ctrl+Home)
345    MoveCursorToDocumentStart {
346        dom_id: DomId,
347        node_id: NodeId,
348        extend_selection: bool,
349    },
350    /// Move cursor to document end (Ctrl+End)
351    MoveCursorToDocumentEnd {
352        dom_id: DomId,
353        node_id: NodeId,
354        extend_selection: bool,
355    },
356
357    // Clipboard Operations (Override)
358    /// Override clipboard content for copy operation
359    SetCopyContent {
360        target: DomNodeId,
361        content: ClipboardContent,
362    },
363    /// Override clipboard content for cut operation
364    SetCutContent {
365        target: DomNodeId,
366        content: ClipboardContent,
367    },
368    /// Override selection range for select-all operation
369    SetSelectAllRange {
370        target: DomNodeId,
371        range: SelectionRange,
372    },
373
374    // Hit Test Request (for Debug API)
375    /// Request a hit test update at a specific position
376    ///
377    /// This is used by the Debug API to update the hover manager's hit test
378    /// data after modifying the mouse position, ensuring that callbacks
379    /// can find the correct nodes under the cursor.
380    RequestHitTestUpdate { position: LogicalPosition },
381
382    // Text Selection (for Debug API)
383    /// Process a text selection click at a specific position
384    ///
385    /// This is used by the Debug API to trigger text selection directly,
386    /// bypassing the normal event pipeline. The handler will:
387    /// 1. Hit-test IFC roots to find selectable text at the position
388    /// 2. Create a text cursor at the clicked position
389    /// 3. Update the selection manager with the new selection
390    ProcessTextSelectionClick {
391        position: LogicalPosition,
392        time_ms: u64,
393    },
394
395    // Cursor Blinking (System Timer Control)
396    /// Set the cursor visibility state (called by blink timer)
397    SetCursorVisibility { visible: bool },
398    /// Reset cursor blink state on user input (makes cursor visible, records time)
399    ResetCursorBlink,
400    /// Start the cursor blink timer for the focused contenteditable element
401    StartCursorBlinkTimer,
402    /// Stop the cursor blink timer (when focus leaves contenteditable)
403    StopCursorBlinkTimer,
404    
405    // Scroll cursor/selection into view
406    /// Scroll the active text cursor into view within its scrollable container
407    /// This is automatically triggered after text input or cursor movement
408    ScrollActiveCursorIntoView,
409    
410    // Create Text Input Event (for Debug API / Programmatic Text Input)
411    /// Create a synthetic text input event
412    ///
413    /// This simulates receiving text input from the OS. The text input flow will:
414    /// 1. Record the text in TextInputManager (creating a PendingTextEdit)
415    /// 2. Generate synthetic TextInput events
416    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
417    /// 4. Apply the changeset if not rejected
418    /// 5. Mark dirty nodes for re-render
419    CreateTextInput {
420        /// The text to insert
421        text: AzString,
422    },
423}
424
425/// Main callback type for UI event handling
426pub type CallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
427
428/// Stores a function pointer that is executed when the given UI element is hit
429///
430/// Must return an `Update` that denotes if the screen should be redrawn.
431#[repr(C)]
432pub struct Callback {
433    pub cb: CallbackType,
434    /// For FFI: stores the foreign callable (e.g., PyFunction)
435    /// Native Rust code sets this to None
436    pub ctx: OptionRefAny,
437}
438
439impl_callback!(Callback, CallbackType);
440
441impl Callback {
442    /// Create a new callback with just a function pointer (for native Rust code)
443    pub fn create<C: Into<Callback>>(cb: C) -> Self {
444        cb.into()
445    }
446
447    /// Convert from CoreCallback (stored as usize) to Callback (actual function pointer)
448    ///
449    /// # Safety
450    /// The caller must ensure that the usize in CoreCallback.cb was originally a valid
451    /// function pointer of type `CallbackType`. This is guaranteed when CoreCallback
452    /// is created through standard APIs, but unsafe code could violate this.
453    pub fn from_core(core: CoreCallback) -> Self {
454        Self {
455            cb: unsafe { core::mem::transmute(core.cb) },
456            ctx: OptionRefAny::None,
457        }
458    }
459
460    /// Convert to CoreCallback (function pointer stored as usize)
461    ///
462    /// This is always safe - we're just casting the function pointer to usize for storage.
463    pub fn to_core(self) -> CoreCallback {
464        CoreCallback {
465            cb: self.cb as usize,
466            ctx: self.ctx,
467        }
468    }
469}
470
471/// Allow Callback to be passed to functions expecting `C: Into<CoreCallback>`
472impl From<Callback> for CoreCallback {
473    fn from(callback: Callback) -> Self {
474        callback.to_core()
475    }
476}
477
478/// Convert a raw function pointer to CoreCallback
479///
480/// This is a helper function that wraps the function pointer cast.
481/// Cannot use From trait due to orphan rules (extern "C" fn is not a local type).
482#[inline]
483pub fn callback_type_to_core(cb: CallbackType) -> CoreCallback {
484    CoreCallback {
485        cb: cb as usize,
486        ctx: OptionRefAny::None,
487    }
488}
489
490impl Callback {
491    /// Safely invoke the callback with the given data and info
492    ///
493    /// This is a safe wrapper around calling the function pointer directly.
494    pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
495        (self.cb)(data, info)
496    }
497}
498
499/// Safe conversion from CoreCallback to function pointer
500///
501/// This provides a type-safe way to convert CoreCallback.cb (usize) to the actual
502/// function pointer type without using transmute directly in application code.
503///
504/// # Safety
505/// The caller must ensure the usize was originally a valid CallbackType function pointer.
506pub unsafe fn core_callback_to_fn(core: CoreCallback) -> CallbackType {
507    core::mem::transmute(core.cb)
508}
509
510/// FFI-safe Option<Callback> type for C interop.
511///
512/// This enum provides an ABI-stable alternative to `Option<Callback>`
513/// that can be safely passed across FFI boundaries.
514#[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
515#[repr(C, u8)]
516pub enum OptionCallback {
517    /// No callback is present.
518    None,
519    /// A callback is present.
520    Some(Callback),
521}
522
523impl OptionCallback {
524    /// Converts this FFI-safe option into a standard Rust `Option<Callback>`.
525    pub fn into_option(self) -> Option<Callback> {
526        match self {
527            OptionCallback::None => None,
528            OptionCallback::Some(c) => Some(c),
529        }
530    }
531
532    /// Returns `true` if a callback is present.
533    pub fn is_some(&self) -> bool {
534        matches!(self, OptionCallback::Some(_))
535    }
536
537    /// Returns `true` if no callback is present.
538    pub fn is_none(&self) -> bool {
539        matches!(self, OptionCallback::None)
540    }
541}
542
543impl From<Option<Callback>> for OptionCallback {
544    fn from(o: Option<Callback>) -> Self {
545        match o {
546            None => OptionCallback::None,
547            Some(c) => OptionCallback::Some(c),
548        }
549    }
550}
551
552impl From<OptionCallback> for Option<Callback> {
553    fn from(o: OptionCallback) -> Self {
554        o.into_option()
555    }
556}
557
558/// Information about the callback that is passed to the callback whenever a callback is invoked
559///
560/// # Architecture
561///
562/// CallbackInfo uses a transaction-based system:
563/// - **Read-only pointers**: Access to layout data, window state, managers for queries
564/// - **Change vector**: All modifications are recorded as CallbackChange items
565/// - **Processing**: Changes are applied atomically after callback returns
566///
567/// This design provides clear separation between queries and modifications, makes debugging
568/// easier, and allows for future extensibility.
569
570/// Reference data container for CallbackInfo (all read-only fields)
571///
572/// This struct consolidates all readonly references that callbacks need to query window state.
573/// By grouping these into a single struct, we reduce the number of parameters to
574/// CallbackInfo::new() from 13 to 3, making the API more maintainable and easier to extend.
575///
576/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
577/// reference.
578pub struct CallbackInfoRefData<'a> {
579    /// Pointer to the LayoutWindow containing all layout results (READ-ONLY for queries)
580    pub layout_window: &'a LayoutWindow,
581    /// Necessary to query FontRefs from callbacks
582    pub renderer_resources: &'a RendererResources,
583    /// Previous window state (for detecting changes)
584    pub previous_window_state: &'a Option<FullWindowState>,
585    /// State of the current window that the callback was called on (read only!)
586    pub current_window_state: &'a FullWindowState,
587    /// An Rc to the OpenGL context, in order to be able to render to OpenGL textures
588    pub gl_context: &'a OptionGlContextPtr,
589    /// Immutable reference to where the nodes are currently scrolled (current position)
590    pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
591    /// Handle of the current window
592    pub current_window_handle: &'a RawWindowHandle,
593    /// Callbacks for creating threads and getting the system time (since this crate uses no_std)
594    pub system_callbacks: &'a ExternalSystemCallbacks,
595    /// Platform-specific system style (colors, spacing, etc.)
596    /// Arc allows safe cloning in callbacks without unsafe pointer manipulation
597    pub system_style: Arc<SystemStyle>,
598    /// ICU4X localizer cache for internationalized formatting (numbers, dates, lists, plurals)
599    /// Caches localizers for multiple locales. Only available when the "icu" feature is enabled.
600    #[cfg(feature = "icu")]
601    pub icu_localizer: IcuLocalizerHandle,
602    /// The callable for FFI language bindings (Python, etc.)
603    /// Cloned from the Callback struct before invocation. Native Rust callbacks have this as None.
604    pub ctx: OptionRefAny,
605}
606
607/// CallbackInfo is a lightweight wrapper around pointers to stack-local data.
608/// It can be safely copied because it only contains pointers - the underlying
609/// data lives on the stack and outlives the callback invocation.
610/// This allows callbacks to "consume" CallbackInfo by value while the caller
611/// retains access to the same underlying data.
612///
613/// The `changes` field uses a pointer to Arc<Mutex<...>> so that cloned CallbackInfo instances
614/// (e.g., passed to timer callbacks) still push changes to the original collection,
615/// while keeping CallbackInfo as Copy.
616#[derive(Debug, Clone, Copy)]
617#[repr(C)]
618pub struct CallbackInfo {
619    // Read-only Data (Query Access)
620    /// Single reference to all readonly reference data
621    /// This consolidates 8 individual parameters into 1, improving API ergonomics
622    ref_data: *const CallbackInfoRefData<'static>,
623    // Context Info (Immutable Event Data)
624    /// The ID of the DOM + the node that was hit
625    hit_dom_node: DomNodeId,
626    /// The (x, y) position of the mouse cursor, **relative to top left of the element that was
627    /// hit**
628    cursor_relative_to_item: OptionLogicalPosition,
629    /// The (x, y) position of the mouse cursor, **relative to top left of the window**
630    cursor_in_viewport: OptionLogicalPosition,
631    // Transaction Container (New System) - Uses pointer to Arc<Mutex> for shared access across clones
632    /// All changes made by the callback, applied atomically after callback returns
633    /// Stored as raw pointer so CallbackInfo remains Copy
634    #[cfg(feature = "std")]
635    changes: *const Arc<Mutex<Vec<CallbackChange>>>,
636    #[cfg(not(feature = "std"))]
637    changes: *mut Vec<CallbackChange>,
638}
639
640impl CallbackInfo {
641    #[cfg(feature = "std")]
642    pub fn new<'a>(
643        ref_data: &'a CallbackInfoRefData<'a>,
644        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
645        hit_dom_node: DomNodeId,
646        cursor_relative_to_item: OptionLogicalPosition,
647        cursor_in_viewport: OptionLogicalPosition,
648    ) -> Self {
649        Self {
650            // Read-only data (single reference to consolidated refs)
651            // SAFETY: We cast away the lifetime 'a to 'static because CallbackInfo
652            // only lives for the duration of the callback, which is shorter than 'a
653            ref_data: unsafe { core::mem::transmute(ref_data) },
654
655            // Context info (immutable event data)
656            hit_dom_node,
657            cursor_relative_to_item,
658            cursor_in_viewport,
659
660            // Transaction container - store pointer to Arc<Mutex> for shared access
661            changes: changes as *const Arc<Mutex<Vec<CallbackChange>>>,
662        }
663    }
664
665    #[cfg(not(feature = "std"))]
666    pub fn new<'a>(
667        ref_data: &'a CallbackInfoRefData<'a>,
668        changes: &'a mut Vec<CallbackChange>,
669        hit_dom_node: DomNodeId,
670        cursor_relative_to_item: OptionLogicalPosition,
671        cursor_in_viewport: OptionLogicalPosition,
672    ) -> Self {
673        Self {
674            ref_data: unsafe { core::mem::transmute(ref_data) },
675            hit_dom_node,
676            cursor_relative_to_item,
677            cursor_in_viewport,
678            changes: changes as *mut Vec<CallbackChange>,
679        }
680    }
681
682    /// Get the callable for FFI language bindings (Python, etc.)
683    ///
684    /// Returns the cloned OptionRefAny if a callable was set, or None if this
685    /// is a native Rust callback.
686    pub fn get_ctx(&self) -> OptionRefAny {
687        unsafe { (*self.ref_data).ctx.clone() }
688    }
689
690    /// Returns the OpenGL context if available
691    pub fn get_gl_context(&self) -> OptionGlContextPtr {
692        unsafe { (*self.ref_data).gl_context.clone() }
693    }
694
695    // Helper methods for transaction system
696
697    /// Push a change to be applied after the callback returns
698    /// This is the primary method for modifying window state from callbacks
699    #[cfg(feature = "std")]
700    pub fn push_change(&mut self, change: CallbackChange) {
701        // SAFETY: The pointer is valid for the lifetime of the callback
702        unsafe {
703            if let Ok(mut changes) = (*self.changes).lock() {
704                changes.push(change);
705            }
706        }
707    }
708
709    #[cfg(not(feature = "std"))]
710    pub fn push_change(&mut self, change: CallbackChange) {
711        unsafe { (*self.changes).push(change) }
712    }
713
714    /// Debug helper to get the changes pointer for debugging
715    #[cfg(feature = "std")]
716    pub fn get_changes_ptr(&self) -> *const () {
717        self.changes as *const ()
718    }
719
720    /// Get the collected changes (consumes them from the Arc<Mutex>)
721    #[cfg(feature = "std")]
722    pub fn take_changes(&self) -> Vec<CallbackChange> {
723        // SAFETY: The pointer is valid for the lifetime of the callback
724        unsafe {
725            if let Ok(mut changes) = (*self.changes).lock() {
726                core::mem::take(&mut *changes)
727            } else {
728                Vec::new()
729            }
730        }
731    }
732
733    #[cfg(not(feature = "std"))]
734    pub fn take_changes(&self) -> Vec<CallbackChange> {
735        unsafe { core::mem::take(&mut *self.changes) }
736    }
737
738    // Modern Api (using CallbackChange transactions)
739
740    /// Add a timer to this window (applied after callback returns)
741    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
742        self.push_change(CallbackChange::AddTimer { timer_id, timer });
743    }
744
745    /// Remove a timer from this window (applied after callback returns)
746    pub fn remove_timer(&mut self, timer_id: TimerId) {
747        self.push_change(CallbackChange::RemoveTimer { timer_id });
748    }
749
750    /// Add a thread to this window (applied after callback returns)
751    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
752        self.push_change(CallbackChange::AddThread { thread_id, thread });
753    }
754
755    /// Remove a thread from this window (applied after callback returns)
756    pub fn remove_thread(&mut self, thread_id: ThreadId) {
757        self.push_change(CallbackChange::RemoveThread { thread_id });
758    }
759
760    /// Stop event propagation (applied after callback returns)
761    pub fn stop_propagation(&mut self) {
762        self.push_change(CallbackChange::StopPropagation);
763    }
764
765    /// Set keyboard focus target (applied after callback returns)
766    pub fn set_focus(&mut self, target: FocusTarget) {
767        self.push_change(CallbackChange::SetFocusTarget { target });
768    }
769
770    /// Create a new window (applied after callback returns)
771    pub fn create_window(&mut self, options: WindowCreateOptions) {
772        self.push_change(CallbackChange::CreateNewWindow { options });
773    }
774
775    /// Close the current window (applied after callback returns)
776    pub fn close_window(&mut self) {
777        self.push_change(CallbackChange::CloseWindow);
778    }
779
780    /// Modify the window state (applied after callback returns)
781    pub fn modify_window_state(&mut self, state: FullWindowState) {
782        self.push_change(CallbackChange::ModifyWindowState { state });
783    }
784
785    /// Queue multiple window state changes to be applied in sequence.
786    /// Each state triggers a separate event processing cycle, which is needed
787    /// for simulating clicks where mouse down and mouse up must be separate events.
788    pub fn queue_window_state_sequence(&mut self, states: Vec<FullWindowState>) {
789        self.push_change(CallbackChange::QueueWindowStateSequence { states });
790    }
791
792    /// Change the text content of a node (applied after callback returns)
793    ///
794    /// This method was previously called `set_string_contents` in older API versions.
795    ///
796    /// # Arguments
797    /// * `node_id` - The text node to modify (DomNodeId containing both DOM and node IDs)
798    /// * `text` - The new text content
799    pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
800        self.push_change(CallbackChange::ChangeNodeText { node_id, text });
801    }
802
803    /// Change the image of a node (applied after callback returns)
804    pub fn change_node_image(
805        &mut self,
806        dom_id: DomId,
807        node_id: NodeId,
808        image: ImageRef,
809        update_type: UpdateImageType,
810    ) {
811        self.push_change(CallbackChange::ChangeNodeImage {
812            dom_id,
813            node_id,
814            image,
815            update_type,
816        });
817    }
818
819    /// Re-render an image callback (for resize/animation updates)
820    ///
821    /// This triggers re-invocation of the RenderImageCallback associated with the node.
822    /// Useful for:
823    /// - Responding to window resize (image needs to match new size)
824    /// - Animation frames (update OpenGL texture each frame)
825    /// - Interactive content (user input changes rendering)
826    pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
827        self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
828    }
829
830    /// Trigger re-rendering of an IFrame (applied after callback returns)
831    ///
832    /// This forces the IFrame to call its layout callback with reason `DomRecreated`
833    /// and submit a new display list to WebRender. The IFrame's pipeline will be updated
834    /// without affecting other parts of the window.
835    ///
836    /// Useful for:
837    /// - Live preview panes (update when source code changes)
838    /// - Dynamic content that needs manual refresh
839    /// - Editor previews (re-parse and display new DOM)
840    pub fn trigger_iframe_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
841        self.push_change(CallbackChange::UpdateIFrame { dom_id, node_id });
842    }
843
844    // Dom Tree Navigation
845
846    /// Find a node by ID attribute in the layout tree
847    ///
848    /// Returns the NodeId of the first node with the given ID attribute, or None if not found.
849    pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
850        let layout_window = self.get_layout_window();
851        let layout_result = layout_window.layout_results.get(&dom_id)?;
852        let styled_dom = &layout_result.styled_dom;
853
854        // Search through all nodes to find one with matching ID attribute
855        for (node_idx, node_data) in styled_dom.node_data.as_ref().iter().enumerate() {
856            for id_or_class in node_data.ids_and_classes.as_ref() {
857                if let IdOrClass::Id(node_id_str) = id_or_class {
858                    if node_id_str.as_str() == id {
859                        return Some(NodeId::new(node_idx));
860                    }
861                }
862            }
863        }
864
865        None
866    }
867
868    /// Get the parent node of the given node
869    ///
870    /// Returns None if the node has no parent (i.e., it's the root node)
871    pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
872        let layout_window = self.get_layout_window();
873        let layout_result = layout_window.layout_results.get(&dom_id)?;
874        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
875        let node = node_hierarchy.as_ref().get(node_id.index())?;
876        node.parent_id()
877    }
878
879    /// Get the next sibling of the given node
880    ///
881    /// Returns None if the node has no next sibling
882    pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
883        let layout_window = self.get_layout_window();
884        let layout_result = layout_window.layout_results.get(&dom_id)?;
885        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
886        let node = node_hierarchy.as_ref().get(node_id.index())?;
887        node.next_sibling_id()
888    }
889
890    /// Get the previous sibling of the given node
891    ///
892    /// Returns None if the node has no previous sibling
893    pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
894        let layout_window = self.get_layout_window();
895        let layout_result = layout_window.layout_results.get(&dom_id)?;
896        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
897        let node = node_hierarchy.as_ref().get(node_id.index())?;
898        node.previous_sibling_id()
899    }
900
901    /// Get the first child of the given node
902    ///
903    /// Returns None if the node has no children
904    pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
905        let layout_window = self.get_layout_window();
906        let layout_result = layout_window.layout_results.get(&dom_id)?;
907        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
908        let node = node_hierarchy.as_ref().get(node_id.index())?;
909        node.first_child_id(node_id)
910    }
911
912    /// Get the last child of the given node
913    ///
914    /// Returns None if the node has no children
915    pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
916        let layout_window = self.get_layout_window();
917        let layout_result = layout_window.layout_results.get(&dom_id)?;
918        let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
919        let node = node_hierarchy.as_ref().get(node_id.index())?;
920        node.last_child_id()
921    }
922
923    /// Get all direct children of the given node
924    ///
925    /// Returns an empty vector if the node has no children.
926    /// Uses the contiguous node layout for efficient iteration.
927    pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeIdVec {
928        let layout_window = self.get_layout_window();
929        let layout_result = match layout_window.layout_results.get(&dom_id) {
930            Some(lr) => lr,
931            None => return NodeIdVec::from_const_slice(&[]),
932        };
933        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
934        let hier_item = match node_hierarchy.get(node_id) {
935            Some(h) => h,
936            None => return NodeIdVec::from_const_slice(&[]),
937        };
938
939        // Get first child - if none, return empty
940        let first_child = match hier_item.first_child_id(node_id) {
941            Some(fc) => fc,
942            None => return NodeIdVec::from_const_slice(&[]),
943        };
944
945        // Collect children by walking the sibling chain
946        let mut children: Vec<NodeHierarchyItemId> = Vec::new();
947        children.push(NodeHierarchyItemId::from_crate_internal(Some(first_child)));
948
949        let mut current = first_child;
950        while let Some(next_sibling) = node_hierarchy
951            .get(current)
952            .and_then(|h| h.next_sibling_id())
953        {
954            children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
955            current = next_sibling;
956        }
957
958        NodeIdVec::from(children)
959    }
960
961    /// Get the number of direct children of the given node
962    ///
963    /// Uses the contiguous node layout for efficient counting.
964    pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
965        let layout_window = self.get_layout_window();
966        let layout_result = match layout_window.layout_results.get(&dom_id) {
967            Some(lr) => lr,
968            None => return 0,
969        };
970        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
971        let hier_item = match node_hierarchy.get(node_id) {
972            Some(h) => h,
973            None => return 0,
974        };
975
976        // Get first child - if none, return 0
977        let first_child = match hier_item.first_child_id(node_id) {
978            Some(fc) => fc,
979            None => return 0,
980        };
981
982        // Count children by walking the sibling chain
983        let mut count = 1;
984        let mut current = first_child;
985        while let Some(next_sibling) = node_hierarchy
986            .get(current)
987            .and_then(|h| h.next_sibling_id())
988        {
989            count += 1;
990            current = next_sibling;
991        }
992
993        count
994    }
995
996    /// Change the image mask of a node (applied after callback returns)
997    pub fn change_node_image_mask(&mut self, dom_id: DomId, node_id: NodeId, mask: ImageMask) {
998        self.push_change(CallbackChange::ChangeNodeImageMask {
999            dom_id,
1000            node_id,
1001            mask,
1002        });
1003    }
1004
1005    /// Change CSS properties of a node (applied after callback returns)
1006    pub fn change_node_css_properties(
1007        &mut self,
1008        dom_id: DomId,
1009        node_id: NodeId,
1010        properties: CssPropertyVec,
1011    ) {
1012        self.push_change(CallbackChange::ChangeNodeCssProperties {
1013            dom_id,
1014            node_id,
1015            properties,
1016        });
1017    }
1018
1019    /// Set a single CSS property on a node (convenience method for widgets)
1020    ///
1021    /// This is a helper method that wraps `change_node_css_properties` for the common case
1022    /// of setting a single property. It uses the hit node's DOM ID automatically.
1023    ///
1024    /// # Arguments
1025    /// * `node_id` - The node to set the property on (uses hit node's DOM ID)
1026    /// * `property` - The CSS property to set
1027    pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1028        let dom_id = node_id.dom;
1029        let internal_node_id = node_id
1030            .node
1031            .into_crate_internal()
1032            .expect("DomNodeId node should not be None");
1033        self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
1034    }
1035
1036    /// Scroll a node to a specific position (applied after callback returns)
1037    pub fn scroll_to(
1038        &mut self,
1039        dom_id: DomId,
1040        node_id: NodeHierarchyItemId,
1041        position: LogicalPosition,
1042    ) {
1043        self.push_change(CallbackChange::ScrollTo {
1044            dom_id,
1045            node_id,
1046            position,
1047        });
1048    }
1049
1050    /// Scroll a node into view (W3C scrollIntoView API)
1051    ///
1052    /// Scrolls the element into the visible area of its scroll container.
1053    /// This is the recommended way to programmatically scroll elements into view.
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `node_id` - The node to scroll into view
1058    /// * `options` - Scroll alignment and animation options
1059    ///
1060    /// # Note
1061    ///
1062    /// This uses the transactional change system - the scroll is queued and applied
1063    /// after the callback returns. The actual scroll adjustments are calculated
1064    /// during change processing.
1065    pub fn scroll_node_into_view(
1066        &mut self,
1067        node_id: DomNodeId,
1068        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1069    ) {
1070        self.push_change(CallbackChange::ScrollIntoView {
1071            node_id,
1072            options,
1073        });
1074    }
1075
1076    /// Add an image to the image cache (applied after callback returns)
1077    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
1078        self.push_change(CallbackChange::AddImageToCache { id, image });
1079    }
1080
1081    /// Remove an image from the image cache (applied after callback returns)
1082    pub fn remove_image_from_cache(&mut self, id: AzString) {
1083        self.push_change(CallbackChange::RemoveImageFromCache { id });
1084    }
1085
1086    /// Reload system fonts (applied after callback returns)
1087    ///
1088    /// Note: This is an expensive operation that rebuilds the entire font cache
1089    pub fn reload_system_fonts(&mut self) {
1090        self.push_change(CallbackChange::ReloadSystemFonts);
1091    }
1092
1093    // Text Input / Changeset Api
1094
1095    /// Get the current text changeset being processed (if any)
1096    ///
1097    /// This allows callbacks to inspect what text input is about to be applied.
1098    /// Returns None if no text input is currently being processed.
1099    ///
1100    /// Use `set_text_changeset()` to modify the text that will be inserted,
1101    /// and `prevent_default()` to block the text input entirely.
1102    pub fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
1103        self.get_layout_window()
1104            .text_input_manager
1105            .get_pending_changeset()
1106    }
1107
1108    /// Set/override the text changeset for the current text input operation
1109    ///
1110    /// This allows you to modify what text will be inserted during text input events.
1111    /// Typically used in combination with `prevent_default()` to transform user input.
1112    ///
1113    /// # Arguments
1114    /// * `changeset` - The modified text changeset to apply
1115    pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
1116        self.push_change(CallbackChange::SetTextChangeset { changeset });
1117    }
1118
1119    /// Create a synthetic text input event
1120    ///
1121    /// This simulates receiving text input from the OS. Use this to programmatically
1122    /// insert text into contenteditable elements, for example from the debug server
1123    /// or from accessibility APIs.
1124    ///
1125    /// The text input flow will:
1126    /// 1. Record the text in TextInputManager (creating a PendingTextEdit)
1127    /// 2. Generate synthetic TextInput events
1128    /// 3. Invoke user callbacks (which can intercept/reject via preventDefault)
1129    /// 4. Apply the changeset if not rejected
1130    /// 5. Mark dirty nodes for re-render
1131    ///
1132    /// # Arguments
1133    /// * `text` - The text to insert at the current cursor position
1134    pub fn create_text_input(&mut self, text: AzString) {
1135        println!("[CallbackInfo::create_text_input] Creating text input: '{}'", text.as_str());
1136        self.push_change(CallbackChange::CreateTextInput { text });
1137    }
1138
1139    /// Prevent the default text input from being applied
1140    ///
1141    /// When called in a TextInput callback, prevents the typed text from being inserted.
1142    /// Useful for custom validation, filtering, or text transformation.
1143    pub fn prevent_default(&mut self) {
1144        self.push_change(CallbackChange::PreventDefault);
1145    }
1146
1147    // Cursor Blinking Api (for system timer control)
1148    
1149    /// Set cursor visibility state
1150    ///
1151    /// This is primarily used internally by the cursor blink timer callback.
1152    /// User code typically doesn't need to call this directly.
1153    pub fn set_cursor_visibility(&mut self, visible: bool) {
1154        self.push_change(CallbackChange::SetCursorVisibility { visible });
1155    }
1156    
1157    /// Reset cursor blink state on user input
1158    ///
1159    /// This makes the cursor visible and records the current time, so the blink
1160    /// timer knows to keep the cursor solid for a while before blinking.
1161    /// Called automatically on keyboard input, but can be called manually.
1162    pub fn reset_cursor_blink(&mut self) {
1163        self.push_change(CallbackChange::ResetCursorBlink);
1164    }
1165    
1166    /// Start the cursor blink timer
1167    ///
1168    /// Called automatically when focus lands on a contenteditable element.
1169    /// The timer will toggle cursor visibility at ~530ms intervals.
1170    pub fn start_cursor_blink_timer(&mut self) {
1171        self.push_change(CallbackChange::StartCursorBlinkTimer);
1172    }
1173    
1174    /// Stop the cursor blink timer
1175    ///
1176    /// Called automatically when focus leaves a contenteditable element.
1177    pub fn stop_cursor_blink_timer(&mut self) {
1178        self.push_change(CallbackChange::StopCursorBlinkTimer);
1179    }
1180    
1181    /// Scroll the active cursor into view
1182    ///
1183    /// This scrolls the focused text element's cursor into the visible area
1184    /// of any scrollable ancestor. Called automatically after text input.
1185    pub fn scroll_active_cursor_into_view(&mut self) {
1186        self.push_change(CallbackChange::ScrollActiveCursorIntoView);
1187    }
1188
1189    /// Open a menu (context menu or dropdown)
1190    ///
1191    /// The menu will be displayed either as a native menu or a fallback DOM-based menu
1192    /// depending on the window's `use_native_context_menus` flag.
1193    /// Uses the position specified in the menu itself.
1194    ///
1195    /// # Arguments
1196    /// * `menu` - The menu to display
1197    pub fn open_menu(&mut self, menu: Menu) {
1198        self.push_change(CallbackChange::OpenMenu {
1199            menu,
1200            position: None,
1201        });
1202    }
1203
1204    /// Open a menu at a specific position
1205    ///
1206    /// # Arguments
1207    /// * `menu` - The menu to display
1208    /// * `position` - The position where the menu should appear (overrides menu's position)
1209    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
1210        self.push_change(CallbackChange::OpenMenu {
1211            menu,
1212            position: Some(position),
1213        });
1214    }
1215
1216    // Tooltip Api
1217
1218    /// Show a tooltip at the current cursor position
1219    ///
1220    /// Displays a simple text tooltip near the mouse cursor.
1221    /// The tooltip will be shown using platform-specific native APIs where available.
1222    ///
1223    /// Platform implementations:
1224    /// - **Windows**: Uses `TOOLTIPS_CLASS` Win32 control
1225    /// - **macOS**: Uses `NSPopover` or custom `NSWindow` with tooltip styling
1226    /// - **X11**: Creates transient window with `_NET_WM_WINDOW_TYPE_TOOLTIP`
1227    /// - **Wayland**: Uses `zwlr_layer_shell_v1` with overlay layer
1228    ///
1229    /// # Arguments
1230    /// * `text` - The tooltip text to display
1231    pub fn show_tooltip(&mut self, text: AzString) {
1232        let position = self
1233            .get_cursor_relative_to_viewport()
1234            .into_option()
1235            .unwrap_or_else(LogicalPosition::zero);
1236        self.push_change(CallbackChange::ShowTooltip { text, position });
1237    }
1238
1239    /// Show a tooltip at a specific position
1240    ///
1241    /// # Arguments
1242    /// * `text` - The tooltip text to display
1243    /// * `position` - The position where the tooltip should appear (in window coordinates)
1244    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
1245        self.push_change(CallbackChange::ShowTooltip { text, position });
1246    }
1247
1248    /// Hide the currently displayed tooltip
1249    pub fn hide_tooltip(&mut self) {
1250        self.push_change(CallbackChange::HideTooltip);
1251    }
1252
1253    // Text Editing Api (transactional)
1254
1255    /// Insert text at the current cursor position in a text node
1256    ///
1257    /// This operation is transactional - the text will be inserted after the callback returns.
1258    /// If there's a selection, it will be replaced with the inserted text.
1259    ///
1260    /// # Arguments
1261    /// * `dom_id` - The DOM containing the text node
1262    /// * `node_id` - The node to insert text into
1263    /// * `text` - The text to insert
1264    pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
1265        self.push_change(CallbackChange::InsertText {
1266            dom_id,
1267            node_id,
1268            text,
1269        });
1270    }
1271
1272    /// Move the text cursor to a specific position
1273    ///
1274    /// # Arguments
1275    /// * `dom_id` - The DOM containing the text node
1276    /// * `node_id` - The node containing the cursor
1277    /// * `cursor` - The new cursor position
1278    pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
1279        self.push_change(CallbackChange::MoveCursor {
1280            dom_id,
1281            node_id,
1282            cursor,
1283        });
1284    }
1285
1286    /// Set the text selection range
1287    ///
1288    /// # Arguments
1289    /// * `dom_id` - The DOM containing the text node
1290    /// * `node_id` - The node containing the selection
1291    /// * `selection` - The new selection (can be a cursor or range)
1292    pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
1293        self.push_change(CallbackChange::SetSelection {
1294            dom_id,
1295            node_id,
1296            selection,
1297        });
1298    }
1299
1300    /// Open a menu positioned relative to a specific DOM node
1301    ///
1302    /// This is useful for dropdowns, combo boxes, and context menus that should appear
1303    /// near a specific UI element. The menu will be positioned below the node by default.
1304    ///
1305    /// # Arguments
1306    /// * `menu` - The menu to display
1307    /// * `node_id` - The DOM node to position the menu relative to
1308    ///
1309    /// # Returns
1310    /// * `true` if the menu was queued for opening
1311    /// * `false` if the node doesn't exist or has no layout information
1312    pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
1313        // Get the node's bounding rectangle
1314        if let Some(rect) = self.get_node_rect(node_id) {
1315            // Position menu at bottom-left of the node
1316            let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
1317            self.push_change(CallbackChange::OpenMenu {
1318                menu,
1319                position: Some(position),
1320            });
1321            true
1322        } else {
1323            false
1324        }
1325    }
1326
1327    /// Open a menu positioned relative to the currently hit node
1328    ///
1329    /// Convenience method for opening a menu at the element that triggered the callback.
1330    /// Equivalent to `open_menu_for_node(menu, info.get_hit_node())`.
1331    ///
1332    /// # Arguments
1333    /// * `menu` - The menu to display
1334    ///
1335    /// # Returns
1336    /// * `true` if the menu was queued for opening
1337    /// * `false` if no node is currently hit or it has no layout information
1338    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
1339        let hit_node = self.get_hit_node();
1340        self.open_menu_for_node(menu, hit_node)
1341    }
1342
1343    // Internal accessors
1344
1345    /// Get reference to the underlying LayoutWindow for queries
1346    ///
1347    /// This provides read-only access to layout data, node hierarchies, managers, etc.
1348    /// All modifications should go through CallbackChange transactions via push_change().
1349    pub fn get_layout_window(&self) -> &LayoutWindow {
1350        unsafe { (*self.ref_data).layout_window }
1351    }
1352
1353    /// Internal helper: Get the inline text layout for a given node
1354    ///
1355    /// This efficiently looks up the text layout by following the chain:
1356    /// LayoutWindow → layout_results → LayoutTree → dom_to_layout → LayoutNode →
1357    /// inline_layout_result
1358    ///
1359    /// Returns None if:
1360    /// - The DOM doesn't exist in layout_results
1361    /// - The node doesn't have a layout node mapping
1362    /// - The layout node doesn't have inline text layout
1363    fn get_inline_layout_for_node(&self, node_id: &DomNodeId) -> Option<&Arc<UnifiedLayout>> {
1364        let layout_window = self.get_layout_window();
1365
1366        // Get the layout result for this DOM
1367        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
1368
1369        // Convert NodeHierarchyItemId to NodeId
1370        let dom_node_id = node_id.node.into_crate_internal()?;
1371
1372        // Look up the layout node index(es) for this DOM node
1373        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&dom_node_id)?;
1374
1375        // Get the first layout node (a DOM node can generate multiple layout nodes,
1376        // but for text we typically only care about the first one)
1377        let layout_index = *layout_indices.first()?;
1378
1379        // Get the layout node and its inline layout result
1380        let layout_node = layout_result.layout_tree.nodes.get(layout_index)?;
1381        layout_node
1382            .inline_layout_result
1383            .as_ref()
1384            .map(|c| c.get_layout())
1385    }
1386
1387    // Public query Api
1388    // All methods below delegate to LayoutWindow for read-only access
1389    pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1390        self.get_layout_window().get_node_size(node_id)
1391    }
1392
1393    pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1394        self.get_layout_window().get_node_position(node_id)
1395    }
1396
1397    /// Get the hit test bounds of a node from the display list
1398    ///
1399    /// This is more reliable than get_node_rect because the display list
1400    /// always contains the correct final rendered positions.
1401    pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1402        self.get_layout_window().get_node_hit_test_bounds(node_id)
1403    }
1404
1405    /// Get the bounding rectangle of a node (position + size)
1406    ///
1407    /// This is particularly useful for menu positioning, where you need
1408    /// to know where a UI element is to popup a menu relative to it.
1409    pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1410        let position = self.get_node_position(node_id)?;
1411        let size = self.get_node_size(node_id)?;
1412        Some(LogicalRect::new(position, size))
1413    }
1414
1415    /// Get the bounding rectangle of the hit node
1416    ///
1417    /// Convenience method that combines get_hit_node() and get_node_rect().
1418    /// Useful for menu positioning based on the clicked element.
1419    pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
1420        let hit_node = self.get_hit_node();
1421        self.get_node_rect(hit_node)
1422    }
1423
1424    // Timer Management (Query APIs)
1425
1426    /// Get a reference to a timer
1427    pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1428        self.get_layout_window().get_timer(timer_id)
1429    }
1430
1431    /// Get all timer IDs
1432    pub fn get_timer_ids(&self) -> TimerIdVec {
1433        self.get_layout_window().get_timer_ids()
1434    }
1435
1436    // Thread Management (Query APIs)
1437
1438    /// Get a reference to a thread
1439    pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1440        self.get_layout_window().get_thread(thread_id)
1441    }
1442
1443    /// Get all thread IDs
1444    pub fn get_thread_ids(&self) -> ThreadIdVec {
1445        self.get_layout_window().get_thread_ids()
1446    }
1447
1448    // Gpu Value Cache Management (Query APIs)
1449
1450    /// Get the GPU value cache for a specific DOM
1451    pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
1452        self.get_layout_window().get_gpu_cache(dom_id)
1453    }
1454
1455    // Layout Result Access (Query APIs)
1456
1457    /// Get a layout result for a specific DOM
1458    pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
1459        self.get_layout_window().get_layout_result(dom_id)
1460    }
1461
1462    /// Get all DOM IDs that have layout results
1463    pub fn get_dom_ids(&self) -> DomIdVec {
1464        self.get_layout_window().get_dom_ids()
1465    }
1466
1467    // Node Hierarchy Navigation
1468
1469    pub fn get_hit_node(&self) -> DomNodeId {
1470        self.hit_dom_node
1471    }
1472
1473    /// Check if a node is anonymous (generated for table layout)
1474    fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
1475        let layout_window = self.get_layout_window();
1476        let layout_result = match layout_window.get_layout_result(dom_id) {
1477            Some(lr) => lr,
1478            None => return false,
1479        };
1480        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1481        let node_data = match node_data_cont.get(node_id) {
1482            Some(nd) => nd,
1483            None => return false,
1484        };
1485        node_data.is_anonymous()
1486    }
1487
1488    pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1489        let layout_window = self.get_layout_window();
1490        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1491        let node_id_internal = node_id.node.into_crate_internal()?;
1492        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1493        let hier_item = node_hierarchy.get(node_id_internal)?;
1494
1495        // Skip anonymous parent nodes - walk up the tree until we find a non-anonymous node
1496        let mut current_parent_id = hier_item.parent_id()?;
1497        loop {
1498            if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
1499                return Some(DomNodeId {
1500                    dom: node_id.dom,
1501                    node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
1502                });
1503            }
1504
1505            // This parent is anonymous, try its parent
1506            let parent_hier_item = node_hierarchy.get(current_parent_id)?;
1507            current_parent_id = parent_hier_item.parent_id()?;
1508        }
1509    }
1510
1511    pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1512        let layout_window = self.get_layout_window();
1513        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1514        let node_id_internal = node_id.node.into_crate_internal()?;
1515        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1516        let hier_item = node_hierarchy.get(node_id_internal)?;
1517
1518        // Skip anonymous siblings - walk backwards until we find a non-anonymous node
1519        let mut current_sibling_id = hier_item.previous_sibling_id()?;
1520        loop {
1521            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
1522                return Some(DomNodeId {
1523                    dom: node_id.dom,
1524                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
1525                });
1526            }
1527
1528            // This sibling is anonymous, try the previous one
1529            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
1530            current_sibling_id = sibling_hier_item.previous_sibling_id()?;
1531        }
1532    }
1533
1534    pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1535        let layout_window = self.get_layout_window();
1536        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1537        let node_id_internal = node_id.node.into_crate_internal()?;
1538        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1539        let hier_item = node_hierarchy.get(node_id_internal)?;
1540
1541        // Skip anonymous siblings - walk forwards until we find a non-anonymous node
1542        let mut current_sibling_id = hier_item.next_sibling_id()?;
1543        loop {
1544            if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
1545                return Some(DomNodeId {
1546                    dom: node_id.dom,
1547                    node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
1548                });
1549            }
1550
1551            // This sibling is anonymous, try the next one
1552            let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
1553            current_sibling_id = sibling_hier_item.next_sibling_id()?;
1554        }
1555    }
1556
1557    pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1558        let layout_window = self.get_layout_window();
1559        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1560        let node_id_internal = node_id.node.into_crate_internal()?;
1561        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1562        let hier_item = node_hierarchy.get(node_id_internal)?;
1563
1564        // Get first child, then skip anonymous nodes
1565        let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
1566        loop {
1567            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
1568                return Some(DomNodeId {
1569                    dom: node_id.dom,
1570                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
1571                });
1572            }
1573
1574            // This child is anonymous, try the next sibling
1575            let child_hier_item = node_hierarchy.get(current_child_id)?;
1576            current_child_id = child_hier_item.next_sibling_id()?;
1577        }
1578    }
1579
1580    pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1581        let layout_window = self.get_layout_window();
1582        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1583        let node_id_internal = node_id.node.into_crate_internal()?;
1584        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1585        let hier_item = node_hierarchy.get(node_id_internal)?;
1586
1587        // Get last child, then skip anonymous nodes by walking backwards
1588        let mut current_child_id = hier_item.last_child_id()?;
1589        loop {
1590            if !self.is_node_anonymous(&node_id.dom, current_child_id) {
1591                return Some(DomNodeId {
1592                    dom: node_id.dom,
1593                    node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
1594                });
1595            }
1596
1597            // This child is anonymous, try the previous sibling
1598            let child_hier_item = node_hierarchy.get(current_child_id)?;
1599            current_child_id = child_hier_item.previous_sibling_id()?;
1600        }
1601    }
1602
1603    // Node Data and State
1604
1605    pub fn get_dataset(&mut self, node_id: DomNodeId) -> Option<RefAny> {
1606        let layout_window = self.get_layout_window();
1607        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1608        let node_id_internal = node_id.node.into_crate_internal()?;
1609        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1610        let node_data = node_data_cont.get(node_id_internal)?;
1611        node_data.get_dataset().clone().into_option()
1612    }
1613
1614    pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
1615        let mut found: Option<(u64, DomNodeId)> = None;
1616        let search_type_id = search_key.get_type_id();
1617
1618        for dom_id in self.get_dom_ids().as_ref().iter().copied() {
1619            let layout_window = self.get_layout_window();
1620            let layout_result = match layout_window.get_layout_result(&dom_id) {
1621                Some(lr) => lr,
1622                None => continue,
1623            };
1624
1625            let node_data_cont = layout_result.styled_dom.node_data.as_container();
1626            for (node_idx, node_data) in node_data_cont.iter().enumerate() {
1627                if let Some(dataset) = node_data.get_dataset().clone().into_option() {
1628                    if dataset.get_type_id() == search_type_id {
1629                        let node_id = DomNodeId {
1630                            dom: dom_id,
1631                            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
1632                                node_idx,
1633                            ))),
1634                        };
1635                        let instance_id = dataset.instance_id;
1636
1637                        match found {
1638                            None => found = Some((instance_id, node_id)),
1639                            Some((prev_instance, _)) => {
1640                                if instance_id < prev_instance {
1641                                    found = Some((instance_id, node_id));
1642                                }
1643                            }
1644                        }
1645                    }
1646                }
1647            }
1648        }
1649
1650        found.map(|s| s.1)
1651    }
1652
1653    pub fn get_string_contents(&self, node_id: DomNodeId) -> Option<AzString> {
1654        let layout_window = self.get_layout_window();
1655        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1656        let node_id_internal = node_id.node.into_crate_internal()?;
1657        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1658        let node_data = node_data_cont.get(node_id_internal)?;
1659
1660        if let NodeType::Text(ref text) = node_data.get_node_type() {
1661            Some(text.clone())
1662        } else {
1663            None
1664        }
1665    }
1666
1667    /// Get the tag name of a node (e.g., "div", "p", "span")
1668    ///
1669    /// Returns the HTML tag name as a string for the given node.
1670    /// For text nodes, returns "text". For image nodes, returns "img".
1671    pub fn get_node_tag_name(&self, node_id: DomNodeId) -> Option<AzString> {
1672        let layout_window = self.get_layout_window();
1673        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1674        let node_id_internal = node_id.node.into_crate_internal()?;
1675        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1676        let node_data = node_data_cont.get(node_id_internal)?;
1677
1678        let tag = node_data.get_node_type().get_path();
1679        Some(tag.to_string().into())
1680    }
1681
1682    /// Get an attribute value from a node by attribute name
1683    ///
1684    /// # Arguments
1685    /// * `node_id` - The node to query
1686    /// * `attr_name` - The attribute name (e.g., "id", "class", "href", "data-custom", "aria-label")
1687    ///
1688    /// Returns the attribute value if found, None otherwise.
1689    /// This searches the strongly-typed AttributeVec on the node.
1690    pub fn get_node_attribute(&self, node_id: DomNodeId, attr_name: &str) -> Option<AzString> {
1691        use azul_core::dom::AttributeType;
1692
1693        let layout_window = self.get_layout_window();
1694        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1695        let node_id_internal = node_id.node.into_crate_internal()?;
1696        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1697        let node_data = node_data_cont.get(node_id_internal)?;
1698
1699        // Check the strongly-typed attributes vec
1700        for attr in node_data.attributes.as_ref() {
1701            match (attr_name, attr) {
1702                ("id", AttributeType::Id(v)) => return Some(v.clone()),
1703                ("class", AttributeType::Class(v)) => return Some(v.clone()),
1704                ("aria-label", AttributeType::AriaLabel(v)) => return Some(v.clone()),
1705                ("aria-labelledby", AttributeType::AriaLabelledBy(v)) => return Some(v.clone()),
1706                ("aria-describedby", AttributeType::AriaDescribedBy(v)) => return Some(v.clone()),
1707                ("role", AttributeType::AriaRole(v)) => return Some(v.clone()),
1708                ("href", AttributeType::Href(v)) => return Some(v.clone()),
1709                ("rel", AttributeType::Rel(v)) => return Some(v.clone()),
1710                ("target", AttributeType::Target(v)) => return Some(v.clone()),
1711                ("src", AttributeType::Src(v)) => return Some(v.clone()),
1712                ("alt", AttributeType::Alt(v)) => return Some(v.clone()),
1713                ("title", AttributeType::Title(v)) => return Some(v.clone()),
1714                ("name", AttributeType::Name(v)) => return Some(v.clone()),
1715                ("value", AttributeType::Value(v)) => return Some(v.clone()),
1716                ("type", AttributeType::InputType(v)) => return Some(v.clone()),
1717                ("placeholder", AttributeType::Placeholder(v)) => return Some(v.clone()),
1718                ("max", AttributeType::Max(v)) => return Some(v.clone()),
1719                ("min", AttributeType::Min(v)) => return Some(v.clone()),
1720                ("step", AttributeType::Step(v)) => return Some(v.clone()),
1721                ("pattern", AttributeType::Pattern(v)) => return Some(v.clone()),
1722                ("autocomplete", AttributeType::Autocomplete(v)) => return Some(v.clone()),
1723                ("scope", AttributeType::Scope(v)) => return Some(v.clone()),
1724                ("lang", AttributeType::Lang(v)) => return Some(v.clone()),
1725                ("dir", AttributeType::Dir(v)) => return Some(v.clone()),
1726                ("required", AttributeType::Required) => return Some("true".into()),
1727                ("disabled", AttributeType::Disabled) => return Some("true".into()),
1728                ("readonly", AttributeType::Readonly) => return Some("true".into()),
1729                ("checked", AttributeType::Checked) => return Some("true".into()),
1730                ("selected", AttributeType::Selected) => return Some("true".into()),
1731                ("hidden", AttributeType::Hidden) => return Some("true".into()),
1732                ("focusable", AttributeType::Focusable) => return Some("true".into()),
1733                ("minlength", AttributeType::MinLength(v)) => return Some(v.to_string().into()),
1734                ("maxlength", AttributeType::MaxLength(v)) => return Some(v.to_string().into()),
1735                ("colspan", AttributeType::ColSpan(v)) => return Some(v.to_string().into()),
1736                ("rowspan", AttributeType::RowSpan(v)) => return Some(v.to_string().into()),
1737                ("tabindex", AttributeType::TabIndex(v)) => return Some(v.to_string().into()),
1738                ("contenteditable", AttributeType::ContentEditable(v)) => {
1739                    return Some(v.to_string().into())
1740                }
1741                ("draggable", AttributeType::Draggable(v)) => return Some(v.to_string().into()),
1742                // Handle data-* attributes
1743                (name, AttributeType::Data(nv))
1744                    if name.starts_with("data-") && nv.attr_name.as_str() == &name[5..] =>
1745                {
1746                    return Some(nv.value.clone());
1747                }
1748                // Handle aria-* state/property attributes
1749                (name, AttributeType::AriaState(nv))
1750                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
1751                {
1752                    return Some(nv.value.clone());
1753                }
1754                (name, AttributeType::AriaProperty(nv))
1755                    if name == format!("aria-{}", nv.attr_name.as_str()) =>
1756                {
1757                    return Some(nv.value.clone());
1758                }
1759                // Handle custom attributes
1760                (name, AttributeType::Custom(nv)) if nv.attr_name.as_str() == name => {
1761                    return Some(nv.value.clone());
1762                }
1763                _ => continue,
1764            }
1765        }
1766
1767        // Fallback: check ids_and_classes for "id" and "class"
1768        if attr_name == "id" {
1769            for id_or_class in node_data.ids_and_classes.as_ref() {
1770                if let IdOrClass::Id(id) = id_or_class {
1771                    return Some(id.clone());
1772                }
1773            }
1774        }
1775
1776        if attr_name == "class" {
1777            let classes: Vec<&str> = node_data
1778                .ids_and_classes
1779                .as_ref()
1780                .iter()
1781                .filter_map(|ioc| {
1782                    if let IdOrClass::Class(class) = ioc {
1783                        Some(class.as_str())
1784                    } else {
1785                        None
1786                    }
1787                })
1788                .collect();
1789            if !classes.is_empty() {
1790                return Some(classes.join(" ").into());
1791            }
1792        }
1793
1794        None
1795    }
1796
1797    /// Get all classes of a node as a vector of strings
1798    pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
1799        let layout_window = match self.get_layout_window().get_layout_result(&node_id.dom) {
1800            Some(lr) => lr,
1801            None => return StringVec::from_const_slice(&[]),
1802        };
1803        let node_id_internal = match node_id.node.into_crate_internal() {
1804            Some(n) => n,
1805            None => return StringVec::from_const_slice(&[]),
1806        };
1807        let node_data_cont = layout_window.styled_dom.node_data.as_container();
1808        let node_data = match node_data_cont.get(node_id_internal) {
1809            Some(n) => n,
1810            None => return StringVec::from_const_slice(&[]),
1811        };
1812
1813        let classes: Vec<AzString> = node_data
1814            .ids_and_classes
1815            .as_ref()
1816            .iter()
1817            .filter_map(|ioc| {
1818                if let IdOrClass::Class(class) = ioc {
1819                    Some(class.clone())
1820                } else {
1821                    None
1822                }
1823            })
1824            .collect();
1825
1826        StringVec::from(classes)
1827    }
1828
1829    /// Get the ID attribute of a node (if it has one)
1830    pub fn get_node_id(&self, node_id: DomNodeId) -> Option<AzString> {
1831        let layout_window = self.get_layout_window();
1832        let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1833        let node_id_internal = node_id.node.into_crate_internal()?;
1834        let node_data_cont = layout_result.styled_dom.node_data.as_container();
1835        let node_data = node_data_cont.get(node_id_internal)?;
1836
1837        for id_or_class in node_data.ids_and_classes.as_ref() {
1838            if let IdOrClass::Id(id) = id_or_class {
1839                return Some(id.clone());
1840            }
1841        }
1842        None
1843    }
1844
1845    // Text Selection Management
1846
1847    /// Get the current selection state for a DOM
1848    pub fn get_selection(&self, dom_id: &DomId) -> Option<&SelectionState> {
1849        self.get_layout_window()
1850            .selection_manager
1851            .get_selection(dom_id)
1852    }
1853
1854    /// Check if a DOM has any selection
1855    pub fn has_selection(&self, dom_id: &DomId) -> bool {
1856        self.get_layout_window()
1857            .selection_manager
1858            .has_selection(dom_id)
1859    }
1860
1861    /// Get the primary cursor for a DOM (first in selection list)
1862    pub fn get_primary_cursor(&self, dom_id: &DomId) -> Option<TextCursor> {
1863        self.get_layout_window()
1864            .selection_manager
1865            .get_primary_cursor(dom_id)
1866    }
1867
1868    /// Get all selection ranges (excludes plain cursors)
1869    pub fn get_selection_ranges(&self, dom_id: &DomId) -> SelectionRangeVec {
1870        self.get_layout_window()
1871            .selection_manager
1872            .get_ranges(dom_id)
1873            .into()
1874    }
1875
1876    /// Get direct access to the text layout cache
1877    ///
1878    /// Note: This provides direct read-only access to the text layout cache, but you need
1879    /// to know the CacheId for the specific text node you want. Currently there's
1880    /// no direct mapping from NodeId to CacheId exposed in the public API.
1881    ///
1882    /// For text modifications, use CallbackChange transactions:
1883    /// - `change_node_text()` for changing text content
1884    /// - `set_selection()` for setting selections
1885    /// - `get_selection()`, `get_primary_cursor()` for reading selections
1886    ///
1887    /// Future: Add NodeId -> CacheId mapping to enable node-specific layout access
1888    pub fn get_text_cache(&self) -> &TextLayoutCache {
1889        &self.get_layout_window().text_cache
1890    }
1891
1892    // Window State Access
1893
1894    /// Get full current window state (immutable reference)
1895    pub fn get_current_window_state(&self) -> &FullWindowState {
1896        // SAFETY: current_window_state is a valid pointer for the lifetime of CallbackInfo
1897        unsafe { (*self.ref_data).current_window_state }
1898    }
1899
1900    /// Get current window flags
1901    pub fn get_current_window_flags(&self) -> WindowFlags {
1902        self.get_current_window_state().flags.clone()
1903    }
1904
1905    /// Get current keyboard state
1906    pub fn get_current_keyboard_state(&self) -> KeyboardState {
1907        self.get_current_window_state().keyboard_state.clone()
1908    }
1909
1910    /// Get current mouse state
1911    pub fn get_current_mouse_state(&self) -> MouseState {
1912        self.get_current_window_state().mouse_state.clone()
1913    }
1914
1915    /// Get full previous window state (immutable reference)
1916    pub fn get_previous_window_state(&self) -> &Option<FullWindowState> {
1917        unsafe { (*self.ref_data).previous_window_state }
1918    }
1919
1920    /// Get previous window flags
1921    pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
1922        Some(self.get_previous_window_state().as_ref()?.flags.clone())
1923    }
1924
1925    /// Get previous keyboard state
1926    pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
1927        Some(
1928            self.get_previous_window_state()
1929                .as_ref()?
1930                .keyboard_state
1931                .clone(),
1932        )
1933    }
1934
1935    /// Get previous mouse state
1936    pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
1937        Some(
1938            self.get_previous_window_state()
1939                .as_ref()?
1940                .mouse_state
1941                .clone(),
1942        )
1943    }
1944
1945    // Cursor and Input
1946
1947    pub fn get_cursor_relative_to_node(&self) -> OptionLogicalPosition {
1948        self.cursor_relative_to_item
1949    }
1950
1951    pub fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
1952        self.cursor_in_viewport
1953    }
1954
1955    pub fn get_current_window_handle(&self) -> RawWindowHandle {
1956        unsafe { (*self.ref_data).current_window_handle.clone() }
1957    }
1958
1959    /// Get the system style (for menu rendering, CSD, etc.)
1960    /// This is useful for creating custom menus or other system-styled UI.
1961    pub fn get_system_style(&self) -> Arc<SystemStyle> {
1962        unsafe { (*self.ref_data).system_style.clone() }
1963    }
1964
1965    // ==================== ICU4X Internationalization API ====================
1966    //
1967    // All formatting functions take a locale string (BCP 47 format) as the first
1968    // parameter, allowing dynamic language switching per-call.
1969    //
1970    // For date/time construction, use the static methods on IcuDate, IcuTime, IcuDateTime:
1971    // - IcuDate::now(), IcuDate::now_utc(), IcuDate::new(year, month, day)
1972    // - IcuTime::now(), IcuTime::now_utc(), IcuTime::new(hour, minute, second)
1973    // - IcuDateTime::now(), IcuDateTime::now_utc(), IcuDateTime::from_timestamp(secs)
1974
1975    /// Get the ICU localizer cache for internationalized formatting.
1976    ///
1977    /// The cache stores localizers for multiple locales. Each locale's formatter
1978    /// is lazily created on first use and cached for subsequent calls.
1979    #[cfg(feature = "icu")]
1980    pub fn get_icu_localizer(&self) -> &IcuLocalizerHandle {
1981        unsafe { &(*self.ref_data).icu_localizer }
1982    }
1983
1984    /// Format an integer with locale-appropriate grouping separators.
1985    ///
1986    /// # Arguments
1987    /// * `locale` - BCP 47 locale string (e.g., "en-US", "de-DE", "ja-JP")
1988    /// * `value` - The integer to format
1989    ///
1990    /// # Example
1991    /// ```rust,ignore
1992    /// info.format_integer("en-US", 1234567) // → "1,234,567"
1993    /// info.format_integer("de-DE", 1234567) // → "1.234.567"
1994    /// info.format_integer("fr-FR", 1234567) // → "1 234 567"
1995    /// ```
1996    #[cfg(feature = "icu")]
1997    pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
1998        self.get_icu_localizer().format_integer(locale, value)
1999    }
2000
2001    /// Format a decimal number with locale-appropriate separators.
2002    ///
2003    /// # Arguments
2004    /// * `locale` - BCP 47 locale string
2005    /// * `integer_part` - The full integer value (e.g., 123456 for 1234.56)
2006    /// * `decimal_places` - Number of decimal places (e.g., 2 for 1234.56)
2007    ///
2008    /// # Example
2009    /// ```rust,ignore
2010    /// info.format_decimal("en-US", 123456, 2) // → "1,234.56"
2011    /// info.format_decimal("de-DE", 123456, 2) // → "1.234,56"
2012    /// ```
2013    #[cfg(feature = "icu")]
2014    pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
2015        self.get_icu_localizer().format_decimal(locale, integer_part, decimal_places)
2016    }
2017
2018    /// Get the plural category for a number (cardinal: "1 item", "2 items").
2019    ///
2020    /// # Arguments
2021    /// * `locale` - BCP 47 locale string
2022    /// * `value` - The number to get the plural category for
2023    ///
2024    /// # Example
2025    /// ```rust,ignore
2026    /// info.get_plural_category("en", 1)  // → PluralCategory::One
2027    /// info.get_plural_category("en", 2)  // → PluralCategory::Other
2028    /// info.get_plural_category("pl", 2)  // → PluralCategory::Few
2029    /// info.get_plural_category("pl", 5)  // → PluralCategory::Many
2030    /// ```
2031    #[cfg(feature = "icu")]
2032    pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
2033        self.get_icu_localizer().get_plural_category(locale, value)
2034    }
2035
2036    /// Select the appropriate string based on plural rules.
2037    ///
2038    /// # Arguments
2039    /// * `locale` - BCP 47 locale string
2040    /// * `value` - The number to pluralize
2041    /// * `zero`, `one`, `two`, `few`, `many`, `other` - Strings for each category
2042    ///
2043    /// # Example
2044    /// ```rust,ignore
2045    /// info.pluralize("en", count, "no items", "1 item", "2 items", "{} items", "{} items", "{} items")
2046    /// info.pluralize("pl", count, "brak", "1 element", "2 elementy", "{} elementy", "{} elementów", "{} elementów")
2047    /// ```
2048    #[cfg(feature = "icu")]
2049    pub fn pluralize(
2050        &self,
2051        locale: &str,
2052        value: i64,
2053        zero: &str,
2054        one: &str,
2055        two: &str,
2056        few: &str,
2057        many: &str,
2058        other: &str,
2059    ) -> AzString {
2060        self.get_icu_localizer().pluralize(locale, value, zero, one, two, few, many, other)
2061    }
2062
2063    /// Format a list of items with locale-appropriate conjunctions.
2064    ///
2065    /// # Arguments
2066    /// * `locale` - BCP 47 locale string
2067    /// * `items` - The items to format as a list
2068    /// * `list_type` - And, Or, or Unit list type
2069    ///
2070    /// # Example
2071    /// ```rust,ignore
2072    /// info.format_list("en-US", &items, ListType::And) // → "A, B, and C"
2073    /// info.format_list("es-ES", &items, ListType::And) // → "A, B y C"
2074    /// ```
2075    #[cfg(feature = "icu")]
2076    pub fn format_list(&self, locale: &str, items: &[AzString], list_type: ListType) -> AzString {
2077        self.get_icu_localizer().format_list(locale, items, list_type)
2078    }
2079
2080    /// Format a date according to the specified locale.
2081    ///
2082    /// # Arguments
2083    /// * `locale` - BCP 47 locale string
2084    /// * `date` - The date to format (use IcuDate::now() or IcuDate::new())
2085    /// * `length` - Short, Medium, or Long format
2086    ///
2087    /// # Example
2088    /// ```rust,ignore
2089    /// let today = IcuDate::now();
2090    /// info.format_date("en-US", today, FormatLength::Medium) // → "Jan 15, 2025"
2091    /// info.format_date("de-DE", today, FormatLength::Medium) // → "15.01.2025"
2092    /// ```
2093    #[cfg(feature = "icu")]
2094    pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
2095        self.get_icu_localizer().format_date(locale, date, length)
2096    }
2097
2098    /// Format a time according to the specified locale.
2099    ///
2100    /// # Arguments
2101    /// * `locale` - BCP 47 locale string
2102    /// * `time` - The time to format (use IcuTime::now() or IcuTime::new())
2103    /// * `include_seconds` - Whether to include seconds in the output
2104    ///
2105    /// # Example
2106    /// ```rust,ignore
2107    /// let now = IcuTime::now();
2108    /// info.format_time("en-US", now, false) // → "4:30 PM"
2109    /// info.format_time("de-DE", now, false) // → "16:30"
2110    /// ```
2111    #[cfg(feature = "icu")]
2112    pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
2113        self.get_icu_localizer().format_time(locale, time, include_seconds)
2114    }
2115
2116    /// Format a date and time according to the specified locale.
2117    ///
2118    /// # Arguments
2119    /// * `locale` - BCP 47 locale string
2120    /// * `datetime` - The date and time to format (use IcuDateTime::now())
2121    /// * `length` - Short, Medium, or Long format
2122    #[cfg(feature = "icu")]
2123    pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
2124        self.get_icu_localizer().format_datetime(locale, datetime, length)
2125    }
2126
2127    /// Compare two strings according to locale-specific collation rules.
2128    ///
2129    /// Returns -1 if a < b, 0 if a == b, 1 if a > b.
2130    /// This is useful for locale-aware sorting where "Ä" should sort with "A" in German.
2131    ///
2132    /// # Arguments
2133    /// * `locale` - BCP 47 locale string
2134    /// * `a` - First string to compare
2135    /// * `b` - Second string to compare
2136    ///
2137    /// # Example
2138    /// ```rust,ignore
2139    /// info.compare_strings("de-DE", "Äpfel", "Banane") // → -1 (Ä sorts with A)
2140    /// info.compare_strings("sv-SE", "Äpple", "Öl")     // → -1 (Swedish: Ä before Ö)
2141    /// ```
2142    #[cfg(feature = "icu")]
2143    pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
2144        self.get_icu_localizer().compare_strings(locale, a, b)
2145    }
2146
2147    /// Sort a list of strings using locale-aware collation.
2148    ///
2149    /// This properly handles accented characters, case sensitivity, and
2150    /// language-specific sorting rules.
2151    ///
2152    /// # Arguments
2153    /// * `locale` - BCP 47 locale string
2154    /// * `strings` - The strings to sort
2155    ///
2156    /// # Example
2157    /// ```rust,ignore
2158    /// let sorted = info.sort_strings("de-DE", &["Österreich", "Andorra", "Ägypten"]);
2159    /// // Result: ["Ägypten", "Andorra", "Österreich"] (Ä sorts with A, Ö with O)
2160    /// ```
2161    #[cfg(feature = "icu")]
2162    pub fn sort_strings(&self, locale: &str, strings: &[AzString]) -> IcuStringVec {
2163        self.get_icu_localizer().sort_strings(locale, strings)
2164    }
2165
2166    /// Check if two strings are equal according to locale collation rules.
2167    ///
2168    /// This may return `true` for strings that differ in case or accents,
2169    /// depending on the collation strength.
2170    ///
2171    /// # Arguments
2172    /// * `locale` - BCP 47 locale string
2173    /// * `a` - First string to compare
2174    /// * `b` - Second string to compare
2175    #[cfg(feature = "icu")]
2176    pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
2177        self.get_icu_localizer().strings_equal(locale, a, b)
2178    }
2179
2180    /// Get the current cursor position in logical coordinates relative to the window
2181    pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
2182        self.cursor_in_viewport.into_option()
2183    }
2184
2185    /// Get the layout rectangle of the currently hit node (in logical coordinates)
2186    pub fn get_hit_node_layout_rect(&self) -> Option<LogicalRect> {
2187        self.get_layout_window()
2188            .get_node_layout_rect(self.hit_dom_node)
2189    }
2190
2191    // Css Property Access
2192
2193    /// Get the computed CSS property for a specific DOM node
2194    ///
2195    /// This queries the CSS property cache and returns the resolved property value
2196    /// for the given node, taking into account:
2197    /// - User overrides (from callbacks)
2198    /// - Node state (:hover, :active, :focus)
2199    /// - CSS rules from stylesheets
2200    /// - Cascaded properties from parents
2201    /// - Inline styles
2202    ///
2203    /// # Arguments
2204    /// * `node_id` - The DOM node to query
2205    /// * `property_type` - The CSS property type to retrieve
2206    ///
2207    /// # Returns
2208    /// * `Some(CssProperty)` if the property is set on this node
2209    /// * `None` if the property is not set (will use default value)
2210    pub fn get_computed_css_property(
2211        &self,
2212        node_id: DomNodeId,
2213        property_type: CssPropertyType,
2214    ) -> Option<CssProperty> {
2215        let layout_window = self.get_layout_window();
2216
2217        // Get the layout result for this DOM
2218        let layout_result = layout_window.layout_results.get(&node_id.dom)?;
2219
2220        // Get the styled DOM
2221        let styled_dom = &layout_result.styled_dom;
2222
2223        // Convert DomNodeId to NodeId using proper decoding
2224        let internal_node_id = node_id.node.into_crate_internal()?;
2225
2226        // Get the node data
2227        let node_data_container = styled_dom.node_data.as_container();
2228        let node_data = node_data_container.get(internal_node_id)?;
2229
2230        // Get the styled node state
2231        let styled_nodes_container = styled_dom.styled_nodes.as_container();
2232        let styled_node = styled_nodes_container.get(internal_node_id)?;
2233        let node_state = &styled_node.styled_node_state;
2234
2235        // Query the CSS property cache
2236        let css_property_cache = &styled_dom.css_property_cache.ptr;
2237        css_property_cache
2238            .get_property(node_data, &internal_node_id, node_state, &property_type)
2239            .cloned()
2240    }
2241
2242    /// Get the computed width of a node from CSS
2243    ///
2244    /// Convenience method for getting the CSS width property.
2245    pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
2246        self.get_computed_css_property(node_id, CssPropertyType::Width)
2247    }
2248
2249    /// Get the computed height of a node from CSS
2250    ///
2251    /// Convenience method for getting the CSS height property.
2252    pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
2253        self.get_computed_css_property(node_id, CssPropertyType::Height)
2254    }
2255
2256    // System Callbacks
2257
2258    pub fn get_system_time_fn(&self) -> GetSystemTimeCallback {
2259        unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
2260    }
2261
2262    pub fn get_current_time(&self) -> task::Instant {
2263        let cb = self.get_system_time_fn();
2264        (cb.cb)()
2265    }
2266
2267    /// Get immutable reference to the renderer resources
2268    ///
2269    /// This provides access to fonts, images, and other rendering resources.
2270    /// Useful for custom rendering or screenshot functionality.
2271    pub fn get_renderer_resources(&self) -> &RendererResources {
2272        unsafe { (*self.ref_data).renderer_resources }
2273    }
2274
2275    // Screenshot API
2276
2277    /// Take a CPU-rendered screenshot of the current window content
2278    ///
2279    /// This renders the current display list to a PNG image using CPU rendering.
2280    /// The screenshot captures the window content as it would appear on screen,
2281    /// without window decorations.
2282    ///
2283    /// # Arguments
2284    /// * `dom_id` - The DOM to screenshot (use the main DOM ID for the full window)
2285    ///
2286    /// # Returns
2287    /// * `Ok(Vec<u8>)` - PNG-encoded image data
2288    /// * `Err(String)` - Error message if rendering failed
2289    ///
2290    /// # Example
2291    /// ```ignore
2292    /// fn on_click(info: &mut CallbackInfo) -> Update {
2293    ///     let dom_id = info.get_hit_node().dom;
2294    ///     match info.take_screenshot(dom_id) {
2295    ///         Ok(png_data) => {
2296    ///             std::fs::write("screenshot.png", png_data).unwrap();
2297    ///         }
2298    ///         Err(e) => eprintln!("Screenshot failed: {}", e),
2299    ///     }
2300    ///     Update::DoNothing
2301    /// }
2302    /// ```
2303    #[cfg(feature = "cpurender")]
2304    pub fn take_screenshot(&self, dom_id: DomId) -> Result<alloc::vec::Vec<u8>, AzString> {
2305        use crate::cpurender::{render, RenderOptions};
2306
2307        let layout_window = self.get_layout_window();
2308        let renderer_resources = self.get_renderer_resources();
2309
2310        // Get the layout result for this DOM
2311        let layout_result = layout_window
2312            .layout_results
2313            .get(&dom_id)
2314            .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
2315
2316        // Get viewport dimensions
2317        let viewport = &layout_result.viewport;
2318        let width = viewport.size.width;
2319        let height = viewport.size.height;
2320
2321        if width <= 0.0 || height <= 0.0 {
2322            return Err(AzString::from("Invalid viewport dimensions"));
2323        }
2324
2325        // Get the display list
2326        let display_list = &layout_result.display_list;
2327
2328        // Get DPI factor from window state
2329        let dpi_factor = self
2330            .get_current_window_state()
2331            .size
2332            .get_hidpi_factor()
2333            .inner
2334            .get();
2335
2336        // Render to pixmap
2337        let opts = RenderOptions {
2338            width,
2339            height,
2340            dpi_factor,
2341        };
2342
2343        let pixmap =
2344            render(display_list, renderer_resources, opts).map_err(|e| AzString::from(e))?;
2345
2346        // Encode to PNG
2347        let png_data = pixmap
2348            .encode_png()
2349            .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {}", e)))?;
2350
2351        Ok(png_data)
2352    }
2353
2354    /// Take a screenshot and save it directly to a file
2355    ///
2356    /// Convenience method that combines `take_screenshot` with file writing.
2357    ///
2358    /// # Arguments
2359    /// * `dom_id` - The DOM to screenshot
2360    /// * `path` - The file path to save the PNG to
2361    ///
2362    /// # Returns
2363    /// * `Ok(())` - Screenshot saved successfully
2364    /// * `Err(String)` - Error message if rendering or saving failed
2365    #[cfg(all(feature = "std", feature = "cpurender"))]
2366    pub fn take_screenshot_to_file(&self, dom_id: DomId, path: &str) -> Result<(), AzString> {
2367        let png_data = self.take_screenshot(dom_id)?;
2368        std::fs::write(path, png_data)
2369            .map_err(|e| AzString::from(alloc::format!("Failed to write file: {}", e)))?;
2370        Ok(())
2371    }
2372
2373    /// Take a native OS-level screenshot of the window including window decorations
2374    ///
2375    /// **NOTE**: This is a stub implementation. For full native screenshot support,
2376    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate, which uses
2377    /// runtime dynamic loading (dlopen) to avoid static linking dependencies.
2378    ///
2379    /// # Returns
2380    /// * `Err(String)` - Always returns an error directing to use the extension trait
2381    #[cfg(feature = "std")]
2382    pub fn take_native_screenshot(&self, _path: &str) -> Result<(), AzString> {
2383        Err(AzString::from(
2384            "Native screenshot requires the NativeScreenshotExt trait from azul-dll crate. \
2385             Import it with: use azul::desktop::NativeScreenshotExt;",
2386        ))
2387    }
2388
2389    /// Take a native OS-level screenshot and return the PNG data as bytes
2390    ///
2391    /// **NOTE**: This is a stub implementation. For full native screenshot support,
2392    /// use the `NativeScreenshotExt` trait from the `azul-dll` crate.
2393    ///
2394    /// # Returns
2395    /// * `Ok(Vec<u8>)` - PNG-encoded image data
2396    /// * `Err(String)` - Error message if screenshot failed
2397    #[cfg(feature = "std")]
2398    pub fn take_native_screenshot_bytes(&self) -> Result<alloc::vec::Vec<u8>, AzString> {
2399        // Create a temporary file, take screenshot, read bytes, delete file
2400        let temp_path = std::env::temp_dir().join("azul_screenshot_temp.png");
2401        let temp_path_str = temp_path.to_string_lossy().to_string();
2402
2403        self.take_native_screenshot(&temp_path_str)?;
2404
2405        let bytes = std::fs::read(&temp_path)
2406            .map_err(|e| AzString::from(alloc::format!("Failed to read screenshot: {}", e)))?;
2407
2408        let _ = std::fs::remove_file(&temp_path);
2409
2410        Ok(bytes)
2411    }
2412
2413    /// Take a native OS-level screenshot and return as a Base64 data URI
2414    ///
2415    /// Returns the screenshot as a "data:image/png;base64,..." string that can
2416    /// be directly used in HTML img tags or JSON responses.
2417    ///
2418    /// # Returns
2419    /// * `Ok(String)` - Base64 data URI string
2420    /// * `Err(String)` - Error message if screenshot failed
2421    ///
2422    #[cfg(feature = "std")]
2423    pub fn take_native_screenshot_base64(&self) -> Result<AzString, AzString> {
2424        let png_bytes = self.take_native_screenshot_bytes()?;
2425        let base64_str = base64_encode(&png_bytes);
2426        Ok(AzString::from(alloc::format!(
2427            "data:image/png;base64,{}",
2428            base64_str
2429        )))
2430    }
2431
2432    /// Take a CPU-rendered screenshot and return as a Base64 data URI
2433    ///
2434    /// Returns the screenshot as a "data:image/png;base64,..." string.
2435    /// This is the software-rendered version without window decorations.
2436    ///
2437    /// # Returns
2438    /// * `Ok(String)` - Base64 data URI string
2439    /// * `Err(String)` - Error message if rendering failed
2440    #[cfg(feature = "cpurender")]
2441    pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
2442        let png_bytes = self.take_screenshot(dom_id)?;
2443        let base64_str = base64_encode(&png_bytes);
2444        Ok(AzString::from(alloc::format!(
2445            "data:image/png;base64,{}",
2446            base64_str
2447        )))
2448    }
2449
2450    // Manager Access (Read-Only)
2451
2452    /// Get immutable reference to the scroll manager
2453    ///
2454    /// Use this to query scroll state for nodes without modifying it.
2455    /// To request programmatic scrolling, use `nodes_scrolled_in_callback`.
2456    pub fn get_scroll_manager(&self) -> &ScrollManager {
2457        unsafe { &(*self.ref_data).layout_window.scroll_manager }
2458    }
2459
2460    /// Get immutable reference to the gesture and drag manager
2461    ///
2462    /// Use this to query current gesture/drag state (e.g., "is this node being dragged?",
2463    /// "what files are being dropped?", "is a long-press active?").
2464    ///
2465    /// The manager is updated by the event loop and provides read-only query access
2466    /// to callbacks for gesture-aware UI behavior.
2467    pub fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
2468        unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
2469    }
2470
2471    /// Get immutable reference to the focus manager
2472    ///
2473    /// Use this to query which node currently has focus and whether focus
2474    /// is being moved to another node.
2475    pub fn get_focus_manager(&self) -> &FocusManager {
2476        &self.get_layout_window().focus_manager
2477    }
2478
2479    /// Get a reference to the undo/redo manager
2480    ///
2481    /// This allows user callbacks to query the undo/redo state and intercept
2482    /// undo/redo operations via preventDefault().
2483    pub fn get_undo_redo_manager(&self) -> &UndoRedoManager {
2484        &self.get_layout_window().undo_redo_manager
2485    }
2486
2487    /// Get immutable reference to the hover manager
2488    ///
2489    /// Use this to query which nodes are currently hovered at various input points
2490    /// (mouse, touch points, pen).
2491    pub fn get_hover_manager(&self) -> &HoverManager {
2492        &self.get_layout_window().hover_manager
2493    }
2494
2495    /// Get immutable reference to the text input manager
2496    ///
2497    /// Use this to query text selection state, cursor positions, and IME composition.
2498    pub fn get_text_input_manager(&self) -> &TextInputManager {
2499        &self.get_layout_window().text_input_manager
2500    }
2501
2502    /// Get immutable reference to the selection manager
2503    ///
2504    /// Use this to query text selections across multiple nodes.
2505    pub fn get_selection_manager(&self) -> &SelectionManager {
2506        &self.get_layout_window().selection_manager
2507    }
2508
2509    /// Check if a specific node is currently focused
2510    pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
2511        self.get_focus_manager().has_focus(&node_id)
2512    }
2513
2514    /// Check if any node in a specific DOM is focused
2515    pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
2516        self.get_focused_node()
2517            .map(|n| n.dom == dom_id)
2518            .unwrap_or(false)
2519    }
2520
2521    // Pen/Stylus Query Methods
2522
2523    /// Get current pen/stylus state if a pen is active
2524    pub fn get_pen_state(&self) -> Option<&PenState> {
2525        self.get_gesture_drag_manager().get_pen_state()
2526    }
2527
2528    /// Get current pen pressure (0.0 to 1.0)
2529    /// Returns None if no pen is active, Some(0.5) for mouse
2530    pub fn get_pen_pressure(&self) -> Option<f32> {
2531        self.get_pen_state().map(|pen| pen.pressure)
2532    }
2533
2534    /// Get current pen tilt angles (x_tilt, y_tilt) in degrees
2535    /// Returns None if no pen is active
2536    pub fn get_pen_tilt(&self) -> Option<PenTilt> {
2537        self.get_pen_state().map(|pen| pen.tilt)
2538    }
2539
2540    /// Check if pen is currently in contact with surface
2541    pub fn is_pen_in_contact(&self) -> bool {
2542        self.get_pen_state()
2543            .map(|pen| pen.in_contact)
2544            .unwrap_or(false)
2545    }
2546
2547    /// Check if pen is in eraser mode
2548    pub fn is_pen_eraser(&self) -> bool {
2549        self.get_pen_state()
2550            .map(|pen| pen.is_eraser)
2551            .unwrap_or(false)
2552    }
2553
2554    /// Check if pen barrel button is pressed
2555    pub fn is_pen_barrel_button_pressed(&self) -> bool {
2556        self.get_pen_state()
2557            .map(|pen| pen.barrel_button_pressed)
2558            .unwrap_or(false)
2559    }
2560
2561    /// Get the last recorded input sample (for event_id and detailed input data)
2562    pub fn get_last_input_sample(&self) -> Option<&InputSample> {
2563        let manager = self.get_gesture_drag_manager();
2564        manager
2565            .get_current_session()
2566            .and_then(|session| session.last_sample())
2567    }
2568
2569    /// Get the event ID of the current event
2570    pub fn get_current_event_id(&self) -> Option<u64> {
2571        self.get_last_input_sample().map(|sample| sample.event_id)
2572    }
2573
2574    // Focus Management Methods
2575
2576    /// Set focus to a specific DOM node by ID
2577    pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
2578        self.set_focus(FocusTarget::Id(DomNodeId {
2579            dom: dom_id,
2580            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2581        }));
2582    }
2583
2584    /// Set focus to a node matching a CSS path
2585    pub fn set_focus_to_path(&mut self, dom_id: DomId, css_path: CssPath) {
2586        self.set_focus(FocusTarget::Path(FocusTargetPath {
2587            dom: dom_id,
2588            css_path,
2589        }));
2590    }
2591
2592    /// Move focus to next focusable element in tab order
2593    pub fn focus_next(&mut self) {
2594        self.set_focus(FocusTarget::Next);
2595    }
2596
2597    /// Move focus to previous focusable element in tab order
2598    pub fn focus_previous(&mut self) {
2599        self.set_focus(FocusTarget::Previous);
2600    }
2601
2602    /// Move focus to first focusable element
2603    pub fn focus_first(&mut self) {
2604        self.set_focus(FocusTarget::First);
2605    }
2606
2607    /// Move focus to last focusable element
2608    pub fn focus_last(&mut self) {
2609        self.set_focus(FocusTarget::Last);
2610    }
2611
2612    /// Remove focus from all elements
2613    pub fn clear_focus(&mut self) {
2614        self.set_focus(FocusTarget::NoFocus);
2615    }
2616
2617    // Manager Access Methods
2618
2619    /// Check if a drag gesture is currently active
2620    ///
2621    /// Convenience method that queries the gesture manager.
2622    pub fn is_dragging(&self) -> bool {
2623        self.get_gesture_drag_manager().is_dragging()
2624    }
2625
2626    /// Get the currently focused node (if any)
2627    ///
2628    /// Returns None if no node has focus.
2629    pub fn get_focused_node(&self) -> Option<DomNodeId> {
2630        self.get_layout_window()
2631            .focus_manager
2632            .get_focused_node()
2633            .copied()
2634    }
2635
2636    /// Check if a specific node has focus
2637    pub fn has_focus(&self, node_id: DomNodeId) -> bool {
2638        self.get_layout_window().focus_manager.has_focus(&node_id)
2639    }
2640
2641    /// Get the currently hovered file (if drag-drop is in progress)
2642    ///
2643    /// Returns None if no file is being hovered over the window.
2644    pub fn get_hovered_file(&self) -> Option<&azul_css::AzString> {
2645        self.get_layout_window()
2646            .file_drop_manager
2647            .get_hovered_file()
2648    }
2649
2650    /// Get the currently dropped file (if a file was just dropped)
2651    ///
2652    /// This is a one-shot value that is cleared after event processing.
2653    /// Returns None if no file was dropped this frame.
2654    pub fn get_dropped_file(&self) -> Option<&azul_css::AzString> {
2655        self.get_layout_window()
2656            .file_drop_manager
2657            .dropped_file
2658            .as_ref()
2659    }
2660
2661    /// Check if a node or file drag is currently active
2662    ///
2663    /// Returns true if either a node drag or file drag is in progress.
2664    pub fn is_drag_active(&self) -> bool {
2665        self.get_layout_window().drag_drop_manager.is_dragging()
2666    }
2667
2668    /// Check if a node drag is specifically active
2669    pub fn is_node_drag_active(&self) -> bool {
2670        self.get_layout_window()
2671            .drag_drop_manager
2672            .is_dragging_node()
2673    }
2674
2675    /// Check if a file drag is specifically active
2676    pub fn is_file_drag_active(&self) -> bool {
2677        self.get_layout_window()
2678            .drag_drop_manager
2679            .is_dragging_file()
2680    }
2681
2682    /// Get the current drag/drop state (if any)
2683    ///
2684    /// Returns None if no drag is active, or Some with drag state.
2685    /// Uses legacy DragState type for backwards compatibility.
2686    pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
2687        self.get_layout_window().drag_drop_manager.get_drag_state()
2688    }
2689
2690    /// Get the current drag context (if any)
2691    ///
2692    /// Returns None if no drag is active, or Some with drag context.
2693    /// Prefer this over get_drag_state for new code.
2694    pub fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
2695        self.get_layout_window().drag_drop_manager.get_drag_context()
2696    }
2697
2698    // Hover Manager Access
2699
2700    /// Get the current mouse cursor hit test result (most recent frame)
2701    pub fn get_current_hit_test(&self) -> Option<&FullHitTest> {
2702        self.get_hover_manager().get_current(&InputPointId::Mouse)
2703    }
2704
2705    /// Get mouse cursor hit test from N frames ago (0 = current, 1 = previous, etc.)
2706    pub fn get_hit_test_frame(&self, frames_ago: usize) -> Option<&FullHitTest> {
2707        self.get_hover_manager()
2708            .get_frame(&InputPointId::Mouse, frames_ago)
2709    }
2710
2711    /// Get the full mouse cursor hit test history (up to 5 frames)
2712    ///
2713    /// Returns None if no mouse history exists yet
2714    pub fn get_hit_test_history(&self) -> Option<&VecDeque<FullHitTest>> {
2715        self.get_hover_manager().get_history(&InputPointId::Mouse)
2716    }
2717
2718    /// Check if there's sufficient mouse history for gesture detection (at least 2 frames)
2719    pub fn has_sufficient_history_for_gestures(&self) -> bool {
2720        self.get_hover_manager()
2721            .has_sufficient_history_for_gestures(&InputPointId::Mouse)
2722    }
2723
2724    // File Drop Manager Access
2725
2726    /// Get immutable reference to the file drop manager
2727    pub fn get_file_drop_manager(&self) -> &FileDropManager {
2728        &self.get_layout_window().file_drop_manager
2729    }
2730
2731    /// Get all selections across all DOMs
2732    pub fn get_all_selections(&self) -> &BTreeMap<DomId, SelectionState> {
2733        self.get_selection_manager().get_all_selections()
2734    }
2735
2736    // Drag-Drop Manager Access
2737
2738    /// Get immutable reference to the drag-drop manager
2739    pub fn get_drag_drop_manager(&self) -> &DragDropManager {
2740        &self.get_layout_window().drag_drop_manager
2741    }
2742
2743    /// Get the node being dragged (if any)
2744    pub fn get_dragged_node(&self) -> Option<DomNodeId> {
2745        self.get_drag_drop_manager()
2746            .get_drag_context()
2747            .and_then(|ctx| {
2748                ctx.as_node_drag().map(|node_drag| {
2749                    DomNodeId {
2750                        dom: node_drag.dom_id,
2751                        node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
2752                    }
2753                })
2754            })
2755    }
2756
2757    /// Get the file path being dragged (if any)
2758    pub fn get_dragged_file(&self) -> Option<&AzString> {
2759        self.get_drag_drop_manager()
2760            .get_drag_context()
2761            .and_then(|ctx| {
2762                ctx.as_file_drop().and_then(|file_drop| {
2763                    file_drop.files.as_ref().first()
2764                })
2765            })
2766    }
2767
2768    // Scroll Manager Query Methods
2769
2770    /// Get the current scroll offset for the hit node (if it's scrollable)
2771    ///
2772    /// Convenience method that uses the `hit_dom_node` from this callback.
2773    /// Use `get_scroll_offset_for_node` if you need to query a specific node.
2774    pub fn get_scroll_offset(&self) -> Option<LogicalPosition> {
2775        self.get_scroll_offset_for_node(
2776            self.hit_dom_node.dom,
2777            self.hit_dom_node.node.into_crate_internal().unwrap(),
2778        )
2779    }
2780
2781    /// Get the current scroll offset for a specific node (if it's scrollable)
2782    pub fn get_scroll_offset_for_node(
2783        &self,
2784        dom_id: DomId,
2785        node_id: NodeId,
2786    ) -> Option<LogicalPosition> {
2787        self.get_scroll_manager()
2788            .get_current_offset(dom_id, node_id)
2789    }
2790
2791    /// Get the scroll delta for a node in the current frame (for scroll event detection)
2792    pub fn get_scroll_delta(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalPosition> {
2793        self.get_scroll_manager().get_scroll_delta(dom_id, node_id)
2794    }
2795
2796    /// Check if a node had scroll activity this frame
2797    pub fn had_scroll_activity(&self, dom_id: DomId, node_id: NodeId) -> bool {
2798        self.get_scroll_manager()
2799            .had_scroll_activity_for_node(dom_id, node_id)
2800    }
2801
2802    /// Get the scroll state (container rect, content rect, current offset) for a node
2803    pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
2804        self.get_scroll_manager().get_scroll_state(dom_id, node_id)
2805    }
2806
2807    // Gpu State Manager Access
2808
2809    /// Get immutable reference to the GPU state manager
2810    pub fn get_gpu_state_manager(&self) -> &GpuStateManager {
2811        &self.get_layout_window().gpu_state_manager
2812    }
2813
2814    // IFrame Manager Access
2815
2816    /// Get immutable reference to the IFrame manager
2817    pub fn get_iframe_manager(&self) -> &IFrameManager {
2818        &self.get_layout_window().iframe_manager
2819    }
2820
2821    // Changeset Inspection/Modification Methods
2822    // These methods allow callbacks to inspect pending operations and modify them before execution
2823
2824    /// Inspect a pending copy operation
2825    ///
2826    /// Returns the clipboard content that would be copied if the operation proceeds.
2827    /// Use this to validate or transform clipboard content before copying.
2828    pub fn inspect_copy_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
2829        let layout_window = self.get_layout_window();
2830        let dom_id = &target.dom;
2831        layout_window.get_selected_content_for_clipboard(dom_id)
2832    }
2833
2834    /// Inspect a pending cut operation
2835    ///
2836    /// Returns the clipboard content that would be cut (copied + deleted).
2837    /// Use this to validate or transform content before cutting.
2838    pub fn inspect_cut_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
2839        // Cut uses same content extraction as copy
2840        self.inspect_copy_changeset(target)
2841    }
2842
2843    /// Inspect the current selection range that would be affected by paste
2844    ///
2845    /// Returns the selection range that will be replaced when pasting.
2846    /// Returns None if no selection exists (paste will insert at cursor).
2847    pub fn inspect_paste_target_range(&self, target: DomNodeId) -> Option<SelectionRange> {
2848        let layout_window = self.get_layout_window();
2849        let dom_id = &target.dom;
2850        layout_window
2851            .selection_manager
2852            .get_ranges(dom_id)
2853            .first()
2854            .copied()
2855    }
2856
2857    /// Inspect what text would be selected by Select All operation
2858    ///
2859    /// Returns the full text content and the range that would be selected.
2860    pub fn inspect_select_all_changeset(&self, target: DomNodeId) -> Option<SelectAllResult> {
2861        use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
2862
2863        let layout_window = self.get_layout_window();
2864        let node_id = target.node.into_crate_internal()?;
2865
2866        // Get text content
2867        let content = layout_window.get_text_before_textinput(target.dom, node_id);
2868        let text = layout_window.extract_text_from_inline_content(&content);
2869
2870        // Create selection range from start to end
2871        let start_cursor = TextCursor {
2872            cluster_id: GraphemeClusterId {
2873                source_run: 0,
2874                start_byte_in_run: 0,
2875            },
2876            affinity: CursorAffinity::Leading,
2877        };
2878
2879        let end_cursor = TextCursor {
2880            cluster_id: GraphemeClusterId {
2881                source_run: 0,
2882                start_byte_in_run: text.len() as u32,
2883            },
2884            affinity: CursorAffinity::Leading,
2885        };
2886
2887        let range = SelectionRange {
2888            start: start_cursor,
2889            end: end_cursor,
2890        };
2891
2892        Some(SelectAllResult {
2893            full_text: text.into(),
2894            selection_range: range,
2895        })
2896    }
2897
2898    /// Inspect what would be deleted by a backspace/delete operation
2899    ///
2900    /// Uses the pure functions from `text3::edit::inspect_delete()` to determine
2901    /// what would be deleted without actually performing the deletion.
2902    ///
2903    /// Returns (range_to_delete, deleted_text).
2904    /// - forward=true: Delete key (delete character after cursor)
2905    /// - forward=false: Backspace key (delete character before cursor)
2906    pub fn inspect_delete_changeset(
2907        &self,
2908        target: DomNodeId,
2909        forward: bool,
2910    ) -> Option<DeleteResult> {
2911        let layout_window = self.get_layout_window();
2912        let dom_id = &target.dom;
2913        let node_id = target.node.into_crate_internal()?;
2914
2915        // Get the inline content for this node
2916        let content = layout_window.get_text_before_textinput(target.dom, node_id);
2917
2918        // Get current selection state
2919        let selection =
2920            if let Some(range) = layout_window.selection_manager.get_ranges(dom_id).first() {
2921                Selection::Range(*range)
2922            } else if let Some(cursor) = layout_window.cursor_manager.get_cursor() {
2923                Selection::Cursor(*cursor)
2924            } else {
2925                return None; // No cursor or selection
2926            };
2927
2928        // Use text3::edit::inspect_delete to determine what would be deleted
2929        crate::text3::edit::inspect_delete(&content, &selection, forward).map(|(range, text)| {
2930            DeleteResult {
2931                range_to_delete: range,
2932                deleted_text: text.into(),
2933            }
2934        })
2935    }
2936
2937    /// Inspect a pending undo operation
2938    ///
2939    /// Returns the operation that would be undone, allowing inspection
2940    /// of what state will be restored.
2941    pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
2942        self.get_undo_redo_manager().peek_undo(node_id)
2943    }
2944
2945    /// Inspect a pending redo operation
2946    ///
2947    /// Returns the operation that would be reapplied.
2948    pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
2949        self.get_undo_redo_manager().peek_redo(node_id)
2950    }
2951
2952    /// Check if undo is available for a specific node
2953    ///
2954    /// Returns true if there is at least one undoable operation in the stack.
2955    pub fn can_undo(&self, node_id: NodeId) -> bool {
2956        self.get_undo_redo_manager()
2957            .get_stack(node_id)
2958            .map(|stack| stack.can_undo())
2959            .unwrap_or(false)
2960    }
2961
2962    /// Check if redo is available for a specific node
2963    ///
2964    /// Returns true if there is at least one redoable operation in the stack.
2965    pub fn can_redo(&self, node_id: NodeId) -> bool {
2966        self.get_undo_redo_manager()
2967            .get_stack(node_id)
2968            .map(|stack| stack.can_redo())
2969            .unwrap_or(false)
2970    }
2971
2972    /// Get the text that would be restored by undo for a specific node
2973    ///
2974    /// Returns the pre-state text content that would be restored if undo is performed.
2975    /// Returns None if no undo operation is available.
2976    pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
2977        self.get_undo_redo_manager()
2978            .peek_undo(node_id)
2979            .map(|op| op.pre_state.text_content.clone())
2980    }
2981
2982    /// Get the text that would be restored by redo for a specific node
2983    ///
2984    /// Returns the pre-state text content that would be restored if redo is performed.
2985    /// Returns None if no redo operation is available.
2986    pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
2987        self.get_undo_redo_manager()
2988            .peek_redo(node_id)
2989            .map(|op| op.pre_state.text_content.clone())
2990    }
2991
2992    // Clipboard Helper Methods
2993
2994    /// Get clipboard content from system clipboard (available during paste operations)
2995    ///
2996    /// This returns content that was read from the system clipboard when Ctrl+V was pressed.
2997    /// It's only available in On::Paste callbacks or similar clipboard-related callbacks.
2998    ///
2999    /// Use this to inspect what will be pasted before allowing or modifying the paste operation.
3000    ///
3001    /// # Returns
3002    /// * `Some(&ClipboardContent)` - If paste is in progress and clipboard has content
3003    /// * `None` - If no paste operation is active or clipboard is empty
3004    pub fn get_clipboard_content(&self) -> Option<&ClipboardContent> {
3005        unsafe {
3006            (*self.ref_data)
3007                .layout_window
3008                .clipboard_manager
3009                .get_paste_content()
3010        }
3011    }
3012
3013    /// Override clipboard content for copy/cut operations
3014    ///
3015    /// This sets custom content that will be written to the system clipboard.
3016    /// Use this in On::Copy or On::Cut callbacks to modify what gets copied.
3017    ///
3018    /// # Arguments
3019    /// * `content` - The clipboard content to write to system clipboard
3020    pub fn set_clipboard_content(&mut self, content: ClipboardContent) {
3021        // Queue the clipboard content to be set after callback returns
3022        // This will be picked up by the clipboard manager
3023        self.push_change(CallbackChange::SetCopyContent {
3024            target: self.hit_dom_node,
3025            content,
3026        });
3027    }
3028
3029    /// Set/modify the clipboard content before a copy operation
3030    ///
3031    /// Use this to transform clipboard content before copying.
3032    /// The change is queued and will be applied after the callback returns,
3033    /// if preventDefault() was not called.
3034    pub fn set_copy_content(&mut self, target: DomNodeId, content: ClipboardContent) {
3035        self.push_change(CallbackChange::SetCopyContent { target, content });
3036    }
3037
3038    /// Set/modify the clipboard content before a cut operation
3039    ///
3040    /// Similar to set_copy_content but for cut operations.
3041    /// The change is queued and will be applied after the callback returns.
3042    pub fn set_cut_content(&mut self, target: DomNodeId, content: ClipboardContent) {
3043        self.push_change(CallbackChange::SetCutContent { target, content });
3044    }
3045
3046    /// Override the selection range for select-all operation
3047    ///
3048    /// Use this to limit what gets selected (e.g., only select visible text).
3049    /// The change is queued and will be applied after the callback returns.
3050    pub fn set_select_all_range(&mut self, target: DomNodeId, range: SelectionRange) {
3051        self.push_change(CallbackChange::SetSelectAllRange { target, range });
3052    }
3053
3054    /// Request a hit test update at a specific position
3055    ///
3056    /// This is used by the Debug API to update the hover manager's hit test
3057    /// data after modifying the mouse position. This ensures that mouse event
3058    /// callbacks can find the correct nodes under the cursor.
3059    ///
3060    /// The hit test is performed during the next frame update.
3061    pub fn request_hit_test_update(&mut self, position: LogicalPosition) {
3062        self.push_change(CallbackChange::RequestHitTestUpdate { position });
3063    }
3064
3065    /// Process a text selection click at a specific position
3066    ///
3067    /// This is used by the Debug API to trigger text selection directly,
3068    /// bypassing the normal event pipeline which generates PreCallbackSystemEvent::TextClick.
3069    ///
3070    /// The selection processing is deferred until the CallbackChange is processed,
3071    /// at which point the LayoutWindow can be mutably accessed.
3072    pub fn process_text_selection_click(&mut self, position: LogicalPosition, time_ms: u64) {
3073        self.push_change(CallbackChange::ProcessTextSelectionClick { position, time_ms });
3074    }
3075
3076    /// Get the current text content of a node
3077    ///
3078    /// Helper for inspecting text before operations.
3079    pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
3080        let layout_window = self.get_layout_window();
3081        let node_id = target.node.into_crate_internal()?;
3082        let content = layout_window.get_text_before_textinput(target.dom, node_id);
3083        Some(layout_window.extract_text_from_inline_content(&content))
3084    }
3085
3086    /// Get the current cursor position in a node
3087    ///
3088    /// Returns the text cursor position if the node is focused.
3089    pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
3090        let layout_window = self.get_layout_window();
3091
3092        // Check if this node is focused
3093        if !layout_window.focus_manager.has_focus(&target) {
3094            return None;
3095        }
3096
3097        layout_window.cursor_manager.get_cursor().copied()
3098    }
3099
3100    /// Get the current selection ranges in a node
3101    ///
3102    /// Returns all active selection ranges for the specified DOM.
3103    pub fn get_node_selection_ranges(&self, target: DomNodeId) -> SelectionRangeVec {
3104        let layout_window = self.get_layout_window();
3105        layout_window
3106            .selection_manager
3107            .get_ranges(&target.dom)
3108            .into()
3109    }
3110
3111    /// Check if a specific node has an active selection
3112    ///
3113    /// This checks if the specific node (identified by DomNodeId) has a selection,
3114    /// as opposed to has_selection(DomId) which checks the entire DOM.
3115    pub fn node_has_selection(&self, target: DomNodeId) -> bool {
3116        self.get_node_selection_ranges(target).as_ref().is_empty() == false
3117    }
3118
3119    /// Get the length of text in a node
3120    ///
3121    /// Useful for bounds checking in custom operations.
3122    pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
3123        self.get_node_text_content(target).map(|text| text.len())
3124    }
3125
3126    // Cursor Movement Inspection/Override Methods
3127
3128    /// Inspect where the cursor would move when pressing left arrow
3129    ///
3130    /// Returns the new cursor position that would result from moving left.
3131    /// Returns None if the cursor is already at the start of the document.
3132    ///
3133    /// # Arguments
3134    /// * `target` - The node containing the cursor
3135    pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
3136        let layout_window = self.get_layout_window();
3137        let cursor = layout_window.cursor_manager.get_cursor()?;
3138
3139        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3140        // inline_layout_result
3141        let layout = self.get_inline_layout_for_node(&target)?;
3142
3143        // Use the text3::cache cursor movement logic
3144        let new_cursor = layout.move_cursor_left(*cursor, &mut None);
3145
3146        // Only return if cursor actually moved
3147        if new_cursor != *cursor {
3148            Some(new_cursor)
3149        } else {
3150            None
3151        }
3152    }
3153
3154    /// Inspect where the cursor would move when pressing right arrow
3155    ///
3156    /// Returns the new cursor position that would result from moving right.
3157    /// Returns None if the cursor is already at the end of the document.
3158    pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
3159        let layout_window = self.get_layout_window();
3160        let cursor = layout_window.cursor_manager.get_cursor()?;
3161
3162        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3163        // inline_layout_result
3164        let layout = self.get_inline_layout_for_node(&target)?;
3165
3166        // Use the text3::cache cursor movement logic
3167        let new_cursor = layout.move_cursor_right(*cursor, &mut None);
3168
3169        // Only return if cursor actually moved
3170        if new_cursor != *cursor {
3171            Some(new_cursor)
3172        } else {
3173            None
3174        }
3175    }
3176
3177    /// Inspect where the cursor would move when pressing up arrow
3178    ///
3179    /// Returns the new cursor position that would result from moving up one line.
3180    /// Returns None if the cursor is already on the first line.
3181    pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
3182        let layout_window = self.get_layout_window();
3183        let cursor = layout_window.cursor_manager.get_cursor()?;
3184
3185        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3186        // inline_layout_result
3187        let layout = self.get_inline_layout_for_node(&target)?;
3188
3189        // Use the text3::cache cursor movement logic
3190        // goal_x maintains horizontal position when moving vertically
3191        let new_cursor = layout.move_cursor_up(*cursor, &mut None, &mut None);
3192
3193        // Only return if cursor actually moved
3194        if new_cursor != *cursor {
3195            Some(new_cursor)
3196        } else {
3197            None
3198        }
3199    }
3200
3201    /// Inspect where the cursor would move when pressing down arrow
3202    ///
3203    /// Returns the new cursor position that would result from moving down one line.
3204    /// Returns None if the cursor is already on the last line.
3205    pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
3206        let layout_window = self.get_layout_window();
3207        let cursor = layout_window.cursor_manager.get_cursor()?;
3208
3209        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3210        // inline_layout_result
3211        let layout = self.get_inline_layout_for_node(&target)?;
3212
3213        // Use the text3::cache cursor movement logic
3214        // goal_x maintains horizontal position when moving vertically
3215        let new_cursor = layout.move_cursor_down(*cursor, &mut None, &mut None);
3216
3217        // Only return if cursor actually moved
3218        if new_cursor != *cursor {
3219            Some(new_cursor)
3220        } else {
3221            None
3222        }
3223    }
3224
3225    /// Inspect where the cursor would move when pressing Home key
3226    ///
3227    /// Returns the cursor position at the start of the current line.
3228    pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
3229        let layout_window = self.get_layout_window();
3230        let cursor = layout_window.cursor_manager.get_cursor()?;
3231
3232        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3233        // inline_layout_result
3234        let layout = self.get_inline_layout_for_node(&target)?;
3235
3236        // Use the text3::cache cursor movement logic
3237        let new_cursor = layout.move_cursor_to_line_start(*cursor, &mut None);
3238
3239        // Always return the result (might be same as input if already at line start)
3240        Some(new_cursor)
3241    }
3242
3243    /// Inspect where the cursor would move when pressing End key
3244    ///
3245    /// Returns the cursor position at the end of the current line.
3246    pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
3247        let layout_window = self.get_layout_window();
3248        let cursor = layout_window.cursor_manager.get_cursor()?;
3249
3250        // Get the text layout directly via layout_results → LayoutTree → LayoutNode →
3251        // inline_layout_result
3252        let layout = self.get_inline_layout_for_node(&target)?;
3253
3254        // Use the text3::cache cursor movement logic
3255        let new_cursor = layout.move_cursor_to_line_end(*cursor, &mut None);
3256
3257        // Always return the result (might be same as input if already at line end)
3258        Some(new_cursor)
3259    }
3260
3261    /// Inspect where the cursor would move when pressing Ctrl+Home
3262    ///
3263    /// Returns the cursor position at the start of the document.
3264    pub fn inspect_move_cursor_to_document_start(&self, target: DomNodeId) -> Option<TextCursor> {
3265        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
3266
3267        Some(TextCursor {
3268            cluster_id: GraphemeClusterId {
3269                source_run: 0,
3270                start_byte_in_run: 0,
3271            },
3272            affinity: CursorAffinity::Leading,
3273        })
3274    }
3275
3276    /// Inspect where the cursor would move when pressing Ctrl+End
3277    ///
3278    /// Returns the cursor position at the end of the document.
3279    pub fn inspect_move_cursor_to_document_end(&self, target: DomNodeId) -> Option<TextCursor> {
3280        use azul_core::selection::{CursorAffinity, GraphemeClusterId};
3281
3282        let text_len = self.get_node_text_length(target)?;
3283
3284        Some(TextCursor {
3285            cluster_id: GraphemeClusterId {
3286                source_run: 0,
3287                start_byte_in_run: text_len as u32,
3288            },
3289            affinity: CursorAffinity::Leading,
3290        })
3291    }
3292
3293    /// Inspect what text would be deleted by backspace (including Shift+Backspace)
3294    ///
3295    /// Returns (range_to_delete, deleted_text).
3296    /// This is a convenience wrapper around inspect_delete_changeset(target, false).
3297    pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
3298        self.inspect_delete_changeset(target, false)
3299    }
3300
3301    /// Inspect what text would be deleted by delete key
3302    ///
3303    /// Returns (range_to_delete, deleted_text).
3304    /// This is a convenience wrapper around inspect_delete_changeset(target, true).
3305    pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
3306        self.inspect_delete_changeset(target, true)
3307    }
3308
3309    // Cursor Movement Override Methods
3310    // These methods queue cursor movement operations to be applied after the callback
3311
3312    /// Move cursor left (arrow left key)
3313    ///
3314    /// # Arguments
3315    /// * `target` - The node containing the cursor
3316    /// * `extend_selection` - If true, extends selection (Shift+Left); if false, moves cursor
3317    pub fn move_cursor_left(&mut self, target: DomNodeId, extend_selection: bool) {
3318        self.push_change(CallbackChange::MoveCursorLeft {
3319            dom_id: target.dom,
3320            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3321            extend_selection,
3322        });
3323    }
3324
3325    /// Move cursor right (arrow right key)
3326    pub fn move_cursor_right(&mut self, target: DomNodeId, extend_selection: bool) {
3327        self.push_change(CallbackChange::MoveCursorRight {
3328            dom_id: target.dom,
3329            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3330            extend_selection,
3331        });
3332    }
3333
3334    /// Move cursor up (arrow up key)
3335    pub fn move_cursor_up(&mut self, target: DomNodeId, extend_selection: bool) {
3336        self.push_change(CallbackChange::MoveCursorUp {
3337            dom_id: target.dom,
3338            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3339            extend_selection,
3340        });
3341    }
3342
3343    /// Move cursor down (arrow down key)
3344    pub fn move_cursor_down(&mut self, target: DomNodeId, extend_selection: bool) {
3345        self.push_change(CallbackChange::MoveCursorDown {
3346            dom_id: target.dom,
3347            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3348            extend_selection,
3349        });
3350    }
3351
3352    /// Move cursor to line start (Home key)
3353    pub fn move_cursor_to_line_start(&mut self, target: DomNodeId, extend_selection: bool) {
3354        self.push_change(CallbackChange::MoveCursorToLineStart {
3355            dom_id: target.dom,
3356            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3357            extend_selection,
3358        });
3359    }
3360
3361    /// Move cursor to line end (End key)
3362    pub fn move_cursor_to_line_end(&mut self, target: DomNodeId, extend_selection: bool) {
3363        self.push_change(CallbackChange::MoveCursorToLineEnd {
3364            dom_id: target.dom,
3365            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3366            extend_selection,
3367        });
3368    }
3369
3370    /// Move cursor to document start (Ctrl+Home)
3371    pub fn move_cursor_to_document_start(&mut self, target: DomNodeId, extend_selection: bool) {
3372        self.push_change(CallbackChange::MoveCursorToDocumentStart {
3373            dom_id: target.dom,
3374            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3375            extend_selection,
3376        });
3377    }
3378
3379    /// Move cursor to document end (Ctrl+End)
3380    pub fn move_cursor_to_document_end(&mut self, target: DomNodeId, extend_selection: bool) {
3381        self.push_change(CallbackChange::MoveCursorToDocumentEnd {
3382            dom_id: target.dom,
3383            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3384            extend_selection,
3385        });
3386    }
3387
3388    /// Delete text backward (backspace or Shift+Backspace)
3389    ///
3390    /// Queues a backspace operation to be applied after the callback.
3391    /// Use inspect_backspace() to see what would be deleted.
3392    pub fn delete_backward(&mut self, target: DomNodeId) {
3393        self.push_change(CallbackChange::DeleteBackward {
3394            dom_id: target.dom,
3395            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3396        });
3397    }
3398
3399    /// Delete text forward (delete key)
3400    ///
3401    /// Queues a delete operation to be applied after the callback.
3402    /// Use inspect_delete() to see what would be deleted.
3403    pub fn delete_forward(&mut self, target: DomNodeId) {
3404        self.push_change(CallbackChange::DeleteForward {
3405            dom_id: target.dom,
3406            node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
3407        });
3408    }
3409}
3410
3411/// Config necessary for threading + animations to work in no_std environments
3412#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
3413#[repr(C)]
3414pub struct ExternalSystemCallbacks {
3415    pub create_thread_fn: CreateThreadCallback,
3416    pub get_system_time_fn: GetSystemTimeCallback,
3417}
3418
3419impl ExternalSystemCallbacks {
3420    #[cfg(not(feature = "std"))]
3421    pub fn rust_internal() -> Self {
3422        use crate::thread::create_thread_libstd;
3423
3424        Self {
3425            create_thread_fn: CreateThreadCallback {
3426                cb: create_thread_libstd,
3427            },
3428            get_system_time_fn: GetSystemTimeCallback {
3429                cb: azul_core::task::get_system_time_libstd,
3430            },
3431        }
3432    }
3433
3434    #[cfg(feature = "std")]
3435    pub fn rust_internal() -> Self {
3436        use crate::thread::create_thread_libstd;
3437
3438        Self {
3439            create_thread_fn: CreateThreadCallback {
3440                cb: create_thread_libstd,
3441            },
3442            get_system_time_fn: GetSystemTimeCallback {
3443                cb: azul_core::task::get_system_time_libstd,
3444            },
3445        }
3446    }
3447}
3448
3449/// Request to change focus, returned from callbacks
3450#[derive(Debug, Clone, PartialEq, Eq)]
3451pub enum FocusUpdateRequest {
3452    /// Focus a specific node
3453    FocusNode(DomNodeId),
3454    /// Clear focus (no node has focus)
3455    ClearFocus,
3456    /// No focus change requested
3457    NoChange,
3458}
3459
3460impl FocusUpdateRequest {
3461    /// Check if this represents a focus change
3462    pub fn is_change(&self) -> bool {
3463        !matches!(self, FocusUpdateRequest::NoChange)
3464    }
3465
3466    /// Convert to the new focused node (Some(node) or None for clear)
3467    pub fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
3468        match self {
3469            FocusUpdateRequest::FocusNode(node) => Some(Some(*node)),
3470            FocusUpdateRequest::ClearFocus => Some(None),
3471            FocusUpdateRequest::NoChange => None,
3472        }
3473    }
3474
3475    /// Create from Option<Option<DomNodeId>> (legacy format)
3476    pub fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
3477        match opt {
3478            Some(Some(node)) => FocusUpdateRequest::FocusNode(node),
3479            Some(None) => FocusUpdateRequest::ClearFocus,
3480            None => FocusUpdateRequest::NoChange,
3481        }
3482    }
3483}
3484
3485/// Result of calling callbacks, containing all state changes
3486#[derive(Debug)]
3487pub struct CallCallbacksResult {
3488    /// Whether the UI should be rendered due to a scroll event
3489    pub should_scroll_render: bool,
3490    /// Whether the callbacks say to rebuild the UI or not
3491    pub callbacks_update_screen: Update,
3492    /// FullWindowState that was (potentially) modified in the callbacks
3493    pub modified_window_state: Option<FullWindowState>,
3494    /// Text changes that don't require full relayout
3495    pub words_changed: Option<BTreeMap<DomId, BTreeMap<NodeId, AzString>>>,
3496    /// Image changes (for animated images/video)
3497    pub images_changed: Option<BTreeMap<DomId, BTreeMap<NodeId, (ImageRef, UpdateImageType)>>>,
3498    /// Clip mask changes (for vector animations)
3499    pub image_masks_changed: Option<BTreeMap<DomId, BTreeMap<NodeId, ImageMask>>>,
3500    /// Image callback changes (for OpenGL texture updates)
3501    pub image_callbacks_changed: Option<BTreeMap<DomId, FastBTreeSet<NodeId>>>,
3502    /// CSS property changes from callbacks
3503    pub css_properties_changed: Option<BTreeMap<DomId, BTreeMap<NodeId, CssPropertyVec>>>,
3504    /// Scroll position changes from callbacks
3505    pub nodes_scrolled_in_callbacks:
3506        Option<BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, LogicalPosition>>>,
3507    /// Focus change request from callback (if any)
3508    pub update_focused_node: FocusUpdateRequest,
3509    /// Timers added in callbacks
3510    pub timers: Option<FastHashMap<TimerId, Timer>>,
3511    /// Threads added in callbacks
3512    pub threads: Option<FastHashMap<ThreadId, Thread>>,
3513    /// Timers removed in callbacks
3514    pub timers_removed: Option<FastBTreeSet<TimerId>>,
3515    /// Threads removed in callbacks
3516    pub threads_removed: Option<FastBTreeSet<ThreadId>>,
3517    /// Windows created in callbacks
3518    pub windows_created: Vec<WindowCreateOptions>,
3519    /// Menus to open (context menus or dropdowns)
3520    pub menus_to_open: Vec<(Menu, Option<LogicalPosition>)>,
3521    /// Tooltips to show
3522    pub tooltips_to_show: Vec<(AzString, LogicalPosition)>,
3523    /// Whether to hide the currently displayed tooltip
3524    pub hide_tooltip: bool,
3525    /// Whether the cursor changed
3526    pub cursor_changed: bool,
3527    /// Whether stopPropagation() was called (prevents bubbling up in DOM-style event propagation)
3528    pub stop_propagation: bool,
3529    /// Whether preventDefault() was called (prevents default browser behavior)
3530    pub prevent_default: bool,
3531    /// Hit test update requested at this position (for Debug API)
3532    /// When set, the shell layer should perform a hit test update before the next event dispatch
3533    pub hit_test_update_requested: Option<LogicalPosition>,
3534    /// Queued window states to apply in sequence (for simulating clicks, etc.)
3535    /// The shell layer should apply these one at a time, processing events after each.
3536    pub queued_window_states: Vec<FullWindowState>,
3537    /// Text input events triggered by CreateTextInput
3538    /// These need to be processed by the recursive event loop to invoke user callbacks
3539    /// Format: Vec<(DomNodeId, Vec<EventFilter>)>
3540    pub text_input_triggered: Vec<(azul_core::dom::DomNodeId, Vec<azul_core::events::EventFilter>)>,
3541}
3542
3543impl Default for CallCallbacksResult {
3544    fn default() -> Self {
3545        Self {
3546            should_scroll_render: false,
3547            callbacks_update_screen: Update::DoNothing,
3548            modified_window_state: None,
3549            words_changed: None,
3550            images_changed: None,
3551            image_masks_changed: None,
3552            image_callbacks_changed: None,
3553            css_properties_changed: None,
3554            nodes_scrolled_in_callbacks: None,
3555            update_focused_node: FocusUpdateRequest::NoChange,
3556            timers: None,
3557            threads: None,
3558            timers_removed: None,
3559            threads_removed: None,
3560            windows_created: Vec::new(),
3561            menus_to_open: Vec::new(),
3562            tooltips_to_show: Vec::new(),
3563            hide_tooltip: false,
3564            cursor_changed: false,
3565            stop_propagation: false,
3566            prevent_default: false,
3567            hit_test_update_requested: None,
3568            queued_window_states: Vec::new(),
3569            text_input_triggered: Vec::new(),
3570        }
3571    }
3572}
3573
3574impl CallCallbacksResult {
3575    pub fn cursor_changed(&self) -> bool {
3576        self.cursor_changed
3577    }
3578
3579    pub fn focus_changed(&self) -> bool {
3580        self.update_focused_node.is_change()
3581    }
3582}
3583
3584impl azul_core::events::CallbackResultRef for CallCallbacksResult {
3585    fn stop_propagation(&self) -> bool {
3586        self.stop_propagation
3587    }
3588
3589    fn prevent_default(&self) -> bool {
3590        self.prevent_default
3591    }
3592
3593    fn should_regenerate_dom(&self) -> bool {
3594        use azul_core::callbacks::Update;
3595        matches!(
3596            self.callbacks_update_screen,
3597            Update::RefreshDom | Update::RefreshDomAllWindows
3598        )
3599    }
3600}
3601
3602/// Menu callback: What data / function pointer should
3603/// be called when the menu item is clicked?
3604#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
3605#[repr(C)]
3606pub struct MenuCallback {
3607    pub callback: Callback,
3608    pub refany: RefAny,
3609}
3610
3611/// Optional MenuCallback
3612#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
3613#[repr(C, u8)]
3614pub enum OptionMenuCallback {
3615    None,
3616    Some(MenuCallback),
3617}
3618
3619impl OptionMenuCallback {
3620    pub fn into_option(self) -> Option<MenuCallback> {
3621        match self {
3622            OptionMenuCallback::None => None,
3623            OptionMenuCallback::Some(c) => Some(c),
3624        }
3625    }
3626
3627    pub fn is_some(&self) -> bool {
3628        matches!(self, OptionMenuCallback::Some(_))
3629    }
3630
3631    pub fn is_none(&self) -> bool {
3632        matches!(self, OptionMenuCallback::None)
3633    }
3634}
3635
3636impl From<Option<MenuCallback>> for OptionMenuCallback {
3637    fn from(o: Option<MenuCallback>) -> Self {
3638        match o {
3639            None => OptionMenuCallback::None,
3640            Some(c) => OptionMenuCallback::Some(c),
3641        }
3642    }
3643}
3644
3645impl From<OptionMenuCallback> for Option<MenuCallback> {
3646    fn from(o: OptionMenuCallback) -> Self {
3647        o.into_option()
3648    }
3649}
3650
3651// -- RenderImage callbacks
3652
3653/// Callback type that renders an OpenGL texture
3654///
3655/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallbackType = usize`
3656/// to avoid circular dependencies. The actual function pointer is cast to usize for
3657/// storage in the data model, then unsafely cast back to this type when invoked.
3658pub type RenderImageCallbackType = extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
3659
3660/// Callback that returns a rendered OpenGL texture
3661///
3662/// **IMPORTANT**: In azul-core, this is stored as `CoreRenderImageCallback` with
3663/// a `cb: usize` field. When creating callbacks in the data model, function pointers
3664/// are cast to usize. This type is used in azul-layout where we can safely work
3665/// with the actual function pointer type.
3666#[repr(C)]
3667pub struct RenderImageCallback {
3668    pub cb: RenderImageCallbackType,
3669    /// For FFI: stores the foreign callable (e.g., PyFunction)
3670    /// Native Rust code sets this to None
3671    pub ctx: OptionRefAny,
3672}
3673
3674impl_callback!(RenderImageCallback, RenderImageCallbackType);
3675
3676impl RenderImageCallback {
3677    /// Create a new callback with just a function pointer (for native Rust code)
3678    pub fn create(cb: RenderImageCallbackType) -> Self {
3679        Self {
3680            cb,
3681            ctx: OptionRefAny::None,
3682        }
3683    }
3684
3685    /// Convert from the core crate's `CoreRenderImageCallback` (which stores cb as usize)
3686    /// back to the layout crate's typed function pointer.
3687    ///
3688    /// # Safety
3689    ///
3690    /// This is safe because we ensure that the usize in CoreRenderImageCallback
3691    /// was originally created from a valid RenderImageCallbackType function pointer.
3692    pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
3693        Self {
3694            cb: unsafe { core::mem::transmute(core_callback.cb) },
3695            ctx: core_callback.ctx.clone(),
3696        }
3697    }
3698
3699    /// Convert to CoreRenderImageCallback (function pointer stored as usize)
3700    ///
3701    /// This is always safe - we're just casting the function pointer to usize for storage.
3702    pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
3703        azul_core::callbacks::CoreRenderImageCallback {
3704            cb: self.cb as usize,
3705            ctx: self.ctx,
3706        }
3707    }
3708}
3709
3710/// Allow RenderImageCallback to be passed to functions expecting `C: Into<CoreRenderImageCallback>`
3711impl From<RenderImageCallback> for azul_core::callbacks::CoreRenderImageCallback {
3712    fn from(callback: RenderImageCallback) -> Self {
3713        callback.to_core()
3714    }
3715}
3716
3717/// Information passed to image rendering callbacks
3718#[derive(Debug)]
3719#[repr(C)]
3720pub struct RenderImageCallbackInfo {
3721    /// The ID of the DOM node that the ImageCallback was attached to
3722    callback_node_id: DomNodeId,
3723    /// Bounds of the laid-out node
3724    bounds: HidpiAdjustedBounds,
3725    /// Optional OpenGL context pointer
3726    gl_context: *const OptionGlContextPtr,
3727    /// Image cache for looking up images
3728    image_cache: *const ImageCache,
3729    /// System font cache
3730    system_fonts: *const FcFontCache,
3731    /// Pointer to callable (Python/FFI callback function)
3732    callable_ptr: *const OptionRefAny,
3733    /// Extension for future ABI stability (mutable data)
3734    _abi_mut: *mut core::ffi::c_void,
3735}
3736
3737impl Clone for RenderImageCallbackInfo {
3738    fn clone(&self) -> Self {
3739        Self {
3740            callback_node_id: self.callback_node_id,
3741            bounds: self.bounds,
3742            gl_context: self.gl_context,
3743            image_cache: self.image_cache,
3744            system_fonts: self.system_fonts,
3745            callable_ptr: self.callable_ptr,
3746            _abi_mut: self._abi_mut,
3747        }
3748    }
3749}
3750
3751impl RenderImageCallbackInfo {
3752    pub fn new<'a>(
3753        callback_node_id: DomNodeId,
3754        bounds: HidpiAdjustedBounds,
3755        gl_context: &'a OptionGlContextPtr,
3756        image_cache: &'a ImageCache,
3757        system_fonts: &'a FcFontCache,
3758    ) -> Self {
3759        Self {
3760            callback_node_id,
3761            bounds,
3762            gl_context: gl_context as *const OptionGlContextPtr,
3763            image_cache: image_cache as *const ImageCache,
3764            system_fonts: system_fonts as *const FcFontCache,
3765            callable_ptr: core::ptr::null(),
3766            _abi_mut: core::ptr::null_mut(),
3767        }
3768    }
3769
3770    /// Get the callable for FFI language bindings (Python, etc.)
3771    pub fn get_ctx(&self) -> OptionRefAny {
3772        if self.callable_ptr.is_null() {
3773            OptionRefAny::None
3774        } else {
3775            unsafe { (*self.callable_ptr).clone() }
3776        }
3777    }
3778
3779    /// Set the callable pointer (called before invoking callback)
3780    pub unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
3781        self.callable_ptr = ptr;
3782    }
3783
3784    pub fn get_callback_node_id(&self) -> DomNodeId {
3785        self.callback_node_id
3786    }
3787
3788    pub fn get_bounds(&self) -> HidpiAdjustedBounds {
3789        self.bounds
3790    }
3791
3792    fn internal_get_gl_context<'a>(&'a self) -> &'a OptionGlContextPtr {
3793        unsafe { &*self.gl_context }
3794    }
3795
3796    fn internal_get_image_cache<'a>(&'a self) -> &'a ImageCache {
3797        unsafe { &*self.image_cache }
3798    }
3799
3800    fn internal_get_system_fonts<'a>(&'a self) -> &'a FcFontCache {
3801        unsafe { &*self.system_fonts }
3802    }
3803
3804    pub fn get_gl_context(&self) -> OptionGlContextPtr {
3805        self.internal_get_gl_context().clone()
3806    }
3807}
3808
3809// ============================================================================
3810// Result types for FFI
3811// ============================================================================
3812
3813/// Result type for functions returning U8Vec or a String error
3814#[derive(Debug, Clone)]
3815#[repr(C, u8)]
3816pub enum ResultU8VecString {
3817    Ok(azul_css::U8Vec),
3818    Err(AzString),
3819}
3820
3821impl From<Result<alloc::vec::Vec<u8>, AzString>> for ResultU8VecString {
3822    fn from(result: Result<alloc::vec::Vec<u8>, AzString>) -> Self {
3823        match result {
3824            Ok(v) => ResultU8VecString::Ok(v.into()),
3825            Err(e) => ResultU8VecString::Err(e),
3826        }
3827    }
3828}
3829
3830/// Result type for functions returning () or a String error  
3831#[derive(Debug, Clone)]
3832#[repr(C, u8)]
3833pub enum ResultVoidString {
3834    Ok,
3835    Err(AzString),
3836}
3837
3838impl From<Result<(), AzString>> for ResultVoidString {
3839    fn from(result: Result<(), AzString>) -> Self {
3840        match result {
3841            Ok(()) => ResultVoidString::Ok,
3842            Err(e) => ResultVoidString::Err(e),
3843        }
3844    }
3845}
3846
3847/// Result type for functions returning String or a String error  
3848#[derive(Debug, Clone)]
3849#[repr(C, u8)]
3850pub enum ResultStringString {
3851    Ok(AzString),
3852    Err(AzString),
3853}
3854
3855impl From<Result<AzString, AzString>> for ResultStringString {
3856    fn from(result: Result<AzString, AzString>) -> Self {
3857        match result {
3858            Ok(s) => ResultStringString::Ok(s),
3859            Err(e) => ResultStringString::Err(e),
3860        }
3861    }
3862}
3863
3864// ============================================================================
3865// Base64 encoding helper
3866// ============================================================================
3867
3868const BASE64_ALPHABET: &[u8; 64] =
3869    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3870
3871/// Encode bytes to Base64 string
3872fn base64_encode(input: &[u8]) -> alloc::string::String {
3873    let mut output = alloc::string::String::with_capacity((input.len() + 2) / 3 * 4);
3874
3875    for chunk in input.chunks(3) {
3876        let b0 = chunk[0] as usize;
3877        let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
3878        let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
3879
3880        let n = (b0 << 16) | (b1 << 8) | b2;
3881
3882        output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
3883        output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
3884
3885        if chunk.len() > 1 {
3886            output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
3887        } else {
3888            output.push('=');
3889        }
3890
3891        if chunk.len() > 2 {
3892            output.push(BASE64_ALPHABET[n & 0x3F] as char);
3893        } else {
3894            output.push('=');
3895        }
3896    }
3897
3898    output
3899}