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