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}