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