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